diff --git a/Cargo.lock b/Cargo.lock index 5fc3dff..895f227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -639,6 +639,37 @@ dependencies = [ "chrono", "futures-util", "git2", + "giterated-models", + "jsonwebtoken", + "log", + "rand", + "reqwest", + "rsa", + "semver", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tokio-tungstenite", + "toml", + "tower", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "giterated-models" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "argon2", + "async-trait", + "base64 0.21.3", + "chrono", + "futures-util", + "git2", "jsonwebtoken", "log", "rand", diff --git a/Cargo.toml b/Cargo.toml index 2522895..209db82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,5 @@ -[package] -name = "giterated-daemon" -version = "0.0.6" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tokio-tungstenite = "*" -tokio = { version = "1.32.0", features = [ "full" ] } -tracing = "*" -futures-util = "*" -serde = { version = "1.0.188", features = [ "derive" ]} -serde_json = "1.0" -tracing-subscriber = "0.3" -base64 = "0.21.3" -jsonwebtoken = { version = "*", features = ["use_pem"]} -log = "*" -rand = "*" -rsa = {version = "0.9", features = ["sha2"]} -reqwest = "*" -argon2 = "*" -aes-gcm = "0.10.2" -semver = {version = "*", features = ["serde"]} -tower = "*" - -toml = { version = "0.7" } - -chrono = { version = "0.4", features = [ "serde" ] } -async-trait = "0.1" - -# Git backend -git2 = "0.17" -thiserror = "1" -anyhow = "1" -sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } - -#uuid = { version = "1.4", features = [ "v4", "serde" ] } +[workspace] +members = [ + "giterated-daemon", + "giterated-models" +] \ No newline at end of file diff --git a/giterated-daemon/Cargo.toml b/giterated-daemon/Cargo.toml new file mode 100644 index 0000000..a2c761b --- /dev/null +++ b/giterated-daemon/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "giterated-daemon" +version = "0.0.6" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio-tungstenite = "*" +tokio = { version = "1.32.0", features = [ "full" ] } +tracing = "*" +futures-util = "*" +serde = { version = "1.0.188", features = [ "derive" ]} +serde_json = "1.0" +tracing-subscriber = "0.3" +base64 = "0.21.3" +jsonwebtoken = { version = "*", features = ["use_pem"]} +log = "*" +rand = "*" +rsa = {version = "0.9", features = ["sha2"]} +reqwest = "*" +argon2 = "*" +aes-gcm = "0.10.2" +semver = {version = "*", features = ["serde"]} +tower = "*" +giterated-models = { path = "../giterated-models" } + +toml = { version = "0.7" } + +chrono = { version = "0.4", features = [ "serde" ] } +async-trait = "0.1" + +# Git backend +git2 = "0.17" +thiserror = "1" +anyhow = "1" +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } + +#uuid = { version = "1.4", features = [ "v4", "serde" ] } diff --git a/giterated-daemon/migrations/20230828083716_create_repositories.sql b/giterated-daemon/migrations/20230828083716_create_repositories.sql new file mode 100644 index 0000000..698612d --- /dev/null +++ b/giterated-daemon/migrations/20230828083716_create_repositories.sql @@ -0,0 +1,18 @@ +CREATE TYPE visibility AS ENUM +( + 'public', + 'unlisted', + 'private' +); + +CREATE TABLE IF NOT EXISTS repositories +( + username TEXT NOT NULL, + instance_url TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + visibility visibility NOT NULL, + default_branch TEXT NOT NULL +); + +CREATE UNIQUE INDEX unique_name_per_user ON repositories (username, instance_url, name); diff --git a/giterated-daemon/migrations/20230829014608_combine_user_instance_url_repositories.sql b/giterated-daemon/migrations/20230829014608_combine_user_instance_url_repositories.sql new file mode 100644 index 0000000..5af2ef7 --- /dev/null +++ b/giterated-daemon/migrations/20230829014608_combine_user_instance_url_repositories.sql @@ -0,0 +1,5 @@ +ALTER TABLE repositories +DROP COLUMN instance_url; + +ALTER TABLE repositories +RENAME COLUMN username TO owner_user; diff --git a/giterated-daemon/migrations/20230829104151_create_users.sql b/giterated-daemon/migrations/20230829104151_create_users.sql new file mode 100644 index 0000000..31eecf8 --- /dev/null +++ b/giterated-daemon/migrations/20230829104151_create_users.sql @@ -0,0 +1,15 @@ +-- Add migration script here + +CREATE TABLE IF NOT EXISTS users +( + username TEXT NOT NULL, + display_name TEXT, + image_url TEXT NOT NULL, + bio TEXT, + email TEXT, + password TEXT NOT NULL, + public_key TEXT NOT NULL, + enc_private_key TEXT NOT NULL +); + +CREATE UNIQUE INDEX unique_username ON users (username); \ No newline at end of file diff --git a/giterated-daemon/migrations/20230829165636_discovery.sql b/giterated-daemon/migrations/20230829165636_discovery.sql new file mode 100644 index 0000000..2c6b546 --- /dev/null +++ b/giterated-daemon/migrations/20230829165636_discovery.sql @@ -0,0 +1,13 @@ +CREATE TYPE discovery_type AS ENUM +( + 'instance', + 'repository' +); + +CREATE TABLE IF NOT EXISTS discoveries +( + discovery_hash TEXT PRIMARY KEY UNIQUE NOT NULL, + discovery_time TEXT NOT NULL, + discovery_type discovery_type NOT NULL, + discovery TEXT NOT NULL +); \ No newline at end of file diff --git a/giterated-daemon/migrations/20230902095036_user_settings.sql b/giterated-daemon/migrations/20230902095036_user_settings.sql new file mode 100644 index 0000000..f83f20a --- /dev/null +++ b/giterated-daemon/migrations/20230902095036_user_settings.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS user_settings +( + username TEXT NOT NULL, + name TEXT NOT NULL, + value TEXT NOT NULL +); + +CREATE UNIQUE INDEX unique_per_name ON user_settings (username, name); \ No newline at end of file diff --git a/giterated-daemon/src/authentication.rs b/giterated-daemon/src/authentication.rs new file mode 100644 index 0000000..518c0ec --- /dev/null +++ b/giterated-daemon/src/authentication.rs @@ -0,0 +1,178 @@ +use anyhow::Error; +use giterated_models::{ + messages::authentication::{AuthenticationTokenResponse, TokenExtensionResponse}, + model::{ + authenticated::{UserAuthenticationToken, UserTokenMetadata}, + instance::Instance, + user::User, + }, +}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, TokenData, Validation}; +use std::time::SystemTime; +use tokio::{fs::File, io::AsyncReadExt}; +use toml::Table; + +pub struct AuthenticationTokenGranter { + pub config: Table, + pub instance: Instance, +} + +impl AuthenticationTokenGranter { + async fn private_key(&self) -> Vec { + let _secret_key = self.config["authentication"]["secret_key"] + .as_str() + .unwrap(); + let mut file = File::open( + self.config["giterated"]["keys"]["private"] + .as_str() + .unwrap(), + ) + .await + .unwrap(); + + let mut key = vec![]; + file.read_to_end(&mut key).await.unwrap(); + + key + } + + pub(crate) async fn create_token_for( + &mut self, + user: &User, + generated_for: &Instance, + ) -> String { + let private_key = self.private_key().await; + + let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); + + let claims = UserTokenMetadata { + user: user.clone(), + generated_for: generated_for.clone(), + exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() + + std::time::Duration::from_secs(24 * 60 * 60)) + .as_secs(), + }; + + encode( + &jsonwebtoken::Header::new(Algorithm::RS256), + &claims, + &encoding_key, + ) + .unwrap() + } + + pub async fn token_request( + &mut self, + issued_for: impl ToOwned, + username: String, + _password: String, + ) -> Result { + let private_key = { + let mut file = File::open( + self.config["giterated"]["keys"]["private"] + .as_str() + .unwrap(), + ) + .await + .unwrap(); + + let mut key = vec![]; + file.read_to_end(&mut key).await.unwrap(); + + key + }; + + let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); + + let claims = UserTokenMetadata { + user: User { + username, + instance: self.instance.clone(), + }, + generated_for: issued_for.to_owned(), + exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() + + std::time::Duration::from_secs(24 * 60 * 60)) + .as_secs(), + }; + + let token = encode( + &jsonwebtoken::Header::new(Algorithm::RS256), + &claims, + &encoding_key, + ) + .unwrap(); + + Ok(AuthenticationTokenResponse { + token: UserAuthenticationToken::from(token), + }) + } + + pub async fn extension_request( + &mut self, + issued_for: &Instance, + token: UserAuthenticationToken, + ) -> Result { + let server_public_key = public_key(&self.instance).await.unwrap(); + + let verification_key = DecodingKey::from_rsa_pem(server_public_key.as_bytes()).unwrap(); + + let data: TokenData = decode( + token.as_ref(), + &verification_key, + &Validation::new(Algorithm::RS256), + ) + .unwrap(); + + if data.claims.generated_for != *issued_for { + panic!() + } + + info!("Token Extension Request Token validated"); + + let private_key = { + let mut file = File::open( + self.config["giterated"]["keys"]["private"] + .as_str() + .unwrap(), + ) + .await + .unwrap(); + + let mut key = vec![]; + file.read_to_end(&mut key).await.unwrap(); + + key + }; + + let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); + + let claims = UserTokenMetadata { + // TODO: Probably exploitable + user: data.claims.user, + generated_for: issued_for.clone(), + exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() + + std::time::Duration::from_secs(24 * 60 * 60)) + .as_secs(), + }; + + let token = encode( + &jsonwebtoken::Header::new(Algorithm::RS256), + &claims, + &encoding_key, + ) + .unwrap(); + + Ok(TokenExtensionResponse { + new_token: Some(token), + }) + } +} + +async fn public_key(instance: &Instance) -> Result { + let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) + .await? + .text() + .await?; + + Ok(key) +} diff --git a/giterated-daemon/src/backend/discovery.rs b/giterated-daemon/src/backend/discovery.rs new file mode 100644 index 0000000..a4d9a7a --- /dev/null +++ b/giterated-daemon/src/backend/discovery.rs @@ -0,0 +1,24 @@ +use std::hash::Hash; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +pub struct GiteratedDiscoveryProtocol { + pub pool: PgPool, +} + +#[derive(Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] +#[sqlx(type_name = "discovery_type", rename_all = "lowercase")] +pub enum DiscoveryType { + Instance, + Repository, +} + +#[derive(Debug, sqlx::FromRow, sqlx::Type)] +pub struct DiscoveriesRow { + discovery_hash: String, + discovery_time: DateTime, + discovery_type: DiscoveryType, + discovery: String, +} diff --git a/giterated-daemon/src/backend/git.rs b/giterated-daemon/src/backend/git.rs new file mode 100644 index 0000000..c0a25e6 --- /dev/null +++ b/giterated-daemon/src/backend/git.rs @@ -0,0 +1,505 @@ +use anyhow::Error; +use async_trait::async_trait; +use futures_util::StreamExt; +use git2::ObjectType; +use giterated_models::{ + messages::repository::{ + RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, + RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, + RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse, + RepositoryIssuesRequest, RepositoryIssuesResponse, + }, + model::{ + instance::Instance, + repository::{ + Commit, Repository, RepositoryObjectType, RepositorySummary, RepositoryTreeEntry, + RepositoryView, RepositoryVisibility, + }, + user::User, + }, +}; +use sqlx::{Either, PgPool}; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +use super::{IssuesBackend, RepositoryBackend}; + +// TODO: Handle this +//region database structures + +/// Repository in the database +#[derive(Debug, sqlx::FromRow)] +pub struct GitRepository { + #[sqlx(try_from = "String")] + pub owner_user: User, + pub name: String, + pub description: Option, + pub visibility: RepositoryVisibility, + pub default_branch: String, +} + +impl GitRepository { + // Separate function because "Private" will be expanded later + /// Checks if the user is allowed to view this repository + pub fn can_user_view_repository(&self, user: Option<&User>) -> bool { + !matches!(self.visibility, RepositoryVisibility::Private) + || (matches!(self.visibility, RepositoryVisibility::Private) + && Some(&self.owner_user) == user) + } + + // This is in it's own function because I assume I'll have to add logic to this later + pub fn open_git2_repository( + &self, + repository_directory: &str, + ) -> Result { + match git2::Repository::open(format!( + "{}/{}/{}/{}", + repository_directory, self.owner_user.instance.url, self.owner_user.username, self.name + )) { + Ok(repository) => Ok(repository), + Err(err) => { + let err = GitBackendError::FailedOpeningFromDisk(err); + error!("Couldn't open a repository, this is bad! {:?}", err); + + Err(err) + } + } + } +} + +//endregion + +#[derive(Error, Debug)] +pub enum GitBackendError { + #[error("Failed creating repository")] + FailedCreatingRepository(git2::Error), + #[error("Failed inserting into the database")] + FailedInsertingIntoDatabase(sqlx::Error), + #[error("Failed finding repository {owner_user:?}/{name:?}")] + RepositoryNotFound { owner_user: String, name: String }, + #[error("Repository {owner_user:?}/{name:?} already exists")] + RepositoryAlreadyExists { owner_user: String, name: String }, + #[error("Repository couldn't be deleted from the disk")] + CouldNotDeleteFromDisk(std::io::Error), + #[error("Failed deleting repository from database")] + FailedDeletingFromDatabase(sqlx::Error), + #[error("Failed opening repository on disk")] + FailedOpeningFromDisk(git2::Error), + #[error("Couldn't find ref with name `{0}`")] + RefNotFound(String), + #[error("Couldn't find path in repository `{0}`")] + PathNotFound(String), + #[error("Couldn't find commit for path `{0}`")] + LastCommitNotFound(String), +} + +pub struct GitBackend { + pub pg_pool: PgPool, + pub repository_folder: String, + pub instance: Instance, +} + +impl GitBackend { + pub fn new( + pg_pool: &PgPool, + repository_folder: &str, + instance: impl ToOwned, + ) -> Self { + Self { + pg_pool: pg_pool.clone(), + repository_folder: repository_folder.to_string(), + instance: instance.to_owned(), + } + } + + pub async fn find_by_owner_user_name( + &self, + user: &User, + repository_name: &str, + ) -> Result { + if let Ok(repository) = sqlx::query_as!(GitRepository, + r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#, + user.to_string(), repository_name) + .fetch_one(&self.pg_pool.clone()) + .await { + Ok(repository) + } else { + Err(GitBackendError::RepositoryNotFound { + owner_user: user.to_string(), + name: repository_name.to_string(), + }) + } + } + + pub async fn delete_by_owner_user_name( + &self, + user: &User, + repository_name: &str, + ) -> Result { + if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!( + "{}/{}/{}/{}", + self.repository_folder, user.instance.url, user.username, repository_name + ))) { + let err = GitBackendError::CouldNotDeleteFromDisk(err); + error!( + "Couldn't delete repository from disk, this is bad! {:?}", + err + ); + + return Err(err); + } + + // Delete the repository from the database + match sqlx::query!( + "DELETE FROM repositories WHERE owner_user = $1 AND name = $2", + user.to_string(), + repository_name + ) + .execute(&self.pg_pool.clone()) + .await + { + Ok(deleted) => Ok(deleted.rows_affected()), + Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)), + } + } + + // TODO: Find where this fits + // TODO: Cache this and general repository tree and invalidate select files on push + // TODO: Find better and faster technique for this + pub fn get_last_commit_of_file( + path: &str, + git: &git2::Repository, + start_commit: &git2::Commit, + ) -> anyhow::Result { + let mut revwalk = git.revwalk()?; + revwalk.set_sorting(git2::Sort::TIME)?; + revwalk.push(start_commit.id())?; + + for oid in revwalk { + let oid = oid?; + let commit = git.find_commit(oid)?; + + // Merge commits have 2 or more parents + // Commits with 0 parents are handled different because we can't diff against them + if commit.parent_count() == 0 { + return Ok(commit.into()); + } else if commit.parent_count() == 1 { + let tree = commit.tree()?; + let last_tree = commit.parent(0)?.tree()?; + + // Get the diff between the current tree and the last one + let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?; + + for dd in diff.deltas() { + // Get the path of the current file we're diffing against + let current_path = dd.new_file().path().unwrap(); + + // Path or directory + if current_path.eq(Path::new(&path)) || current_path.starts_with(path) { + return Ok(commit.into()); + } + } + } + } + + Err(GitBackendError::LastCommitNotFound(path.to_string()))? + } +} + +#[async_trait] +impl RepositoryBackend for GitBackend { + async fn create_repository( + &mut self, + _user: &User, + request: &RepositoryCreateRequest, + ) -> Result { + // Check if repository already exists in the database + if let Ok(repository) = self + .find_by_owner_user_name(&request.owner, &request.name) + .await + { + let err = GitBackendError::RepositoryAlreadyExists { + owner_user: repository.owner_user.to_string(), + name: repository.name, + }; + error!("{:?}", err); + + return Err(err); + } + + // Insert the repository into the database + let _ = match sqlx::query_as!(GitRepository, + r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#, + request.owner.to_string(), request.name, request.description, request.visibility as _, "master") + .fetch_one(&self.pg_pool.clone()) + .await { + Ok(repository) => repository, + Err(err) => { + let err = GitBackendError::FailedInsertingIntoDatabase(err); + error!("Failed inserting into the database! {:?}", err); + + return Err(err); + } + }; + + // Create bare (server side) repository on disk + match git2::Repository::init_bare(PathBuf::from(format!( + "{}/{}/{}/{}", + self.repository_folder, + request.owner.instance.url, + request.owner.username, + request.name + ))) { + Ok(_) => { + debug!( + "Created new repository with the name {}/{}/{}", + request.owner.instance.url, request.owner.username, request.name + ); + Ok(RepositoryCreateResponse) + } + Err(err) => { + let err = GitBackendError::FailedCreatingRepository(err); + error!("Failed creating repository on disk!? {:?}", err); + + // Delete repository from database + self.delete_by_owner_user_name(&request.owner, request.name.as_str()) + .await?; + + // ??? + Err(err) + } + } + } + + async fn repository_info( + &mut self, + requester: Option<&User>, + request: &RepositoryInfoRequest, + ) -> Result { + let repository = match self + .find_by_owner_user_name( + // &request.owner.instance.url, + &request.repository.owner, + &request.repository.name, + ) + .await + { + Ok(repository) => repository, + Err(err) => return Err(Box::new(err).into()), + }; + + if let Some(requester) = requester { + if !repository.can_user_view_repository(Some(&requester)) { + return Err(Box::new(GitBackendError::RepositoryNotFound { + owner_user: request.repository.owner.to_string(), + name: request.repository.name.clone(), + }) + .into()); + } + } else if matches!(repository.visibility, RepositoryVisibility::Private) { + // Unauthenticated users can never view private repositories + + return Err(Box::new(GitBackendError::RepositoryNotFound { + owner_user: request.repository.owner.to_string(), + name: request.repository.name.clone(), + }) + .into()); + } + + let git = match repository.open_git2_repository(&self.repository_folder) { + Ok(git) => git, + Err(err) => return Err(Box::new(err).into()), + }; + + let rev_name = match &request.rev { + None => { + if let Ok(head) = git.head() { + head.name().unwrap().to_string() + } else { + // Nothing in database, render empty tree. + return Ok(RepositoryView { + name: repository.name, + owner: request.repository.owner.clone(), + description: repository.description, + visibility: repository.visibility, + default_branch: repository.default_branch, + latest_commit: None, + tree_rev: None, + tree: vec![], + }); + } + } + Some(rev_name) => { + // Find the reference, otherwise return GitBackendError + match git + .find_reference(format!("refs/heads/{}", rev_name).as_str()) + .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) + { + Ok(reference) => reference.name().unwrap().to_string(), + Err(err) => return Err(Box::new(err).into()), + } + } + }; + + // Get the git object as a commit + let rev = match git + .revparse_single(rev_name.as_str()) + .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) + { + Ok(rev) => rev, + Err(err) => return Err(Box::new(err).into()), + }; + let commit = rev.as_commit().unwrap(); + + // this is stupid + let mut current_path = rev_name.replace("refs/heads/", ""); + + // Get the commit tree + let git_tree = if let Some(path) = &request.path { + // Add it to our full path string + current_path.push_str(format!("/{}", path).as_str()); + // Get the specified path, return an error if it wasn't found. + let entry = match commit + .tree() + .unwrap() + .get_path(&PathBuf::from(path)) + .map_err(|_| GitBackendError::PathNotFound(path.to_string())) + { + Ok(entry) => entry, + Err(err) => return Err(Box::new(err).into()), + }; + // Turn the entry into a git tree + entry.to_object(&git).unwrap().as_tree().unwrap().clone() + } else { + commit.tree().unwrap() + }; + + // Iterate over the git tree and collect it into our own tree types + let mut tree = git_tree + .iter() + .map(|entry| { + let object_type = match entry.kind().unwrap() { + ObjectType::Tree => RepositoryObjectType::Tree, + ObjectType::Blob => RepositoryObjectType::Blob, + _ => unreachable!(), + }; + let mut tree_entry = + RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode()); + + if request.extra_metadata { + // Get the file size if It's a blob + let object = entry.to_object(&git).unwrap(); + if let Some(blob) = object.as_blob() { + tree_entry.size = Some(blob.size()); + } + + // Could possibly be done better + let path = if let Some(path) = current_path.split_once('/') { + format!("{}/{}", path.1, entry.name().unwrap()) + } else { + entry.name().unwrap().to_string() + }; + + // Get the last commit made to the entry + if let Ok(last_commit) = + GitBackend::get_last_commit_of_file(&path, &git, commit) + { + tree_entry.last_commit = Some(last_commit); + } + } + + tree_entry + }) + .collect::>(); + + // Sort the tree alphabetically and with tree first + tree.sort_unstable_by_key(|entry| entry.name.to_lowercase()); + tree.sort_unstable_by_key(|entry| { + std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase()) + }); + + Ok(RepositoryView { + name: repository.name, + owner: request.repository.owner.clone(), + description: repository.description, + visibility: repository.visibility, + default_branch: repository.default_branch, + latest_commit: None, + tree_rev: Some(rev_name), + tree, + }) + } + + async fn repository_file_inspect( + &mut self, + _requester: Option<&User>, + _request: &RepositoryFileInspectRequest, + ) -> Result { + todo!() + } + + async fn repositories_for_user( + &mut self, + requester: Option<&User>, + user: &User, + ) -> Result, Error> { + let mut repositories = sqlx::query_as!( + GitRepository, + r#"SELECT visibility as "visibility: _", owner_user, name, description, default_branch FROM repositories WHERE owner_user = $1"#, + user.to_string() + ) + .fetch_many(&self.pg_pool); + + let mut result = vec![]; + + while let Some(Ok(Either::Right(repository))) = repositories.next().await { + // Check if the requesting user is allowed to see the repository + if !(matches!( + repository.visibility, + RepositoryVisibility::Unlisted | RepositoryVisibility::Private + ) && Some(&repository.owner_user.clone()) != requester) + { + result.push(RepositorySummary { + repository: Repository { + owner: repository.owner_user.clone(), + name: repository.name, + instance: self.instance.clone(), + }, + owner: repository.owner_user.clone(), + visibility: repository.visibility, + description: repository.description, + // TODO + last_commit: None, + }); + } + } + + Ok(result) + } +} + +impl IssuesBackend for GitBackend { + fn issues_count( + &mut self, + _requester: Option<&User>, + _request: &RepositoryIssuesCountRequest, + ) -> Result { + todo!() + } + + fn issue_labels( + &mut self, + _requester: Option<&User>, + _request: &RepositoryIssueLabelsRequest, + ) -> Result { + todo!() + } + + fn issues( + &mut self, + _requester: Option<&User>, + _request: &RepositoryIssuesRequest, + ) -> Result { + todo!() + } +} diff --git a/giterated-daemon/src/backend/github.rs b/giterated-daemon/src/backend/github.rs new file mode 100644 index 0000000..051c8cb --- /dev/null +++ b/giterated-daemon/src/backend/github.rs @@ -0,0 +1,2 @@ +//! TODO: GitHub backend to allow for login with GitHub +//! accounts and interacting with GitHub repositories diff --git a/giterated-daemon/src/backend/mod.rs b/giterated-daemon/src/backend/mod.rs new file mode 100644 index 0000000..62275f4 --- /dev/null +++ b/giterated-daemon/src/backend/mod.rs @@ -0,0 +1,110 @@ +pub mod discovery; +pub mod git; +pub mod github; +pub mod user; + +use anyhow::Error; +use async_trait::async_trait; +use serde_json::Value; + +use crate::backend::git::GitBackendError; +use giterated_models::{ + messages::{ + authentication::{ + AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, + RegisterAccountResponse, + }, + repository::{ + RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, + RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, + RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, + RepositoryIssuesCountResponse, RepositoryIssuesRequest, RepositoryIssuesResponse, + }, + user::{ + UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, + UserDisplayNameRequest, UserDisplayNameResponse, + }, + }, + model::{ + repository::{RepositorySummary, RepositoryView}, + user::User, + }, +}; + +#[async_trait] +pub trait RepositoryBackend: IssuesBackend { + async fn create_repository( + &mut self, + user: &User, + request: &RepositoryCreateRequest, + ) -> Result; + async fn repository_info( + &mut self, + requester: Option<&User>, + request: &RepositoryInfoRequest, + ) -> Result; + async fn repository_file_inspect( + &mut self, + requester: Option<&User>, + request: &RepositoryFileInspectRequest, + ) -> Result; + async fn repositories_for_user( + &mut self, + requester: Option<&User>, + user: &User, + ) -> Result, Error>; +} + +pub trait IssuesBackend { + fn issues_count( + &mut self, + requester: Option<&User>, + request: &RepositoryIssuesCountRequest, + ) -> Result; + fn issue_labels( + &mut self, + requester: Option<&User>, + request: &RepositoryIssueLabelsRequest, + ) -> Result; + fn issues( + &mut self, + requester: Option<&User>, + request: &RepositoryIssuesRequest, + ) -> Result; +} + +#[async_trait::async_trait] +pub trait AuthBackend { + async fn register( + &mut self, + request: RegisterAccountRequest, + ) -> Result; + + async fn login( + &mut self, + request: AuthenticationTokenRequest, + ) -> Result; +} + +#[async_trait::async_trait] +pub trait UserBackend: AuthBackend { + async fn display_name( + &mut self, + request: UserDisplayNameRequest, + ) -> Result; + + async fn display_image( + &mut self, + request: UserDisplayImageRequest, + ) -> Result; + + async fn bio(&mut self, request: UserBioRequest) -> Result; + async fn exists(&mut self, user: &User) -> Result; + + async fn settings(&mut self, user: &User) -> Result, Error>; + async fn write_settings( + &mut self, + user: &User, + settings: &[(String, Value)], + ) -> Result<(), Error>; +} diff --git a/giterated-daemon/src/backend/user.rs b/giterated-daemon/src/backend/user.rs new file mode 100644 index 0000000..4010d86 --- /dev/null +++ b/giterated-daemon/src/backend/user.rs @@ -0,0 +1,269 @@ +use std::sync::Arc; + +use anyhow::Error; + +use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use futures_util::StreamExt; +use giterated_models::{ + messages::{ + authentication::{ + AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, + RegisterAccountResponse, + }, + user::{ + UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, + UserDisplayNameRequest, UserDisplayNameResponse, + }, + }, + model::{instance::Instance, user::User}, +}; +use rsa::{ + pkcs8::{EncodePrivateKey, EncodePublicKey}, + rand_core::OsRng, + RsaPrivateKey, RsaPublicKey, +}; +use serde_json::Value; +use sqlx::{Either, PgPool}; +use tokio::sync::Mutex; + +use crate::authentication::AuthenticationTokenGranter; + +use super::{AuthBackend, UserBackend}; + +pub struct UserAuth { + pub pg_pool: PgPool, + pub this_instance: Instance, + pub auth_granter: Arc>, +} + +impl UserAuth { + pub fn new( + pool: PgPool, + this_instance: &Instance, + granter: Arc>, + ) -> Self { + Self { + pg_pool: pool, + this_instance: this_instance.clone(), + auth_granter: granter, + } + } +} + +#[async_trait::async_trait] +impl UserBackend for UserAuth { + async fn display_name( + &mut self, + request: UserDisplayNameRequest, + ) -> Result { + let db_row = sqlx::query_as!( + UserRow, + r#"SELECT * FROM users WHERE username = $1"#, + request.user.username + ) + .fetch_one(&self.pg_pool.clone()) + .await + .unwrap(); + + Ok(UserDisplayNameResponse { + display_name: db_row.display_name, + }) + } + + async fn display_image( + &mut self, + request: UserDisplayImageRequest, + ) -> Result { + let db_row = sqlx::query_as!( + UserRow, + r#"SELECT * FROM users WHERE username = $1"#, + request.user.username + ) + .fetch_one(&self.pg_pool.clone()) + .await + .unwrap(); + + Ok(UserDisplayImageResponse { + image_url: db_row.image_url, + }) + } + + async fn bio(&mut self, request: UserBioRequest) -> Result { + let db_row = sqlx::query_as!( + UserRow, + r#"SELECT * FROM users WHERE username = $1"#, + request.user.username + ) + .fetch_one(&self.pg_pool.clone()) + .await?; + + Ok(UserBioResponse { bio: db_row.bio }) + } + + async fn exists(&mut self, user: &User) -> Result { + Ok(sqlx::query_as!( + UserRow, + r#"SELECT * FROM users WHERE username = $1"#, + user.username + ) + .fetch_one(&self.pg_pool.clone()) + .await + .is_err()) + } + + async fn settings(&mut self, user: &User) -> Result, Error> { + let settings = sqlx::query_as!( + UserSettingRow, + r#"SELECT * FROM user_settings WHERE username = $1"#, + user.username + ) + .fetch_many(&self.pg_pool) + .filter_map(|result| async move { + if let Ok(Either::Right(row)) = result { + Some(row) + } else { + None + } + }) + .filter_map(|row| async move { + if let Ok(value) = serde_json::from_str(&row.value) { + Some((row.name, value)) + } else { + None + } + }) + .collect::>() + .await; + + Ok(settings) + } + + async fn write_settings( + &mut self, + user: &User, + settings: &[(String, Value)], + ) -> Result<(), Error> { + for (name, value) in settings { + let serialized = serde_json::to_string(value)?; + + sqlx::query!("INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", + user.username, name, serialized) + .execute(&self.pg_pool).await?; + } + + Ok(()) + } +} + +#[async_trait::async_trait] +impl AuthBackend for UserAuth { + async fn register( + &mut self, + request: RegisterAccountRequest, + ) -> Result { + const BITS: usize = 2048; + + let private_key = RsaPrivateKey::new(&mut OsRng, BITS).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + + let key = { + let mut target: [u8; 32] = [0; 32]; + + let mut index = 0; + let mut iterator = request.password.as_bytes().iter(); + while index < 32 { + if let Some(next) = iterator.next() { + target[index] = *next; + index += 1; + } else { + iterator = request.password.as_bytes().iter(); + } + } + + target + }; + + let key: &Key = &key.into(); + let cipher = Aes256Gcm::new(key); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let ciphertext = cipher + .encrypt(&nonce, private_key.to_pkcs8_der().unwrap().as_bytes()) + .unwrap(); + + let private_key_enc = format!("{}#{}", STANDARD.encode(nonce), STANDARD.encode(ciphertext)); + + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + + let password_hash = argon2 + .hash_password(request.password.as_bytes(), &salt) + .unwrap() + .to_string(); + + let user = match sqlx::query_as!( + UserRow, + r#"INSERT INTO users VALUES ($1, null, $2, null, null, $3, $4, $5) returning *"#, + request.username, + "example.com", + password_hash, + public_key + .to_public_key_pem(rsa::pkcs8::LineEnding::LF) + .unwrap(), + private_key_enc + ) + .fetch_one(&self.pg_pool) + .await + { + Ok(user) => user, + Err(err) => { + error!("Failed inserting into the database! {:?}", err); + + return Err(err.into()); + } + }; + + let mut granter = self.auth_granter.lock().await; + let token = granter + .create_token_for( + &User { + username: user.username, + instance: self.this_instance.clone(), + }, + &self.this_instance, + ) + .await; + + Ok(RegisterAccountResponse { token }) + } + + async fn login( + &mut self, + _request: AuthenticationTokenRequest, + ) -> Result { + todo!() + } +} + +#[allow(unused)] +#[derive(Debug, sqlx::FromRow)] +struct UserRow { + pub username: String, + pub image_url: Option, + pub display_name: Option, + pub bio: Option, + pub email: Option, + pub password: String, + pub public_key: String, + pub enc_private_key: Vec, +} + +#[allow(unused)] +#[derive(Debug, sqlx::FromRow)] +struct UserSettingRow { + pub username: String, + pub name: String, + pub value: String, +} diff --git a/giterated-daemon/src/connection.rs b/giterated-daemon/src/connection.rs new file mode 100644 index 0000000..cb1f370 --- /dev/null +++ b/giterated-daemon/src/connection.rs @@ -0,0 +1,94 @@ +pub mod authentication; +pub mod handshake; +pub mod repository; +pub mod user; +pub mod wrapper; + +use std::{any::type_name, collections::HashMap}; + +use anyhow::Error; +use giterated_models::{ + messages::ErrorMessage, + model::instance::{Instance, InstanceMeta}, +}; +use serde::{de::DeserializeOwned, Serialize}; +use tokio::{net::TcpStream, task::JoinHandle}; +use tokio_tungstenite::WebSocketStream; + +#[derive(Debug, thiserror::Error)] +pub enum ConnectionError { + #[error("connection error message {0}")] + ErrorMessage(#[from] ErrorMessage), + #[error("connection should close")] + Shutdown, + #[error("internal error {0}")] + InternalError(#[from] Error), +} + +pub struct RawConnection { + pub task: JoinHandle<()>, +} + +pub struct InstanceConnection { + pub instance: InstanceMeta, + pub task: JoinHandle<()>, +} + +/// Represents a connection which hasn't finished the handshake. +pub struct UnestablishedConnection { + pub socket: WebSocketStream, +} + +#[derive(Default)] +pub struct Connections { + pub connections: Vec, + pub instance_connections: HashMap, +} + +#[derive(Debug, thiserror::Error)] +#[error("handler did not handle")] +pub struct HandlerUnhandled; + +pub trait MessageHandling { + fn message_type() -> &'static str; +} + +impl MessageHandling<(T1,), M, R> for F +where + F: FnOnce(T1) -> R, + T1: Serialize + DeserializeOwned, +{ + fn message_type() -> &'static str { + type_name::() + } +} + +impl MessageHandling<(T1, T2), M, R> for F +where + F: FnOnce(T1, T2) -> R, + T1: Serialize + DeserializeOwned, +{ + fn message_type() -> &'static str { + type_name::() + } +} + +impl MessageHandling<(T1, T2, T3), M, R> for F +where + F: FnOnce(T1, T2, T3) -> R, + T1: Serialize + DeserializeOwned, +{ + fn message_type() -> &'static str { + type_name::() + } +} + +impl MessageHandling<(T1, T2, T3, T4), M, R> for F +where + F: FnOnce(T1, T2, T3, T4) -> R, + T1: Serialize + DeserializeOwned, +{ + fn message_type() -> &'static str { + type_name::() + } +} diff --git a/giterated-daemon/src/connection/authentication.rs b/giterated-daemon/src/connection/authentication.rs new file mode 100644 index 0000000..8a3444b --- /dev/null +++ b/giterated-daemon/src/connection/authentication.rs @@ -0,0 +1,123 @@ +use anyhow::Error; +use thiserror::Error; + +use crate::message::{AuthenticatedInstance, Message, MessageHandler, NetworkMessage, State}; +use giterated_models::messages::authentication::{ + AuthenticationTokenRequest, RegisterAccountRequest, TokenExtensionRequest, +}; + +use super::wrapper::ConnectionState; + +pub async fn authentication_handle( + message_type: &str, + message: &NetworkMessage, + state: &ConnectionState, +) -> Result { + match message_type { + "&giterated_daemon::messages::authentication::RegisterAccountRequest" => { + register_account_request + .handle_message(&message, state) + .await?; + + Ok(true) + } + "&giterated_daemon::messages::authentication::AuthenticationTokenRequest" => { + authentication_token_request + .handle_message(&message, state) + .await?; + + Ok(true) + } + "&giterated_daemon::messages::authentication::TokenExtensionRequest" => { + token_extension_request + .handle_message(&message, state) + .await?; + + Ok(true) + } + _ => Ok(false), + } +} + +async fn register_account_request( + State(connection_state): State, + Message(request): Message, + instance: AuthenticatedInstance, +) -> Result<(), AuthenticationConnectionError> { + if *instance.inner() != connection_state.instance { + return Err(AuthenticationConnectionError::SameInstance); + } + + let mut user_backend = connection_state.user_backend.lock().await; + + let response = user_backend + .register(request.clone()) + .await + .map_err(|e| AuthenticationConnectionError::Registration(e))?; + drop(user_backend); + + connection_state + .send(response) + .await + .map_err(|e| AuthenticationConnectionError::Sending(e))?; + + Ok(()) +} + +async fn authentication_token_request( + State(connection_state): State, + Message(request): Message, + instance: AuthenticatedInstance, +) -> Result<(), AuthenticationConnectionError> { + let issued_for = instance.inner().clone(); + + let mut token_granter = connection_state.auth_granter.lock().await; + + let response = token_granter + .token_request(issued_for, request.username, request.password) + .await + .map_err(|e| AuthenticationConnectionError::TokenIssuance(e))?; + + connection_state + .send(response) + .await + .map_err(|e| AuthenticationConnectionError::Sending(e))?; + + Ok(()) +} + +async fn token_extension_request( + State(connection_state): State, + Message(request): Message, + instance: AuthenticatedInstance, +) -> Result<(), AuthenticationConnectionError> { + let issued_for = instance.inner().clone(); + + let mut token_granter = connection_state.auth_granter.lock().await; + + let response = token_granter + .extension_request(&issued_for, request.token) + .await + .map_err(|e| AuthenticationConnectionError::TokenIssuance(e))?; + + connection_state + .send(response) + .await + .map_err(|e| AuthenticationConnectionError::Sending(e))?; + + Ok(()) +} + +#[derive(Debug, Error)] +pub enum AuthenticationConnectionError { + #[error("the request was invalid")] + InvalidRequest, + #[error("request must be from the same instance")] + SameInstance, + #[error("issue during registration {0}")] + Registration(Error), + #[error("sending error")] + Sending(Error), + #[error("error issuing token")] + TokenIssuance(Error), +} diff --git a/giterated-daemon/src/connection/handshake.rs b/giterated-daemon/src/connection/handshake.rs new file mode 100644 index 0000000..5cff8d7 --- /dev/null +++ b/giterated-daemon/src/connection/handshake.rs @@ -0,0 +1,128 @@ +use std::{str::FromStr, sync::atomic::Ordering}; + +use anyhow::Error; +use giterated_models::messages::handshake::{ + HandshakeFinalize, HandshakeResponse, InitiateHandshake, +}; +use semver::Version; + +use crate::{ + connection::ConnectionError, + message::{Message, MessageHandler, NetworkMessage, State}, + validate_version, version, +}; + +use super::{wrapper::ConnectionState, HandlerUnhandled}; + +pub async fn handshake_handle( + message: &NetworkMessage, + state: &ConnectionState, +) -> Result<(), Error> { + if initiate_handshake + .handle_message(&message, state) + .await + .is_ok() + { + Ok(()) + } else if handshake_response + .handle_message(&message, state) + .await + .is_ok() + { + Ok(()) + } else if handshake_finalize + .handle_message(&message, state) + .await + .is_ok() + { + Ok(()) + } else { + Err(Error::from(HandlerUnhandled)) + } +} + +async fn initiate_handshake( + Message(initiation): Message, + State(connection_state): State, +) -> Result<(), HandshakeError> { + if !validate_version(&initiation.version) { + error!( + "Version compatibility failure! Our Version: {}, Their Version: {}", + Version::from_str(&std::env::var("CARGO_PKG_VERSION").unwrap()).unwrap(), + initiation.version + ); + + connection_state + .send(HandshakeFinalize { success: false }) + .await + .map_err(|e| HandshakeError::SendError(e))?; + + Ok(()) + } else { + connection_state + .send(HandshakeResponse { + identity: connection_state.instance.clone(), + version: version(), + }) + .await + .map_err(|e| HandshakeError::SendError(e))?; + + Ok(()) + } +} + +async fn handshake_response( + Message(response): Message, + State(connection_state): State, +) -> Result<(), HandshakeError> { + if !validate_version(&response.version) { + error!( + "Version compatibility failure! Our Version: {}, Their Version: {}", + Version::from_str(&std::env::var("CARGO_PKG_VERSION").unwrap()).unwrap(), + response.version + ); + + connection_state + .send(HandshakeFinalize { success: false }) + .await + .map_err(|e| HandshakeError::SendError(e))?; + + Ok(()) + } else { + connection_state + .send(HandshakeFinalize { success: true }) + .await + .map_err(|e| HandshakeError::SendError(e))?; + + Ok(()) + } +} + +async fn handshake_finalize( + Message(finalize): Message, + State(connection_state): State, +) -> Result<(), HandshakeError> { + if !finalize.success { + error!("Error during handshake, aborting connection"); + return Err(Error::from(ConnectionError::Shutdown).into()); + } else { + connection_state.handshaked.store(true, Ordering::SeqCst); + + connection_state + .send(HandshakeFinalize { success: true }) + .await + .map_err(|e| HandshakeError::SendError(e))?; + + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum HandshakeError { + #[error("version mismatch during handshake, ours: {0}, theirs: {1}")] + VersionMismatch(Version, Version), + #[error("while sending message: {0}")] + SendError(Error), + #[error("{0}")] + Other(#[from] Error), +} diff --git a/giterated-daemon/src/connection/repository.rs b/giterated-daemon/src/connection/repository.rs new file mode 100644 index 0000000..3bfc034 --- /dev/null +++ b/giterated-daemon/src/connection/repository.rs @@ -0,0 +1,141 @@ +use anyhow::Error; + +use crate::{ + backend::git::GitBackendError, + message::{AuthenticatedUser, Message, MessageHandler, NetworkMessage, State}, +}; +use giterated_models::messages::repository::{ + RepositoryCreateRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, + RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, +}; + +use super::wrapper::ConnectionState; + +pub async fn repository_handle( + message_type: &str, + message: &NetworkMessage, + state: &ConnectionState, +) -> Result { + match message_type { + "&giterated_daemon::messages::repository::RepositoryCreateRequest" => { + create_repository.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::repository::RepositoryFileInspectRequest" => { + repository_file_inspect + .handle_message(&message, state) + .await?; + + Ok(true) + } + "&giterated_daemon::messages::repository::RepositoryInfoRequest" => { + repository_info.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::repository::RepositoryIssuesCountRequest" => { + issues_count.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::repository::RepositoryIssueLabelsRequest" => { + issue_labels.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::repository::RepositoryIssuesRequest" => { + issues.handle_message(&message, state).await?; + + Ok(true) + } + _ => Ok(false), + } +} + +async fn create_repository( + Message(request): Message, + State(connection_state): State, + AuthenticatedUser(user): AuthenticatedUser, +) -> Result<(), RepositoryError> { + let mut repository_backend = connection_state.repository_backend.lock().await; + let response = repository_backend + .create_repository(&user, &request) + .await?; + + drop(repository_backend); + + connection_state.send(response).await?; + + Ok(()) +} + +async fn repository_file_inspect( + Message(request): Message, + State(connection_state): State, + user: Option, +) -> Result<(), RepositoryError> { + let user = user.map(|u| u.0); + + let mut repository_backend = connection_state.repository_backend.lock().await; + let response = repository_backend + .repository_file_inspect(user.as_ref(), &request) + .await?; + + drop(repository_backend); + + connection_state.send(response).await?; + + Ok(()) +} + +async fn repository_info( + Message(request): Message, + State(connection_state): State, + user: Option, +) -> Result<(), RepositoryError> { + let user = user.map(|u| u.0); + + let mut repository_backend = connection_state.repository_backend.lock().await; + let response = repository_backend + .repository_info(user.as_ref(), &request) + .await?; + + drop(repository_backend); + + connection_state.send(response).await?; + + Ok(()) +} + +async fn issues_count( + Message(_request): Message, + State(_connection_state): State, + _user: Option, +) -> Result<(), RepositoryError> { + unimplemented!(); +} + +async fn issue_labels( + Message(_request): Message, + State(_connection_state): State, + _user: Option, +) -> Result<(), RepositoryError> { + unimplemented!(); +} + +async fn issues( + Message(_request): Message, + State(_connection_state): State, + _user: Option, +) -> Result<(), RepositoryError> { + unimplemented!(); +} + +#[derive(Debug, thiserror::Error)] +pub enum RepositoryError { + #[error("{0}")] + GitBackendError(#[from] GitBackendError), + #[error("{0}")] + Other(#[from] Error), +} diff --git a/giterated-daemon/src/connection/user.rs b/giterated-daemon/src/connection/user.rs new file mode 100644 index 0000000..ba9783f --- /dev/null +++ b/giterated-daemon/src/connection/user.rs @@ -0,0 +1,184 @@ +use anyhow::Error; +use giterated_models::{ + messages::user::{ + UserBioRequest, UserDisplayImageRequest, UserDisplayNameRequest, UserRepositoriesRequest, + UserRepositoriesResponse, UserSettingsRequest, UserSettingsResponse, + UserWriteSettingsRequest, UserWriteSettingsResponse, + }, + model::user::User, +}; + +use crate::message::{AuthenticatedUser, Message, MessageHandler, NetworkMessage, State}; + +use super::wrapper::ConnectionState; + +pub async fn user_handle( + message_type: &str, + message: &NetworkMessage, + state: &ConnectionState, +) -> Result { + match message_type { + "&giterated_daemon::messages::user::UserDisplayNameRequest" => { + display_name.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::user::UserDisplayImageRequest" => { + display_image.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::user::UserBioRequest" => { + bio.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::user::UserRepositoriesRequest" => { + repositories.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::user::UserSettingsRequest" => { + user_settings.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::user::UserWriteSettingsRequest" => { + write_user_settings.handle_message(&message, state).await?; + + Ok(true) + } + _ => Ok(false), + } +} + +async fn display_name( + Message(request): Message, + State(connection_state): State, +) -> Result<(), UserError> { + let mut user_backend = connection_state.user_backend.lock().await; + let response = user_backend.display_name(request.clone()).await?; + + drop(user_backend); + + connection_state.send(response).await?; + + Ok(()) +} + +async fn display_image( + Message(request): Message, + State(connection_state): State, +) -> Result<(), UserError> { + let mut user_backend = connection_state.user_backend.lock().await; + let response = user_backend.display_image(request.clone()).await?; + + drop(user_backend); + + connection_state.send(response).await?; + + Ok(()) +} + +async fn bio( + Message(request): Message, + State(connection_state): State, +) -> Result<(), UserError> { + let mut user_backend = connection_state.user_backend.lock().await; + let response = user_backend.bio(request.clone()).await?; + + drop(user_backend); + + connection_state.send(response).await?; + + Ok(()) +} + +async fn repositories( + Message(request): Message, + State(connection_state): State, + requesting_user: Option, +) -> Result<(), UserError> { + let requesting_user = requesting_user.map(|u| u.0); + + let mut repository_backend = connection_state.repository_backend.lock().await; + let repositories = repository_backend + .repositories_for_user(requesting_user.as_ref(), &request.user) + .await; + + let repositories = match repositories { + Ok(repositories) => repositories, + Err(err) => { + error!("Error handling request: {:?}", err); + return Ok(()); + } + }; + drop(repository_backend); + + let mut user_backend = connection_state.user_backend.lock().await; + let user_exists = user_backend.exists(&request.user).await; + + if repositories.is_empty() && !matches!(user_exists, Ok(true)) { + return Err(UserError::InvalidUser(request.user)); + } + + let response: UserRepositoriesResponse = UserRepositoriesResponse { repositories }; + + connection_state.send(response).await?; + + Ok(()) +} + +async fn user_settings( + Message(request): Message, + State(connection_state): State, + AuthenticatedUser(requesting_user): AuthenticatedUser, +) -> Result<(), UserError> { + if request.user != requesting_user { + return Err(UserError::InvalidUser(request.user)); + } + + let mut user_backend = connection_state.user_backend.lock().await; + let mut settings = user_backend.settings(&request.user).await?; + + drop(user_backend); + + let response = UserSettingsResponse { + settings: settings.drain(..).collect(), + }; + + connection_state.send(response).await?; + + Ok(()) +} + +async fn write_user_settings( + Message(request): Message, + State(connection_state): State, + AuthenticatedUser(requesting_user): AuthenticatedUser, +) -> Result<(), UserError> { + if request.user != requesting_user { + return Err(UserError::InvalidUser(request.user)); + } + + let mut user_backend = connection_state.user_backend.lock().await; + user_backend + .write_settings(&request.user, &request.settings) + .await?; + + drop(user_backend); + + let response = UserWriteSettingsResponse {}; + + connection_state.send(response).await?; + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +pub enum UserError { + #[error("invalid user {0}")] + InvalidUser(User), + #[error("{0}")] + Other(#[from] Error), +} diff --git a/giterated-daemon/src/connection/wrapper.rs b/giterated-daemon/src/connection/wrapper.rs new file mode 100644 index 0000000..4954ee8 --- /dev/null +++ b/giterated-daemon/src/connection/wrapper.rs @@ -0,0 +1,162 @@ +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use anyhow::Error; +use futures_util::{SinkExt, StreamExt}; +use giterated_models::{messages::error::ConnectionError, model::instance::Instance}; +use rsa::RsaPublicKey; +use serde::Serialize; +use serde_json::Value; +use tokio::{ + net::TcpStream, + sync::{Mutex, RwLock}, +}; +use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; + +use crate::{ + authentication::AuthenticationTokenGranter, + backend::{RepositoryBackend, UserBackend}, + message::NetworkMessage, +}; + +use super::{ + authentication::authentication_handle, handshake::handshake_handle, + repository::repository_handle, user::user_handle, Connections, +}; + +pub async fn connection_wrapper( + socket: WebSocketStream, + connections: Arc>, + repository_backend: Arc>, + user_backend: Arc>, + auth_granter: Arc>, + addr: SocketAddr, + instance: impl ToOwned, +) { + let connection_state = ConnectionState { + socket: Arc::new(Mutex::new(socket)), + connections, + repository_backend, + user_backend, + auth_granter, + addr, + instance: instance.to_owned(), + handshaked: Arc::new(AtomicBool::new(false)), + cached_keys: Arc::default(), + }; + + let mut handshaked = false; + + loop { + let mut socket = connection_state.socket.lock().await; + let message = socket.next().await; + drop(socket); + + match message { + Some(Ok(message)) => { + let payload = match message { + Message::Binary(payload) => payload, + Message::Ping(_) => { + let mut socket = connection_state.socket.lock().await; + let _ = socket.send(Message::Pong(vec![])).await; + drop(socket); + continue; + } + Message::Close(_) => return, + _ => continue, + }; + + let message = NetworkMessage(payload.clone()); + + if !handshaked { + if handshake_handle(&message, &connection_state).await.is_ok() { + if connection_state.handshaked.load(Ordering::SeqCst) { + handshaked = true; + } + } + } else { + let raw = serde_json::from_slice::(&payload).unwrap(); + let message_type = raw.get("message_type").unwrap().as_str().unwrap(); + + match authentication_handle(message_type, &message, &connection_state).await { + Err(e) => { + let _ = connection_state.send(ConnectionError(e.to_string())).await; + } + Ok(true) => continue, + Ok(false) => {} + } + + match repository_handle(message_type, &message, &connection_state).await { + Err(e) => { + let _ = connection_state.send(ConnectionError(e.to_string())).await; + } + Ok(true) => continue, + Ok(false) => {} + } + + match user_handle(message_type, &message, &connection_state).await { + Err(e) => { + let _ = connection_state.send(ConnectionError(e.to_string())).await; + } + Ok(true) => continue, + Ok(false) => {} + } + + match authentication_handle(message_type, &message, &connection_state).await { + Err(e) => { + let _ = connection_state.send(ConnectionError(e.to_string())).await; + } + Ok(true) => continue, + Ok(false) => {} + } + + error!( + "Message completely unhandled: {}", + std::str::from_utf8(&payload).unwrap() + ); + } + } + Some(Err(e)) => { + error!("Closing connection for {:?} for {}", e, addr); + return; + } + _ => { + info!("Unhandled"); + continue; + } + } + } +} + +#[derive(Clone)] +pub struct ConnectionState { + socket: Arc>>, + pub connections: Arc>, + pub repository_backend: Arc>, + pub user_backend: Arc>, + pub auth_granter: Arc>, + pub addr: SocketAddr, + pub instance: Instance, + pub handshaked: Arc, + pub cached_keys: Arc>>, +} + +impl ConnectionState { + pub async fn send(&self, message: T) -> Result<(), Error> { + let payload = serde_json::to_string(&message)?; + info!("Sending payload: {}", &payload); + self.socket + .lock() + .await + .send(Message::Binary(payload.into_bytes())) + .await?; + + Ok(()) + } +} diff --git a/giterated-daemon/src/lib.rs b/giterated-daemon/src/lib.rs new file mode 100644 index 0000000..e70ce9f --- /dev/null +++ b/giterated-daemon/src/lib.rs @@ -0,0 +1,21 @@ +use std::str::FromStr; + +use semver::{Version, VersionReq}; + +pub mod authentication; +pub mod backend; +pub mod connection; +pub mod message; + +#[macro_use] +extern crate tracing; + +pub fn version() -> Version { + Version::from_str(env!("CARGO_PKG_VERSION")).unwrap() +} + +pub fn validate_version(other: &Version) -> bool { + let version_req = VersionReq::from_str("=0.0.6").unwrap(); + + version_req.matches(other) +} diff --git a/giterated-daemon/src/main.rs b/giterated-daemon/src/main.rs new file mode 100644 index 0000000..dfcb56f --- /dev/null +++ b/giterated-daemon/src/main.rs @@ -0,0 +1,128 @@ +use anyhow::Error; +use connection::{Connections, RawConnection}; +use giterated_daemon::{ + authentication::AuthenticationTokenGranter, + backend::{git::GitBackend, user::UserAuth, RepositoryBackend, UserBackend}, + connection::{self, wrapper::connection_wrapper}, +}; +use giterated_models::model::instance::Instance; +use sqlx::{postgres::PgConnectOptions, ConnectOptions, PgPool}; +use std::{net::SocketAddr, str::FromStr, sync::Arc}; +use tokio::{ + fs::File, + io::{AsyncRead, AsyncReadExt, AsyncWrite}, + net::{TcpListener, TcpStream}, + sync::Mutex, +}; +use tokio_tungstenite::{accept_async, WebSocketStream}; +use toml::Table; + +#[macro_use] +extern crate tracing; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt::init(); + let mut listener = TcpListener::bind("0.0.0.0:7270").await?; + let connections: Arc> = Arc::default(); + let config: Table = { + let mut file = File::open("Giterated.toml").await?; + let mut text = String::new(); + file.read_to_string(&mut text).await?; + text.parse()? + }; + let db_conn_options = PgConnectOptions::new() + .host(config["postgres"]["host"].as_str().unwrap()) + .port(config["postgres"]["port"].as_integer().unwrap() as u16) + .database(config["postgres"]["database"].as_str().unwrap()) + .username(config["postgres"]["user"].as_str().unwrap()) + .password(config["postgres"]["password"].as_str().unwrap()) + .log_statements(log::LevelFilter::Off); + let db_pool = PgPool::connect_with(db_conn_options).await?; + + debug!("Running database migrations..."); + sqlx::migrate!().run(&db_pool).await?; + info!("Connected"); + + let repository_backend: Arc> = + Arc::new(Mutex::new(GitBackend { + pg_pool: db_pool.clone(), + repository_folder: String::from( + config["giterated"]["backend"]["git"]["root"] + .as_str() + .unwrap(), + ), + instance: Instance::from_str("giterated.dev").unwrap(), + })); + + let token_granter = Arc::new(Mutex::new(AuthenticationTokenGranter { + config: config.clone(), + instance: Instance::from_str("giterated.dev").unwrap(), + })); + + let user_backend: Arc> = Arc::new(Mutex::new(UserAuth::new( + db_pool.clone(), + &Instance::from_str("giterated.dev").unwrap(), + token_granter.clone(), + ))); + + info!("Connected"); + + loop { + let stream = accept_stream(&mut listener).await; + info!("Connected"); + + let (stream, address) = match stream { + Ok(stream) => stream, + Err(err) => { + error!("Failed to accept connection. {:?}", err); + continue; + } + }; + + info!("Accepted connection from {}", address); + + let connection = accept_websocket_connection(stream).await; + + let connection = match connection { + Ok(connection) => connection, + Err(err) => { + error!( + "Failed to initiate Websocket connection from {}. {:?}", + address, err + ); + continue; + } + }; + + info!("Websocket connection established with {}", address); + + let connection = RawConnection { + task: tokio::spawn(connection_wrapper( + connection, + connections.clone(), + repository_backend.clone(), + user_backend.clone(), + token_granter.clone(), + address, + Instance::from_str("giterated.dev").unwrap(), + )), + }; + + connections.lock().await.connections.push(connection); + } +} + +async fn accept_stream(listener: &mut TcpListener) -> Result<(TcpStream, SocketAddr), Error> { + let stream = listener.accept().await?; + + Ok(stream) +} + +async fn accept_websocket_connection( + stream: S, +) -> Result, Error> { + let connection = accept_async(stream).await?; + + Ok(connection) +} diff --git a/giterated-daemon/src/message.rs b/giterated-daemon/src/message.rs new file mode 100644 index 0000000..8db301e --- /dev/null +++ b/giterated-daemon/src/message.rs @@ -0,0 +1,262 @@ +use std::{collections::HashMap, ops::Deref}; + +use anyhow::Error; +use futures_util::Future; +use giterated_models::model::{ + authenticated::{Authenticated, AuthenticationSource, UserTokenMetadata}, + instance::Instance, + user::User, +}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; +use rsa::{ + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, + pss::{Signature, VerifyingKey}, + sha2::Sha256, + signature::Verifier, + RsaPublicKey, +}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; + +use crate::connection::wrapper::ConnectionState; + +pub struct NetworkMessage(pub Vec); + +impl Deref for NetworkMessage { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct AuthenticatedUser(pub User); + +#[derive(Debug, thiserror::Error)] +pub enum UserAuthenticationError { + #[error("user authentication missing")] + Missing, + // #[error("{0}")] + // InstanceAuthentication(#[from] Error), + #[error("user token was invalid")] + InvalidToken, + #[error("an error has occured")] + Other(#[from] Error), +} + +pub struct AuthenticatedInstance(Instance); + +impl AuthenticatedInstance { + pub fn inner(&self) -> &Instance { + &self.0 + } +} + +#[async_trait::async_trait] +pub trait FromMessage: Sized + Send + Sync { + async fn from_message(message: &NetworkMessage, state: &S) -> Result; +} + +#[async_trait::async_trait] +impl FromMessage for AuthenticatedUser { + async fn from_message( + network_message: &NetworkMessage, + state: &ConnectionState, + ) -> Result { + let message: Authenticated> = + serde_json::from_slice(&network_message).map_err(|e| Error::from(e))?; + + let (auth_user, auth_token) = message + .source + .iter() + .filter_map(|auth| { + if let AuthenticationSource::User { user, token } = auth { + Some((user, token)) + } else { + None + } + }) + .next() + .ok_or_else(|| UserAuthenticationError::Missing)?; + + let authenticated_instance = + AuthenticatedInstance::from_message(network_message, state).await?; + + let public_key_raw = public_key(&auth_user.instance).await?; + let verification_key = DecodingKey::from_rsa_pem(public_key_raw.as_bytes()).unwrap(); + + let data: TokenData = decode( + auth_token.as_ref(), + &verification_key, + &Validation::new(Algorithm::RS256), + ) + .unwrap(); + + if data.claims.user != *auth_user + || data.claims.generated_for != *authenticated_instance.inner() + { + Err(Error::from(UserAuthenticationError::InvalidToken)) + } else { + Ok(AuthenticatedUser(data.claims.user)) + } + } +} + +#[async_trait::async_trait] +impl FromMessage for AuthenticatedInstance { + async fn from_message( + network_message: &NetworkMessage, + state: &ConnectionState, + ) -> Result { + let message: Authenticated = + serde_json::from_slice(&network_message).map_err(|e| Error::from(e))?; + + let (instance, signature) = message + .source + .iter() + .filter_map(|auth: &AuthenticationSource| { + if let AuthenticationSource::Instance { + instance, + signature, + } = auth + { + Some((instance, signature)) + } else { + None + } + }) + .next() + // TODO: Instance authentication error + .ok_or_else(|| UserAuthenticationError::Missing)?; + + let public_key = { + let cached_keys = state.cached_keys.read().await; + + if let Some(key) = cached_keys.get(&instance) { + key.clone() + } else { + drop(cached_keys); + let mut cached_keys = state.cached_keys.write().await; + let key = public_key(instance).await?; + let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); + cached_keys.insert(instance.clone(), public_key.clone()); + public_key + } + }; + + let verifying_key: VerifyingKey = VerifyingKey::new(public_key); + + let message_json = serde_json::to_vec(&message.message).unwrap(); + + verifying_key.verify( + &message_json, + &Signature::try_from(signature.as_ref()).unwrap(), + )?; + + Ok(AuthenticatedInstance(instance.clone())) + } +} + +#[async_trait::async_trait] +impl FromMessage for Option +where + T: FromMessage, + S: Send + Sync + 'static, +{ + async fn from_message(message: &NetworkMessage, state: &S) -> Result { + Ok(T::from_message(message, state).await.ok()) + } +} + +#[async_trait::async_trait] +pub trait MessageHandler { + async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result; +} +#[async_trait::async_trait] +impl MessageHandler<(T1,), S, R> for T +where + T: FnOnce(T1) -> F + Clone + Send + 'static, + F: Future> + Send, + T1: FromMessage + Send, + S: Send + Sync, + E: std::error::Error + Send + Sync + 'static, +{ + async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result { + let value = T1::from_message(message, state).await?; + self(value).await.map_err(|e| Error::from(e)) + } +} + +#[async_trait::async_trait] +impl MessageHandler<(T1, T2), S, R> for T +where + T: FnOnce(T1, T2) -> F + Clone + Send + 'static, + F: Future> + Send, + T1: FromMessage + Send, + T2: FromMessage + Send, + S: Send + Sync, + E: std::error::Error + Send + Sync + 'static, +{ + async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result { + let value = T1::from_message(message, state).await?; + let value_2 = T2::from_message(message, state).await?; + self(value, value_2).await.map_err(|e| Error::from(e)) + } +} + +#[async_trait::async_trait] +impl MessageHandler<(T1, T2, T3), S, R> for T +where + T: FnOnce(T1, T2, T3) -> F + Clone + Send + 'static, + F: Future> + Send, + T1: FromMessage + Send, + T2: FromMessage + Send, + T3: FromMessage + Send, + S: Send + Sync, + E: std::error::Error + Send + Sync + 'static, +{ + async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result { + let value = T1::from_message(message, state).await?; + let value_2 = T2::from_message(message, state).await?; + let value_3 = T3::from_message(message, state).await?; + + self(value, value_2, value_3) + .await + .map_err(|e| Error::from(e)) + } +} + +pub struct State(pub T); + +#[async_trait::async_trait] +impl FromMessage for State +where + T: Clone + Send + Sync, +{ + async fn from_message(_: &NetworkMessage, state: &T) -> Result { + Ok(Self(state.clone())) + } +} + +// Temp +#[async_trait::async_trait] +impl FromMessage for Message +where + T: DeserializeOwned + Send + Sync + Serialize, + S: Clone + Send + Sync, +{ + async fn from_message(message: &NetworkMessage, _: &S) -> Result { + Ok(Message(serde_json::from_slice(&message)?)) + } +} + +pub struct Message(pub T); + +async fn public_key(instance: &Instance) -> Result { + let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) + .await? + .text() + .await?; + + Ok(key) +} diff --git a/giterated-models/Cargo.toml b/giterated-models/Cargo.toml new file mode 100644 index 0000000..4aff76d --- /dev/null +++ b/giterated-models/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "giterated-models" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio-tungstenite = "*" +tokio = { version = "1.32.0", features = [ "full" ] } +tracing = "*" +futures-util = "*" +serde = { version = "1.0.188", features = [ "derive" ]} +serde_json = "1.0" +tracing-subscriber = "0.3" +base64 = "0.21.3" +jsonwebtoken = { version = "*", features = ["use_pem"]} +log = "*" +rand = "*" +rsa = {version = "0.9", features = ["sha2"]} +reqwest = "*" +argon2 = "*" +aes-gcm = "0.10.2" +semver = {version = "*", features = ["serde"]} +tower = "*" + +toml = { version = "0.7" } + +chrono = { version = "0.4", features = [ "serde" ] } +async-trait = "0.1" + +# Git backend +git2 = "0.17" +thiserror = "1" +anyhow = "1" +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } + +#uuid = { version = "1.4", features = [ "v4", "serde" ] } diff --git a/giterated-models/src/lib.rs b/giterated-models/src/lib.rs new file mode 100644 index 0000000..69c9adf --- /dev/null +++ b/giterated-models/src/lib.rs @@ -0,0 +1,2 @@ +pub mod messages; +pub mod model; diff --git a/giterated-models/src/messages/authentication.rs b/giterated-models/src/messages/authentication.rs new file mode 100644 index 0000000..c714771 --- /dev/null +++ b/giterated-models/src/messages/authentication.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use crate::model::authenticated::UserAuthenticationToken; + +/// An account registration request. +/// +/// # Authentication +/// - Instance Authentication +/// - **ONLY ACCEPTED WHEN SAME-INSTANCE** +#[derive(Clone, Serialize, Deserialize)] +pub struct RegisterAccountRequest { + pub username: String, + pub email: Option, + pub password: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegisterAccountResponse { + pub token: String, +} + +/// An authentication token request. +/// +/// AKA Login Request +/// +/// # Authentication +/// - Instance Authentication +/// - Identifies the Instance to issue the token for +/// # Authorization +/// - Credentials ([`crate::backend::AuthBackend`]-based) +/// - Identifies the User account to issue a token for +/// - Decrypts user private key to issue to +#[derive(Clone, Serialize, Deserialize)] +pub struct AuthenticationTokenRequest { + pub username: String, + pub password: String, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct AuthenticationTokenResponse { + pub token: UserAuthenticationToken, +} + +/// An authentication token extension request. +/// +/// # Authentication +/// - Instance Authentication +/// - Identifies the Instance to issue the token for +/// - User Authentication +/// - Authenticates the validity of the token +/// # Authorization +/// - Token-based +/// - Validates authorization using token's authenticity +#[derive(Clone, Serialize, Deserialize)] +pub struct TokenExtensionRequest { + pub token: UserAuthenticationToken, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct TokenExtensionResponse { + pub new_token: Option, +} diff --git a/giterated-models/src/messages/discovery.rs b/giterated-models/src/messages/discovery.rs new file mode 100644 index 0000000..3b4387d --- /dev/null +++ b/giterated-models/src/messages/discovery.rs @@ -0,0 +1,21 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::model::discovery::DiscoveryItem; + +#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct DiscoveryOffer { + pub earliest: DateTime, + pub hashes: Vec, +} + +#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct DiscoveryRequest { + pub since: DateTime, + pub hashes: Vec, +} + +#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct Discoveries { + pub discoveries: Vec, +} diff --git a/giterated-models/src/messages/error.rs b/giterated-models/src/messages/error.rs new file mode 100644 index 0000000..4dcbc82 --- /dev/null +++ b/giterated-models/src/messages/error.rs @@ -0,0 +1,5 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, thiserror::Error)] +#[error("error from connection: {0}")] +pub struct ConnectionError(pub String); diff --git a/giterated-models/src/messages/handshake.rs b/giterated-models/src/messages/handshake.rs new file mode 100644 index 0000000..e62f3ad --- /dev/null +++ b/giterated-models/src/messages/handshake.rs @@ -0,0 +1,22 @@ +use semver::Version; +use serde::{Deserialize, Serialize}; + +use crate::model::instance::Instance; + +/// Sent by the initiator of a new inter-daemon connection. +#[derive(Clone, Serialize, Deserialize)] +pub struct InitiateHandshake { + pub version: Version, +} + +/// Sent in response to [`InitiateHandshake`] +#[derive(Clone, Serialize, Deserialize)] +pub struct HandshakeResponse { + pub identity: Instance, + pub version: Version, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct HandshakeFinalize { + pub success: bool, +} diff --git a/giterated-models/src/messages/issues.rs b/giterated-models/src/messages/issues.rs new file mode 100644 index 0000000..b4a9c3a --- /dev/null +++ b/giterated-models/src/messages/issues.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +use crate::model::repository::Repository; + +#[derive(Clone)] +pub struct IssuesCountCommand { + pub respository: Repository, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct IssuesCountResponse { + pub count: u64, +} + +#[derive(Clone)] +pub struct IssuesLabelsCommand { + pub repository: Repository, +} diff --git a/giterated-models/src/messages/mod.rs b/giterated-models/src/messages/mod.rs new file mode 100644 index 0000000..bc9f449 --- /dev/null +++ b/giterated-models/src/messages/mod.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +use crate::model::user::User; + +pub mod authentication; +pub mod discovery; +pub mod error; +pub mod handshake; +pub mod issues; +pub mod repository; +pub mod user; + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum ErrorMessage { + #[error("user {0} doesn't exist or isn't valid in this context")] + InvalidUser(User), + #[error("internal error: shutdown")] + Shutdown, +} diff --git a/giterated-models/src/messages/repository.rs b/giterated-models/src/messages/repository.rs new file mode 100644 index 0000000..3826c84 --- /dev/null +++ b/giterated-models/src/messages/repository.rs @@ -0,0 +1,146 @@ +use serde::{Deserialize, Serialize}; + +use crate::model::repository::RepositoryVisibility; +use crate::model::{ + repository::{Commit, Repository, RepositoryTreeEntry}, + user::User, +}; + +/// A request to create a repository. +/// +/// # Authentication +/// - Instance Authentication +/// - Used to validate User token `issued_for` +/// - User Authentication +/// - Used to source owning user +/// - Used to authorize user token against user's instance +/// # Authorization +/// - Instance Authorization +/// - Used to authorize action using User token requiring a correct `issued_for` and valid issuance from user's instance +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryCreateRequest { + pub name: String, + pub description: Option, + pub visibility: RepositoryVisibility, + pub default_branch: String, + pub owner: User, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryCreateResponse; + +/// A request to inspect the tree of a repository. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryFileInspectRequest { + pub path: RepositoryTreeEntry, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum RepositoryFileInspectionResponse { + File { + commit_metadata: Commit, + }, + Folder { + commit_metadata: Commit, + members: Vec, + }, + Invalid { + path: RepositoryTreeEntry, + }, +} + +/// A request to get a repository's information. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssuesCountRequest; + +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssuesCountResponse { + pub count: u64, +} + +/// A request to get a repository's issues count. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssueLabelsRequest; + +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssueLabelsResponse { + pub labels: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct IssueLabel { + pub name: String, + pub color: String, +} + +/// A request to get a repository's issue labels. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssuesRequest; + +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssuesResponse { + pub issues: Vec, +} + +/// A request to get a repository's issues. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryIssue { + pub author: User, + pub id: u64, + pub title: String, + pub contents: String, + pub labels: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct RepositoryInfoRequest { + pub repository: Repository, + /// Whether to fetch extra metadata like the last commit made to file or size + pub extra_metadata: bool, + /// Rev (branch) being requested + pub rev: Option, + /// Tree path being requested + pub path: Option, +} diff --git a/giterated-models/src/messages/user.rs b/giterated-models/src/messages/user.rs new file mode 100644 index 0000000..291368c --- /dev/null +++ b/giterated-models/src/messages/user.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::model::{repository::RepositorySummary, user::User}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserDisplayNameRequest { + pub user: User, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserDisplayNameResponse { + pub display_name: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserDisplayImageRequest { + pub user: User, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserDisplayImageResponse { + pub image_url: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserBioRequest { + pub user: User, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserBioResponse { + pub bio: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserRepositoriesRequest { + pub user: User, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserRepositoriesResponse { + pub repositories: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserSettingsRequest { + pub user: User, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserSettingsResponse { + pub settings: HashMap, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserWriteSettingsRequest { + pub user: User, + pub settings: Vec<(String, serde_json::Value)>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserWriteSettingsResponse { + // IDK? +} diff --git a/giterated-models/src/model/authenticated.rs b/giterated-models/src/model/authenticated.rs new file mode 100644 index 0000000..858f8f0 --- /dev/null +++ b/giterated-models/src/model/authenticated.rs @@ -0,0 +1,187 @@ +use std::any::type_name; + +use rsa::{ + pkcs1::DecodeRsaPrivateKey, + pss::SigningKey, + sha2::Sha256, + signature::{RandomizedSigner, SignatureEncoding}, + RsaPrivateKey, +}; +use serde::{Deserialize, Serialize}; + +use super::{instance::Instance, user::User}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserTokenMetadata { + pub user: User, + pub generated_for: Instance, + pub exp: u64, +} + +#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct Authenticated { + pub target_instance: Option, + pub source: Vec, + pub message_type: String, + #[serde(flatten)] + pub message: T, +} + +pub trait AuthenticationSourceProvider: Sized { + fn authenticate(self, payload: &Vec) -> AuthenticationSource; +} + +pub trait AuthenticationSourceProviders: Sized { + fn authenticate_all(self, payload: &Vec) -> Vec; +} + +impl AuthenticationSourceProviders for A +where + A: AuthenticationSourceProvider, +{ + fn authenticate_all(self, payload: &Vec) -> Vec { + vec![self.authenticate(payload)] + } +} + +impl AuthenticationSourceProviders for (A, B) +where + A: AuthenticationSourceProvider, + B: AuthenticationSourceProvider, +{ + fn authenticate_all(self, payload: &Vec) -> Vec { + let (first, second) = self; + + vec![first.authenticate(payload), second.authenticate(payload)] + } +} + +impl Authenticated { + pub fn new(message: T, auth_sources: impl AuthenticationSourceProvider) -> Self { + let message_payload = serde_json::to_vec(&message).unwrap(); + + let authentication = auth_sources.authenticate_all(&message_payload); + + Self { + source: authentication, + message_type: type_name::().to_string(), + message, + target_instance: None, + } + } + + pub fn new_for( + instance: impl ToOwned, + message: T, + auth_sources: impl AuthenticationSourceProvider, + ) -> Self { + let message_payload = serde_json::to_vec(&message).unwrap(); + + let authentication = auth_sources.authenticate_all(&message_payload); + + Self { + source: authentication, + message_type: type_name::().to_string(), + message, + target_instance: Some(instance.to_owned()), + } + } + + pub fn new_empty(message: T) -> Self { + Self { + source: vec![], + message_type: type_name::().to_string(), + message, + target_instance: None, + } + } + + pub fn append_authentication(&mut self, authentication: impl AuthenticationSourceProvider) { + let message_payload = serde_json::to_vec(&self.message).unwrap(); + + self.source + .push(authentication.authenticate(&message_payload)); + } +} + +mod verified {} + +#[derive(Clone, Debug)] +pub struct UserAuthenticator { + pub user: User, + pub token: UserAuthenticationToken, +} + +impl AuthenticationSourceProvider for UserAuthenticator { + fn authenticate(self, _payload: &Vec) -> AuthenticationSource { + AuthenticationSource::User { + user: self.user, + token: self.token, + } + } +} + +#[derive(Clone)] +pub struct InstanceAuthenticator<'a> { + pub instance: Instance, + pub private_key: &'a str, +} + +impl AuthenticationSourceProvider for InstanceAuthenticator<'_> { + fn authenticate(self, payload: &Vec) -> AuthenticationSource { + let mut rng = rand::thread_rng(); + + let private_key = RsaPrivateKey::from_pkcs1_pem(self.private_key).unwrap(); + let signing_key = SigningKey::::new(private_key); + let signature = signing_key.sign_with_rng(&mut rng, &payload); + + AuthenticationSource::Instance { + instance: self.instance, + // TODO: Actually parse signature from private key + signature: InstanceSignature(signature.to_bytes().into_vec()), + } + } +} + +#[repr(transparent)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserAuthenticationToken(String); + +impl From for UserAuthenticationToken { + fn from(value: String) -> Self { + Self(value) + } +} + +impl ToString for UserAuthenticationToken { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsRef for UserAuthenticationToken { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstanceSignature(Vec); + +impl AsRef<[u8]> for InstanceSignature { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuthenticationSource { + User { + user: User, + token: UserAuthenticationToken, + }, + Instance { + instance: Instance, + signature: InstanceSignature, + }, +} diff --git a/giterated-models/src/model/discovery.rs b/giterated-models/src/model/discovery.rs new file mode 100644 index 0000000..c49db64 --- /dev/null +++ b/giterated-models/src/model/discovery.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::model::{instance::Instance, repository::Repository}; + +#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub enum DiscoveryItem { + Instance { + instance: Instance, + signature: Vec, + }, + Repository { + repository: Repository, + signature: Vec, + }, +} diff --git a/giterated-models/src/model/instance.rs b/giterated-models/src/model/instance.rs new file mode 100644 index 0000000..45e0b20 --- /dev/null +++ b/giterated-models/src/model/instance.rs @@ -0,0 +1,51 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +pub struct InstanceMeta { + pub url: String, + pub public_key: String, +} + +/// An instance, defined by the URL it can be reached at. +/// +/// # Textual Format +/// An instance's textual format is its URL. +/// +/// ## Examples +/// For the instance `giterated.dev`, the following [`Instance`] initialization +/// would be valid: +/// +/// ``` +/// let instance = Instance { +/// url: String::from("giterated.dev") +/// }; +/// +/// // This is correct +/// assert_eq!(Instance::from_str("giterated.dev").unwrap(), instance); +/// ``` +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct Instance { + pub url: String, +} + +impl ToString for Instance { + fn to_string(&self) -> String { + self.url.clone() + } +} + +impl FromStr for Instance { + type Err = InstanceParseError; + + fn from_str(s: &str) -> Result { + Ok(Self { url: s.to_string() }) + } +} + +#[derive(Debug, Error)] +pub enum InstanceParseError { + #[error("invalid format")] + InvalidFormat, +} diff --git a/giterated-models/src/model/mod.rs b/giterated-models/src/model/mod.rs new file mode 100644 index 0000000..e09885d --- /dev/null +++ b/giterated-models/src/model/mod.rs @@ -0,0 +1,11 @@ +//! # Giterated Network Data Model Types +//! +//! All network data model types that are not directly associated with +//! individual requests or responses. + +pub mod authenticated; +pub mod discovery; +pub mod instance; +pub mod repository; +pub mod settings; +pub mod user; diff --git a/giterated-models/src/model/repository.rs b/giterated-models/src/model/repository.rs new file mode 100644 index 0000000..6655aa4 --- /dev/null +++ b/giterated-models/src/model/repository.rs @@ -0,0 +1,197 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use super::{instance::Instance, user::User}; + +/// A repository, defined by the instance it exists on along with +/// its owner and name. +/// +/// # Textual Format +/// A repository's textual reference is defined as: +/// +/// `{owner: User}/{name: String}@{instance: Instance}` +/// +/// # Examples +/// For the repository named `foo` owned by `barson:giterated.dev` on the instance +/// `giterated.dev`, the following [`Repository`] initialization would +/// be valid: +/// +/// ``` +/// let repository = Repository { +/// owner: User::from_str("barson:giterated.dev").unwrap(), +/// name: String::from("foo"), +/// instance: Instance::from_str("giterated.dev").unwrap() +/// }; +/// +/// // This is correct +/// assert_eq!(Repository::from_str("barson:giterated.dev/foo@giterated.dev").unwrap(), repository); +/// ``` +#[derive(Hash, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Repository { + pub owner: User, + pub name: String, + /// Instance the repository is on + pub instance: Instance, +} + +impl ToString for Repository { + fn to_string(&self) -> String { + format!("{}/{}@{}", self.owner, self.name, self.instance.to_string()) + } +} + +impl FromStr for Repository { + type Err = (); + + fn from_str(s: &str) -> Result { + let mut by_ampersand = s.split('@'); + let mut path_split = by_ampersand.next().unwrap().split('/'); + + let instance = Instance::from_str(by_ampersand.next().unwrap()).unwrap(); + let owner = User::from_str(path_split.next().unwrap()).unwrap(); + let name = path_split.next().unwrap().to_string(); + + Ok(Self { + instance, + owner, + name, + }) + } +} + +/// Visibility of the repository to the general eye +#[derive(PartialEq, Eq, Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] +#[sqlx(type_name = "visibility", rename_all = "lowercase")] +pub enum RepositoryVisibility { + Public, + Unlisted, + Private, +} + +/// Implements [`Display`] for [`RepositoryVisiblity`] using [`Debug`] +impl Display for RepositoryVisibility { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryView { + /// Name of the repository + /// + /// This is different than the [`Repository`] name, + /// which may be a path. + pub name: String, + /// Owner of the Repository + pub owner: User, + /// Repository description + pub description: Option, + /// Repository visibility + pub visibility: RepositoryVisibility, + /// Default branch of the repository + pub default_branch: String, + /// Last commit made to the repository + pub latest_commit: Option, + /// Revision of the displayed tree + pub tree_rev: Option, + /// Repository tree + pub tree: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RepositoryObjectType { + Tree, + Blob, +} + +/// Stored info for our tree entries +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RepositoryTreeEntry { + /// Name of the tree/blob + pub name: String, + /// Type of the tree entry + pub object_type: RepositoryObjectType, + /// Git supplies us with the mode at all times, and people like it displayed. + pub mode: i32, + /// File size + pub size: Option, + /// Last commit made to the tree/blob + pub last_commit: Option, +} + +impl RepositoryTreeEntry { + // I love you Emilia <3 + pub fn new(name: &str, object_type: RepositoryObjectType, mode: i32) -> Self { + Self { + name: name.to_string(), + object_type, + mode, + size: None, + last_commit: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RepositoryTreeEntryWithCommit { + pub tree_entry: RepositoryTreeEntry, + pub commit: Commit, +} + +/// Info about a git commit +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct Commit { + /// Unique commit ID + pub oid: String, + /// Full commit message + pub message: Option, + /// Who created the commit + pub author: CommitSignature, + /// Who committed the commit + pub committer: CommitSignature, + /// Time when the commit happened + pub time: chrono::NaiveDateTime, +} + +/// Gets all info from [`git2::Commit`] for easy use +impl From> for Commit { + fn from(commit: git2::Commit<'_>) -> Self { + Self { + oid: commit.id().to_string(), + message: commit.message().map(|message| message.to_string()), + author: commit.author().into(), + committer: commit.committer().into(), + time: chrono::NaiveDateTime::from_timestamp_opt(commit.time().seconds(), 0).unwrap(), + } + } +} + +/// Git commit signature +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct CommitSignature { + pub name: Option, + pub email: Option, + pub time: chrono::NaiveDateTime, +} + +/// Converts the signature from git2 into something usable without explicit lifetimes. +impl From> for CommitSignature { + fn from(signature: git2::Signature<'_>) -> Self { + Self { + name: signature.name().map(|name| name.to_string()), + email: signature.email().map(|email| email.to_string()), + time: chrono::NaiveDateTime::from_timestamp_opt(signature.when().seconds(), 0).unwrap(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RepositorySummary { + pub repository: Repository, + pub owner: User, + pub visibility: RepositoryVisibility, + pub description: Option, + pub last_commit: Option, +} diff --git a/giterated-models/src/model/settings.rs b/giterated-models/src/model/settings.rs new file mode 100644 index 0000000..a3ea173 --- /dev/null +++ b/giterated-models/src/model/settings.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +pub trait Setting: Serialize { + fn name(&self) -> &'static str; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserBio(pub String); + +impl Setting for UserBio { + fn name(&self) -> &'static str { + "Bio" + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserDisplayName(pub String); + +impl Setting for UserDisplayName { + fn name(&self) -> &'static str { + "Display Name" + } +} diff --git a/giterated-models/src/model/user.rs b/giterated-models/src/model/user.rs new file mode 100644 index 0000000..5fa8ead --- /dev/null +++ b/giterated-models/src/model/user.rs @@ -0,0 +1,56 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use super::instance::Instance; + +/// A user, defined by its username and instance. +/// +/// # Textual Format +/// A user's textual reference is defined as: +/// +/// `{username: String}:{instance: Instance}` +/// +/// # Examples +/// For the user with the username `barson` and the instance `giterated.dev`, +/// the following [`User`] initialization would be valid: +/// +/// ``` +/// let user = User { +/// username: String::from("barson"), +/// instance: Instance::from_str("giterated.dev").unwrap() +/// }; +/// +/// // This is correct +/// assert_eq!(User::from_str("barson:giterated.dev").unwrap(), user); +/// ``` +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct User { + pub username: String, + pub instance: Instance, +} + +impl Display for User { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.username, self.instance.url) + } +} + +impl From for User { + fn from(user_string: String) -> Self { + User::from_str(&user_string).unwrap() + } +} + +impl FromStr for User { + type Err = (); + + fn from_str(s: &str) -> Result { + let mut colon_split = s.split(':'); + let username = colon_split.next().unwrap().to_string(); + let instance = Instance::from_str(colon_split.next().unwrap()).unwrap(); + + Ok(Self { username, instance }) + } +} diff --git a/migrations/20230828083716_create_repositories.sql b/migrations/20230828083716_create_repositories.sql deleted file mode 100644 index 698612d..0000000 --- a/migrations/20230828083716_create_repositories.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TYPE visibility AS ENUM -( - 'public', - 'unlisted', - 'private' -); - -CREATE TABLE IF NOT EXISTS repositories -( - username TEXT NOT NULL, - instance_url TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - visibility visibility NOT NULL, - default_branch TEXT NOT NULL -); - -CREATE UNIQUE INDEX unique_name_per_user ON repositories (username, instance_url, name); diff --git a/migrations/20230829014608_combine_user_instance_url_repositories.sql b/migrations/20230829014608_combine_user_instance_url_repositories.sql deleted file mode 100644 index 5af2ef7..0000000 --- a/migrations/20230829014608_combine_user_instance_url_repositories.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE repositories -DROP COLUMN instance_url; - -ALTER TABLE repositories -RENAME COLUMN username TO owner_user; diff --git a/migrations/20230829104151_create_users.sql b/migrations/20230829104151_create_users.sql deleted file mode 100644 index 31eecf8..0000000 --- a/migrations/20230829104151_create_users.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Add migration script here - -CREATE TABLE IF NOT EXISTS users -( - username TEXT NOT NULL, - display_name TEXT, - image_url TEXT NOT NULL, - bio TEXT, - email TEXT, - password TEXT NOT NULL, - public_key TEXT NOT NULL, - enc_private_key TEXT NOT NULL -); - -CREATE UNIQUE INDEX unique_username ON users (username); \ No newline at end of file diff --git a/migrations/20230829165636_discovery.sql b/migrations/20230829165636_discovery.sql deleted file mode 100644 index 2c6b546..0000000 --- a/migrations/20230829165636_discovery.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TYPE discovery_type AS ENUM -( - 'instance', - 'repository' -); - -CREATE TABLE IF NOT EXISTS discoveries -( - discovery_hash TEXT PRIMARY KEY UNIQUE NOT NULL, - discovery_time TEXT NOT NULL, - discovery_type discovery_type NOT NULL, - discovery TEXT NOT NULL -); \ No newline at end of file diff --git a/migrations/20230902095036_user_settings.sql b/migrations/20230902095036_user_settings.sql deleted file mode 100644 index f83f20a..0000000 --- a/migrations/20230902095036_user_settings.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS user_settings -( - username TEXT NOT NULL, - name TEXT NOT NULL, - value TEXT NOT NULL -); - -CREATE UNIQUE INDEX unique_per_name ON user_settings (username, name); \ No newline at end of file diff --git a/src/authentication.rs b/src/authentication.rs deleted file mode 100644 index f0f682c..0000000 --- a/src/authentication.rs +++ /dev/null @@ -1,183 +0,0 @@ -use anyhow::Error; -use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, TokenData, Validation}; -use serde::{Deserialize, Serialize}; -use std::time::SystemTime; -use tokio::{fs::File, io::AsyncReadExt}; -use toml::Table; - -use crate::{ - messages::authentication::{AuthenticationTokenResponse, TokenExtensionResponse}, - model::{authenticated::UserAuthenticationToken, instance::Instance, user::User}, -}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserTokenMetadata { - pub user: User, - pub generated_for: Instance, - exp: u64, -} - -pub struct AuthenticationTokenGranter { - pub config: Table, - pub instance: Instance, -} - -impl AuthenticationTokenGranter { - async fn private_key(&self) -> Vec { - let _secret_key = self.config["authentication"]["secret_key"] - .as_str() - .unwrap(); - let mut file = File::open( - self.config["giterated"]["keys"]["private"] - .as_str() - .unwrap(), - ) - .await - .unwrap(); - - let mut key = vec![]; - file.read_to_end(&mut key).await.unwrap(); - - key - } - - pub(crate) async fn create_token_for( - &mut self, - user: &User, - generated_for: &Instance, - ) -> String { - let private_key = self.private_key().await; - - let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); - - let claims = UserTokenMetadata { - user: user.clone(), - generated_for: generated_for.clone(), - exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() - + std::time::Duration::from_secs(24 * 60 * 60)) - .as_secs(), - }; - - encode( - &jsonwebtoken::Header::new(Algorithm::RS256), - &claims, - &encoding_key, - ) - .unwrap() - } - - pub async fn token_request( - &mut self, - issued_for: impl ToOwned, - username: String, - _password: String, - ) -> Result { - let private_key = { - let mut file = File::open( - self.config["giterated"]["keys"]["private"] - .as_str() - .unwrap(), - ) - .await - .unwrap(); - - let mut key = vec![]; - file.read_to_end(&mut key).await.unwrap(); - - key - }; - - let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); - - let claims = UserTokenMetadata { - user: User { - username, - instance: self.instance.clone(), - }, - generated_for: issued_for.to_owned(), - exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() - + std::time::Duration::from_secs(24 * 60 * 60)) - .as_secs(), - }; - - let token = encode( - &jsonwebtoken::Header::new(Algorithm::RS256), - &claims, - &encoding_key, - ) - .unwrap(); - - Ok(AuthenticationTokenResponse { - token: UserAuthenticationToken::from(token), - }) - } - - pub async fn extension_request( - &mut self, - issued_for: &Instance, - token: UserAuthenticationToken, - ) -> Result { - let server_public_key = public_key(&self.instance).await.unwrap(); - - let verification_key = DecodingKey::from_rsa_pem(server_public_key.as_bytes()).unwrap(); - - let data: TokenData = decode( - token.as_ref(), - &verification_key, - &Validation::new(Algorithm::RS256), - ) - .unwrap(); - - if data.claims.generated_for != *issued_for { - panic!() - } - - info!("Token Extension Request Token validated"); - - let private_key = { - let mut file = File::open( - self.config["giterated"]["keys"]["private"] - .as_str() - .unwrap(), - ) - .await - .unwrap(); - - let mut key = vec![]; - file.read_to_end(&mut key).await.unwrap(); - - key - }; - - let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); - - let claims = UserTokenMetadata { - // TODO: Probably exploitable - user: data.claims.user, - generated_for: issued_for.clone(), - exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() - + std::time::Duration::from_secs(24 * 60 * 60)) - .as_secs(), - }; - - let token = encode( - &jsonwebtoken::Header::new(Algorithm::RS256), - &claims, - &encoding_key, - ) - .unwrap(); - - Ok(TokenExtensionResponse { - new_token: Some(token), - }) - } -} - -async fn public_key(instance: &Instance) -> Result { - let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) - .await? - .text() - .await?; - - Ok(key) -} diff --git a/src/backend/discovery.rs b/src/backend/discovery.rs deleted file mode 100644 index a4d9a7a..0000000 --- a/src/backend/discovery.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::hash::Hash; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; - -pub struct GiteratedDiscoveryProtocol { - pub pool: PgPool, -} - -#[derive(Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] -#[sqlx(type_name = "discovery_type", rename_all = "lowercase")] -pub enum DiscoveryType { - Instance, - Repository, -} - -#[derive(Debug, sqlx::FromRow, sqlx::Type)] -pub struct DiscoveriesRow { - discovery_hash: String, - discovery_time: DateTime, - discovery_type: DiscoveryType, - discovery: String, -} diff --git a/src/backend/git.rs b/src/backend/git.rs deleted file mode 100644 index a3789da..0000000 --- a/src/backend/git.rs +++ /dev/null @@ -1,505 +0,0 @@ -use anyhow::Error; -use async_trait::async_trait; -use futures_util::StreamExt; -use git2::ObjectType; -use sqlx::{Either, PgPool}; -use std::path::{Path, PathBuf}; -use thiserror::Error; - -use crate::model::instance::Instance; -use crate::model::repository::{ - Commit, Repository, RepositoryObjectType, RepositorySummary, RepositoryTreeEntry, - RepositoryVisibility, -}; -use crate::model::user::User; -use crate::{ - messages::repository::{ - RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, - RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, - RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse, - RepositoryIssuesRequest, RepositoryIssuesResponse, - }, - model::repository::RepositoryView, -}; - -use super::{IssuesBackend, RepositoryBackend}; - -// TODO: Handle this -//region database structures - -/// Repository in the database -#[derive(Debug, sqlx::FromRow)] -pub struct GitRepository { - #[sqlx(try_from = "String")] - pub owner_user: User, - pub name: String, - pub description: Option, - pub visibility: RepositoryVisibility, - pub default_branch: String, -} - -impl GitRepository { - // Separate function because "Private" will be expanded later - /// Checks if the user is allowed to view this repository - pub fn can_user_view_repository(&self, user: Option<&User>) -> bool { - !matches!(self.visibility, RepositoryVisibility::Private) - || (matches!(self.visibility, RepositoryVisibility::Private) - && Some(&self.owner_user) == user) - } - - // This is in it's own function because I assume I'll have to add logic to this later - pub fn open_git2_repository( - &self, - repository_directory: &str, - ) -> Result { - match git2::Repository::open(format!( - "{}/{}/{}/{}", - repository_directory, self.owner_user.instance.url, self.owner_user.username, self.name - )) { - Ok(repository) => Ok(repository), - Err(err) => { - let err = GitBackendError::FailedOpeningFromDisk(err); - error!("Couldn't open a repository, this is bad! {:?}", err); - - Err(err) - } - } - } -} - -//endregion - -#[derive(Error, Debug)] -pub enum GitBackendError { - #[error("Failed creating repository")] - FailedCreatingRepository(git2::Error), - #[error("Failed inserting into the database")] - FailedInsertingIntoDatabase(sqlx::Error), - #[error("Failed finding repository {owner_user:?}/{name:?}")] - RepositoryNotFound { owner_user: String, name: String }, - #[error("Repository {owner_user:?}/{name:?} already exists")] - RepositoryAlreadyExists { owner_user: String, name: String }, - #[error("Repository couldn't be deleted from the disk")] - CouldNotDeleteFromDisk(std::io::Error), - #[error("Failed deleting repository from database")] - FailedDeletingFromDatabase(sqlx::Error), - #[error("Failed opening repository on disk")] - FailedOpeningFromDisk(git2::Error), - #[error("Couldn't find ref with name `{0}`")] - RefNotFound(String), - #[error("Couldn't find path in repository `{0}`")] - PathNotFound(String), - #[error("Couldn't find commit for path `{0}`")] - LastCommitNotFound(String), -} - -pub struct GitBackend { - pub pg_pool: PgPool, - pub repository_folder: String, - pub instance: Instance, -} - -impl GitBackend { - pub fn new( - pg_pool: &PgPool, - repository_folder: &str, - instance: impl ToOwned, - ) -> Self { - Self { - pg_pool: pg_pool.clone(), - repository_folder: repository_folder.to_string(), - instance: instance.to_owned(), - } - } - - pub async fn find_by_owner_user_name( - &self, - user: &User, - repository_name: &str, - ) -> Result { - if let Ok(repository) = sqlx::query_as!(GitRepository, - r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#, - user.to_string(), repository_name) - .fetch_one(&self.pg_pool.clone()) - .await { - Ok(repository) - } else { - Err(GitBackendError::RepositoryNotFound { - owner_user: user.to_string(), - name: repository_name.to_string(), - }) - } - } - - pub async fn delete_by_owner_user_name( - &self, - user: &User, - repository_name: &str, - ) -> Result { - if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!( - "{}/{}/{}/{}", - self.repository_folder, user.instance.url, user.username, repository_name - ))) { - let err = GitBackendError::CouldNotDeleteFromDisk(err); - error!( - "Couldn't delete repository from disk, this is bad! {:?}", - err - ); - - return Err(err); - } - - // Delete the repository from the database - match sqlx::query!( - "DELETE FROM repositories WHERE owner_user = $1 AND name = $2", - user.to_string(), - repository_name - ) - .execute(&self.pg_pool.clone()) - .await - { - Ok(deleted) => Ok(deleted.rows_affected()), - Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)), - } - } - - // TODO: Find where this fits - // TODO: Cache this and general repository tree and invalidate select files on push - // TODO: Find better and faster technique for this - pub fn get_last_commit_of_file( - path: &str, - git: &git2::Repository, - start_commit: &git2::Commit, - ) -> anyhow::Result { - let mut revwalk = git.revwalk()?; - revwalk.set_sorting(git2::Sort::TIME)?; - revwalk.push(start_commit.id())?; - - for oid in revwalk { - let oid = oid?; - let commit = git.find_commit(oid)?; - - // Merge commits have 2 or more parents - // Commits with 0 parents are handled different because we can't diff against them - if commit.parent_count() == 0 { - return Ok(commit.into()); - } else if commit.parent_count() == 1 { - let tree = commit.tree()?; - let last_tree = commit.parent(0)?.tree()?; - - // Get the diff between the current tree and the last one - let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?; - - for dd in diff.deltas() { - // Get the path of the current file we're diffing against - let current_path = dd.new_file().path().unwrap(); - - // Path or directory - if current_path.eq(Path::new(&path)) || current_path.starts_with(path) { - return Ok(commit.into()); - } - } - } - } - - Err(GitBackendError::LastCommitNotFound(path.to_string()))? - } -} - -#[async_trait] -impl RepositoryBackend for GitBackend { - async fn create_repository( - &mut self, - _user: &User, - request: &RepositoryCreateRequest, - ) -> Result { - // Check if repository already exists in the database - if let Ok(repository) = self - .find_by_owner_user_name(&request.owner, &request.name) - .await - { - let err = GitBackendError::RepositoryAlreadyExists { - owner_user: repository.owner_user.to_string(), - name: repository.name, - }; - error!("{:?}", err); - - return Err(err); - } - - // Insert the repository into the database - let _ = match sqlx::query_as!(GitRepository, - r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#, - request.owner.to_string(), request.name, request.description, request.visibility as _, "master") - .fetch_one(&self.pg_pool.clone()) - .await { - Ok(repository) => repository, - Err(err) => { - let err = GitBackendError::FailedInsertingIntoDatabase(err); - error!("Failed inserting into the database! {:?}", err); - - return Err(err); - } - }; - - // Create bare (server side) repository on disk - match git2::Repository::init_bare(PathBuf::from(format!( - "{}/{}/{}/{}", - self.repository_folder, - request.owner.instance.url, - request.owner.username, - request.name - ))) { - Ok(_) => { - debug!( - "Created new repository with the name {}/{}/{}", - request.owner.instance.url, request.owner.username, request.name - ); - Ok(RepositoryCreateResponse) - } - Err(err) => { - let err = GitBackendError::FailedCreatingRepository(err); - error!("Failed creating repository on disk!? {:?}", err); - - // Delete repository from database - self.delete_by_owner_user_name(&request.owner, request.name.as_str()) - .await?; - - // ??? - Err(err) - } - } - } - - async fn repository_info( - &mut self, - requester: Option<&User>, - request: &RepositoryInfoRequest, - ) -> Result { - let repository = match self - .find_by_owner_user_name( - // &request.owner.instance.url, - &request.repository.owner, - &request.repository.name, - ) - .await - { - Ok(repository) => repository, - Err(err) => return Err(Box::new(err).into()), - }; - - if let Some(requester) = requester { - if !repository.can_user_view_repository(Some(&requester)) { - return Err(Box::new(GitBackendError::RepositoryNotFound { - owner_user: request.repository.owner.to_string(), - name: request.repository.name.clone(), - }) - .into()); - } - } else if matches!(repository.visibility, RepositoryVisibility::Private) { - // Unauthenticated users can never view private repositories - - return Err(Box::new(GitBackendError::RepositoryNotFound { - owner_user: request.repository.owner.to_string(), - name: request.repository.name.clone(), - }) - .into()); - } - - let git = match repository.open_git2_repository(&self.repository_folder) { - Ok(git) => git, - Err(err) => return Err(Box::new(err).into()), - }; - - let rev_name = match &request.rev { - None => { - if let Ok(head) = git.head() { - head.name().unwrap().to_string() - } else { - // Nothing in database, render empty tree. - return Ok(RepositoryView { - name: repository.name, - owner: request.repository.owner.clone(), - description: repository.description, - visibility: repository.visibility, - default_branch: repository.default_branch, - latest_commit: None, - tree_rev: None, - tree: vec![], - }); - } - } - Some(rev_name) => { - // Find the reference, otherwise return GitBackendError - match git - .find_reference(format!("refs/heads/{}", rev_name).as_str()) - .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) - { - Ok(reference) => reference.name().unwrap().to_string(), - Err(err) => return Err(Box::new(err).into()), - } - } - }; - - // Get the git object as a commit - let rev = match git - .revparse_single(rev_name.as_str()) - .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) - { - Ok(rev) => rev, - Err(err) => return Err(Box::new(err).into()), - }; - let commit = rev.as_commit().unwrap(); - - // this is stupid - let mut current_path = rev_name.replace("refs/heads/", ""); - - // Get the commit tree - let git_tree = if let Some(path) = &request.path { - // Add it to our full path string - current_path.push_str(format!("/{}", path).as_str()); - // Get the specified path, return an error if it wasn't found. - let entry = match commit - .tree() - .unwrap() - .get_path(&PathBuf::from(path)) - .map_err(|_| GitBackendError::PathNotFound(path.to_string())) - { - Ok(entry) => entry, - Err(err) => return Err(Box::new(err).into()), - }; - // Turn the entry into a git tree - entry.to_object(&git).unwrap().as_tree().unwrap().clone() - } else { - commit.tree().unwrap() - }; - - // Iterate over the git tree and collect it into our own tree types - let mut tree = git_tree - .iter() - .map(|entry| { - let object_type = match entry.kind().unwrap() { - ObjectType::Tree => RepositoryObjectType::Tree, - ObjectType::Blob => RepositoryObjectType::Blob, - _ => unreachable!(), - }; - let mut tree_entry = - RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode()); - - if request.extra_metadata { - // Get the file size if It's a blob - let object = entry.to_object(&git).unwrap(); - if let Some(blob) = object.as_blob() { - tree_entry.size = Some(blob.size()); - } - - // Could possibly be done better - let path = if let Some(path) = current_path.split_once('/') { - format!("{}/{}", path.1, entry.name().unwrap()) - } else { - entry.name().unwrap().to_string() - }; - - // Get the last commit made to the entry - if let Ok(last_commit) = - GitBackend::get_last_commit_of_file(&path, &git, commit) - { - tree_entry.last_commit = Some(last_commit); - } - } - - tree_entry - }) - .collect::>(); - - // Sort the tree alphabetically and with tree first - tree.sort_unstable_by_key(|entry| entry.name.to_lowercase()); - tree.sort_unstable_by_key(|entry| { - std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase()) - }); - - Ok(RepositoryView { - name: repository.name, - owner: request.repository.owner.clone(), - description: repository.description, - visibility: repository.visibility, - default_branch: repository.default_branch, - latest_commit: None, - tree_rev: Some(rev_name), - tree, - }) - } - - async fn repository_file_inspect( - &mut self, - _requester: Option<&User>, - _request: &RepositoryFileInspectRequest, - ) -> Result { - todo!() - } - - async fn repositories_for_user( - &mut self, - requester: Option<&User>, - user: &User, - ) -> Result, Error> { - let mut repositories = sqlx::query_as!( - GitRepository, - r#"SELECT visibility as "visibility: _", owner_user, name, description, default_branch FROM repositories WHERE owner_user = $1"#, - user.to_string() - ) - .fetch_many(&self.pg_pool); - - let mut result = vec![]; - - while let Some(Ok(Either::Right(repository))) = repositories.next().await { - // Check if the requesting user is allowed to see the repository - if !(matches!( - repository.visibility, - RepositoryVisibility::Unlisted | RepositoryVisibility::Private - ) && Some(&repository.owner_user.clone()) != requester) - { - result.push(RepositorySummary { - repository: Repository { - owner: repository.owner_user.clone(), - name: repository.name, - instance: self.instance.clone(), - }, - owner: repository.owner_user.clone(), - visibility: repository.visibility, - description: repository.description, - // TODO - last_commit: None, - }); - } - } - - Ok(result) - } -} - -impl IssuesBackend for GitBackend { - fn issues_count( - &mut self, - _requester: Option<&User>, - _request: &RepositoryIssuesCountRequest, - ) -> Result { - todo!() - } - - fn issue_labels( - &mut self, - _requester: Option<&User>, - _request: &RepositoryIssueLabelsRequest, - ) -> Result { - todo!() - } - - fn issues( - &mut self, - _requester: Option<&User>, - _request: &RepositoryIssuesRequest, - ) -> Result { - todo!() - } -} diff --git a/src/backend/github.rs b/src/backend/github.rs deleted file mode 100644 index 051c8cb..0000000 --- a/src/backend/github.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! TODO: GitHub backend to allow for login with GitHub -//! accounts and interacting with GitHub repositories diff --git a/src/backend/mod.rs b/src/backend/mod.rs deleted file mode 100644 index 88cddc6..0000000 --- a/src/backend/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -pub mod discovery; -pub mod git; -pub mod github; -pub mod user; - -use anyhow::Error; -use async_trait::async_trait; -use serde_json::Value; - -use crate::backend::git::GitBackendError; -use crate::{ - messages::{ - authentication::{ - AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, - RegisterAccountResponse, - }, - repository::{ - RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, - RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, - RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, - RepositoryIssuesCountResponse, RepositoryIssuesRequest, RepositoryIssuesResponse, - }, - user::{ - UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, - UserDisplayNameRequest, UserDisplayNameResponse, - }, - }, - model::{ - repository::{RepositorySummary, RepositoryView}, - user::User, - }, -}; - -#[async_trait] -pub trait RepositoryBackend: IssuesBackend { - async fn create_repository( - &mut self, - user: &User, - request: &RepositoryCreateRequest, - ) -> Result; - async fn repository_info( - &mut self, - requester: Option<&User>, - request: &RepositoryInfoRequest, - ) -> Result; - async fn repository_file_inspect( - &mut self, - requester: Option<&User>, - request: &RepositoryFileInspectRequest, - ) -> Result; - async fn repositories_for_user( - &mut self, - requester: Option<&User>, - user: &User, - ) -> Result, Error>; -} - -pub trait IssuesBackend { - fn issues_count( - &mut self, - requester: Option<&User>, - request: &RepositoryIssuesCountRequest, - ) -> Result; - fn issue_labels( - &mut self, - requester: Option<&User>, - request: &RepositoryIssueLabelsRequest, - ) -> Result; - fn issues( - &mut self, - requester: Option<&User>, - request: &RepositoryIssuesRequest, - ) -> Result; -} - -#[async_trait::async_trait] -pub trait AuthBackend { - async fn register( - &mut self, - request: RegisterAccountRequest, - ) -> Result; - - async fn login( - &mut self, - request: AuthenticationTokenRequest, - ) -> Result; -} - -#[async_trait::async_trait] -pub trait UserBackend: AuthBackend { - async fn display_name( - &mut self, - request: UserDisplayNameRequest, - ) -> Result; - - async fn display_image( - &mut self, - request: UserDisplayImageRequest, - ) -> Result; - - async fn bio(&mut self, request: UserBioRequest) -> Result; - async fn exists(&mut self, user: &User) -> Result; - - async fn settings(&mut self, user: &User) -> Result, Error>; - async fn write_settings( - &mut self, - user: &User, - settings: &[(String, Value)], - ) -> Result<(), Error>; -} diff --git a/src/backend/user.rs b/src/backend/user.rs deleted file mode 100644 index da7cf80..0000000 --- a/src/backend/user.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::sync::Arc; - -use anyhow::Error; - -use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; -use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; -use base64::{engine::general_purpose::STANDARD, Engine as _}; -use futures_util::StreamExt; -use rsa::{ - pkcs8::{EncodePrivateKey, EncodePublicKey}, - rand_core::OsRng, - RsaPrivateKey, RsaPublicKey, -}; -use serde_json::Value; -use sqlx::{Either, PgPool}; -use tokio::sync::Mutex; - -use crate::{ - authentication::AuthenticationTokenGranter, - messages::{ - authentication::{ - AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, - RegisterAccountResponse, - }, - user::{ - UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, - UserDisplayNameRequest, UserDisplayNameResponse, - }, - }, - model::{instance::Instance, user::User}, -}; - -use super::{AuthBackend, UserBackend}; - -pub struct UserAuth { - pub pg_pool: PgPool, - pub this_instance: Instance, - pub auth_granter: Arc>, -} - -impl UserAuth { - pub fn new( - pool: PgPool, - this_instance: &Instance, - granter: Arc>, - ) -> Self { - Self { - pg_pool: pool, - this_instance: this_instance.clone(), - auth_granter: granter, - } - } -} - -#[async_trait::async_trait] -impl UserBackend for UserAuth { - async fn display_name( - &mut self, - request: UserDisplayNameRequest, - ) -> Result { - let db_row = sqlx::query_as!( - UserRow, - r#"SELECT * FROM users WHERE username = $1"#, - request.user.username - ) - .fetch_one(&self.pg_pool.clone()) - .await - .unwrap(); - - Ok(UserDisplayNameResponse { - display_name: db_row.display_name, - }) - } - - async fn display_image( - &mut self, - request: UserDisplayImageRequest, - ) -> Result { - let db_row = sqlx::query_as!( - UserRow, - r#"SELECT * FROM users WHERE username = $1"#, - request.user.username - ) - .fetch_one(&self.pg_pool.clone()) - .await - .unwrap(); - - Ok(UserDisplayImageResponse { - image_url: db_row.image_url, - }) - } - - async fn bio(&mut self, request: UserBioRequest) -> Result { - let db_row = sqlx::query_as!( - UserRow, - r#"SELECT * FROM users WHERE username = $1"#, - request.user.username - ) - .fetch_one(&self.pg_pool.clone()) - .await?; - - Ok(UserBioResponse { bio: db_row.bio }) - } - - async fn exists(&mut self, user: &User) -> Result { - Ok(sqlx::query_as!( - UserRow, - r#"SELECT * FROM users WHERE username = $1"#, - user.username - ) - .fetch_one(&self.pg_pool.clone()) - .await - .is_err()) - } - - async fn settings(&mut self, user: &User) -> Result, Error> { - let settings = sqlx::query_as!( - UserSettingRow, - r#"SELECT * FROM user_settings WHERE username = $1"#, - user.username - ) - .fetch_many(&self.pg_pool) - .filter_map(|result| async move { - if let Ok(Either::Right(row)) = result { - Some(row) - } else { - None - } - }) - .filter_map(|row| async move { - if let Ok(value) = serde_json::from_str(&row.value) { - Some((row.name, value)) - } else { - None - } - }) - .collect::>() - .await; - - Ok(settings) - } - - async fn write_settings( - &mut self, - user: &User, - settings: &[(String, Value)], - ) -> Result<(), Error> { - for (name, value) in settings { - let serialized = serde_json::to_string(value)?; - - sqlx::query!("INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", - user.username, name, serialized) - .execute(&self.pg_pool).await?; - } - - Ok(()) - } -} - -#[async_trait::async_trait] -impl AuthBackend for UserAuth { - async fn register( - &mut self, - request: RegisterAccountRequest, - ) -> Result { - const BITS: usize = 2048; - - let private_key = RsaPrivateKey::new(&mut OsRng, BITS).unwrap(); - let public_key = RsaPublicKey::from(&private_key); - - let key = { - let mut target: [u8; 32] = [0; 32]; - - let mut index = 0; - let mut iterator = request.password.as_bytes().iter(); - while index < 32 { - if let Some(next) = iterator.next() { - target[index] = *next; - index += 1; - } else { - iterator = request.password.as_bytes().iter(); - } - } - - target - }; - - let key: &Key = &key.into(); - let cipher = Aes256Gcm::new(key); - let nonce = Aes256Gcm::generate_nonce(&mut OsRng); - let ciphertext = cipher - .encrypt(&nonce, private_key.to_pkcs8_der().unwrap().as_bytes()) - .unwrap(); - - let private_key_enc = format!("{}#{}", STANDARD.encode(nonce), STANDARD.encode(ciphertext)); - - let salt = SaltString::generate(&mut OsRng); - - let argon2 = Argon2::default(); - - let password_hash = argon2 - .hash_password(request.password.as_bytes(), &salt) - .unwrap() - .to_string(); - - let user = match sqlx::query_as!( - UserRow, - r#"INSERT INTO users VALUES ($1, null, $2, null, null, $3, $4, $5) returning *"#, - request.username, - "example.com", - password_hash, - public_key - .to_public_key_pem(rsa::pkcs8::LineEnding::LF) - .unwrap(), - private_key_enc - ) - .fetch_one(&self.pg_pool) - .await - { - Ok(user) => user, - Err(err) => { - error!("Failed inserting into the database! {:?}", err); - - return Err(err.into()); - } - }; - - let mut granter = self.auth_granter.lock().await; - let token = granter - .create_token_for( - &User { - username: user.username, - instance: self.this_instance.clone(), - }, - &self.this_instance, - ) - .await; - - Ok(RegisterAccountResponse { token }) - } - - async fn login( - &mut self, - _request: AuthenticationTokenRequest, - ) -> Result { - todo!() - } -} - -#[allow(unused)] -#[derive(Debug, sqlx::FromRow)] -struct UserRow { - pub username: String, - pub image_url: Option, - pub display_name: Option, - pub bio: Option, - pub email: Option, - pub password: String, - pub public_key: String, - pub enc_private_key: Vec, -} - -#[allow(unused)] -#[derive(Debug, sqlx::FromRow)] -struct UserSettingRow { - pub username: String, - pub name: String, - pub value: String, -} diff --git a/src/connection.rs b/src/connection.rs deleted file mode 100644 index fb14a52..0000000 --- a/src/connection.rs +++ /dev/null @@ -1,95 +0,0 @@ -pub mod authentication; -pub mod handshake; -pub mod repository; -pub mod user; -pub mod wrapper; - -use std::{any::type_name, collections::HashMap}; - -use anyhow::Error; -use serde::{de::DeserializeOwned, Serialize}; -use tokio::{net::TcpStream, task::JoinHandle}; -use tokio_tungstenite::WebSocketStream; - -use crate::{ - messages::ErrorMessage, - model::instance::{Instance, InstanceMeta}, -}; - -#[derive(Debug, thiserror::Error)] -pub enum ConnectionError { - #[error("connection error message {0}")] - ErrorMessage(#[from] ErrorMessage), - #[error("connection should close")] - Shutdown, - #[error("internal error {0}")] - InternalError(#[from] Error), -} - -pub struct RawConnection { - pub task: JoinHandle<()>, -} - -pub struct InstanceConnection { - pub instance: InstanceMeta, - pub task: JoinHandle<()>, -} - -/// Represents a connection which hasn't finished the handshake. -pub struct UnestablishedConnection { - pub socket: WebSocketStream, -} - -#[derive(Default)] -pub struct Connections { - pub connections: Vec, - pub instance_connections: HashMap, -} - -#[derive(Debug, thiserror::Error)] -#[error("handler did not handle")] -pub struct HandlerUnhandled; - -pub trait MessageHandling { - fn message_type() -> &'static str; -} - -impl MessageHandling<(T1,), M, R> for F -where - F: FnOnce(T1) -> R, - T1: Serialize + DeserializeOwned, -{ - fn message_type() -> &'static str { - type_name::() - } -} - -impl MessageHandling<(T1, T2), M, R> for F -where - F: FnOnce(T1, T2) -> R, - T1: Serialize + DeserializeOwned, -{ - fn message_type() -> &'static str { - type_name::() - } -} - -impl MessageHandling<(T1, T2, T3), M, R> for F -where - F: FnOnce(T1, T2, T3) -> R, - T1: Serialize + DeserializeOwned, -{ - fn message_type() -> &'static str { - type_name::() - } -} - -impl MessageHandling<(T1, T2, T3, T4), M, R> for F -where - F: FnOnce(T1, T2, T3, T4) -> R, - T1: Serialize + DeserializeOwned, -{ - fn message_type() -> &'static str { - type_name::() - } -} diff --git a/src/connection/authentication.rs b/src/connection/authentication.rs deleted file mode 100644 index e505bdc..0000000 --- a/src/connection/authentication.rs +++ /dev/null @@ -1,124 +0,0 @@ -use anyhow::Error; -use thiserror::Error; - -use crate::messages::authentication::{ - AuthenticationTokenRequest, RegisterAccountRequest, TokenExtensionRequest, -}; -use crate::model::authenticated::{AuthenticatedInstance, NetworkMessage, State}; -use crate::model::authenticated::{Message, MessageHandler}; - -use super::wrapper::ConnectionState; - -pub async fn authentication_handle( - message_type: &str, - message: &NetworkMessage, - state: &ConnectionState, -) -> Result { - match message_type { - "&giterated_daemon::messages::authentication::RegisterAccountRequest" => { - register_account_request - .handle_message(&message, state) - .await?; - - Ok(true) - } - "&giterated_daemon::messages::authentication::AuthenticationTokenRequest" => { - authentication_token_request - .handle_message(&message, state) - .await?; - - Ok(true) - } - "&giterated_daemon::messages::authentication::TokenExtensionRequest" => { - token_extension_request - .handle_message(&message, state) - .await?; - - Ok(true) - } - _ => Ok(false), - } -} - -async fn register_account_request( - State(connection_state): State, - Message(request): Message, - instance: AuthenticatedInstance, -) -> Result<(), AuthenticationConnectionError> { - if *instance.inner() != connection_state.instance { - return Err(AuthenticationConnectionError::SameInstance); - } - - let mut user_backend = connection_state.user_backend.lock().await; - - let response = user_backend - .register(request.clone()) - .await - .map_err(|e| AuthenticationConnectionError::Registration(e))?; - drop(user_backend); - - connection_state - .send(response) - .await - .map_err(|e| AuthenticationConnectionError::Sending(e))?; - - Ok(()) -} - -async fn authentication_token_request( - State(connection_state): State, - Message(request): Message, - instance: AuthenticatedInstance, -) -> Result<(), AuthenticationConnectionError> { - let issued_for = instance.inner().clone(); - - let mut token_granter = connection_state.auth_granter.lock().await; - - let response = token_granter - .token_request(issued_for, request.username, request.password) - .await - .map_err(|e| AuthenticationConnectionError::TokenIssuance(e))?; - - connection_state - .send(response) - .await - .map_err(|e| AuthenticationConnectionError::Sending(e))?; - - Ok(()) -} - -async fn token_extension_request( - State(connection_state): State, - Message(request): Message, - instance: AuthenticatedInstance, -) -> Result<(), AuthenticationConnectionError> { - let issued_for = instance.inner().clone(); - - let mut token_granter = connection_state.auth_granter.lock().await; - - let response = token_granter - .extension_request(&issued_for, request.token) - .await - .map_err(|e| AuthenticationConnectionError::TokenIssuance(e))?; - - connection_state - .send(response) - .await - .map_err(|e| AuthenticationConnectionError::Sending(e))?; - - Ok(()) -} - -#[derive(Debug, Error)] -pub enum AuthenticationConnectionError { - #[error("the request was invalid")] - InvalidRequest, - #[error("request must be from the same instance")] - SameInstance, - #[error("issue during registration {0}")] - Registration(Error), - #[error("sending error")] - Sending(Error), - #[error("error issuing token")] - TokenIssuance(Error), -} diff --git a/src/connection/handshake.rs b/src/connection/handshake.rs deleted file mode 100644 index c0bb8ab..0000000 --- a/src/connection/handshake.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::{str::FromStr, sync::atomic::Ordering}; - -use anyhow::Error; -use semver::Version; - -use crate::{ - connection::ConnectionError, - messages::handshake::{HandshakeFinalize, HandshakeResponse, InitiateHandshake}, - model::authenticated::{Message, MessageHandler, NetworkMessage, State}, - validate_version, version, -}; - -use super::{wrapper::ConnectionState, HandlerUnhandled}; - -pub async fn handshake_handle( - message: &NetworkMessage, - state: &ConnectionState, -) -> Result<(), Error> { - if initiate_handshake - .handle_message(&message, state) - .await - .is_ok() - { - Ok(()) - } else if handshake_response - .handle_message(&message, state) - .await - .is_ok() - { - Ok(()) - } else if handshake_finalize - .handle_message(&message, state) - .await - .is_ok() - { - Ok(()) - } else { - Err(Error::from(HandlerUnhandled)) - } -} - -async fn initiate_handshake( - Message(initiation): Message, - State(connection_state): State, -) -> Result<(), HandshakeError> { - if !validate_version(&initiation.version) { - error!( - "Version compatibility failure! Our Version: {}, Their Version: {}", - Version::from_str(&std::env::var("CARGO_PKG_VERSION").unwrap()).unwrap(), - initiation.version - ); - - connection_state - .send(HandshakeFinalize { success: false }) - .await - .map_err(|e| HandshakeError::SendError(e))?; - - Ok(()) - } else { - connection_state - .send(HandshakeResponse { - identity: connection_state.instance.clone(), - version: version(), - }) - .await - .map_err(|e| HandshakeError::SendError(e))?; - - Ok(()) - } -} - -async fn handshake_response( - Message(response): Message, - State(connection_state): State, -) -> Result<(), HandshakeError> { - if !validate_version(&response.version) { - error!( - "Version compatibility failure! Our Version: {}, Their Version: {}", - Version::from_str(&std::env::var("CARGO_PKG_VERSION").unwrap()).unwrap(), - response.version - ); - - connection_state - .send(HandshakeFinalize { success: false }) - .await - .map_err(|e| HandshakeError::SendError(e))?; - - Ok(()) - } else { - connection_state - .send(HandshakeFinalize { success: true }) - .await - .map_err(|e| HandshakeError::SendError(e))?; - - Ok(()) - } -} - -async fn handshake_finalize( - Message(finalize): Message, - State(connection_state): State, -) -> Result<(), HandshakeError> { - if !finalize.success { - error!("Error during handshake, aborting connection"); - return Err(Error::from(ConnectionError::Shutdown).into()); - } else { - connection_state.handshaked.store(true, Ordering::SeqCst); - - connection_state - .send(HandshakeFinalize { success: true }) - .await - .map_err(|e| HandshakeError::SendError(e))?; - - Ok(()) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum HandshakeError { - #[error("version mismatch during handshake, ours: {0}, theirs: {1}")] - VersionMismatch(Version, Version), - #[error("while sending message: {0}")] - SendError(Error), - #[error("{0}")] - Other(#[from] Error), -} diff --git a/src/connection/repository.rs b/src/connection/repository.rs deleted file mode 100644 index 87d13ae..0000000 --- a/src/connection/repository.rs +++ /dev/null @@ -1,141 +0,0 @@ -use anyhow::Error; - -use crate::backend::git::GitBackendError; -use crate::{ - messages::repository::{ - RepositoryCreateRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, - RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, - }, - model::authenticated::{AuthenticatedUser, Message, MessageHandler, NetworkMessage, State}, -}; - -use super::wrapper::ConnectionState; - -pub async fn repository_handle( - message_type: &str, - message: &NetworkMessage, - state: &ConnectionState, -) -> Result { - match message_type { - "&giterated_daemon::messages::repository::RepositoryCreateRequest" => { - create_repository.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::repository::RepositoryFileInspectRequest" => { - repository_file_inspect - .handle_message(&message, state) - .await?; - - Ok(true) - } - "&giterated_daemon::messages::repository::RepositoryInfoRequest" => { - repository_info.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::repository::RepositoryIssuesCountRequest" => { - issues_count.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::repository::RepositoryIssueLabelsRequest" => { - issue_labels.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::repository::RepositoryIssuesRequest" => { - issues.handle_message(&message, state).await?; - - Ok(true) - } - _ => Ok(false), - } -} - -async fn create_repository( - Message(request): Message, - State(connection_state): State, - AuthenticatedUser(user): AuthenticatedUser, -) -> Result<(), RepositoryError> { - let mut repository_backend = connection_state.repository_backend.lock().await; - let response = repository_backend - .create_repository(&user, &request) - .await?; - - drop(repository_backend); - - connection_state.send(response).await?; - - Ok(()) -} - -async fn repository_file_inspect( - Message(request): Message, - State(connection_state): State, - user: Option, -) -> Result<(), RepositoryError> { - let user = user.map(|u| u.0); - - let mut repository_backend = connection_state.repository_backend.lock().await; - let response = repository_backend - .repository_file_inspect(user.as_ref(), &request) - .await?; - - drop(repository_backend); - - connection_state.send(response).await?; - - Ok(()) -} - -async fn repository_info( - Message(request): Message, - State(connection_state): State, - user: Option, -) -> Result<(), RepositoryError> { - let user = user.map(|u| u.0); - - let mut repository_backend = connection_state.repository_backend.lock().await; - let response = repository_backend - .repository_info(user.as_ref(), &request) - .await?; - - drop(repository_backend); - - connection_state.send(response).await?; - - Ok(()) -} - -async fn issues_count( - Message(_request): Message, - State(_connection_state): State, - _user: Option, -) -> Result<(), RepositoryError> { - unimplemented!(); -} - -async fn issue_labels( - Message(_request): Message, - State(_connection_state): State, - _user: Option, -) -> Result<(), RepositoryError> { - unimplemented!(); -} - -async fn issues( - Message(_request): Message, - State(_connection_state): State, - _user: Option, -) -> Result<(), RepositoryError> { - unimplemented!(); -} - -#[derive(Debug, thiserror::Error)] -pub enum RepositoryError { - #[error("{0}")] - GitBackendError(#[from] GitBackendError), - #[error("{0}")] - Other(#[from] Error), -} diff --git a/src/connection/user.rs b/src/connection/user.rs deleted file mode 100644 index 8a20020..0000000 --- a/src/connection/user.rs +++ /dev/null @@ -1,187 +0,0 @@ -use anyhow::Error; - -use crate::messages::user::{ - UserSettingsRequest, UserSettingsResponse, UserWriteSettingsRequest, UserWriteSettingsResponse, -}; -use crate::model::authenticated::AuthenticatedUser; -use crate::model::user::User; -use crate::{ - messages::user::{ - UserBioRequest, UserDisplayImageRequest, UserDisplayNameRequest, UserRepositoriesRequest, - UserRepositoriesResponse, - }, - model::authenticated::{Message, MessageHandler, NetworkMessage, State}, -}; - -use super::wrapper::ConnectionState; - -pub async fn user_handle( - message_type: &str, - message: &NetworkMessage, - state: &ConnectionState, -) -> Result { - match message_type { - "&giterated_daemon::messages::user::UserDisplayNameRequest" => { - display_name.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::user::UserDisplayImageRequest" => { - display_image.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::user::UserBioRequest" => { - bio.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::user::UserRepositoriesRequest" => { - repositories.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::user::UserSettingsRequest" => { - user_settings.handle_message(&message, state).await?; - - Ok(true) - } - "&giterated_daemon::messages::user::UserWriteSettingsRequest" => { - write_user_settings.handle_message(&message, state).await?; - - Ok(true) - } - _ => Ok(false), - } -} - -async fn display_name( - Message(request): Message, - State(connection_state): State, -) -> Result<(), UserError> { - let mut user_backend = connection_state.user_backend.lock().await; - let response = user_backend.display_name(request.clone()).await?; - - drop(user_backend); - - connection_state.send(response).await?; - - Ok(()) -} - -async fn display_image( - Message(request): Message, - State(connection_state): State, -) -> Result<(), UserError> { - let mut user_backend = connection_state.user_backend.lock().await; - let response = user_backend.display_image(request.clone()).await?; - - drop(user_backend); - - connection_state.send(response).await?; - - Ok(()) -} - -async fn bio( - Message(request): Message, - State(connection_state): State, -) -> Result<(), UserError> { - let mut user_backend = connection_state.user_backend.lock().await; - let response = user_backend.bio(request.clone()).await?; - - drop(user_backend); - - connection_state.send(response).await?; - - Ok(()) -} - -async fn repositories( - Message(request): Message, - State(connection_state): State, - requesting_user: Option, -) -> Result<(), UserError> { - let requesting_user = requesting_user.map(|u| u.0); - - let mut repository_backend = connection_state.repository_backend.lock().await; - let repositories = repository_backend - .repositories_for_user(requesting_user.as_ref(), &request.user) - .await; - - let repositories = match repositories { - Ok(repositories) => repositories, - Err(err) => { - error!("Error handling request: {:?}", err); - return Ok(()); - } - }; - drop(repository_backend); - - let mut user_backend = connection_state.user_backend.lock().await; - let user_exists = user_backend.exists(&request.user).await; - - if repositories.is_empty() && !matches!(user_exists, Ok(true)) { - return Err(UserError::InvalidUser(request.user)); - } - - let response: UserRepositoriesResponse = UserRepositoriesResponse { repositories }; - - connection_state.send(response).await?; - - Ok(()) -} - -async fn user_settings( - Message(request): Message, - State(connection_state): State, - AuthenticatedUser(requesting_user): AuthenticatedUser, -) -> Result<(), UserError> { - if request.user != requesting_user { - return Err(UserError::InvalidUser(request.user)); - } - - let mut user_backend = connection_state.user_backend.lock().await; - let mut settings = user_backend.settings(&request.user).await?; - - drop(user_backend); - - let response = UserSettingsResponse { - settings: settings.drain(..).collect(), - }; - - connection_state.send(response).await?; - - Ok(()) -} - -async fn write_user_settings( - Message(request): Message, - State(connection_state): State, - AuthenticatedUser(requesting_user): AuthenticatedUser, -) -> Result<(), UserError> { - if request.user != requesting_user { - return Err(UserError::InvalidUser(request.user)); - } - - let mut user_backend = connection_state.user_backend.lock().await; - user_backend - .write_settings(&request.user, &request.settings) - .await?; - - drop(user_backend); - - let response = UserWriteSettingsResponse {}; - - connection_state.send(response).await?; - - Ok(()) -} - -#[derive(Debug, thiserror::Error)] -pub enum UserError { - #[error("invalid user {0}")] - InvalidUser(User), - #[error("{0}")] - Other(#[from] Error), -} diff --git a/src/connection/wrapper.rs b/src/connection/wrapper.rs deleted file mode 100644 index 7218238..0000000 --- a/src/connection/wrapper.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; - -use anyhow::Error; -use futures_util::{SinkExt, StreamExt}; -use rsa::RsaPublicKey; -use serde::Serialize; -use serde_json::Value; -use tokio::{ - net::TcpStream, - sync::{Mutex, RwLock}, -}; -use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; - -use crate::{ - authentication::AuthenticationTokenGranter, - backend::{RepositoryBackend, UserBackend}, - messages::error::ConnectionError, - model::{authenticated::NetworkMessage, instance::Instance}, -}; - -use super::{ - authentication::authentication_handle, handshake::handshake_handle, - repository::repository_handle, user::user_handle, Connections, -}; - -pub async fn connection_wrapper( - socket: WebSocketStream, - connections: Arc>, - repository_backend: Arc>, - user_backend: Arc>, - auth_granter: Arc>, - addr: SocketAddr, - instance: impl ToOwned, -) { - let connection_state = ConnectionState { - socket: Arc::new(Mutex::new(socket)), - connections, - repository_backend, - user_backend, - auth_granter, - addr, - instance: instance.to_owned(), - handshaked: Arc::new(AtomicBool::new(false)), - cached_keys: Arc::default(), - }; - - let mut handshaked = false; - - loop { - let mut socket = connection_state.socket.lock().await; - let message = socket.next().await; - drop(socket); - - match message { - Some(Ok(message)) => { - let payload = match message { - Message::Binary(payload) => payload, - Message::Ping(_) => { - let mut socket = connection_state.socket.lock().await; - let _ = socket.send(Message::Pong(vec![])).await; - drop(socket); - continue; - } - Message::Close(_) => return, - _ => continue, - }; - - let message = NetworkMessage(payload.clone()); - - if !handshaked { - if handshake_handle(&message, &connection_state).await.is_ok() { - if connection_state.handshaked.load(Ordering::SeqCst) { - handshaked = true; - } - } - } else { - let raw = serde_json::from_slice::(&payload).unwrap(); - let message_type = raw.get("message_type").unwrap().as_str().unwrap(); - - match authentication_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state.send(ConnectionError(e.to_string())).await; - } - Ok(true) => continue, - Ok(false) => {} - } - - match repository_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state.send(ConnectionError(e.to_string())).await; - } - Ok(true) => continue, - Ok(false) => {} - } - - match user_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state.send(ConnectionError(e.to_string())).await; - } - Ok(true) => continue, - Ok(false) => {} - } - - match authentication_handle(message_type, &message, &connection_state).await { - Err(e) => { - let _ = connection_state.send(ConnectionError(e.to_string())).await; - } - Ok(true) => continue, - Ok(false) => {} - } - - error!( - "Message completely unhandled: {}", - std::str::from_utf8(&payload).unwrap() - ); - } - } - Some(Err(e)) => { - error!("Closing connection for {:?} for {}", e, addr); - return; - } - _ => { - info!("Unhandled"); - continue; - } - } - } -} - -#[derive(Clone)] -pub struct ConnectionState { - socket: Arc>>, - pub connections: Arc>, - pub repository_backend: Arc>, - pub user_backend: Arc>, - pub auth_granter: Arc>, - pub addr: SocketAddr, - pub instance: Instance, - pub handshaked: Arc, - pub cached_keys: Arc>>, -} - -impl ConnectionState { - pub async fn send(&self, message: T) -> Result<(), Error> { - let payload = serde_json::to_string(&message)?; - info!("Sending payload: {}", &payload); - self.socket - .lock() - .await - .send(Message::Binary(payload.into_bytes())) - .await?; - - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 9c1400e..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::str::FromStr; - -use semver::{Version, VersionReq}; - -pub mod authentication; -pub mod backend; -pub mod connection; -pub mod messages; -pub mod model; - -#[macro_use] -extern crate tracing; - -pub fn version() -> Version { - Version::from_str(env!("CARGO_PKG_VERSION")).unwrap() -} - -pub fn validate_version(other: &Version) -> bool { - let version_req = VersionReq::from_str("=0.0.6").unwrap(); - - version_req.matches(other) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index db7a852..0000000 --- a/src/main.rs +++ /dev/null @@ -1,128 +0,0 @@ -use anyhow::Error; -use connection::{Connections, RawConnection}; -use giterated_daemon::{ - authentication::AuthenticationTokenGranter, - backend::{git::GitBackend, user::UserAuth, RepositoryBackend, UserBackend}, - connection::{self, wrapper::connection_wrapper}, - model::instance::Instance, -}; -use sqlx::{postgres::PgConnectOptions, ConnectOptions, PgPool}; -use std::{net::SocketAddr, str::FromStr, sync::Arc}; -use tokio::{ - fs::File, - io::{AsyncRead, AsyncReadExt, AsyncWrite}, - net::{TcpListener, TcpStream}, - sync::Mutex, -}; -use tokio_tungstenite::{accept_async, WebSocketStream}; -use toml::Table; - -#[macro_use] -extern crate tracing; - -#[tokio::main] -async fn main() -> Result<(), Error> { - tracing_subscriber::fmt::init(); - let mut listener = TcpListener::bind("0.0.0.0:7270").await?; - let connections: Arc> = Arc::default(); - let config: Table = { - let mut file = File::open("Giterated.toml").await?; - let mut text = String::new(); - file.read_to_string(&mut text).await?; - text.parse()? - }; - let db_conn_options = PgConnectOptions::new() - .host(config["postgres"]["host"].as_str().unwrap()) - .port(config["postgres"]["port"].as_integer().unwrap() as u16) - .database(config["postgres"]["database"].as_str().unwrap()) - .username(config["postgres"]["user"].as_str().unwrap()) - .password(config["postgres"]["password"].as_str().unwrap()) - .log_statements(log::LevelFilter::Off); - let db_pool = PgPool::connect_with(db_conn_options).await?; - - debug!("Running database migrations..."); - sqlx::migrate!().run(&db_pool).await?; - info!("Connected"); - - let repository_backend: Arc> = - Arc::new(Mutex::new(GitBackend { - pg_pool: db_pool.clone(), - repository_folder: String::from( - config["giterated"]["backend"]["git"]["root"] - .as_str() - .unwrap(), - ), - instance: Instance::from_str("giterated.dev").unwrap(), - })); - - let token_granter = Arc::new(Mutex::new(AuthenticationTokenGranter { - config: config.clone(), - instance: Instance::from_str("giterated.dev").unwrap(), - })); - - let user_backend: Arc> = Arc::new(Mutex::new(UserAuth::new( - db_pool.clone(), - &Instance::from_str("giterated.dev").unwrap(), - token_granter.clone(), - ))); - - info!("Connected"); - - loop { - let stream = accept_stream(&mut listener).await; - info!("Connected"); - - let (stream, address) = match stream { - Ok(stream) => stream, - Err(err) => { - error!("Failed to accept connection. {:?}", err); - continue; - } - }; - - info!("Accepted connection from {}", address); - - let connection = accept_websocket_connection(stream).await; - - let connection = match connection { - Ok(connection) => connection, - Err(err) => { - error!( - "Failed to initiate Websocket connection from {}. {:?}", - address, err - ); - continue; - } - }; - - info!("Websocket connection established with {}", address); - - let connection = RawConnection { - task: tokio::spawn(connection_wrapper( - connection, - connections.clone(), - repository_backend.clone(), - user_backend.clone(), - token_granter.clone(), - address, - Instance::from_str("giterated.dev").unwrap(), - )), - }; - - connections.lock().await.connections.push(connection); - } -} - -async fn accept_stream(listener: &mut TcpListener) -> Result<(TcpStream, SocketAddr), Error> { - let stream = listener.accept().await?; - - Ok(stream) -} - -async fn accept_websocket_connection( - stream: S, -) -> Result, Error> { - let connection = accept_async(stream).await?; - - Ok(connection) -} diff --git a/src/messages/authentication.rs b/src/messages/authentication.rs deleted file mode 100644 index c714771..0000000 --- a/src/messages/authentication.rs +++ /dev/null @@ -1,62 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::model::authenticated::UserAuthenticationToken; - -/// An account registration request. -/// -/// # Authentication -/// - Instance Authentication -/// - **ONLY ACCEPTED WHEN SAME-INSTANCE** -#[derive(Clone, Serialize, Deserialize)] -pub struct RegisterAccountRequest { - pub username: String, - pub email: Option, - pub password: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RegisterAccountResponse { - pub token: String, -} - -/// An authentication token request. -/// -/// AKA Login Request -/// -/// # Authentication -/// - Instance Authentication -/// - Identifies the Instance to issue the token for -/// # Authorization -/// - Credentials ([`crate::backend::AuthBackend`]-based) -/// - Identifies the User account to issue a token for -/// - Decrypts user private key to issue to -#[derive(Clone, Serialize, Deserialize)] -pub struct AuthenticationTokenRequest { - pub username: String, - pub password: String, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct AuthenticationTokenResponse { - pub token: UserAuthenticationToken, -} - -/// An authentication token extension request. -/// -/// # Authentication -/// - Instance Authentication -/// - Identifies the Instance to issue the token for -/// - User Authentication -/// - Authenticates the validity of the token -/// # Authorization -/// - Token-based -/// - Validates authorization using token's authenticity -#[derive(Clone, Serialize, Deserialize)] -pub struct TokenExtensionRequest { - pub token: UserAuthenticationToken, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct TokenExtensionResponse { - pub new_token: Option, -} diff --git a/src/messages/discovery.rs b/src/messages/discovery.rs deleted file mode 100644 index 3b4387d..0000000 --- a/src/messages/discovery.rs +++ /dev/null @@ -1,21 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -use crate::model::discovery::DiscoveryItem; - -#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct DiscoveryOffer { - pub earliest: DateTime, - pub hashes: Vec, -} - -#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct DiscoveryRequest { - pub since: DateTime, - pub hashes: Vec, -} - -#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct Discoveries { - pub discoveries: Vec, -} diff --git a/src/messages/error.rs b/src/messages/error.rs deleted file mode 100644 index 4dcbc82..0000000 --- a/src/messages/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, thiserror::Error)] -#[error("error from connection: {0}")] -pub struct ConnectionError(pub String); diff --git a/src/messages/handshake.rs b/src/messages/handshake.rs deleted file mode 100644 index e62f3ad..0000000 --- a/src/messages/handshake.rs +++ /dev/null @@ -1,22 +0,0 @@ -use semver::Version; -use serde::{Deserialize, Serialize}; - -use crate::model::instance::Instance; - -/// Sent by the initiator of a new inter-daemon connection. -#[derive(Clone, Serialize, Deserialize)] -pub struct InitiateHandshake { - pub version: Version, -} - -/// Sent in response to [`InitiateHandshake`] -#[derive(Clone, Serialize, Deserialize)] -pub struct HandshakeResponse { - pub identity: Instance, - pub version: Version, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct HandshakeFinalize { - pub success: bool, -} diff --git a/src/messages/issues.rs b/src/messages/issues.rs deleted file mode 100644 index b4a9c3a..0000000 --- a/src/messages/issues.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::model::repository::Repository; - -#[derive(Clone)] -pub struct IssuesCountCommand { - pub respository: Repository, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct IssuesCountResponse { - pub count: u64, -} - -#[derive(Clone)] -pub struct IssuesLabelsCommand { - pub repository: Repository, -} diff --git a/src/messages/mod.rs b/src/messages/mod.rs deleted file mode 100644 index bc9f449..0000000 --- a/src/messages/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; - -use crate::model::user::User; - -pub mod authentication; -pub mod discovery; -pub mod error; -pub mod handshake; -pub mod issues; -pub mod repository; -pub mod user; - -#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] -pub enum ErrorMessage { - #[error("user {0} doesn't exist or isn't valid in this context")] - InvalidUser(User), - #[error("internal error: shutdown")] - Shutdown, -} diff --git a/src/messages/repository.rs b/src/messages/repository.rs deleted file mode 100644 index 3826c84..0000000 --- a/src/messages/repository.rs +++ /dev/null @@ -1,146 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::model::repository::RepositoryVisibility; -use crate::model::{ - repository::{Commit, Repository, RepositoryTreeEntry}, - user::User, -}; - -/// A request to create a repository. -/// -/// # Authentication -/// - Instance Authentication -/// - Used to validate User token `issued_for` -/// - User Authentication -/// - Used to source owning user -/// - Used to authorize user token against user's instance -/// # Authorization -/// - Instance Authorization -/// - Used to authorize action using User token requiring a correct `issued_for` and valid issuance from user's instance -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryCreateRequest { - pub name: String, - pub description: Option, - pub visibility: RepositoryVisibility, - pub default_branch: String, - pub owner: User, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryCreateResponse; - -/// A request to inspect the tree of a repository. -/// -/// # Authentication -/// - Instance Authentication -/// - Validate request against the `issued_for` public key -/// - Validate User token against the user's instance's public key -/// # Authorization -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryFileInspectRequest { - pub path: RepositoryTreeEntry, -} - -#[derive(Clone, Serialize, Deserialize)] -pub enum RepositoryFileInspectionResponse { - File { - commit_metadata: Commit, - }, - Folder { - commit_metadata: Commit, - members: Vec, - }, - Invalid { - path: RepositoryTreeEntry, - }, -} - -/// A request to get a repository's information. -/// -/// # Authentication -/// - Instance Authentication -/// - Validate request against the `issued_for` public key -/// - Validate User token against the user's instance's public key -/// # Authorization -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssuesCountRequest; - -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssuesCountResponse { - pub count: u64, -} - -/// A request to get a repository's issues count. -/// -/// # Authentication -/// - Instance Authentication -/// - Validate request against the `issued_for` public key -/// - Validate User token against the user's instance's public key -/// # Authorization -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssueLabelsRequest; - -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssueLabelsResponse { - pub labels: Vec, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct IssueLabel { - pub name: String, - pub color: String, -} - -/// A request to get a repository's issue labels. -/// -/// # Authentication -/// - Instance Authentication -/// - Validate request against the `issued_for` public key -/// - Validate User token against the user's instance's public key -/// # Authorization -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssuesRequest; - -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssuesResponse { - pub issues: Vec, -} - -/// A request to get a repository's issues. -/// -/// # Authentication -/// - Instance Authentication -/// - Validate request against the `issued_for` public key -/// - Validate User token against the user's instance's public key -/// # Authorization -/// - User Authorization -/// - Potential User permissions checks -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryIssue { - pub author: User, - pub id: u64, - pub title: String, - pub contents: String, - pub labels: Vec, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct RepositoryInfoRequest { - pub repository: Repository, - /// Whether to fetch extra metadata like the last commit made to file or size - pub extra_metadata: bool, - /// Rev (branch) being requested - pub rev: Option, - /// Tree path being requested - pub path: Option, -} diff --git a/src/messages/user.rs b/src/messages/user.rs deleted file mode 100644 index 291368c..0000000 --- a/src/messages/user.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -use crate::model::{repository::RepositorySummary, user::User}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserDisplayNameRequest { - pub user: User, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserDisplayNameResponse { - pub display_name: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserDisplayImageRequest { - pub user: User, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserDisplayImageResponse { - pub image_url: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserBioRequest { - pub user: User, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserBioResponse { - pub bio: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserRepositoriesRequest { - pub user: User, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserRepositoriesResponse { - pub repositories: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserSettingsRequest { - pub user: User, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserSettingsResponse { - pub settings: HashMap, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserWriteSettingsRequest { - pub user: User, - pub settings: Vec<(String, serde_json::Value)>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserWriteSettingsResponse { - // IDK? -} diff --git a/src/model/authenticated.rs b/src/model/authenticated.rs deleted file mode 100644 index 8706451..0000000 --- a/src/model/authenticated.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::{any::type_name, collections::HashMap, ops::Deref}; - -use anyhow::Error; -use futures_util::Future; -use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; -use rsa::{ - pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, - pss::{Signature, SigningKey, VerifyingKey}, - sha2::Sha256, - signature::{RandomizedSigner, SignatureEncoding, Verifier}, - RsaPrivateKey, RsaPublicKey, -}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::Value; - -use crate::{authentication::UserTokenMetadata, connection::wrapper::ConnectionState}; - -use super::{instance::Instance, user::User}; - -#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct Authenticated { - // TODO: Can not flatten vec's/enums, might want to just type tag the enum instead. not recommended for anything but languages like JSON anyways - // #[serde(flatten)] - source: Vec, - message_type: String, - #[serde(flatten)] - message: T, -} - -pub trait AuthenticationSourceProvider: Sized { - fn authenticate(self, payload: &Vec) -> AuthenticationSource; -} - -pub trait AuthenticationSourceProviders: Sized { - fn authenticate_all(self, payload: &Vec) -> Vec; -} - -impl AuthenticationSourceProviders for A -where - A: AuthenticationSourceProvider, -{ - fn authenticate_all(self, payload: &Vec) -> Vec { - vec![self.authenticate(payload)] - } -} - -impl AuthenticationSourceProviders for (A, B) -where - A: AuthenticationSourceProvider, - B: AuthenticationSourceProvider, -{ - fn authenticate_all(self, payload: &Vec) -> Vec { - let (first, second) = self; - - vec![first.authenticate(payload), second.authenticate(payload)] - } -} - -impl Authenticated { - pub fn new(message: T, auth_sources: impl AuthenticationSourceProvider) -> Self { - let message_payload = serde_json::to_vec(&message).unwrap(); - - let authentication = auth_sources.authenticate_all(&message_payload); - - Self { - source: authentication, - message_type: type_name::().to_string(), - message, - } - } - - pub fn new_empty(message: T) -> Self { - Self { - source: vec![], - message_type: type_name::().to_string(), - message, - } - } - - pub fn append_authentication(&mut self, authentication: impl AuthenticationSourceProvider) { - let message_payload = serde_json::to_vec(&self.message).unwrap(); - - self.source - .push(authentication.authenticate(&message_payload)); - } -} - -mod verified {} - -#[derive(Clone, Debug)] -pub struct UserAuthenticator { - pub user: User, - pub token: UserAuthenticationToken, -} - -impl AuthenticationSourceProvider for UserAuthenticator { - fn authenticate(self, _payload: &Vec) -> AuthenticationSource { - AuthenticationSource::User { - user: self.user, - token: self.token, - } - } -} - -#[derive(Clone)] -pub struct InstanceAuthenticator<'a> { - pub instance: Instance, - pub private_key: &'a str, -} - -impl AuthenticationSourceProvider for InstanceAuthenticator<'_> { - fn authenticate(self, payload: &Vec) -> AuthenticationSource { - let mut rng = rand::thread_rng(); - - let private_key = RsaPrivateKey::from_pkcs1_pem(self.private_key).unwrap(); - let signing_key = SigningKey::::new(private_key); - let signature = signing_key.sign_with_rng(&mut rng, &payload); - - AuthenticationSource::Instance { - instance: self.instance, - // TODO: Actually parse signature from private key - signature: InstanceSignature(signature.to_bytes().into_vec()), - } - } -} - -#[repr(transparent)] -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserAuthenticationToken(String); - -impl From for UserAuthenticationToken { - fn from(value: String) -> Self { - Self(value) - } -} - -impl ToString for UserAuthenticationToken { - fn to_string(&self) -> String { - self.0.clone() - } -} - -impl AsRef for UserAuthenticationToken { - fn as_ref(&self) -> &str { - &self.0 - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct InstanceSignature(Vec); - -impl AsRef<[u8]> for InstanceSignature { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub enum AuthenticationSource { - User { - user: User, - token: UserAuthenticationToken, - }, - Instance { - instance: Instance, - signature: InstanceSignature, - }, -} - -pub struct NetworkMessage(pub Vec); - -impl Deref for NetworkMessage { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct AuthenticatedUser(pub User); - -#[derive(Debug, thiserror::Error)] -pub enum UserAuthenticationError { - #[error("user authentication missing")] - Missing, - // #[error("{0}")] - // InstanceAuthentication(#[from] Error), - #[error("user token was invalid")] - InvalidToken, - #[error("an error has occured")] - Other(#[from] Error), -} - -pub struct AuthenticatedInstance(Instance); - -impl AuthenticatedInstance { - pub fn inner(&self) -> &Instance { - &self.0 - } -} - -#[async_trait::async_trait] -pub trait FromMessage: Sized + Send + Sync { - async fn from_message(message: &NetworkMessage, state: &S) -> Result; -} - -#[async_trait::async_trait] -impl FromMessage for AuthenticatedUser { - async fn from_message( - network_message: &NetworkMessage, - state: &ConnectionState, - ) -> Result { - let message: Authenticated> = - serde_json::from_slice(&network_message).map_err(|e| Error::from(e))?; - - let (auth_user, auth_token) = message - .source - .iter() - .filter_map(|auth| { - if let AuthenticationSource::User { user, token } = auth { - Some((user, token)) - } else { - None - } - }) - .next() - .ok_or_else(|| UserAuthenticationError::Missing)?; - - let authenticated_instance = - AuthenticatedInstance::from_message(network_message, state).await?; - - let public_key_raw = public_key(&auth_user.instance).await?; - let verification_key = DecodingKey::from_rsa_pem(public_key_raw.as_bytes()).unwrap(); - - let data: TokenData = decode( - auth_token.as_ref(), - &verification_key, - &Validation::new(Algorithm::RS256), - ) - .unwrap(); - - if data.claims.user != *auth_user - || data.claims.generated_for != *authenticated_instance.inner() - { - Err(Error::from(UserAuthenticationError::InvalidToken)) - } else { - Ok(AuthenticatedUser(data.claims.user)) - } - } -} - -#[async_trait::async_trait] -impl FromMessage for AuthenticatedInstance { - async fn from_message( - network_message: &NetworkMessage, - state: &ConnectionState, - ) -> Result { - let message: Authenticated = - serde_json::from_slice(&network_message).map_err(|e| Error::from(e))?; - - let (instance, signature) = message - .source - .iter() - .filter_map(|auth: &AuthenticationSource| { - if let AuthenticationSource::Instance { - instance, - signature, - } = auth - { - Some((instance, signature)) - } else { - None - } - }) - .next() - // TODO: Instance authentication error - .ok_or_else(|| UserAuthenticationError::Missing)?; - - let public_key = { - let cached_keys = state.cached_keys.read().await; - - if let Some(key) = cached_keys.get(&instance) { - key.clone() - } else { - drop(cached_keys); - let mut cached_keys = state.cached_keys.write().await; - let key = public_key(instance).await?; - let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); - cached_keys.insert(instance.clone(), public_key.clone()); - public_key - } - }; - - let verifying_key: VerifyingKey = VerifyingKey::new(public_key); - - let message_json = serde_json::to_vec(&message.message).unwrap(); - - verifying_key.verify( - &message_json, - &Signature::try_from(signature.as_ref()).unwrap(), - )?; - - Ok(AuthenticatedInstance(instance.clone())) - } -} - -#[async_trait::async_trait] -impl FromMessage for Option -where - T: FromMessage, - S: Send + Sync + 'static, -{ - async fn from_message(message: &NetworkMessage, state: &S) -> Result { - Ok(T::from_message(message, state).await.ok()) - } -} - -#[async_trait::async_trait] -pub trait MessageHandler { - async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result; -} -#[async_trait::async_trait] -impl MessageHandler<(T1,), S, R> for T -where - T: FnOnce(T1) -> F + Clone + Send + 'static, - F: Future> + Send, - T1: FromMessage + Send, - S: Send + Sync, - E: std::error::Error + Send + Sync + 'static, -{ - async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result { - let value = T1::from_message(message, state).await?; - self(value).await.map_err(|e| Error::from(e)) - } -} - -#[async_trait::async_trait] -impl MessageHandler<(T1, T2), S, R> for T -where - T: FnOnce(T1, T2) -> F + Clone + Send + 'static, - F: Future> + Send, - T1: FromMessage + Send, - T2: FromMessage + Send, - S: Send + Sync, - E: std::error::Error + Send + Sync + 'static, -{ - async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result { - let value = T1::from_message(message, state).await?; - let value_2 = T2::from_message(message, state).await?; - self(value, value_2).await.map_err(|e| Error::from(e)) - } -} - -#[async_trait::async_trait] -impl MessageHandler<(T1, T2, T3), S, R> for T -where - T: FnOnce(T1, T2, T3) -> F + Clone + Send + 'static, - F: Future> + Send, - T1: FromMessage + Send, - T2: FromMessage + Send, - T3: FromMessage + Send, - S: Send + Sync, - E: std::error::Error + Send + Sync + 'static, -{ - async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result { - let value = T1::from_message(message, state).await?; - let value_2 = T2::from_message(message, state).await?; - let value_3 = T3::from_message(message, state).await?; - - self(value, value_2, value_3) - .await - .map_err(|e| Error::from(e)) - } -} - -pub struct State(pub T); - -#[async_trait::async_trait] -impl FromMessage for State -where - T: Clone + Send + Sync, -{ - async fn from_message(_: &NetworkMessage, state: &T) -> Result { - Ok(Self(state.clone())) - } -} - -// Temp -#[async_trait::async_trait] -impl FromMessage for Message -where - T: DeserializeOwned + Send + Sync + Serialize, - S: Clone + Send + Sync, -{ - async fn from_message(message: &NetworkMessage, _: &S) -> Result { - Ok(Message(serde_json::from_slice(&message)?)) - } -} - -pub struct Message(pub T); - -async fn public_key(instance: &Instance) -> Result { - let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) - .await? - .text() - .await?; - - Ok(key) -} diff --git a/src/model/discovery.rs b/src/model/discovery.rs deleted file mode 100644 index c49db64..0000000 --- a/src/model/discovery.rs +++ /dev/null @@ -1,15 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::model::{instance::Instance, repository::Repository}; - -#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub enum DiscoveryItem { - Instance { - instance: Instance, - signature: Vec, - }, - Repository { - repository: Repository, - signature: Vec, - }, -} diff --git a/src/model/instance.rs b/src/model/instance.rs deleted file mode 100644 index 45e0b20..0000000 --- a/src/model/instance.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -pub struct InstanceMeta { - pub url: String, - pub public_key: String, -} - -/// An instance, defined by the URL it can be reached at. -/// -/// # Textual Format -/// An instance's textual format is its URL. -/// -/// ## Examples -/// For the instance `giterated.dev`, the following [`Instance`] initialization -/// would be valid: -/// -/// ``` -/// let instance = Instance { -/// url: String::from("giterated.dev") -/// }; -/// -/// // This is correct -/// assert_eq!(Instance::from_str("giterated.dev").unwrap(), instance); -/// ``` -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct Instance { - pub url: String, -} - -impl ToString for Instance { - fn to_string(&self) -> String { - self.url.clone() - } -} - -impl FromStr for Instance { - type Err = InstanceParseError; - - fn from_str(s: &str) -> Result { - Ok(Self { url: s.to_string() }) - } -} - -#[derive(Debug, Error)] -pub enum InstanceParseError { - #[error("invalid format")] - InvalidFormat, -} diff --git a/src/model/mod.rs b/src/model/mod.rs deleted file mode 100644 index e09885d..0000000 --- a/src/model/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! # Giterated Network Data Model Types -//! -//! All network data model types that are not directly associated with -//! individual requests or responses. - -pub mod authenticated; -pub mod discovery; -pub mod instance; -pub mod repository; -pub mod settings; -pub mod user; diff --git a/src/model/repository.rs b/src/model/repository.rs deleted file mode 100644 index 6655aa4..0000000 --- a/src/model/repository.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; - -use super::{instance::Instance, user::User}; - -/// A repository, defined by the instance it exists on along with -/// its owner and name. -/// -/// # Textual Format -/// A repository's textual reference is defined as: -/// -/// `{owner: User}/{name: String}@{instance: Instance}` -/// -/// # Examples -/// For the repository named `foo` owned by `barson:giterated.dev` on the instance -/// `giterated.dev`, the following [`Repository`] initialization would -/// be valid: -/// -/// ``` -/// let repository = Repository { -/// owner: User::from_str("barson:giterated.dev").unwrap(), -/// name: String::from("foo"), -/// instance: Instance::from_str("giterated.dev").unwrap() -/// }; -/// -/// // This is correct -/// assert_eq!(Repository::from_str("barson:giterated.dev/foo@giterated.dev").unwrap(), repository); -/// ``` -#[derive(Hash, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Repository { - pub owner: User, - pub name: String, - /// Instance the repository is on - pub instance: Instance, -} - -impl ToString for Repository { - fn to_string(&self) -> String { - format!("{}/{}@{}", self.owner, self.name, self.instance.to_string()) - } -} - -impl FromStr for Repository { - type Err = (); - - fn from_str(s: &str) -> Result { - let mut by_ampersand = s.split('@'); - let mut path_split = by_ampersand.next().unwrap().split('/'); - - let instance = Instance::from_str(by_ampersand.next().unwrap()).unwrap(); - let owner = User::from_str(path_split.next().unwrap()).unwrap(); - let name = path_split.next().unwrap().to_string(); - - Ok(Self { - instance, - owner, - name, - }) - } -} - -/// Visibility of the repository to the general eye -#[derive(PartialEq, Eq, Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] -#[sqlx(type_name = "visibility", rename_all = "lowercase")] -pub enum RepositoryVisibility { - Public, - Unlisted, - Private, -} - -/// Implements [`Display`] for [`RepositoryVisiblity`] using [`Debug`] -impl Display for RepositoryVisibility { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RepositoryView { - /// Name of the repository - /// - /// This is different than the [`Repository`] name, - /// which may be a path. - pub name: String, - /// Owner of the Repository - pub owner: User, - /// Repository description - pub description: Option, - /// Repository visibility - pub visibility: RepositoryVisibility, - /// Default branch of the repository - pub default_branch: String, - /// Last commit made to the repository - pub latest_commit: Option, - /// Revision of the displayed tree - pub tree_rev: Option, - /// Repository tree - pub tree: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum RepositoryObjectType { - Tree, - Blob, -} - -/// Stored info for our tree entries -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RepositoryTreeEntry { - /// Name of the tree/blob - pub name: String, - /// Type of the tree entry - pub object_type: RepositoryObjectType, - /// Git supplies us with the mode at all times, and people like it displayed. - pub mode: i32, - /// File size - pub size: Option, - /// Last commit made to the tree/blob - pub last_commit: Option, -} - -impl RepositoryTreeEntry { - // I love you Emilia <3 - pub fn new(name: &str, object_type: RepositoryObjectType, mode: i32) -> Self { - Self { - name: name.to_string(), - object_type, - mode, - size: None, - last_commit: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RepositoryTreeEntryWithCommit { - pub tree_entry: RepositoryTreeEntry, - pub commit: Commit, -} - -/// Info about a git commit -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -pub struct Commit { - /// Unique commit ID - pub oid: String, - /// Full commit message - pub message: Option, - /// Who created the commit - pub author: CommitSignature, - /// Who committed the commit - pub committer: CommitSignature, - /// Time when the commit happened - pub time: chrono::NaiveDateTime, -} - -/// Gets all info from [`git2::Commit`] for easy use -impl From> for Commit { - fn from(commit: git2::Commit<'_>) -> Self { - Self { - oid: commit.id().to_string(), - message: commit.message().map(|message| message.to_string()), - author: commit.author().into(), - committer: commit.committer().into(), - time: chrono::NaiveDateTime::from_timestamp_opt(commit.time().seconds(), 0).unwrap(), - } - } -} - -/// Git commit signature -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -pub struct CommitSignature { - pub name: Option, - pub email: Option, - pub time: chrono::NaiveDateTime, -} - -/// Converts the signature from git2 into something usable without explicit lifetimes. -impl From> for CommitSignature { - fn from(signature: git2::Signature<'_>) -> Self { - Self { - name: signature.name().map(|name| name.to_string()), - email: signature.email().map(|email| email.to_string()), - time: chrono::NaiveDateTime::from_timestamp_opt(signature.when().seconds(), 0).unwrap(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct RepositorySummary { - pub repository: Repository, - pub owner: User, - pub visibility: RepositoryVisibility, - pub description: Option, - pub last_commit: Option, -} diff --git a/src/model/settings.rs b/src/model/settings.rs deleted file mode 100644 index a3ea173..0000000 --- a/src/model/settings.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub trait Setting: Serialize { - fn name(&self) -> &'static str; -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserBio(pub String); - -impl Setting for UserBio { - fn name(&self) -> &'static str { - "Bio" - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserDisplayName(pub String); - -impl Setting for UserDisplayName { - fn name(&self) -> &'static str { - "Display Name" - } -} diff --git a/src/model/user.rs b/src/model/user.rs deleted file mode 100644 index 5fa8ead..0000000 --- a/src/model/user.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; - -use super::instance::Instance; - -/// A user, defined by its username and instance. -/// -/// # Textual Format -/// A user's textual reference is defined as: -/// -/// `{username: String}:{instance: Instance}` -/// -/// # Examples -/// For the user with the username `barson` and the instance `giterated.dev`, -/// the following [`User`] initialization would be valid: -/// -/// ``` -/// let user = User { -/// username: String::from("barson"), -/// instance: Instance::from_str("giterated.dev").unwrap() -/// }; -/// -/// // This is correct -/// assert_eq!(User::from_str("barson:giterated.dev").unwrap(), user); -/// ``` -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct User { - pub username: String, - pub instance: Instance, -} - -impl Display for User { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.username, self.instance.url) - } -} - -impl From for User { - fn from(user_string: String) -> Self { - User::from_str(&user_string).unwrap() - } -} - -impl FromStr for User { - type Err = (); - - fn from_str(s: &str) -> Result { - let mut colon_split = s.split(':'); - let username = colon_split.next().unwrap().to_string(); - let instance = Instance::from_str(colon_split.next().unwrap()).unwrap(); - - Ok(Self { username, instance }) - } -}