Completely refactor project structure
parent: tbd commit: ae8ff44
Showing 74 changed files with 3405 insertions and 3297 deletions
Cargo.lock
@@ -639,6 +639,37 @@ dependencies = [ | ||
639 | 639 | "chrono", |
640 | 640 | "futures-util", |
641 | 641 | "git2", |
642 | "giterated-models", | |
643 | "jsonwebtoken", | |
644 | "log", | |
645 | "rand", | |
646 | "reqwest", | |
647 | "rsa", | |
648 | "semver", | |
649 | "serde", | |
650 | "serde_json", | |
651 | "sqlx", | |
652 | "thiserror", | |
653 | "tokio", | |
654 | "tokio-tungstenite", | |
655 | "toml", | |
656 | "tower", | |
657 | "tracing", | |
658 | "tracing-subscriber", | |
659 | ] | |
660 | ||
661 | [[package]] | |
662 | name = "giterated-models" | |
663 | version = "0.1.0" | |
664 | dependencies = [ | |
665 | "aes-gcm", | |
666 | "anyhow", | |
667 | "argon2", | |
668 | "async-trait", | |
669 | "base64 0.21.3", | |
670 | "chrono", | |
671 | "futures-util", | |
672 | "git2", | |
642 | 673 | "jsonwebtoken", |
643 | 674 | "log", |
644 | 675 | "rand", |
Cargo.toml
@@ -1,38 +1,5 @@ | ||
1 | [package] | |
2 | name = "giterated-daemon" | |
3 | version = "0.0.6" | |
4 | edition = "2021" | |
5 | ||
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
7 | ||
8 | [dependencies] | |
9 | tokio-tungstenite = "*" | |
10 | tokio = { version = "1.32.0", features = [ "full" ] } | |
11 | tracing = "*" | |
12 | futures-util = "*" | |
13 | serde = { version = "1.0.188", features = [ "derive" ]} | |
14 | serde_json = "1.0" | |
15 | tracing-subscriber = "0.3" | |
16 | base64 = "0.21.3" | |
17 | jsonwebtoken = { version = "*", features = ["use_pem"]} | |
18 | log = "*" | |
19 | rand = "*" | |
20 | rsa = {version = "0.9", features = ["sha2"]} | |
21 | reqwest = "*" | |
22 | argon2 = "*" | |
23 | aes-gcm = "0.10.2" | |
24 | semver = {version = "*", features = ["serde"]} | |
25 | tower = "*" | |
26 | ||
27 | toml = { version = "0.7" } | |
28 | ||
29 | chrono = { version = "0.4", features = [ "serde" ] } | |
30 | async-trait = "0.1" | |
31 | ||
32 | # Git backend | |
33 | git2 = "0.17" | |
34 | thiserror = "1" | |
35 | anyhow = "1" | |
36 | sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } | |
37 | ||
38 | #uuid = { version = "1.4", features = [ "v4", "serde" ] } | |
1 | [workspace] | |
2 | members = [ | |
3 | "giterated-daemon", | |
4 | "giterated-models" | |
5 | ] | |
5 | \ No newline at end of file |
giterated-daemon/Cargo.toml
@@ -0,0 +1,39 @@ | ||
1 | [package] | |
2 | name = "giterated-daemon" | |
3 | version = "0.0.6" | |
4 | edition = "2021" | |
5 | ||
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
7 | ||
8 | [dependencies] | |
9 | tokio-tungstenite = "*" | |
10 | tokio = { version = "1.32.0", features = [ "full" ] } | |
11 | tracing = "*" | |
12 | futures-util = "*" | |
13 | serde = { version = "1.0.188", features = [ "derive" ]} | |
14 | serde_json = "1.0" | |
15 | tracing-subscriber = "0.3" | |
16 | base64 = "0.21.3" | |
17 | jsonwebtoken = { version = "*", features = ["use_pem"]} | |
18 | log = "*" | |
19 | rand = "*" | |
20 | rsa = {version = "0.9", features = ["sha2"]} | |
21 | reqwest = "*" | |
22 | argon2 = "*" | |
23 | aes-gcm = "0.10.2" | |
24 | semver = {version = "*", features = ["serde"]} | |
25 | tower = "*" | |
26 | giterated-models = { path = "../giterated-models" } | |
27 | ||
28 | toml = { version = "0.7" } | |
29 | ||
30 | chrono = { version = "0.4", features = [ "serde" ] } | |
31 | async-trait = "0.1" | |
32 | ||
33 | # Git backend | |
34 | git2 = "0.17" | |
35 | thiserror = "1" | |
36 | anyhow = "1" | |
37 | sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } | |
38 | ||
39 | #uuid = { version = "1.4", features = [ "v4", "serde" ] } |
giterated-daemon/migrations/20230828083716_create_repositories.sql
@@ -0,0 +1,18 @@ | ||
1 | CREATE TYPE visibility AS ENUM | |
2 | ( | |
3 | 'public', | |
4 | 'unlisted', | |
5 | 'private' | |
6 | ); | |
7 | ||
8 | CREATE TABLE IF NOT EXISTS repositories | |
9 | ( | |
10 | username TEXT NOT NULL, | |
11 | instance_url TEXT NOT NULL, | |
12 | name TEXT NOT NULL, | |
13 | description TEXT, | |
14 | visibility visibility NOT NULL, | |
15 | default_branch TEXT NOT NULL | |
16 | ); | |
17 | ||
18 | CREATE UNIQUE INDEX unique_name_per_user ON repositories (username, instance_url, name); |
giterated-daemon/migrations/20230829014608_combine_user_instance_url_repositories.sql
@@ -0,0 +1,5 @@ | ||
1 | ALTER TABLE repositories | |
2 | DROP COLUMN instance_url; | |
3 | ||
4 | ALTER TABLE repositories | |
5 | RENAME COLUMN username TO owner_user; |
giterated-daemon/migrations/20230829104151_create_users.sql
@@ -0,0 +1,15 @@ | ||
1 | -- Add migration script here | |
2 | ||
3 | CREATE TABLE IF NOT EXISTS users | |
4 | ( | |
5 | username TEXT NOT NULL, | |
6 | display_name TEXT, | |
7 | image_url TEXT NOT NULL, | |
8 | bio TEXT, | |
9 | email TEXT, | |
10 | password TEXT NOT NULL, | |
11 | public_key TEXT NOT NULL, | |
12 | enc_private_key TEXT NOT NULL | |
13 | ); | |
14 | ||
15 | CREATE UNIQUE INDEX unique_username ON users (username); | |
15 | \ No newline at end of file |
giterated-daemon/migrations/20230829165636_discovery.sql
@@ -0,0 +1,13 @@ | ||
1 | CREATE TYPE discovery_type AS ENUM | |
2 | ( | |
3 | 'instance', | |
4 | 'repository' | |
5 | ); | |
6 | ||
7 | CREATE TABLE IF NOT EXISTS discoveries | |
8 | ( | |
9 | discovery_hash TEXT PRIMARY KEY UNIQUE NOT NULL, | |
10 | discovery_time TEXT NOT NULL, | |
11 | discovery_type discovery_type NOT NULL, | |
12 | discovery TEXT NOT NULL | |
13 | ); | |
13 | \ No newline at end of file |
giterated-daemon/migrations/20230902095036_user_settings.sql
@@ -0,0 +1,8 @@ | ||
1 | CREATE TABLE IF NOT EXISTS user_settings | |
2 | ( | |
3 | username TEXT NOT NULL, | |
4 | name TEXT NOT NULL, | |
5 | value TEXT NOT NULL | |
6 | ); | |
7 | ||
8 | CREATE UNIQUE INDEX unique_per_name ON user_settings (username, name); | |
8 | \ No newline at end of file |
giterated-daemon/src/authentication.rs
@@ -0,0 +1,178 @@ | ||
1 | use anyhow::Error; | |
2 | use giterated_models::{ | |
3 | messages::authentication::{AuthenticationTokenResponse, TokenExtensionResponse}, | |
4 | model::{ | |
5 | authenticated::{UserAuthenticationToken, UserTokenMetadata}, | |
6 | instance::Instance, | |
7 | user::User, | |
8 | }, | |
9 | }; | |
10 | use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, TokenData, Validation}; | |
11 | use std::time::SystemTime; | |
12 | use tokio::{fs::File, io::AsyncReadExt}; | |
13 | use toml::Table; | |
14 | ||
15 | pub struct AuthenticationTokenGranter { | |
16 | pub config: Table, | |
17 | pub instance: Instance, | |
18 | } | |
19 | ||
20 | impl AuthenticationTokenGranter { | |
21 | async fn private_key(&self) -> Vec<u8> { | |
22 | let _secret_key = self.config["authentication"]["secret_key"] | |
23 | .as_str() | |
24 | .unwrap(); | |
25 | let mut file = File::open( | |
26 | self.config["giterated"]["keys"]["private"] | |
27 | .as_str() | |
28 | .unwrap(), | |
29 | ) | |
30 | .await | |
31 | .unwrap(); | |
32 | ||
33 | let mut key = vec![]; | |
34 | file.read_to_end(&mut key).await.unwrap(); | |
35 | ||
36 | key | |
37 | } | |
38 | ||
39 | pub(crate) async fn create_token_for( | |
40 | &mut self, | |
41 | user: &User, | |
42 | generated_for: &Instance, | |
43 | ) -> String { | |
44 | let private_key = self.private_key().await; | |
45 | ||
46 | let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); | |
47 | ||
48 | let claims = UserTokenMetadata { | |
49 | user: user.clone(), | |
50 | generated_for: generated_for.clone(), | |
51 | exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() | |
52 | + std::time::Duration::from_secs(24 * 60 * 60)) | |
53 | .as_secs(), | |
54 | }; | |
55 | ||
56 | encode( | |
57 | &jsonwebtoken::Header::new(Algorithm::RS256), | |
58 | &claims, | |
59 | &encoding_key, | |
60 | ) | |
61 | .unwrap() | |
62 | } | |
63 | ||
64 | pub async fn token_request( | |
65 | &mut self, | |
66 | issued_for: impl ToOwned<Owned = Instance>, | |
67 | username: String, | |
68 | _password: String, | |
69 | ) -> Result<AuthenticationTokenResponse, Error> { | |
70 | let private_key = { | |
71 | let mut file = File::open( | |
72 | self.config["giterated"]["keys"]["private"] | |
73 | .as_str() | |
74 | .unwrap(), | |
75 | ) | |
76 | .await | |
77 | .unwrap(); | |
78 | ||
79 | let mut key = vec![]; | |
80 | file.read_to_end(&mut key).await.unwrap(); | |
81 | ||
82 | key | |
83 | }; | |
84 | ||
85 | let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); | |
86 | ||
87 | let claims = UserTokenMetadata { | |
88 | user: User { | |
89 | username, | |
90 | instance: self.instance.clone(), | |
91 | }, | |
92 | generated_for: issued_for.to_owned(), | |
93 | exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() | |
94 | + std::time::Duration::from_secs(24 * 60 * 60)) | |
95 | .as_secs(), | |
96 | }; | |
97 | ||
98 | let token = encode( | |
99 | &jsonwebtoken::Header::new(Algorithm::RS256), | |
100 | &claims, | |
101 | &encoding_key, | |
102 | ) | |
103 | .unwrap(); | |
104 | ||
105 | Ok(AuthenticationTokenResponse { | |
106 | token: UserAuthenticationToken::from(token), | |
107 | }) | |
108 | } | |
109 | ||
110 | pub async fn extension_request( | |
111 | &mut self, | |
112 | issued_for: &Instance, | |
113 | token: UserAuthenticationToken, | |
114 | ) -> Result<TokenExtensionResponse, Error> { | |
115 | let server_public_key = public_key(&self.instance).await.unwrap(); | |
116 | ||
117 | let verification_key = DecodingKey::from_rsa_pem(server_public_key.as_bytes()).unwrap(); | |
118 | ||
119 | let data: TokenData<UserTokenMetadata> = decode( | |
120 | token.as_ref(), | |
121 | &verification_key, | |
122 | &Validation::new(Algorithm::RS256), | |
123 | ) | |
124 | .unwrap(); | |
125 | ||
126 | if data.claims.generated_for != *issued_for { | |
127 | panic!() | |
128 | } | |
129 | ||
130 | info!("Token Extension Request Token validated"); | |
131 | ||
132 | let private_key = { | |
133 | let mut file = File::open( | |
134 | self.config["giterated"]["keys"]["private"] | |
135 | .as_str() | |
136 | .unwrap(), | |
137 | ) | |
138 | .await | |
139 | .unwrap(); | |
140 | ||
141 | let mut key = vec![]; | |
142 | file.read_to_end(&mut key).await.unwrap(); | |
143 | ||
144 | key | |
145 | }; | |
146 | ||
147 | let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); | |
148 | ||
149 | let claims = UserTokenMetadata { | |
150 | // TODO: Probably exploitable | |
151 | user: data.claims.user, | |
152 | generated_for: issued_for.clone(), | |
153 | exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() | |
154 | + std::time::Duration::from_secs(24 * 60 * 60)) | |
155 | .as_secs(), | |
156 | }; | |
157 | ||
158 | let token = encode( | |
159 | &jsonwebtoken::Header::new(Algorithm::RS256), | |
160 | &claims, | |
161 | &encoding_key, | |
162 | ) | |
163 | .unwrap(); | |
164 | ||
165 | Ok(TokenExtensionResponse { | |
166 | new_token: Some(token), | |
167 | }) | |
168 | } | |
169 | } | |
170 | ||
171 | async fn public_key(instance: &Instance) -> Result<String, Error> { | |
172 | let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) | |
173 | .await? | |
174 | .text() | |
175 | .await?; | |
176 | ||
177 | Ok(key) | |
178 | } |
giterated-daemon/src/backend/discovery.rs
@@ -0,0 +1,24 @@ | ||
1 | use std::hash::Hash; | |
2 | ||
3 | use chrono::{DateTime, Utc}; | |
4 | use serde::{Deserialize, Serialize}; | |
5 | use sqlx::PgPool; | |
6 | ||
7 | pub struct GiteratedDiscoveryProtocol { | |
8 | pub pool: PgPool, | |
9 | } | |
10 | ||
11 | #[derive(Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] | |
12 | #[sqlx(type_name = "discovery_type", rename_all = "lowercase")] | |
13 | pub enum DiscoveryType { | |
14 | Instance, | |
15 | Repository, | |
16 | } | |
17 | ||
18 | #[derive(Debug, sqlx::FromRow, sqlx::Type)] | |
19 | pub struct DiscoveriesRow { | |
20 | discovery_hash: String, | |
21 | discovery_time: DateTime<Utc>, | |
22 | discovery_type: DiscoveryType, | |
23 | discovery: String, | |
24 | } |
giterated-daemon/src/backend/git.rs
@@ -0,0 +1,505 @@ | ||
1 | use anyhow::Error; | |
2 | use async_trait::async_trait; | |
3 | use futures_util::StreamExt; | |
4 | use git2::ObjectType; | |
5 | use giterated_models::{ | |
6 | messages::repository::{ | |
7 | RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, | |
8 | RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, | |
9 | RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse, | |
10 | RepositoryIssuesRequest, RepositoryIssuesResponse, | |
11 | }, | |
12 | model::{ | |
13 | instance::Instance, | |
14 | repository::{ | |
15 | Commit, Repository, RepositoryObjectType, RepositorySummary, RepositoryTreeEntry, | |
16 | RepositoryView, RepositoryVisibility, | |
17 | }, | |
18 | user::User, | |
19 | }, | |
20 | }; | |
21 | use sqlx::{Either, PgPool}; | |
22 | use std::path::{Path, PathBuf}; | |
23 | use thiserror::Error; | |
24 | ||
25 | use super::{IssuesBackend, RepositoryBackend}; | |
26 | ||
27 | // TODO: Handle this | |
28 | //region database structures | |
29 | ||
30 | /// Repository in the database | |
31 | #[derive(Debug, sqlx::FromRow)] | |
32 | pub struct GitRepository { | |
33 | #[sqlx(try_from = "String")] | |
34 | pub owner_user: User, | |
35 | pub name: String, | |
36 | pub description: Option<String>, | |
37 | pub visibility: RepositoryVisibility, | |
38 | pub default_branch: String, | |
39 | } | |
40 | ||
41 | impl GitRepository { | |
42 | // Separate function because "Private" will be expanded later | |
43 | /// Checks if the user is allowed to view this repository | |
44 | pub fn can_user_view_repository(&self, user: Option<&User>) -> bool { | |
45 | !matches!(self.visibility, RepositoryVisibility::Private) | |
46 | || (matches!(self.visibility, RepositoryVisibility::Private) | |
47 | && Some(&self.owner_user) == user) | |
48 | } | |
49 | ||
50 | // This is in it's own function because I assume I'll have to add logic to this later | |
51 | pub fn open_git2_repository( | |
52 | &self, | |
53 | repository_directory: &str, | |
54 | ) -> Result<git2::Repository, GitBackendError> { | |
55 | match git2::Repository::open(format!( | |
56 | "{}/{}/{}/{}", | |
57 | repository_directory, self.owner_user.instance.url, self.owner_user.username, self.name | |
58 | )) { | |
59 | Ok(repository) => Ok(repository), | |
60 | Err(err) => { | |
61 | let err = GitBackendError::FailedOpeningFromDisk(err); | |
62 | error!("Couldn't open a repository, this is bad! {:?}", err); | |
63 | ||
64 | Err(err) | |
65 | } | |
66 | } | |
67 | } | |
68 | } | |
69 | ||
70 | //endregion | |
71 | ||
72 | #[derive(Error, Debug)] | |
73 | pub enum GitBackendError { | |
74 | #[error("Failed creating repository")] | |
75 | FailedCreatingRepository(git2::Error), | |
76 | #[error("Failed inserting into the database")] | |
77 | FailedInsertingIntoDatabase(sqlx::Error), | |
78 | #[error("Failed finding repository {owner_user:?}/{name:?}")] | |
79 | RepositoryNotFound { owner_user: String, name: String }, | |
80 | #[error("Repository {owner_user:?}/{name:?} already exists")] | |
81 | RepositoryAlreadyExists { owner_user: String, name: String }, | |
82 | #[error("Repository couldn't be deleted from the disk")] | |
83 | CouldNotDeleteFromDisk(std::io::Error), | |
84 | #[error("Failed deleting repository from database")] | |
85 | FailedDeletingFromDatabase(sqlx::Error), | |
86 | #[error("Failed opening repository on disk")] | |
87 | FailedOpeningFromDisk(git2::Error), | |
88 | #[error("Couldn't find ref with name `{0}`")] | |
89 | RefNotFound(String), | |
90 | #[error("Couldn't find path in repository `{0}`")] | |
91 | PathNotFound(String), | |
92 | #[error("Couldn't find commit for path `{0}`")] | |
93 | LastCommitNotFound(String), | |
94 | } | |
95 | ||
96 | pub struct GitBackend { | |
97 | pub pg_pool: PgPool, | |
98 | pub repository_folder: String, | |
99 | pub instance: Instance, | |
100 | } | |
101 | ||
102 | impl GitBackend { | |
103 | pub fn new( | |
104 | pg_pool: &PgPool, | |
105 | repository_folder: &str, | |
106 | instance: impl ToOwned<Owned = Instance>, | |
107 | ) -> Self { | |
108 | Self { | |
109 | pg_pool: pg_pool.clone(), | |
110 | repository_folder: repository_folder.to_string(), | |
111 | instance: instance.to_owned(), | |
112 | } | |
113 | } | |
114 | ||
115 | pub async fn find_by_owner_user_name( | |
116 | &self, | |
117 | user: &User, | |
118 | repository_name: &str, | |
119 | ) -> Result<GitRepository, GitBackendError> { | |
120 | if let Ok(repository) = sqlx::query_as!(GitRepository, | |
121 | r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#, | |
122 | user.to_string(), repository_name) | |
123 | .fetch_one(&self.pg_pool.clone()) | |
124 | .await { | |
125 | Ok(repository) | |
126 | } else { | |
127 | Err(GitBackendError::RepositoryNotFound { | |
128 | owner_user: user.to_string(), | |
129 | name: repository_name.to_string(), | |
130 | }) | |
131 | } | |
132 | } | |
133 | ||
134 | pub async fn delete_by_owner_user_name( | |
135 | &self, | |
136 | user: &User, | |
137 | repository_name: &str, | |
138 | ) -> Result<u64, GitBackendError> { | |
139 | if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!( | |
140 | "{}/{}/{}/{}", | |
141 | self.repository_folder, user.instance.url, user.username, repository_name | |
142 | ))) { | |
143 | let err = GitBackendError::CouldNotDeleteFromDisk(err); | |
144 | error!( | |
145 | "Couldn't delete repository from disk, this is bad! {:?}", | |
146 | err | |
147 | ); | |
148 | ||
149 | return Err(err); | |
150 | } | |
151 | ||
152 | // Delete the repository from the database | |
153 | match sqlx::query!( | |
154 | "DELETE FROM repositories WHERE owner_user = $1 AND name = $2", | |
155 | user.to_string(), | |
156 | repository_name | |
157 | ) | |
158 | .execute(&self.pg_pool.clone()) | |
159 | .await | |
160 | { | |
161 | Ok(deleted) => Ok(deleted.rows_affected()), | |
162 | Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)), | |
163 | } | |
164 | } | |
165 | ||
166 | // TODO: Find where this fits | |
167 | // TODO: Cache this and general repository tree and invalidate select files on push | |
168 | // TODO: Find better and faster technique for this | |
169 | pub fn get_last_commit_of_file( | |
170 | path: &str, | |
171 | git: &git2::Repository, | |
172 | start_commit: &git2::Commit, | |
173 | ) -> anyhow::Result<Commit> { | |
174 | let mut revwalk = git.revwalk()?; | |
175 | revwalk.set_sorting(git2::Sort::TIME)?; | |
176 | revwalk.push(start_commit.id())?; | |
177 | ||
178 | for oid in revwalk { | |
179 | let oid = oid?; | |
180 | let commit = git.find_commit(oid)?; | |
181 | ||
182 | // Merge commits have 2 or more parents | |
183 | // Commits with 0 parents are handled different because we can't diff against them | |
184 | if commit.parent_count() == 0 { | |
185 | return Ok(commit.into()); | |
186 | } else if commit.parent_count() == 1 { | |
187 | let tree = commit.tree()?; | |
188 | let last_tree = commit.parent(0)?.tree()?; | |
189 | ||
190 | // Get the diff between the current tree and the last one | |
191 | let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?; | |
192 | ||
193 | for dd in diff.deltas() { | |
194 | // Get the path of the current file we're diffing against | |
195 | let current_path = dd.new_file().path().unwrap(); | |
196 | ||
197 | // Path or directory | |
198 | if current_path.eq(Path::new(&path)) || current_path.starts_with(path) { | |
199 | return Ok(commit.into()); | |
200 | } | |
201 | } | |
202 | } | |
203 | } | |
204 | ||
205 | Err(GitBackendError::LastCommitNotFound(path.to_string()))? | |
206 | } | |
207 | } | |
208 | ||
209 | #[async_trait] | |
210 | impl RepositoryBackend for GitBackend { | |
211 | async fn create_repository( | |
212 | &mut self, | |
213 | _user: &User, | |
214 | request: &RepositoryCreateRequest, | |
215 | ) -> Result<RepositoryCreateResponse, GitBackendError> { | |
216 | // Check if repository already exists in the database | |
217 | if let Ok(repository) = self | |
218 | .find_by_owner_user_name(&request.owner, &request.name) | |
219 | .await | |
220 | { | |
221 | let err = GitBackendError::RepositoryAlreadyExists { | |
222 | owner_user: repository.owner_user.to_string(), | |
223 | name: repository.name, | |
224 | }; | |
225 | error!("{:?}", err); | |
226 | ||
227 | return Err(err); | |
228 | } | |
229 | ||
230 | // Insert the repository into the database | |
231 | let _ = match sqlx::query_as!(GitRepository, | |
232 | r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#, | |
233 | request.owner.to_string(), request.name, request.description, request.visibility as _, "master") | |
234 | .fetch_one(&self.pg_pool.clone()) | |
235 | .await { | |
236 | Ok(repository) => repository, | |
237 | Err(err) => { | |
238 | let err = GitBackendError::FailedInsertingIntoDatabase(err); | |
239 | error!("Failed inserting into the database! {:?}", err); | |
240 | ||
241 | return Err(err); | |
242 | } | |
243 | }; | |
244 | ||
245 | // Create bare (server side) repository on disk | |
246 | match git2::Repository::init_bare(PathBuf::from(format!( | |
247 | "{}/{}/{}/{}", | |
248 | self.repository_folder, | |
249 | request.owner.instance.url, | |
250 | request.owner.username, | |
251 | request.name | |
252 | ))) { | |
253 | Ok(_) => { | |
254 | debug!( | |
255 | "Created new repository with the name {}/{}/{}", | |
256 | request.owner.instance.url, request.owner.username, request.name | |
257 | ); | |
258 | Ok(RepositoryCreateResponse) | |
259 | } | |
260 | Err(err) => { | |
261 | let err = GitBackendError::FailedCreatingRepository(err); | |
262 | error!("Failed creating repository on disk!? {:?}", err); | |
263 | ||
264 | // Delete repository from database | |
265 | self.delete_by_owner_user_name(&request.owner, request.name.as_str()) | |
266 | .await?; | |
267 | ||
268 | // ??? | |
269 | Err(err) | |
270 | } | |
271 | } | |
272 | } | |
273 | ||
274 | async fn repository_info( | |
275 | &mut self, | |
276 | requester: Option<&User>, | |
277 | request: &RepositoryInfoRequest, | |
278 | ) -> Result<RepositoryView, Error> { | |
279 | let repository = match self | |
280 | .find_by_owner_user_name( | |
281 | // &request.owner.instance.url, | |
282 | &request.repository.owner, | |
283 | &request.repository.name, | |
284 | ) | |
285 | .await | |
286 | { | |
287 | Ok(repository) => repository, | |
288 | Err(err) => return Err(Box::new(err).into()), | |
289 | }; | |
290 | ||
291 | if let Some(requester) = requester { | |
292 | if !repository.can_user_view_repository(Some(&requester)) { | |
293 | return Err(Box::new(GitBackendError::RepositoryNotFound { | |
294 | owner_user: request.repository.owner.to_string(), | |
295 | name: request.repository.name.clone(), | |
296 | }) | |
297 | .into()); | |
298 | } | |
299 | } else if matches!(repository.visibility, RepositoryVisibility::Private) { | |
300 | // Unauthenticated users can never view private repositories | |
301 | ||
302 | return Err(Box::new(GitBackendError::RepositoryNotFound { | |
303 | owner_user: request.repository.owner.to_string(), | |
304 | name: request.repository.name.clone(), | |
305 | }) | |
306 | .into()); | |
307 | } | |
308 | ||
309 | let git = match repository.open_git2_repository(&self.repository_folder) { | |
310 | Ok(git) => git, | |
311 | Err(err) => return Err(Box::new(err).into()), | |
312 | }; | |
313 | ||
314 | let rev_name = match &request.rev { | |
315 | None => { | |
316 | if let Ok(head) = git.head() { | |
317 | head.name().unwrap().to_string() | |
318 | } else { | |
319 | // Nothing in database, render empty tree. | |
320 | return Ok(RepositoryView { | |
321 | name: repository.name, | |
322 | owner: request.repository.owner.clone(), | |
323 | description: repository.description, | |
324 | visibility: repository.visibility, | |
325 | default_branch: repository.default_branch, | |
326 | latest_commit: None, | |
327 | tree_rev: None, | |
328 | tree: vec![], | |
329 | }); | |
330 | } | |
331 | } | |
332 | Some(rev_name) => { | |
333 | // Find the reference, otherwise return GitBackendError | |
334 | match git | |
335 | .find_reference(format!("refs/heads/{}", rev_name).as_str()) | |
336 | .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) | |
337 | { | |
338 | Ok(reference) => reference.name().unwrap().to_string(), | |
339 | Err(err) => return Err(Box::new(err).into()), | |
340 | } | |
341 | } | |
342 | }; | |
343 | ||
344 | // Get the git object as a commit | |
345 | let rev = match git | |
346 | .revparse_single(rev_name.as_str()) | |
347 | .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) | |
348 | { | |
349 | Ok(rev) => rev, | |
350 | Err(err) => return Err(Box::new(err).into()), | |
351 | }; | |
352 | let commit = rev.as_commit().unwrap(); | |
353 | ||
354 | // this is stupid | |
355 | let mut current_path = rev_name.replace("refs/heads/", ""); | |
356 | ||
357 | // Get the commit tree | |
358 | let git_tree = if let Some(path) = &request.path { | |
359 | // Add it to our full path string | |
360 | current_path.push_str(format!("/{}", path).as_str()); | |
361 | // Get the specified path, return an error if it wasn't found. | |
362 | let entry = match commit | |
363 | .tree() | |
364 | .unwrap() | |
365 | .get_path(&PathBuf::from(path)) | |
366 | .map_err(|_| GitBackendError::PathNotFound(path.to_string())) | |
367 | { | |
368 | Ok(entry) => entry, | |
369 | Err(err) => return Err(Box::new(err).into()), | |
370 | }; | |
371 | // Turn the entry into a git tree | |
372 | entry.to_object(&git).unwrap().as_tree().unwrap().clone() | |
373 | } else { | |
374 | commit.tree().unwrap() | |
375 | }; | |
376 | ||
377 | // Iterate over the git tree and collect it into our own tree types | |
378 | let mut tree = git_tree | |
379 | .iter() | |
380 | .map(|entry| { | |
381 | let object_type = match entry.kind().unwrap() { | |
382 | ObjectType::Tree => RepositoryObjectType::Tree, | |
383 | ObjectType::Blob => RepositoryObjectType::Blob, | |
384 | _ => unreachable!(), | |
385 | }; | |
386 | let mut tree_entry = | |
387 | RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode()); | |
388 | ||
389 | if request.extra_metadata { | |
390 | // Get the file size if It's a blob | |
391 | let object = entry.to_object(&git).unwrap(); | |
392 | if let Some(blob) = object.as_blob() { | |
393 | tree_entry.size = Some(blob.size()); | |
394 | } | |
395 | ||
396 | // Could possibly be done better | |
397 | let path = if let Some(path) = current_path.split_once('/') { | |
398 | format!("{}/{}", path.1, entry.name().unwrap()) | |
399 | } else { | |
400 | entry.name().unwrap().to_string() | |
401 | }; | |
402 | ||
403 | // Get the last commit made to the entry | |
404 | if let Ok(last_commit) = | |
405 | GitBackend::get_last_commit_of_file(&path, &git, commit) | |
406 | { | |
407 | tree_entry.last_commit = Some(last_commit); | |
408 | } | |
409 | } | |
410 | ||
411 | tree_entry | |
412 | }) | |
413 | .collect::<Vec<RepositoryTreeEntry>>(); | |
414 | ||
415 | // Sort the tree alphabetically and with tree first | |
416 | tree.sort_unstable_by_key(|entry| entry.name.to_lowercase()); | |
417 | tree.sort_unstable_by_key(|entry| { | |
418 | std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase()) | |
419 | }); | |
420 | ||
421 | Ok(RepositoryView { | |
422 | name: repository.name, | |
423 | owner: request.repository.owner.clone(), | |
424 | description: repository.description, | |
425 | visibility: repository.visibility, | |
426 | default_branch: repository.default_branch, | |
427 | latest_commit: None, | |
428 | tree_rev: Some(rev_name), | |
429 | tree, | |
430 | }) | |
431 | } | |
432 | ||
433 | async fn repository_file_inspect( | |
434 | &mut self, | |
435 | _requester: Option<&User>, | |
436 | _request: &RepositoryFileInspectRequest, | |
437 | ) -> Result<RepositoryFileInspectionResponse, Error> { | |
438 | todo!() | |
439 | } | |
440 | ||
441 | async fn repositories_for_user( | |
442 | &mut self, | |
443 | requester: Option<&User>, | |
444 | user: &User, | |
445 | ) -> Result<Vec<RepositorySummary>, Error> { | |
446 | let mut repositories = sqlx::query_as!( | |
447 | GitRepository, | |
448 | r#"SELECT visibility as "visibility: _", owner_user, name, description, default_branch FROM repositories WHERE owner_user = $1"#, | |
449 | user.to_string() | |
450 | ) | |
451 | .fetch_many(&self.pg_pool); | |
452 | ||
453 | let mut result = vec![]; | |
454 | ||
455 | while let Some(Ok(Either::Right(repository))) = repositories.next().await { | |
456 | // Check if the requesting user is allowed to see the repository | |
457 | if !(matches!( | |
458 | repository.visibility, | |
459 | RepositoryVisibility::Unlisted | RepositoryVisibility::Private | |
460 | ) && Some(&repository.owner_user.clone()) != requester) | |
461 | { | |
462 | result.push(RepositorySummary { | |
463 | repository: Repository { | |
464 | owner: repository.owner_user.clone(), | |
465 | name: repository.name, | |
466 | instance: self.instance.clone(), | |
467 | }, | |
468 | owner: repository.owner_user.clone(), | |
469 | visibility: repository.visibility, | |
470 | description: repository.description, | |
471 | // TODO | |
472 | last_commit: None, | |
473 | }); | |
474 | } | |
475 | } | |
476 | ||
477 | Ok(result) | |
478 | } | |
479 | } | |
480 | ||
481 | impl IssuesBackend for GitBackend { | |
482 | fn issues_count( | |
483 | &mut self, | |
484 | _requester: Option<&User>, | |
485 | _request: &RepositoryIssuesCountRequest, | |
486 | ) -> Result<RepositoryIssuesCountResponse, Error> { | |
487 | todo!() | |
488 | } | |
489 | ||
490 | fn issue_labels( | |
491 | &mut self, | |
492 | _requester: Option<&User>, | |
493 | _request: &RepositoryIssueLabelsRequest, | |
494 | ) -> Result<RepositoryIssueLabelsResponse, Error> { | |
495 | todo!() | |
496 | } | |
497 | ||
498 | fn issues( | |
499 | &mut self, | |
500 | _requester: Option<&User>, | |
501 | _request: &RepositoryIssuesRequest, | |
502 | ) -> Result<RepositoryIssuesResponse, Error> { | |
503 | todo!() | |
504 | } | |
505 | } |
giterated-daemon/src/backend/github.rs
@@ -0,0 +1,2 @@ | ||
1 | //! TODO: GitHub backend to allow for login with GitHub | |
2 | //! accounts and interacting with GitHub repositories |
giterated-daemon/src/backend/mod.rs
@@ -0,0 +1,110 @@ | ||
1 | pub mod discovery; | |
2 | pub mod git; | |
3 | pub mod github; | |
4 | pub mod user; | |
5 | ||
6 | use anyhow::Error; | |
7 | use async_trait::async_trait; | |
8 | use serde_json::Value; | |
9 | ||
10 | use crate::backend::git::GitBackendError; | |
11 | use giterated_models::{ | |
12 | messages::{ | |
13 | authentication::{ | |
14 | AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, | |
15 | RegisterAccountResponse, | |
16 | }, | |
17 | repository::{ | |
18 | RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, | |
19 | RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, | |
20 | RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, | |
21 | RepositoryIssuesCountResponse, RepositoryIssuesRequest, RepositoryIssuesResponse, | |
22 | }, | |
23 | user::{ | |
24 | UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, | |
25 | UserDisplayNameRequest, UserDisplayNameResponse, | |
26 | }, | |
27 | }, | |
28 | model::{ | |
29 | repository::{RepositorySummary, RepositoryView}, | |
30 | user::User, | |
31 | }, | |
32 | }; | |
33 | ||
34 | #[async_trait] | |
35 | pub trait RepositoryBackend: IssuesBackend { | |
36 | async fn create_repository( | |
37 | &mut self, | |
38 | user: &User, | |
39 | request: &RepositoryCreateRequest, | |
40 | ) -> Result<RepositoryCreateResponse, GitBackendError>; | |
41 | async fn repository_info( | |
42 | &mut self, | |
43 | requester: Option<&User>, | |
44 | request: &RepositoryInfoRequest, | |
45 | ) -> Result<RepositoryView, Error>; | |
46 | async fn repository_file_inspect( | |
47 | &mut self, | |
48 | requester: Option<&User>, | |
49 | request: &RepositoryFileInspectRequest, | |
50 | ) -> Result<RepositoryFileInspectionResponse, Error>; | |
51 | async fn repositories_for_user( | |
52 | &mut self, | |
53 | requester: Option<&User>, | |
54 | user: &User, | |
55 | ) -> Result<Vec<RepositorySummary>, Error>; | |
56 | } | |
57 | ||
58 | pub trait IssuesBackend { | |
59 | fn issues_count( | |
60 | &mut self, | |
61 | requester: Option<&User>, | |
62 | request: &RepositoryIssuesCountRequest, | |
63 | ) -> Result<RepositoryIssuesCountResponse, Error>; | |
64 | fn issue_labels( | |
65 | &mut self, | |
66 | requester: Option<&User>, | |
67 | request: &RepositoryIssueLabelsRequest, | |
68 | ) -> Result<RepositoryIssueLabelsResponse, Error>; | |
69 | fn issues( | |
70 | &mut self, | |
71 | requester: Option<&User>, | |
72 | request: &RepositoryIssuesRequest, | |
73 | ) -> Result<RepositoryIssuesResponse, Error>; | |
74 | } | |
75 | ||
76 | #[async_trait::async_trait] | |
77 | pub trait AuthBackend { | |
78 | async fn register( | |
79 | &mut self, | |
80 | request: RegisterAccountRequest, | |
81 | ) -> Result<RegisterAccountResponse, Error>; | |
82 | ||
83 | async fn login( | |
84 | &mut self, | |
85 | request: AuthenticationTokenRequest, | |
86 | ) -> Result<AuthenticationTokenResponse, Error>; | |
87 | } | |
88 | ||
89 | #[async_trait::async_trait] | |
90 | pub trait UserBackend: AuthBackend { | |
91 | async fn display_name( | |
92 | &mut self, | |
93 | request: UserDisplayNameRequest, | |
94 | ) -> Result<UserDisplayNameResponse, Error>; | |
95 | ||
96 | async fn display_image( | |
97 | &mut self, | |
98 | request: UserDisplayImageRequest, | |
99 | ) -> Result<UserDisplayImageResponse, Error>; | |
100 | ||
101 | async fn bio(&mut self, request: UserBioRequest) -> Result<UserBioResponse, Error>; | |
102 | async fn exists(&mut self, user: &User) -> Result<bool, Error>; | |
103 | ||
104 | async fn settings(&mut self, user: &User) -> Result<Vec<(String, Value)>, Error>; | |
105 | async fn write_settings( | |
106 | &mut self, | |
107 | user: &User, | |
108 | settings: &[(String, Value)], | |
109 | ) -> Result<(), Error>; | |
110 | } |
giterated-daemon/src/backend/user.rs
@@ -0,0 +1,269 @@ | ||
1 | use std::sync::Arc; | |
2 | ||
3 | use anyhow::Error; | |
4 | ||
5 | use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; | |
6 | use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | |
7 | use base64::{engine::general_purpose::STANDARD, Engine as _}; | |
8 | use futures_util::StreamExt; | |
9 | use giterated_models::{ | |
10 | messages::{ | |
11 | authentication::{ | |
12 | AuthenticationTokenRequest, AuthenticationTokenResponse, RegisterAccountRequest, | |
13 | RegisterAccountResponse, | |
14 | }, | |
15 | user::{ | |
16 | UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, | |
17 | UserDisplayNameRequest, UserDisplayNameResponse, | |
18 | }, | |
19 | }, | |
20 | model::{instance::Instance, user::User}, | |
21 | }; | |
22 | use rsa::{ | |
23 | pkcs8::{EncodePrivateKey, EncodePublicKey}, | |
24 | rand_core::OsRng, | |
25 | RsaPrivateKey, RsaPublicKey, | |
26 | }; | |
27 | use serde_json::Value; | |
28 | use sqlx::{Either, PgPool}; | |
29 | use tokio::sync::Mutex; | |
30 | ||
31 | use crate::authentication::AuthenticationTokenGranter; | |
32 | ||
33 | use super::{AuthBackend, UserBackend}; | |
34 | ||
35 | pub struct UserAuth { | |
36 | pub pg_pool: PgPool, | |
37 | pub this_instance: Instance, | |
38 | pub auth_granter: Arc<Mutex<AuthenticationTokenGranter>>, | |
39 | } | |
40 | ||
41 | impl UserAuth { | |
42 | pub fn new( | |
43 | pool: PgPool, | |
44 | this_instance: &Instance, | |
45 | granter: Arc<Mutex<AuthenticationTokenGranter>>, | |
46 | ) -> Self { | |
47 | Self { | |
48 | pg_pool: pool, | |
49 | this_instance: this_instance.clone(), | |
50 | auth_granter: granter, | |
51 | } | |
52 | } | |
53 | } | |
54 | ||
55 | #[async_trait::async_trait] | |
56 | impl UserBackend for UserAuth { | |
57 | async fn display_name( | |
58 | &mut self, | |
59 | request: UserDisplayNameRequest, | |
60 | ) -> Result<UserDisplayNameResponse, Error> { | |
61 | let db_row = sqlx::query_as!( | |
62 | UserRow, | |
63 | r#"SELECT * FROM users WHERE username = $1"#, | |
64 | request.user.username | |
65 | ) | |
66 | .fetch_one(&self.pg_pool.clone()) | |
67 | .await | |
68 | .unwrap(); | |
69 | ||
70 | Ok(UserDisplayNameResponse { | |
71 | display_name: db_row.display_name, | |
72 | }) | |
73 | } | |
74 | ||
75 | async fn display_image( | |
76 | &mut self, | |
77 | request: UserDisplayImageRequest, | |
78 | ) -> Result<UserDisplayImageResponse, anyhow::Error> { | |
79 | let db_row = sqlx::query_as!( | |
80 | UserRow, | |
81 | r#"SELECT * FROM users WHERE username = $1"#, | |
82 | request.user.username | |
83 | ) | |
84 | .fetch_one(&self.pg_pool.clone()) | |
85 | .await | |
86 | .unwrap(); | |
87 | ||
88 | Ok(UserDisplayImageResponse { | |
89 | image_url: db_row.image_url, | |
90 | }) | |
91 | } | |
92 | ||
93 | async fn bio(&mut self, request: UserBioRequest) -> Result<UserBioResponse, Error> { | |
94 | let db_row = sqlx::query_as!( | |
95 | UserRow, | |
96 | r#"SELECT * FROM users WHERE username = $1"#, | |
97 | request.user.username | |
98 | ) | |
99 | .fetch_one(&self.pg_pool.clone()) | |
100 | .await?; | |
101 | ||
102 | Ok(UserBioResponse { bio: db_row.bio }) | |
103 | } | |
104 | ||
105 | async fn exists(&mut self, user: &User) -> Result<bool, Error> { | |
106 | Ok(sqlx::query_as!( | |
107 | UserRow, | |
108 | r#"SELECT * FROM users WHERE username = $1"#, | |
109 | user.username | |
110 | ) | |
111 | .fetch_one(&self.pg_pool.clone()) | |
112 | .await | |
113 | .is_err()) | |
114 | } | |
115 | ||
116 | async fn settings(&mut self, user: &User) -> Result<Vec<(String, Value)>, Error> { | |
117 | let settings = sqlx::query_as!( | |
118 | UserSettingRow, | |
119 | r#"SELECT * FROM user_settings WHERE username = $1"#, | |
120 | user.username | |
121 | ) | |
122 | .fetch_many(&self.pg_pool) | |
123 | .filter_map(|result| async move { | |
124 | if let Ok(Either::Right(row)) = result { | |
125 | Some(row) | |
126 | } else { | |
127 | None | |
128 | } | |
129 | }) | |
130 | .filter_map(|row| async move { | |
131 | if let Ok(value) = serde_json::from_str(&row.value) { | |
132 | Some((row.name, value)) | |
133 | } else { | |
134 | None | |
135 | } | |
136 | }) | |
137 | .collect::<Vec<_>>() | |
138 | .await; | |
139 | ||
140 | Ok(settings) | |
141 | } | |
142 | ||
143 | async fn write_settings( | |
144 | &mut self, | |
145 | user: &User, | |
146 | settings: &[(String, Value)], | |
147 | ) -> Result<(), Error> { | |
148 | for (name, value) in settings { | |
149 | let serialized = serde_json::to_string(value)?; | |
150 | ||
151 | sqlx::query!("INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", | |
152 | user.username, name, serialized) | |
153 | .execute(&self.pg_pool).await?; | |
154 | } | |
155 | ||
156 | Ok(()) | |
157 | } | |
158 | } | |
159 | ||
160 | #[async_trait::async_trait] | |
161 | impl AuthBackend for UserAuth { | |
162 | async fn register( | |
163 | &mut self, | |
164 | request: RegisterAccountRequest, | |
165 | ) -> Result<RegisterAccountResponse, Error> { | |
166 | const BITS: usize = 2048; | |
167 | ||
168 | let private_key = RsaPrivateKey::new(&mut OsRng, BITS).unwrap(); | |
169 | let public_key = RsaPublicKey::from(&private_key); | |
170 | ||
171 | let key = { | |
172 | let mut target: [u8; 32] = [0; 32]; | |
173 | ||
174 | let mut index = 0; | |
175 | let mut iterator = request.password.as_bytes().iter(); | |
176 | while index < 32 { | |
177 | if let Some(next) = iterator.next() { | |
178 | target[index] = *next; | |
179 | index += 1; | |
180 | } else { | |
181 | iterator = request.password.as_bytes().iter(); | |
182 | } | |
183 | } | |
184 | ||
185 | target | |
186 | }; | |
187 | ||
188 | let key: &Key<Aes256Gcm> = &key.into(); | |
189 | let cipher = Aes256Gcm::new(key); | |
190 | let nonce = Aes256Gcm::generate_nonce(&mut OsRng); | |
191 | let ciphertext = cipher | |
192 | .encrypt(&nonce, private_key.to_pkcs8_der().unwrap().as_bytes()) | |
193 | .unwrap(); | |
194 | ||
195 | let private_key_enc = format!("{}#{}", STANDARD.encode(nonce), STANDARD.encode(ciphertext)); | |
196 | ||
197 | let salt = SaltString::generate(&mut OsRng); | |
198 | ||
199 | let argon2 = Argon2::default(); | |
200 | ||
201 | let password_hash = argon2 | |
202 | .hash_password(request.password.as_bytes(), &salt) | |
203 | .unwrap() | |
204 | .to_string(); | |
205 | ||
206 | let user = match sqlx::query_as!( | |
207 | UserRow, | |
208 | r#"INSERT INTO users VALUES ($1, null, $2, null, null, $3, $4, $5) returning *"#, | |
209 | request.username, | |
210 | "example.com", | |
211 | password_hash, | |
212 | public_key | |
213 | .to_public_key_pem(rsa::pkcs8::LineEnding::LF) | |
214 | .unwrap(), | |
215 | private_key_enc | |
216 | ) | |
217 | .fetch_one(&self.pg_pool) | |
218 | .await | |
219 | { | |
220 | Ok(user) => user, | |
221 | Err(err) => { | |
222 | error!("Failed inserting into the database! {:?}", err); | |
223 | ||
224 | return Err(err.into()); | |
225 | } | |
226 | }; | |
227 | ||
228 | let mut granter = self.auth_granter.lock().await; | |
229 | let token = granter | |
230 | .create_token_for( | |
231 | &User { | |
232 | username: user.username, | |
233 | instance: self.this_instance.clone(), | |
234 | }, | |
235 | &self.this_instance, | |
236 | ) | |
237 | .await; | |
238 | ||
239 | Ok(RegisterAccountResponse { token }) | |
240 | } | |
241 | ||
242 | async fn login( | |
243 | &mut self, | |
244 | _request: AuthenticationTokenRequest, | |
245 | ) -> Result<AuthenticationTokenResponse, Error> { | |
246 | todo!() | |
247 | } | |
248 | } | |
249 | ||
250 | #[allow(unused)] | |
251 | #[derive(Debug, sqlx::FromRow)] | |
252 | struct UserRow { | |
253 | pub username: String, | |
254 | pub image_url: Option<String>, | |
255 | pub display_name: Option<String>, | |
256 | pub bio: Option<String>, | |
257 | pub email: Option<String>, | |
258 | pub password: String, | |
259 | pub public_key: String, | |
260 | pub enc_private_key: Vec<u8>, | |
261 | } | |
262 | ||
263 | #[allow(unused)] | |
264 | #[derive(Debug, sqlx::FromRow)] | |
265 | struct UserSettingRow { | |
266 | pub username: String, | |
267 | pub name: String, | |
268 | pub value: String, | |
269 | } |
giterated-daemon/src/connection.rs
@@ -0,0 +1,94 @@ | ||
1 | pub mod authentication; | |
2 | pub mod handshake; | |
3 | pub mod repository; | |
4 | pub mod user; | |
5 | pub mod wrapper; | |
6 | ||
7 | use std::{any::type_name, collections::HashMap}; | |
8 | ||
9 | use anyhow::Error; | |
10 | use giterated_models::{ | |
11 | messages::ErrorMessage, | |
12 | model::instance::{Instance, InstanceMeta}, | |
13 | }; | |
14 | use serde::{de::DeserializeOwned, Serialize}; | |
15 | use tokio::{net::TcpStream, task::JoinHandle}; | |
16 | use tokio_tungstenite::WebSocketStream; | |
17 | ||
18 | #[derive(Debug, thiserror::Error)] | |
19 | pub enum ConnectionError { | |
20 | #[error("connection error message {0}")] | |
21 | ErrorMessage(#[from] ErrorMessage), | |
22 | #[error("connection should close")] | |
23 | Shutdown, | |
24 | #[error("internal error {0}")] | |
25 | InternalError(#[from] Error), | |
26 | } | |
27 | ||
28 | pub struct RawConnection { | |
29 | pub task: JoinHandle<()>, | |
30 | } | |
31 | ||
32 | pub struct InstanceConnection { | |
33 | pub instance: InstanceMeta, | |
34 | pub task: JoinHandle<()>, | |
35 | } | |
36 | ||
37 | /// Represents a connection which hasn't finished the handshake. | |
38 | pub struct UnestablishedConnection { | |
39 | pub socket: WebSocketStream<TcpStream>, | |
40 | } | |
41 | ||
42 | #[derive(Default)] | |
43 | pub struct Connections { | |
44 | pub connections: Vec<RawConnection>, | |
45 | pub instance_connections: HashMap<Instance, InstanceConnection>, | |
46 | } | |
47 | ||
48 | #[derive(Debug, thiserror::Error)] | |
49 | #[error("handler did not handle")] | |
50 | pub struct HandlerUnhandled; | |
51 | ||
52 | pub trait MessageHandling<A, M, R> { | |
53 | fn message_type() -> &'static str; | |
54 | } | |
55 | ||
56 | impl<T1, F, M, R> MessageHandling<(T1,), M, R> for F | |
57 | where | |
58 | F: FnOnce(T1) -> R, | |
59 | T1: Serialize + DeserializeOwned, | |
60 | { | |
61 | fn message_type() -> &'static str { | |
62 | type_name::<T1>() | |
63 | } | |
64 | } | |
65 | ||
66 | impl<T1, T2, F, M, R> MessageHandling<(T1, T2), M, R> for F | |
67 | where | |
68 | F: FnOnce(T1, T2) -> R, | |
69 | T1: Serialize + DeserializeOwned, | |
70 | { | |
71 | fn message_type() -> &'static str { | |
72 | type_name::<T1>() | |
73 | } | |
74 | } | |
75 | ||
76 | impl<T1, T2, T3, F, M, R> MessageHandling<(T1, T2, T3), M, R> for F | |
77 | where | |
78 | F: FnOnce(T1, T2, T3) -> R, | |
79 | T1: Serialize + DeserializeOwned, | |
80 | { | |
81 | fn message_type() -> &'static str { | |
82 | type_name::<T1>() | |
83 | } | |
84 | } | |
85 | ||
86 | impl<T1, T2, T3, T4, F, M, R> MessageHandling<(T1, T2, T3, T4), M, R> for F | |
87 | where | |
88 | F: FnOnce(T1, T2, T3, T4) -> R, | |
89 | T1: Serialize + DeserializeOwned, | |
90 | { | |
91 | fn message_type() -> &'static str { | |
92 | type_name::<T1>() | |
93 | } | |
94 | } |
giterated-daemon/src/connection/authentication.rs
@@ -0,0 +1,123 @@ | ||
1 | use anyhow::Error; | |
2 | use thiserror::Error; | |
3 | ||
4 | use crate::message::{AuthenticatedInstance, Message, MessageHandler, NetworkMessage, State}; | |
5 | use giterated_models::messages::authentication::{ | |
6 | AuthenticationTokenRequest, RegisterAccountRequest, TokenExtensionRequest, | |
7 | }; | |
8 | ||
9 | use super::wrapper::ConnectionState; | |
10 | ||
11 | pub async fn authentication_handle( | |
12 | message_type: &str, | |
13 | message: &NetworkMessage, | |
14 | state: &ConnectionState, | |
15 | ) -> Result<bool, Error> { | |
16 | match message_type { | |
17 | "&giterated_daemon::messages::authentication::RegisterAccountRequest" => { | |
18 | register_account_request | |
19 | .handle_message(&message, state) | |
20 | .await?; | |
21 | ||
22 | Ok(true) | |
23 | } | |
24 | "&giterated_daemon::messages::authentication::AuthenticationTokenRequest" => { | |
25 | authentication_token_request | |
26 | .handle_message(&message, state) | |
27 | .await?; | |
28 | ||
29 | Ok(true) | |
30 | } | |
31 | "&giterated_daemon::messages::authentication::TokenExtensionRequest" => { | |
32 | token_extension_request | |
33 | .handle_message(&message, state) | |
34 | .await?; | |
35 | ||
36 | Ok(true) | |
37 | } | |
38 | _ => Ok(false), | |
39 | } | |
40 | } | |
41 | ||
42 | async fn register_account_request( | |
43 | State(connection_state): State<ConnectionState>, | |
44 | Message(request): Message<RegisterAccountRequest>, | |
45 | instance: AuthenticatedInstance, | |
46 | ) -> Result<(), AuthenticationConnectionError> { | |
47 | if *instance.inner() != connection_state.instance { | |
48 | return Err(AuthenticationConnectionError::SameInstance); | |
49 | } | |
50 | ||
51 | let mut user_backend = connection_state.user_backend.lock().await; | |
52 | ||
53 | let response = user_backend | |
54 | .register(request.clone()) | |
55 | .await | |
56 | .map_err(|e| AuthenticationConnectionError::Registration(e))?; | |
57 | drop(user_backend); | |
58 | ||
59 | connection_state | |
60 | .send(response) | |
61 | .await | |
62 | .map_err(|e| AuthenticationConnectionError::Sending(e))?; | |
63 | ||
64 | Ok(()) | |
65 | } | |
66 | ||
67 | async fn authentication_token_request( | |
68 | State(connection_state): State<ConnectionState>, | |
69 | Message(request): Message<AuthenticationTokenRequest>, | |
70 | instance: AuthenticatedInstance, | |
71 | ) -> Result<(), AuthenticationConnectionError> { | |
72 | let issued_for = instance.inner().clone(); | |
73 | ||
74 | let mut token_granter = connection_state.auth_granter.lock().await; | |
75 | ||
76 | let response = token_granter | |
77 | .token_request(issued_for, request.username, request.password) | |
78 | .await | |
79 | .map_err(|e| AuthenticationConnectionError::TokenIssuance(e))?; | |
80 | ||
81 | connection_state | |
82 | .send(response) | |
83 | .await | |
84 | .map_err(|e| AuthenticationConnectionError::Sending(e))?; | |
85 | ||
86 | Ok(()) | |
87 | } | |
88 | ||
89 | async fn token_extension_request( | |
90 | State(connection_state): State<ConnectionState>, | |
91 | Message(request): Message<TokenExtensionRequest>, | |
92 | instance: AuthenticatedInstance, | |
93 | ) -> Result<(), AuthenticationConnectionError> { | |
94 | let issued_for = instance.inner().clone(); | |
95 | ||
96 | let mut token_granter = connection_state.auth_granter.lock().await; | |
97 | ||
98 | let response = token_granter | |
99 | .extension_request(&issued_for, request.token) | |
100 | .await | |
101 | .map_err(|e| AuthenticationConnectionError::TokenIssuance(e))?; | |
102 | ||
103 | connection_state | |
104 | .send(response) | |
105 | .await | |
106 | .map_err(|e| AuthenticationConnectionError::Sending(e))?; | |
107 | ||
108 | Ok(()) | |
109 | } | |
110 | ||
111 | #[derive(Debug, Error)] | |
112 | pub enum AuthenticationConnectionError { | |
113 | #[error("the request was invalid")] | |
114 | InvalidRequest, | |
115 | #[error("request must be from the same instance")] | |
116 | SameInstance, | |
117 | #[error("issue during registration {0}")] | |
118 | Registration(Error), | |
119 | #[error("sending error")] | |
120 | Sending(Error), | |
121 | #[error("error issuing token")] | |
122 | TokenIssuance(Error), | |
123 | } |
giterated-daemon/src/connection/handshake.rs
@@ -0,0 +1,128 @@ | ||
1 | use std::{str::FromStr, sync::atomic::Ordering}; | |
2 | ||
3 | use anyhow::Error; | |
4 | use giterated_models::messages::handshake::{ | |
5 | HandshakeFinalize, HandshakeResponse, InitiateHandshake, | |
6 | }; | |
7 | use semver::Version; | |
8 | ||
9 | use crate::{ | |
10 | connection::ConnectionError, | |
11 | message::{Message, MessageHandler, NetworkMessage, State}, | |
12 | validate_version, version, | |
13 | }; | |
14 | ||
15 | use super::{wrapper::ConnectionState, HandlerUnhandled}; | |
16 | ||
17 | pub async fn handshake_handle( | |
18 | message: &NetworkMessage, | |
19 | state: &ConnectionState, | |
20 | ) -> Result<(), Error> { | |
21 | if initiate_handshake | |
22 | .handle_message(&message, state) | |
23 | .await | |
24 | .is_ok() | |
25 | { | |
26 | Ok(()) | |
27 | } else if handshake_response | |
28 | .handle_message(&message, state) | |
29 | .await | |
30 | .is_ok() | |
31 | { | |
32 | Ok(()) | |
33 | } else if handshake_finalize | |
34 | .handle_message(&message, state) | |
35 | .await | |
36 | .is_ok() | |
37 | { | |
38 | Ok(()) | |
39 | } else { | |
40 | Err(Error::from(HandlerUnhandled)) | |
41 | } | |
42 | } | |
43 | ||
44 | async fn initiate_handshake( | |
45 | Message(initiation): Message<InitiateHandshake>, | |
46 | State(connection_state): State<ConnectionState>, | |
47 | ) -> Result<(), HandshakeError> { | |
48 | if !validate_version(&initiation.version) { | |
49 | error!( | |
50 | "Version compatibility failure! Our Version: {}, Their Version: {}", | |
51 | Version::from_str(&std::env::var("CARGO_PKG_VERSION").unwrap()).unwrap(), | |
52 | initiation.version | |
53 | ); | |
54 | ||
55 | connection_state | |
56 | .send(HandshakeFinalize { success: false }) | |
57 | .await | |
58 | .map_err(|e| HandshakeError::SendError(e))?; | |
59 | ||
60 | Ok(()) | |
61 | } else { | |
62 | connection_state | |
63 | .send(HandshakeResponse { | |
64 | identity: connection_state.instance.clone(), | |
65 | version: version(), | |
66 | }) | |
67 | .await | |
68 | .map_err(|e| HandshakeError::SendError(e))?; | |
69 | ||
70 | Ok(()) | |
71 | } | |
72 | } | |
73 | ||
74 | async fn handshake_response( | |
75 | Message(response): Message<HandshakeResponse>, | |
76 | State(connection_state): State<ConnectionState>, | |
77 | ) -> Result<(), HandshakeError> { | |
78 | if !validate_version(&response.version) { | |
79 | error!( | |
80 | "Version compatibility failure! Our Version: {}, Their Version: {}", | |
81 | Version::from_str(&std::env::var("CARGO_PKG_VERSION").unwrap()).unwrap(), | |
82 | response.version | |
83 | ); | |
84 | ||
85 | connection_state | |
86 | .send(HandshakeFinalize { success: false }) | |
87 | .await | |
88 | .map_err(|e| HandshakeError::SendError(e))?; | |
89 | ||
90 | Ok(()) | |
91 | } else { | |
92 | connection_state | |
93 | .send(HandshakeFinalize { success: true }) | |
94 | .await | |
95 | .map_err(|e| HandshakeError::SendError(e))?; | |
96 | ||
97 | Ok(()) | |
98 | } | |
99 | } | |
100 | ||
101 | async fn handshake_finalize( | |
102 | Message(finalize): Message<HandshakeFinalize>, | |
103 | State(connection_state): State<ConnectionState>, | |
104 | ) -> Result<(), HandshakeError> { | |
105 | if !finalize.success { | |
106 | error!("Error during handshake, aborting connection"); | |
107 | return Err(Error::from(ConnectionError::Shutdown).into()); | |
108 | } else { | |
109 | connection_state.handshaked.store(true, Ordering::SeqCst); | |
110 | ||
111 | connection_state | |
112 | .send(HandshakeFinalize { success: true }) | |
113 | .await | |
114 | .map_err(|e| HandshakeError::SendError(e))?; | |
115 | ||
116 | Ok(()) | |
117 | } | |
118 | } | |
119 | ||
120 | #[derive(Debug, thiserror::Error)] | |
121 | pub enum HandshakeError { | |
122 | #[error("version mismatch during handshake, ours: {0}, theirs: {1}")] | |
123 | VersionMismatch(Version, Version), | |
124 | #[error("while sending message: {0}")] | |
125 | SendError(Error), | |
126 | #[error("{0}")] | |
127 | Other(#[from] Error), | |
128 | } |
giterated-daemon/src/connection/repository.rs
@@ -0,0 +1,141 @@ | ||
1 | use anyhow::Error; | |
2 | ||
3 | use crate::{ | |
4 | backend::git::GitBackendError, | |
5 | message::{AuthenticatedUser, Message, MessageHandler, NetworkMessage, State}, | |
6 | }; | |
7 | use giterated_models::messages::repository::{ | |
8 | RepositoryCreateRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, | |
9 | RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, | |
10 | }; | |
11 | ||
12 | use super::wrapper::ConnectionState; | |
13 | ||
14 | pub async fn repository_handle( | |
15 | message_type: &str, | |
16 | message: &NetworkMessage, | |
17 | state: &ConnectionState, | |
18 | ) -> Result<bool, Error> { | |
19 | match message_type { | |
20 | "&giterated_daemon::messages::repository::RepositoryCreateRequest" => { | |
21 | create_repository.handle_message(&message, state).await?; | |
22 | ||
23 | Ok(true) | |
24 | } | |
25 | "&giterated_daemon::messages::repository::RepositoryFileInspectRequest" => { | |
26 | repository_file_inspect | |
27 | .handle_message(&message, state) | |
28 | .await?; | |
29 | ||
30 | Ok(true) | |
31 | } | |
32 | "&giterated_daemon::messages::repository::RepositoryInfoRequest" => { | |
33 | repository_info.handle_message(&message, state).await?; | |
34 | ||
35 | Ok(true) | |
36 | } | |
37 | "&giterated_daemon::messages::repository::RepositoryIssuesCountRequest" => { | |
38 | issues_count.handle_message(&message, state).await?; | |
39 | ||
40 | Ok(true) | |
41 | } | |
42 | "&giterated_daemon::messages::repository::RepositoryIssueLabelsRequest" => { | |
43 | issue_labels.handle_message(&message, state).await?; | |
44 | ||
45 | Ok(true) | |
46 | } | |
47 | "&giterated_daemon::messages::repository::RepositoryIssuesRequest" => { | |
48 | issues.handle_message(&message, state).await?; | |
49 | ||
50 | Ok(true) | |
51 | } | |
52 | _ => Ok(false), | |
53 | } | |
54 | } | |
55 | ||
56 | async fn create_repository( | |
57 | Message(request): Message<RepositoryCreateRequest>, | |
58 | State(connection_state): State<ConnectionState>, | |
59 | AuthenticatedUser(user): AuthenticatedUser, | |
60 | ) -> Result<(), RepositoryError> { | |
61 | let mut repository_backend = connection_state.repository_backend.lock().await; | |
62 | let response = repository_backend | |
63 | .create_repository(&user, &request) | |
64 | .await?; | |
65 | ||
66 | drop(repository_backend); | |
67 | ||
68 | connection_state.send(response).await?; | |
69 | ||
70 | Ok(()) | |
71 | } | |
72 | ||
73 | async fn repository_file_inspect( | |
74 | Message(request): Message<RepositoryFileInspectRequest>, | |
75 | State(connection_state): State<ConnectionState>, | |
76 | user: Option<AuthenticatedUser>, | |
77 | ) -> Result<(), RepositoryError> { | |
78 | let user = user.map(|u| u.0); | |
79 | ||
80 | let mut repository_backend = connection_state.repository_backend.lock().await; | |
81 | let response = repository_backend | |
82 | .repository_file_inspect(user.as_ref(), &request) | |
83 | .await?; | |
84 | ||
85 | drop(repository_backend); | |
86 | ||
87 | connection_state.send(response).await?; | |
88 | ||
89 | Ok(()) | |
90 | } | |
91 | ||
92 | async fn repository_info( | |
93 | Message(request): Message<RepositoryInfoRequest>, | |
94 | State(connection_state): State<ConnectionState>, | |
95 | user: Option<AuthenticatedUser>, | |
96 | ) -> Result<(), RepositoryError> { | |
97 | let user = user.map(|u| u.0); | |
98 | ||
99 | let mut repository_backend = connection_state.repository_backend.lock().await; | |
100 | let response = repository_backend | |
101 | .repository_info(user.as_ref(), &request) | |
102 | .await?; | |
103 | ||
104 | drop(repository_backend); | |
105 | ||
106 | connection_state.send(response).await?; | |
107 | ||
108 | Ok(()) | |
109 | } | |
110 | ||
111 | async fn issues_count( | |
112 | Message(_request): Message<RepositoryIssuesCountRequest>, | |
113 | State(_connection_state): State<ConnectionState>, | |
114 | _user: Option<AuthenticatedUser>, | |
115 | ) -> Result<(), RepositoryError> { | |
116 | unimplemented!(); | |
117 | } | |
118 | ||
119 | async fn issue_labels( | |
120 | Message(_request): Message<RepositoryIssueLabelsRequest>, | |
121 | State(_connection_state): State<ConnectionState>, | |
122 | _user: Option<AuthenticatedUser>, | |
123 | ) -> Result<(), RepositoryError> { | |
124 | unimplemented!(); | |
125 | } | |
126 | ||
127 | async fn issues( | |
128 | Message(_request): Message<RepositoryIssuesRequest>, | |
129 | State(_connection_state): State<ConnectionState>, | |
130 | _user: Option<AuthenticatedUser>, | |
131 | ) -> Result<(), RepositoryError> { | |
132 | unimplemented!(); | |
133 | } | |
134 | ||
135 | #[derive(Debug, thiserror::Error)] | |
136 | pub enum RepositoryError { | |
137 | #[error("{0}")] | |
138 | GitBackendError(#[from] GitBackendError), | |
139 | #[error("{0}")] | |
140 | Other(#[from] Error), | |
141 | } |
giterated-daemon/src/connection/user.rs
@@ -0,0 +1,184 @@ | ||
1 | use anyhow::Error; | |
2 | use giterated_models::{ | |
3 | messages::user::{ | |
4 | UserBioRequest, UserDisplayImageRequest, UserDisplayNameRequest, UserRepositoriesRequest, | |
5 | UserRepositoriesResponse, UserSettingsRequest, UserSettingsResponse, | |
6 | UserWriteSettingsRequest, UserWriteSettingsResponse, | |
7 | }, | |
8 | model::user::User, | |
9 | }; | |
10 | ||
11 | use crate::message::{AuthenticatedUser, Message, MessageHandler, NetworkMessage, State}; | |
12 | ||
13 | use super::wrapper::ConnectionState; | |
14 | ||
15 | pub async fn user_handle( | |
16 | message_type: &str, | |
17 | message: &NetworkMessage, | |
18 | state: &ConnectionState, | |
19 | ) -> Result<bool, Error> { | |
20 | match message_type { | |
21 | "&giterated_daemon::messages::user::UserDisplayNameRequest" => { | |
22 | display_name.handle_message(&message, state).await?; | |
23 | ||
24 | Ok(true) | |
25 | } | |
26 | "&giterated_daemon::messages::user::UserDisplayImageRequest" => { | |
27 | display_image.handle_message(&message, state).await?; | |
28 | ||
29 | Ok(true) | |
30 | } | |
31 | "&giterated_daemon::messages::user::UserBioRequest" => { | |
32 | bio.handle_message(&message, state).await?; | |
33 | ||
34 | Ok(true) | |
35 | } | |
36 | "&giterated_daemon::messages::user::UserRepositoriesRequest" => { | |
37 | repositories.handle_message(&message, state).await?; | |
38 | ||
39 | Ok(true) | |
40 | } | |
41 | "&giterated_daemon::messages::user::UserSettingsRequest" => { | |
42 | user_settings.handle_message(&message, state).await?; | |
43 | ||
44 | Ok(true) | |
45 | } | |
46 | "&giterated_daemon::messages::user::UserWriteSettingsRequest" => { | |
47 | write_user_settings.handle_message(&message, state).await?; | |
48 | ||
49 | Ok(true) | |
50 | } | |
51 | _ => Ok(false), | |
52 | } | |
53 | } | |
54 | ||
55 | async fn display_name( | |
56 | Message(request): Message<UserDisplayNameRequest>, | |
57 | State(connection_state): State<ConnectionState>, | |
58 | ) -> Result<(), UserError> { | |
59 | let mut user_backend = connection_state.user_backend.lock().await; | |
60 | let response = user_backend.display_name(request.clone()).await?; | |
61 | ||
62 | drop(user_backend); | |
63 | ||
64 | connection_state.send(response).await?; | |
65 | ||
66 | Ok(()) | |
67 | } | |
68 | ||
69 | async fn display_image( | |
70 | Message(request): Message<UserDisplayImageRequest>, | |
71 | State(connection_state): State<ConnectionState>, | |
72 | ) -> Result<(), UserError> { | |
73 | let mut user_backend = connection_state.user_backend.lock().await; | |
74 | let response = user_backend.display_image(request.clone()).await?; | |
75 | ||
76 | drop(user_backend); | |
77 | ||
78 | connection_state.send(response).await?; | |
79 | ||
80 | Ok(()) | |
81 | } | |
82 | ||
83 | async fn bio( | |
84 | Message(request): Message<UserBioRequest>, | |
85 | State(connection_state): State<ConnectionState>, | |
86 | ) -> Result<(), UserError> { | |
87 | let mut user_backend = connection_state.user_backend.lock().await; | |
88 | let response = user_backend.bio(request.clone()).await?; | |
89 | ||
90 | drop(user_backend); | |
91 | ||
92 | connection_state.send(response).await?; | |
93 | ||
94 | Ok(()) | |
95 | } | |
96 | ||
97 | async fn repositories( | |
98 | Message(request): Message<UserRepositoriesRequest>, | |
99 | State(connection_state): State<ConnectionState>, | |
100 | requesting_user: Option<AuthenticatedUser>, | |
101 | ) -> Result<(), UserError> { | |
102 | let requesting_user = requesting_user.map(|u| u.0); | |
103 | ||
104 | let mut repository_backend = connection_state.repository_backend.lock().await; | |
105 | let repositories = repository_backend | |
106 | .repositories_for_user(requesting_user.as_ref(), &request.user) | |
107 | .await; | |
108 | ||
109 | let repositories = match repositories { | |
110 | Ok(repositories) => repositories, | |
111 | Err(err) => { | |
112 | error!("Error handling request: {:?}", err); | |
113 | return Ok(()); | |
114 | } | |
115 | }; | |
116 | drop(repository_backend); | |
117 | ||
118 | let mut user_backend = connection_state.user_backend.lock().await; | |
119 | let user_exists = user_backend.exists(&request.user).await; | |
120 | ||
121 | if repositories.is_empty() && !matches!(user_exists, Ok(true)) { | |
122 | return Err(UserError::InvalidUser(request.user)); | |
123 | } | |
124 | ||
125 | let response: UserRepositoriesResponse = UserRepositoriesResponse { repositories }; | |
126 | ||
127 | connection_state.send(response).await?; | |
128 | ||
129 | Ok(()) | |
130 | } | |
131 | ||
132 | async fn user_settings( | |
133 | Message(request): Message<UserSettingsRequest>, | |
134 | State(connection_state): State<ConnectionState>, | |
135 | AuthenticatedUser(requesting_user): AuthenticatedUser, | |
136 | ) -> Result<(), UserError> { | |
137 | if request.user != requesting_user { | |
138 | return Err(UserError::InvalidUser(request.user)); | |
139 | } | |
140 | ||
141 | let mut user_backend = connection_state.user_backend.lock().await; | |
142 | let mut settings = user_backend.settings(&request.user).await?; | |
143 | ||
144 | drop(user_backend); | |
145 | ||
146 | let response = UserSettingsResponse { | |
147 | settings: settings.drain(..).collect(), | |
148 | }; | |
149 | ||
150 | connection_state.send(response).await?; | |
151 | ||
152 | Ok(()) | |
153 | } | |
154 | ||
155 | async fn write_user_settings( | |
156 | Message(request): Message<UserWriteSettingsRequest>, | |
157 | State(connection_state): State<ConnectionState>, | |
158 | AuthenticatedUser(requesting_user): AuthenticatedUser, | |
159 | ) -> Result<(), UserError> { | |
160 | if request.user != requesting_user { | |
161 | return Err(UserError::InvalidUser(request.user)); | |
162 | } | |
163 | ||
164 | let mut user_backend = connection_state.user_backend.lock().await; | |
165 | user_backend | |
166 | .write_settings(&request.user, &request.settings) | |
167 | .await?; | |
168 | ||
169 | drop(user_backend); | |
170 | ||
171 | let response = UserWriteSettingsResponse {}; | |
172 | ||
173 | connection_state.send(response).await?; | |
174 | ||
175 | Ok(()) | |
176 | } | |
177 | ||
178 | #[derive(Debug, thiserror::Error)] | |
179 | pub enum UserError { | |
180 | #[error("invalid user {0}")] | |
181 | InvalidUser(User), | |
182 | #[error("{0}")] | |
183 | Other(#[from] Error), | |
184 | } |
giterated-daemon/src/connection/wrapper.rs
@@ -0,0 +1,162 @@ | ||
1 | use std::{ | |
2 | collections::HashMap, | |
3 | net::SocketAddr, | |
4 | sync::{ | |
5 | atomic::{AtomicBool, Ordering}, | |
6 | Arc, | |
7 | }, | |
8 | }; | |
9 | ||
10 | use anyhow::Error; | |
11 | use futures_util::{SinkExt, StreamExt}; | |
12 | use giterated_models::{messages::error::ConnectionError, model::instance::Instance}; | |
13 | use rsa::RsaPublicKey; | |
14 | use serde::Serialize; | |
15 | use serde_json::Value; | |
16 | use tokio::{ | |
17 | net::TcpStream, | |
18 | sync::{Mutex, RwLock}, | |
19 | }; | |
20 | use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; | |
21 | ||
22 | use crate::{ | |
23 | authentication::AuthenticationTokenGranter, | |
24 | backend::{RepositoryBackend, UserBackend}, | |
25 | message::NetworkMessage, | |
26 | }; | |
27 | ||
28 | use super::{ | |
29 | authentication::authentication_handle, handshake::handshake_handle, | |
30 | repository::repository_handle, user::user_handle, Connections, | |
31 | }; | |
32 | ||
33 | pub async fn connection_wrapper( | |
34 | socket: WebSocketStream<TcpStream>, | |
35 | connections: Arc<Mutex<Connections>>, | |
36 | repository_backend: Arc<Mutex<dyn RepositoryBackend + Send>>, | |
37 | user_backend: Arc<Mutex<dyn UserBackend + Send>>, | |
38 | auth_granter: Arc<Mutex<AuthenticationTokenGranter>>, | |
39 | addr: SocketAddr, | |
40 | instance: impl ToOwned<Owned = Instance>, | |
41 | ) { | |
42 | let connection_state = ConnectionState { | |
43 | socket: Arc::new(Mutex::new(socket)), | |
44 | connections, | |
45 | repository_backend, | |
46 | user_backend, | |
47 | auth_granter, | |
48 | addr, | |
49 | instance: instance.to_owned(), | |
50 | handshaked: Arc::new(AtomicBool::new(false)), | |
51 | cached_keys: Arc::default(), | |
52 | }; | |
53 | ||
54 | let mut handshaked = false; | |
55 | ||
56 | loop { | |
57 | let mut socket = connection_state.socket.lock().await; | |
58 | let message = socket.next().await; | |
59 | drop(socket); | |
60 | ||
61 | match message { | |
62 | Some(Ok(message)) => { | |
63 | let payload = match message { | |
64 | Message::Binary(payload) => payload, | |
65 | Message::Ping(_) => { | |
66 | let mut socket = connection_state.socket.lock().await; | |
67 | let _ = socket.send(Message::Pong(vec![])).await; | |
68 | drop(socket); | |
69 | continue; | |
70 | } | |
71 | Message::Close(_) => return, | |
72 | _ => continue, | |
73 | }; | |
74 | ||
75 | let message = NetworkMessage(payload.clone()); | |
76 | ||
77 | if !handshaked { | |
78 | if handshake_handle(&message, &connection_state).await.is_ok() { | |
79 | if connection_state.handshaked.load(Ordering::SeqCst) { | |
80 | handshaked = true; | |
81 | } | |
82 | } | |
83 | } else { | |
84 | let raw = serde_json::from_slice::<Value>(&payload).unwrap(); | |
85 | let message_type = raw.get("message_type").unwrap().as_str().unwrap(); | |
86 | ||
87 | match authentication_handle(message_type, &message, &connection_state).await { | |
88 | Err(e) => { | |
89 | let _ = connection_state.send(ConnectionError(e.to_string())).await; | |
90 | } | |
91 | Ok(true) => continue, | |
92 | Ok(false) => {} | |
93 | } | |
94 | ||
95 | match repository_handle(message_type, &message, &connection_state).await { | |
96 | Err(e) => { | |
97 | let _ = connection_state.send(ConnectionError(e.to_string())).await; | |
98 | } | |
99 | Ok(true) => continue, | |
100 | Ok(false) => {} | |
101 | } | |
102 | ||
103 | match user_handle(message_type, &message, &connection_state).await { | |
104 | Err(e) => { | |
105 | let _ = connection_state.send(ConnectionError(e.to_string())).await; | |
106 | } | |
107 | Ok(true) => continue, | |
108 | Ok(false) => {} | |
109 | } | |
110 | ||
111 | match authentication_handle(message_type, &message, &connection_state).await { | |
112 | Err(e) => { | |
113 | let _ = connection_state.send(ConnectionError(e.to_string())).await; | |
114 | } | |
115 | Ok(true) => continue, | |
116 | Ok(false) => {} | |
117 | } | |
118 | ||
119 | error!( | |
120 | "Message completely unhandled: {}", | |
121 | std::str::from_utf8(&payload).unwrap() | |
122 | ); | |
123 | } | |
124 | } | |
125 | Some(Err(e)) => { | |
126 | error!("Closing connection for {:?} for {}", e, addr); | |
127 | return; | |
128 | } | |
129 | _ => { | |
130 | info!("Unhandled"); | |
131 | continue; | |
132 | } | |
133 | } | |
134 | } | |
135 | } | |
136 | ||
137 | #[derive(Clone)] | |
138 | pub struct ConnectionState { | |
139 | socket: Arc<Mutex<WebSocketStream<TcpStream>>>, | |
140 | pub connections: Arc<Mutex<Connections>>, | |
141 | pub repository_backend: Arc<Mutex<dyn RepositoryBackend + Send>>, | |
142 | pub user_backend: Arc<Mutex<dyn UserBackend + Send>>, | |
143 | pub auth_granter: Arc<Mutex<AuthenticationTokenGranter>>, | |
144 | pub addr: SocketAddr, | |
145 | pub instance: Instance, | |
146 | pub handshaked: Arc<AtomicBool>, | |
147 | pub cached_keys: Arc<RwLock<HashMap<Instance, RsaPublicKey>>>, | |
148 | } | |
149 | ||
150 | impl ConnectionState { | |
151 | pub async fn send<T: Serialize>(&self, message: T) -> Result<(), Error> { | |
152 | let payload = serde_json::to_string(&message)?; | |
153 | info!("Sending payload: {}", &payload); | |
154 | self.socket | |
155 | .lock() | |
156 | .await | |
157 | .send(Message::Binary(payload.into_bytes())) | |
158 | .await?; | |
159 | ||
160 | Ok(()) | |
161 | } | |
162 | } |
giterated-daemon/src/lib.rs
@@ -0,0 +1,21 @@ | ||
1 | use std::str::FromStr; | |
2 | ||
3 | use semver::{Version, VersionReq}; | |
4 | ||
5 | pub mod authentication; | |
6 | pub mod backend; | |
7 | pub mod connection; | |
8 | pub mod message; | |
9 | ||
10 | #[macro_use] | |
11 | extern crate tracing; | |
12 | ||
13 | pub fn version() -> Version { | |
14 | Version::from_str(env!("CARGO_PKG_VERSION")).unwrap() | |
15 | } | |
16 | ||
17 | pub fn validate_version(other: &Version) -> bool { | |
18 | let version_req = VersionReq::from_str("=0.0.6").unwrap(); | |
19 | ||
20 | version_req.matches(other) | |
21 | } |
giterated-daemon/src/main.rs
@@ -0,0 +1,128 @@ | ||
1 | use anyhow::Error; | |
2 | use connection::{Connections, RawConnection}; | |
3 | use giterated_daemon::{ | |
4 | authentication::AuthenticationTokenGranter, | |
5 | backend::{git::GitBackend, user::UserAuth, RepositoryBackend, UserBackend}, | |
6 | connection::{self, wrapper::connection_wrapper}, | |
7 | }; | |
8 | use giterated_models::model::instance::Instance; | |
9 | use sqlx::{postgres::PgConnectOptions, ConnectOptions, PgPool}; | |
10 | use std::{net::SocketAddr, str::FromStr, sync::Arc}; | |
11 | use tokio::{ | |
12 | fs::File, | |
13 | io::{AsyncRead, AsyncReadExt, AsyncWrite}, | |
14 | net::{TcpListener, TcpStream}, | |
15 | sync::Mutex, | |
16 | }; | |
17 | use tokio_tungstenite::{accept_async, WebSocketStream}; | |
18 | use toml::Table; | |
19 | ||
20 | #[macro_use] | |
21 | extern crate tracing; | |
22 | ||
23 | #[tokio::main] | |
24 | async fn main() -> Result<(), Error> { | |
25 | tracing_subscriber::fmt::init(); | |
26 | let mut listener = TcpListener::bind("0.0.0.0:7270").await?; | |
27 | let connections: Arc<Mutex<Connections>> = Arc::default(); | |
28 | let config: Table = { | |
29 | let mut file = File::open("Giterated.toml").await?; | |
30 | let mut text = String::new(); | |
31 | file.read_to_string(&mut text).await?; | |
32 | text.parse()? | |
33 | }; | |
34 | let db_conn_options = PgConnectOptions::new() | |
35 | .host(config["postgres"]["host"].as_str().unwrap()) | |
36 | .port(config["postgres"]["port"].as_integer().unwrap() as u16) | |
37 | .database(config["postgres"]["database"].as_str().unwrap()) | |
38 | .username(config["postgres"]["user"].as_str().unwrap()) | |
39 | .password(config["postgres"]["password"].as_str().unwrap()) | |
40 | .log_statements(log::LevelFilter::Off); | |
41 | let db_pool = PgPool::connect_with(db_conn_options).await?; | |
42 | ||
43 | debug!("Running database migrations..."); | |
44 | sqlx::migrate!().run(&db_pool).await?; | |
45 | info!("Connected"); | |
46 | ||
47 | let repository_backend: Arc<Mutex<dyn RepositoryBackend + Send>> = | |
48 | Arc::new(Mutex::new(GitBackend { | |
49 | pg_pool: db_pool.clone(), | |
50 | repository_folder: String::from( | |
51 | config["giterated"]["backend"]["git"]["root"] | |
52 | .as_str() | |
53 | .unwrap(), | |
54 | ), | |
55 | instance: Instance::from_str("giterated.dev").unwrap(), | |
56 | })); | |
57 | ||
58 | let token_granter = Arc::new(Mutex::new(AuthenticationTokenGranter { | |
59 | config: config.clone(), | |
60 | instance: Instance::from_str("giterated.dev").unwrap(), | |
61 | })); | |
62 | ||
63 | let user_backend: Arc<Mutex<dyn UserBackend + Send>> = Arc::new(Mutex::new(UserAuth::new( | |
64 | db_pool.clone(), | |
65 | &Instance::from_str("giterated.dev").unwrap(), | |
66 | token_granter.clone(), | |
67 | ))); | |
68 | ||
69 | info!("Connected"); | |
70 | ||
71 | loop { | |
72 | let stream = accept_stream(&mut listener).await; | |
73 | info!("Connected"); | |
74 | ||
75 | let (stream, address) = match stream { | |
76 | Ok(stream) => stream, | |
77 | Err(err) => { | |
78 | error!("Failed to accept connection. {:?}", err); | |
79 | continue; | |
80 | } | |
81 | }; | |
82 | ||
83 | info!("Accepted connection from {}", address); | |
84 | ||
85 | let connection = accept_websocket_connection(stream).await; | |
86 | ||
87 | let connection = match connection { | |
88 | Ok(connection) => connection, | |
89 | Err(err) => { | |
90 | error!( | |
91 | "Failed to initiate Websocket connection from {}. {:?}", | |
92 | address, err | |
93 | ); | |
94 | continue; | |
95 | } | |
96 | }; | |
97 | ||
98 | info!("Websocket connection established with {}", address); | |
99 | ||
100 | let connection = RawConnection { | |
101 | task: tokio::spawn(connection_wrapper( | |
102 | connection, | |
103 | connections.clone(), | |
104 | repository_backend.clone(), | |
105 | user_backend.clone(), | |
106 | token_granter.clone(), | |
107 | address, | |
108 | Instance::from_str("giterated.dev").unwrap(), | |
109 | )), | |
110 | }; | |
111 | ||
112 | connections.lock().await.connections.push(connection); | |
113 | } | |
114 | } | |
115 | ||
116 | async fn accept_stream(listener: &mut TcpListener) -> Result<(TcpStream, SocketAddr), Error> { | |
117 | let stream = listener.accept().await?; | |
118 | ||
119 | Ok(stream) | |
120 | } | |
121 | ||
122 | async fn accept_websocket_connection<S: AsyncRead + AsyncWrite + Unpin>( | |
123 | stream: S, | |
124 | ) -> Result<WebSocketStream<S>, Error> { | |
125 | let connection = accept_async(stream).await?; | |
126 | ||
127 | Ok(connection) | |
128 | } |
giterated-daemon/src/message.rs
@@ -0,0 +1,262 @@ | ||
1 | use std::{collections::HashMap, ops::Deref}; | |
2 | ||
3 | use anyhow::Error; | |
4 | use futures_util::Future; | |
5 | use giterated_models::model::{ | |
6 | authenticated::{Authenticated, AuthenticationSource, UserTokenMetadata}, | |
7 | instance::Instance, | |
8 | user::User, | |
9 | }; | |
10 | use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; | |
11 | use rsa::{ | |
12 | pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, | |
13 | pss::{Signature, VerifyingKey}, | |
14 | sha2::Sha256, | |
15 | signature::Verifier, | |
16 | RsaPublicKey, | |
17 | }; | |
18 | use serde::{de::DeserializeOwned, Serialize}; | |
19 | use serde_json::Value; | |
20 | ||
21 | use crate::connection::wrapper::ConnectionState; | |
22 | ||
23 | pub struct NetworkMessage(pub Vec<u8>); | |
24 | ||
25 | impl Deref for NetworkMessage { | |
26 | type Target = [u8]; | |
27 | ||
28 | fn deref(&self) -> &Self::Target { | |
29 | &self.0 | |
30 | } | |
31 | } | |
32 | ||
33 | pub struct AuthenticatedUser(pub User); | |
34 | ||
35 | #[derive(Debug, thiserror::Error)] | |
36 | pub enum UserAuthenticationError { | |
37 | #[error("user authentication missing")] | |
38 | Missing, | |
39 | // #[error("{0}")] | |
40 | // InstanceAuthentication(#[from] Error), | |
41 | #[error("user token was invalid")] | |
42 | InvalidToken, | |
43 | #[error("an error has occured")] | |
44 | Other(#[from] Error), | |
45 | } | |
46 | ||
47 | pub struct AuthenticatedInstance(Instance); | |
48 | ||
49 | impl AuthenticatedInstance { | |
50 | pub fn inner(&self) -> &Instance { | |
51 | &self.0 | |
52 | } | |
53 | } | |
54 | ||
55 | #[async_trait::async_trait] | |
56 | pub trait FromMessage<S: Send + Sync>: Sized + Send + Sync { | |
57 | async fn from_message(message: &NetworkMessage, state: &S) -> Result<Self, Error>; | |
58 | } | |
59 | ||
60 | #[async_trait::async_trait] | |
61 | impl FromMessage<ConnectionState> for AuthenticatedUser { | |
62 | async fn from_message( | |
63 | network_message: &NetworkMessage, | |
64 | state: &ConnectionState, | |
65 | ) -> Result<Self, Error> { | |
66 | let message: Authenticated<HashMap<String, Value>> = | |
67 | serde_json::from_slice(&network_message).map_err(|e| Error::from(e))?; | |
68 | ||
69 | let (auth_user, auth_token) = message | |
70 | .source | |
71 | .iter() | |
72 | .filter_map(|auth| { | |
73 | if let AuthenticationSource::User { user, token } = auth { | |
74 | Some((user, token)) | |
75 | } else { | |
76 | None | |
77 | } | |
78 | }) | |
79 | .next() | |
80 | .ok_or_else(|| UserAuthenticationError::Missing)?; | |
81 | ||
82 | let authenticated_instance = | |
83 | AuthenticatedInstance::from_message(network_message, state).await?; | |
84 | ||
85 | let public_key_raw = public_key(&auth_user.instance).await?; | |
86 | let verification_key = DecodingKey::from_rsa_pem(public_key_raw.as_bytes()).unwrap(); | |
87 | ||
88 | let data: TokenData<UserTokenMetadata> = decode( | |
89 | auth_token.as_ref(), | |
90 | &verification_key, | |
91 | &Validation::new(Algorithm::RS256), | |
92 | ) | |
93 | .unwrap(); | |
94 | ||
95 | if data.claims.user != *auth_user | |
96 | || data.claims.generated_for != *authenticated_instance.inner() | |
97 | { | |
98 | Err(Error::from(UserAuthenticationError::InvalidToken)) | |
99 | } else { | |
100 | Ok(AuthenticatedUser(data.claims.user)) | |
101 | } | |
102 | } | |
103 | } | |
104 | ||
105 | #[async_trait::async_trait] | |
106 | impl FromMessage<ConnectionState> for AuthenticatedInstance { | |
107 | async fn from_message( | |
108 | network_message: &NetworkMessage, | |
109 | state: &ConnectionState, | |
110 | ) -> Result<Self, Error> { | |
111 | let message: Authenticated<Value> = | |
112 | serde_json::from_slice(&network_message).map_err(|e| Error::from(e))?; | |
113 | ||
114 | let (instance, signature) = message | |
115 | .source | |
116 | .iter() | |
117 | .filter_map(|auth: &AuthenticationSource| { | |
118 | if let AuthenticationSource::Instance { | |
119 | instance, | |
120 | signature, | |
121 | } = auth | |
122 | { | |
123 | Some((instance, signature)) | |
124 | } else { | |
125 | None | |
126 | } | |
127 | }) | |
128 | .next() | |
129 | // TODO: Instance authentication error | |
130 | .ok_or_else(|| UserAuthenticationError::Missing)?; | |
131 | ||
132 | let public_key = { | |
133 | let cached_keys = state.cached_keys.read().await; | |
134 | ||
135 | if let Some(key) = cached_keys.get(&instance) { | |
136 | key.clone() | |
137 | } else { | |
138 | drop(cached_keys); | |
139 | let mut cached_keys = state.cached_keys.write().await; | |
140 | let key = public_key(instance).await?; | |
141 | let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); | |
142 | cached_keys.insert(instance.clone(), public_key.clone()); | |
143 | public_key | |
144 | } | |
145 | }; | |
146 | ||
147 | let verifying_key: VerifyingKey<Sha256> = VerifyingKey::new(public_key); | |
148 | ||
149 | let message_json = serde_json::to_vec(&message.message).unwrap(); | |
150 | ||
151 | verifying_key.verify( | |
152 | &message_json, | |
153 | &Signature::try_from(signature.as_ref()).unwrap(), | |
154 | )?; | |
155 | ||
156 | Ok(AuthenticatedInstance(instance.clone())) | |
157 | } | |
158 | } | |
159 | ||
160 | #[async_trait::async_trait] | |
161 | impl<S, T> FromMessage<S> for Option<T> | |
162 | where | |
163 | T: FromMessage<S>, | |
164 | S: Send + Sync + 'static, | |
165 | { | |
166 | async fn from_message(message: &NetworkMessage, state: &S) -> Result<Self, Error> { | |
167 | Ok(T::from_message(message, state).await.ok()) | |
168 | } | |
169 | } | |
170 | ||
171 | #[async_trait::async_trait] | |
172 | pub trait MessageHandler<T, S, R> { | |
173 | async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result<R, Error>; | |
174 | } | |
175 | #[async_trait::async_trait] | |
176 | impl<F, R, S, T1, T, E> MessageHandler<(T1,), S, R> for T | |
177 | where | |
178 | T: FnOnce(T1) -> F + Clone + Send + 'static, | |
179 | F: Future<Output = Result<R, E>> + Send, | |
180 | T1: FromMessage<S> + Send, | |
181 | S: Send + Sync, | |
182 | E: std::error::Error + Send + Sync + 'static, | |
183 | { | |
184 | async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result<R, Error> { | |
185 | let value = T1::from_message(message, state).await?; | |
186 | self(value).await.map_err(|e| Error::from(e)) | |
187 | } | |
188 | } | |
189 | ||
190 | #[async_trait::async_trait] | |
191 | impl<F, R, S, T1, T2, T, E> MessageHandler<(T1, T2), S, R> for T | |
192 | where | |
193 | T: FnOnce(T1, T2) -> F + Clone + Send + 'static, | |
194 | F: Future<Output = Result<R, E>> + Send, | |
195 | T1: FromMessage<S> + Send, | |
196 | T2: FromMessage<S> + Send, | |
197 | S: Send + Sync, | |
198 | E: std::error::Error + Send + Sync + 'static, | |
199 | { | |
200 | async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result<R, Error> { | |
201 | let value = T1::from_message(message, state).await?; | |
202 | let value_2 = T2::from_message(message, state).await?; | |
203 | self(value, value_2).await.map_err(|e| Error::from(e)) | |
204 | } | |
205 | } | |
206 | ||
207 | #[async_trait::async_trait] | |
208 | impl<F, R, S, T1, T2, T3, T, E> MessageHandler<(T1, T2, T3), S, R> for T | |
209 | where | |
210 | T: FnOnce(T1, T2, T3) -> F + Clone + Send + 'static, | |
211 | F: Future<Output = Result<R, E>> + Send, | |
212 | T1: FromMessage<S> + Send, | |
213 | T2: FromMessage<S> + Send, | |
214 | T3: FromMessage<S> + Send, | |
215 | S: Send + Sync, | |
216 | E: std::error::Error + Send + Sync + 'static, | |
217 | { | |
218 | async fn handle_message(self, message: &NetworkMessage, state: &S) -> Result<R, Error> { | |
219 | let value = T1::from_message(message, state).await?; | |
220 | let value_2 = T2::from_message(message, state).await?; | |
221 | let value_3 = T3::from_message(message, state).await?; | |
222 | ||
223 | self(value, value_2, value_3) | |
224 | .await | |
225 | .map_err(|e| Error::from(e)) | |
226 | } | |
227 | } | |
228 | ||
229 | pub struct State<T>(pub T); | |
230 | ||
231 | #[async_trait::async_trait] | |
232 | impl<T> FromMessage<T> for State<T> | |
233 | where | |
234 | T: Clone + Send + Sync, | |
235 | { | |
236 | async fn from_message(_: &NetworkMessage, state: &T) -> Result<Self, Error> { | |
237 | Ok(Self(state.clone())) | |
238 | } | |
239 | } | |
240 | ||
241 | // Temp | |
242 | #[async_trait::async_trait] | |
243 | impl<T, S> FromMessage<S> for Message<T> | |
244 | where | |
245 | T: DeserializeOwned + Send + Sync + Serialize, | |
246 | S: Clone + Send + Sync, | |
247 | { | |
248 | async fn from_message(message: &NetworkMessage, _: &S) -> Result<Self, Error> { | |
249 | Ok(Message(serde_json::from_slice(&message)?)) | |
250 | } | |
251 | } | |
252 | ||
253 | pub struct Message<T: Serialize + DeserializeOwned>(pub T); | |
254 | ||
255 | async fn public_key(instance: &Instance) -> Result<String, Error> { | |
256 | let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) | |
257 | .await? | |
258 | .text() | |
259 | .await?; | |
260 | ||
261 | Ok(key) | |
262 | } |
giterated-models/Cargo.toml
@@ -0,0 +1,38 @@ | ||
1 | [package] | |
2 | name = "giterated-models" | |
3 | version = "0.1.0" | |
4 | edition = "2021" | |
5 | ||
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
7 | ||
8 | [dependencies] | |
9 | tokio-tungstenite = "*" | |
10 | tokio = { version = "1.32.0", features = [ "full" ] } | |
11 | tracing = "*" | |
12 | futures-util = "*" | |
13 | serde = { version = "1.0.188", features = [ "derive" ]} | |
14 | serde_json = "1.0" | |
15 | tracing-subscriber = "0.3" | |
16 | base64 = "0.21.3" | |
17 | jsonwebtoken = { version = "*", features = ["use_pem"]} | |
18 | log = "*" | |
19 | rand = "*" | |
20 | rsa = {version = "0.9", features = ["sha2"]} | |
21 | reqwest = "*" | |
22 | argon2 = "*" | |
23 | aes-gcm = "0.10.2" | |
24 | semver = {version = "*", features = ["serde"]} | |
25 | tower = "*" | |
26 | ||
27 | toml = { version = "0.7" } | |
28 | ||
29 | chrono = { version = "0.4", features = [ "serde" ] } | |
30 | async-trait = "0.1" | |
31 | ||
32 | # Git backend | |
33 | git2 = "0.17" | |
34 | thiserror = "1" | |
35 | anyhow = "1" | |
36 | sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "macros", "migrate", "chrono" ] } | |
37 | ||
38 | #uuid = { version = "1.4", features = [ "v4", "serde" ] } |
giterated-models/src/messages/authentication.rs
@@ -0,0 +1,62 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | use crate::model::authenticated::UserAuthenticationToken; | |
4 | ||
5 | /// An account registration request. | |
6 | /// | |
7 | /// # Authentication | |
8 | /// - Instance Authentication | |
9 | /// - **ONLY ACCEPTED WHEN SAME-INSTANCE** | |
10 | #[derive(Clone, Serialize, Deserialize)] | |
11 | pub struct RegisterAccountRequest { | |
12 | pub username: String, | |
13 | pub email: Option<String>, | |
14 | pub password: String, | |
15 | } | |
16 | ||
17 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
18 | pub struct RegisterAccountResponse { | |
19 | pub token: String, | |
20 | } | |
21 | ||
22 | /// An authentication token request. | |
23 | /// | |
24 | /// AKA Login Request | |
25 | /// | |
26 | /// # Authentication | |
27 | /// - Instance Authentication | |
28 | /// - Identifies the Instance to issue the token for | |
29 | /// # Authorization | |
30 | /// - Credentials ([`crate::backend::AuthBackend`]-based) | |
31 | /// - Identifies the User account to issue a token for | |
32 | /// - Decrypts user private key to issue to | |
33 | #[derive(Clone, Serialize, Deserialize)] | |
34 | pub struct AuthenticationTokenRequest { | |
35 | pub username: String, | |
36 | pub password: String, | |
37 | } | |
38 | ||
39 | #[derive(Clone, Serialize, Deserialize)] | |
40 | pub struct AuthenticationTokenResponse { | |
41 | pub token: UserAuthenticationToken, | |
42 | } | |
43 | ||
44 | /// An authentication token extension request. | |
45 | /// | |
46 | /// # Authentication | |
47 | /// - Instance Authentication | |
48 | /// - Identifies the Instance to issue the token for | |
49 | /// - User Authentication | |
50 | /// - Authenticates the validity of the token | |
51 | /// # Authorization | |
52 | /// - Token-based | |
53 | /// - Validates authorization using token's authenticity | |
54 | #[derive(Clone, Serialize, Deserialize)] | |
55 | pub struct TokenExtensionRequest { | |
56 | pub token: UserAuthenticationToken, | |
57 | } | |
58 | ||
59 | #[derive(Clone, Serialize, Deserialize)] | |
60 | pub struct TokenExtensionResponse { | |
61 | pub new_token: Option<String>, | |
62 | } |
giterated-models/src/messages/discovery.rs
@@ -0,0 +1,21 @@ | ||
1 | use chrono::{DateTime, Utc}; | |
2 | use serde::{Deserialize, Serialize}; | |
3 | ||
4 | use crate::model::discovery::DiscoveryItem; | |
5 | ||
6 | #[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] | |
7 | pub struct DiscoveryOffer { | |
8 | pub earliest: DateTime<Utc>, | |
9 | pub hashes: Vec<u128>, | |
10 | } | |
11 | ||
12 | #[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] | |
13 | pub struct DiscoveryRequest { | |
14 | pub since: DateTime<Utc>, | |
15 | pub hashes: Vec<u128>, | |
16 | } | |
17 | ||
18 | #[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] | |
19 | pub struct Discoveries { | |
20 | pub discoveries: Vec<DiscoveryItem>, | |
21 | } |
giterated-models/src/messages/error.rs
@@ -0,0 +1,5 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | #[derive(Debug, Serialize, Deserialize, thiserror::Error)] | |
4 | #[error("error from connection: {0}")] | |
5 | pub struct ConnectionError(pub String); |
giterated-models/src/messages/handshake.rs
@@ -0,0 +1,22 @@ | ||
1 | use semver::Version; | |
2 | use serde::{Deserialize, Serialize}; | |
3 | ||
4 | use crate::model::instance::Instance; | |
5 | ||
6 | /// Sent by the initiator of a new inter-daemon connection. | |
7 | #[derive(Clone, Serialize, Deserialize)] | |
8 | pub struct InitiateHandshake { | |
9 | pub version: Version, | |
10 | } | |
11 | ||
12 | /// Sent in response to [`InitiateHandshake`] | |
13 | #[derive(Clone, Serialize, Deserialize)] | |
14 | pub struct HandshakeResponse { | |
15 | pub identity: Instance, | |
16 | pub version: Version, | |
17 | } | |
18 | ||
19 | #[derive(Clone, Serialize, Deserialize)] | |
20 | pub struct HandshakeFinalize { | |
21 | pub success: bool, | |
22 | } |
giterated-models/src/messages/issues.rs
@@ -0,0 +1,18 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | use crate::model::repository::Repository; | |
4 | ||
5 | #[derive(Clone)] | |
6 | pub struct IssuesCountCommand { | |
7 | pub respository: Repository, | |
8 | } | |
9 | ||
10 | #[derive(Clone, Serialize, Deserialize)] | |
11 | pub struct IssuesCountResponse { | |
12 | pub count: u64, | |
13 | } | |
14 | ||
15 | #[derive(Clone)] | |
16 | pub struct IssuesLabelsCommand { | |
17 | pub repository: Repository, | |
18 | } |
giterated-models/src/messages/mod.rs
@@ -0,0 +1,20 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | use std::fmt::Debug; | |
3 | ||
4 | use crate::model::user::User; | |
5 | ||
6 | pub mod authentication; | |
7 | pub mod discovery; | |
8 | pub mod error; | |
9 | pub mod handshake; | |
10 | pub mod issues; | |
11 | pub mod repository; | |
12 | pub mod user; | |
13 | ||
14 | #[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] | |
15 | pub enum ErrorMessage { | |
16 | #[error("user {0} doesn't exist or isn't valid in this context")] | |
17 | InvalidUser(User), | |
18 | #[error("internal error: shutdown")] | |
19 | Shutdown, | |
20 | } |
giterated-models/src/messages/repository.rs
@@ -0,0 +1,146 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | use crate::model::repository::RepositoryVisibility; | |
4 | use crate::model::{ | |
5 | repository::{Commit, Repository, RepositoryTreeEntry}, | |
6 | user::User, | |
7 | }; | |
8 | ||
9 | /// A request to create a repository. | |
10 | /// | |
11 | /// # Authentication | |
12 | /// - Instance Authentication | |
13 | /// - Used to validate User token `issued_for` | |
14 | /// - User Authentication | |
15 | /// - Used to source owning user | |
16 | /// - Used to authorize user token against user's instance | |
17 | /// # Authorization | |
18 | /// - Instance Authorization | |
19 | /// - Used to authorize action using User token requiring a correct `issued_for` and valid issuance from user's instance | |
20 | /// - User Authorization | |
21 | /// - Potential User permissions checks | |
22 | #[derive(Clone, Serialize, Deserialize)] | |
23 | pub struct RepositoryCreateRequest { | |
24 | pub name: String, | |
25 | pub description: Option<String>, | |
26 | pub visibility: RepositoryVisibility, | |
27 | pub default_branch: String, | |
28 | pub owner: User, | |
29 | } | |
30 | ||
31 | #[derive(Clone, Serialize, Deserialize)] | |
32 | pub struct RepositoryCreateResponse; | |
33 | ||
34 | /// A request to inspect the tree of a repository. | |
35 | /// | |
36 | /// # Authentication | |
37 | /// - Instance Authentication | |
38 | /// - Validate request against the `issued_for` public key | |
39 | /// - Validate User token against the user's instance's public key | |
40 | /// # Authorization | |
41 | /// - User Authorization | |
42 | /// - Potential User permissions checks | |
43 | #[derive(Clone, Serialize, Deserialize)] | |
44 | pub struct RepositoryFileInspectRequest { | |
45 | pub path: RepositoryTreeEntry, | |
46 | } | |
47 | ||
48 | #[derive(Clone, Serialize, Deserialize)] | |
49 | pub enum RepositoryFileInspectionResponse { | |
50 | File { | |
51 | commit_metadata: Commit, | |
52 | }, | |
53 | Folder { | |
54 | commit_metadata: Commit, | |
55 | members: Vec<RepositoryTreeEntry>, | |
56 | }, | |
57 | Invalid { | |
58 | path: RepositoryTreeEntry, | |
59 | }, | |
60 | } | |
61 | ||
62 | /// A request to get a repository's information. | |
63 | /// | |
64 | /// # Authentication | |
65 | /// - Instance Authentication | |
66 | /// - Validate request against the `issued_for` public key | |
67 | /// - Validate User token against the user's instance's public key | |
68 | /// # Authorization | |
69 | /// - User Authorization | |
70 | /// - Potential User permissions checks | |
71 | #[derive(Clone, Serialize, Deserialize)] | |
72 | pub struct RepositoryIssuesCountRequest; | |
73 | ||
74 | #[derive(Clone, Serialize, Deserialize)] | |
75 | pub struct RepositoryIssuesCountResponse { | |
76 | pub count: u64, | |
77 | } | |
78 | ||
79 | /// A request to get a repository's issues count. | |
80 | /// | |
81 | /// # Authentication | |
82 | /// - Instance Authentication | |
83 | /// - Validate request against the `issued_for` public key | |
84 | /// - Validate User token against the user's instance's public key | |
85 | /// # Authorization | |
86 | /// - User Authorization | |
87 | /// - Potential User permissions checks | |
88 | #[derive(Clone, Serialize, Deserialize)] | |
89 | pub struct RepositoryIssueLabelsRequest; | |
90 | ||
91 | #[derive(Clone, Serialize, Deserialize)] | |
92 | pub struct RepositoryIssueLabelsResponse { | |
93 | pub labels: Vec<IssueLabel>, | |
94 | } | |
95 | ||
96 | #[derive(Clone, Serialize, Deserialize)] | |
97 | pub struct IssueLabel { | |
98 | pub name: String, | |
99 | pub color: String, | |
100 | } | |
101 | ||
102 | /// A request to get a repository's issue labels. | |
103 | /// | |
104 | /// # Authentication | |
105 | /// - Instance Authentication | |
106 | /// - Validate request against the `issued_for` public key | |
107 | /// - Validate User token against the user's instance's public key | |
108 | /// # Authorization | |
109 | /// - User Authorization | |
110 | /// - Potential User permissions checks | |
111 | #[derive(Clone, Serialize, Deserialize)] | |
112 | pub struct RepositoryIssuesRequest; | |
113 | ||
114 | #[derive(Clone, Serialize, Deserialize)] | |
115 | pub struct RepositoryIssuesResponse { | |
116 | pub issues: Vec<RepositoryIssue>, | |
117 | } | |
118 | ||
119 | /// A request to get a repository's issues. | |
120 | /// | |
121 | /// # Authentication | |
122 | /// - Instance Authentication | |
123 | /// - Validate request against the `issued_for` public key | |
124 | /// - Validate User token against the user's instance's public key | |
125 | /// # Authorization | |
126 | /// - User Authorization | |
127 | /// - Potential User permissions checks | |
128 | #[derive(Clone, Serialize, Deserialize)] | |
129 | pub struct RepositoryIssue { | |
130 | pub author: User, | |
131 | pub id: u64, | |
132 | pub title: String, | |
133 | pub contents: String, | |
134 | pub labels: Vec<IssueLabel>, | |
135 | } | |
136 | ||
137 | #[derive(Clone, Serialize, Deserialize)] | |
138 | pub struct RepositoryInfoRequest { | |
139 | pub repository: Repository, | |
140 | /// Whether to fetch extra metadata like the last commit made to file or size | |
141 | pub extra_metadata: bool, | |
142 | /// Rev (branch) being requested | |
143 | pub rev: Option<String>, | |
144 | /// Tree path being requested | |
145 | pub path: Option<String>, | |
146 | } |
giterated-models/src/messages/user.rs
@@ -0,0 +1,66 @@ | ||
1 | use std::collections::HashMap; | |
2 | ||
3 | use serde::{Deserialize, Serialize}; | |
4 | ||
5 | use crate::model::{repository::RepositorySummary, user::User}; | |
6 | ||
7 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
8 | pub struct UserDisplayNameRequest { | |
9 | pub user: User, | |
10 | } | |
11 | ||
12 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
13 | pub struct UserDisplayNameResponse { | |
14 | pub display_name: Option<String>, | |
15 | } | |
16 | ||
17 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
18 | pub struct UserDisplayImageRequest { | |
19 | pub user: User, | |
20 | } | |
21 | ||
22 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
23 | pub struct UserDisplayImageResponse { | |
24 | pub image_url: Option<String>, | |
25 | } | |
26 | ||
27 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
28 | pub struct UserBioRequest { | |
29 | pub user: User, | |
30 | } | |
31 | ||
32 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
33 | pub struct UserBioResponse { | |
34 | pub bio: Option<String>, | |
35 | } | |
36 | ||
37 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
38 | pub struct UserRepositoriesRequest { | |
39 | pub user: User, | |
40 | } | |
41 | ||
42 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
43 | pub struct UserRepositoriesResponse { | |
44 | pub repositories: Vec<RepositorySummary>, | |
45 | } | |
46 | ||
47 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
48 | pub struct UserSettingsRequest { | |
49 | pub user: User, | |
50 | } | |
51 | ||
52 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
53 | pub struct UserSettingsResponse { | |
54 | pub settings: HashMap<String, serde_json::Value>, | |
55 | } | |
56 | ||
57 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
58 | pub struct UserWriteSettingsRequest { | |
59 | pub user: User, | |
60 | pub settings: Vec<(String, serde_json::Value)>, | |
61 | } | |
62 | ||
63 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
64 | pub struct UserWriteSettingsResponse { | |
65 | // IDK? | |
66 | } |
giterated-models/src/model/authenticated.rs
@@ -0,0 +1,187 @@ | ||
1 | use std::any::type_name; | |
2 | ||
3 | use rsa::{ | |
4 | pkcs1::DecodeRsaPrivateKey, | |
5 | pss::SigningKey, | |
6 | sha2::Sha256, | |
7 | signature::{RandomizedSigner, SignatureEncoding}, | |
8 | RsaPrivateKey, | |
9 | }; | |
10 | use serde::{Deserialize, Serialize}; | |
11 | ||
12 | use super::{instance::Instance, user::User}; | |
13 | ||
14 | #[derive(Debug, Serialize, Deserialize)] | |
15 | pub struct UserTokenMetadata { | |
16 | pub user: User, | |
17 | pub generated_for: Instance, | |
18 | pub exp: u64, | |
19 | } | |
20 | ||
21 | #[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] | |
22 | pub struct Authenticated<T: Serialize> { | |
23 | pub target_instance: Option<Instance>, | |
24 | pub source: Vec<AuthenticationSource>, | |
25 | pub message_type: String, | |
26 | #[serde(flatten)] | |
27 | pub message: T, | |
28 | } | |
29 | ||
30 | pub trait AuthenticationSourceProvider: Sized { | |
31 | fn authenticate(self, payload: &Vec<u8>) -> AuthenticationSource; | |
32 | } | |
33 | ||
34 | pub trait AuthenticationSourceProviders: Sized { | |
35 | fn authenticate_all(self, payload: &Vec<u8>) -> Vec<AuthenticationSource>; | |
36 | } | |
37 | ||
38 | impl<A> AuthenticationSourceProviders for A | |
39 | where | |
40 | A: AuthenticationSourceProvider, | |
41 | { | |
42 | fn authenticate_all(self, payload: &Vec<u8>) -> Vec<AuthenticationSource> { | |
43 | vec![self.authenticate(payload)] | |
44 | } | |
45 | } | |
46 | ||
47 | impl<A, B> AuthenticationSourceProviders for (A, B) | |
48 | where | |
49 | A: AuthenticationSourceProvider, | |
50 | B: AuthenticationSourceProvider, | |
51 | { | |
52 | fn authenticate_all(self, payload: &Vec<u8>) -> Vec<AuthenticationSource> { | |
53 | let (first, second) = self; | |
54 | ||
55 | vec![first.authenticate(payload), second.authenticate(payload)] | |
56 | } | |
57 | } | |
58 | ||
59 | impl<T: Serialize> Authenticated<T> { | |
60 | pub fn new(message: T, auth_sources: impl AuthenticationSourceProvider) -> Self { | |
61 | let message_payload = serde_json::to_vec(&message).unwrap(); | |
62 | ||
63 | let authentication = auth_sources.authenticate_all(&message_payload); | |
64 | ||
65 | Self { | |
66 | source: authentication, | |
67 | message_type: type_name::<T>().to_string(), | |
68 | message, | |
69 | target_instance: None, | |
70 | } | |
71 | } | |
72 | ||
73 | pub fn new_for( | |
74 | instance: impl ToOwned<Owned = Instance>, | |
75 | message: T, | |
76 | auth_sources: impl AuthenticationSourceProvider, | |
77 | ) -> Self { | |
78 | let message_payload = serde_json::to_vec(&message).unwrap(); | |
79 | ||
80 | let authentication = auth_sources.authenticate_all(&message_payload); | |
81 | ||
82 | Self { | |
83 | source: authentication, | |
84 | message_type: type_name::<T>().to_string(), | |
85 | message, | |
86 | target_instance: Some(instance.to_owned()), | |
87 | } | |
88 | } | |
89 | ||
90 | pub fn new_empty(message: T) -> Self { | |
91 | Self { | |
92 | source: vec![], | |
93 | message_type: type_name::<T>().to_string(), | |
94 | message, | |
95 | target_instance: None, | |
96 | } | |
97 | } | |
98 | ||
99 | pub fn append_authentication(&mut self, authentication: impl AuthenticationSourceProvider) { | |
100 | let message_payload = serde_json::to_vec(&self.message).unwrap(); | |
101 | ||
102 | self.source | |
103 | .push(authentication.authenticate(&message_payload)); | |
104 | } | |
105 | } | |
106 | ||
107 | mod verified {} | |
108 | ||
109 | #[derive(Clone, Debug)] | |
110 | pub struct UserAuthenticator { | |
111 | pub user: User, | |
112 | pub token: UserAuthenticationToken, | |
113 | } | |
114 | ||
115 | impl AuthenticationSourceProvider for UserAuthenticator { | |
116 | fn authenticate(self, _payload: &Vec<u8>) -> AuthenticationSource { | |
117 | AuthenticationSource::User { | |
118 | user: self.user, | |
119 | token: self.token, | |
120 | } | |
121 | } | |
122 | } | |
123 | ||
124 | #[derive(Clone)] | |
125 | pub struct InstanceAuthenticator<'a> { | |
126 | pub instance: Instance, | |
127 | pub private_key: &'a str, | |
128 | } | |
129 | ||
130 | impl AuthenticationSourceProvider for InstanceAuthenticator<'_> { | |
131 | fn authenticate(self, payload: &Vec<u8>) -> AuthenticationSource { | |
132 | let mut rng = rand::thread_rng(); | |
133 | ||
134 | let private_key = RsaPrivateKey::from_pkcs1_pem(self.private_key).unwrap(); | |
135 | let signing_key = SigningKey::<Sha256>::new(private_key); | |
136 | let signature = signing_key.sign_with_rng(&mut rng, &payload); | |
137 | ||
138 | AuthenticationSource::Instance { | |
139 | instance: self.instance, | |
140 | // TODO: Actually parse signature from private key | |
141 | signature: InstanceSignature(signature.to_bytes().into_vec()), | |
142 | } | |
143 | } | |
144 | } | |
145 | ||
146 | #[repr(transparent)] | |
147 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] | |
148 | pub struct UserAuthenticationToken(String); | |
149 | ||
150 | impl From<String> for UserAuthenticationToken { | |
151 | fn from(value: String) -> Self { | |
152 | Self(value) | |
153 | } | |
154 | } | |
155 | ||
156 | impl ToString for UserAuthenticationToken { | |
157 | fn to_string(&self) -> String { | |
158 | self.0.clone() | |
159 | } | |
160 | } | |
161 | ||
162 | impl AsRef<str> for UserAuthenticationToken { | |
163 | fn as_ref(&self) -> &str { | |
164 | &self.0 | |
165 | } | |
166 | } | |
167 | ||
168 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] | |
169 | pub struct InstanceSignature(Vec<u8>); | |
170 | ||
171 | impl AsRef<[u8]> for InstanceSignature { | |
172 | fn as_ref(&self) -> &[u8] { | |
173 | &self.0 | |
174 | } | |
175 | } | |
176 | ||
177 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] | |
178 | pub enum AuthenticationSource { | |
179 | User { | |
180 | user: User, | |
181 | token: UserAuthenticationToken, | |
182 | }, | |
183 | Instance { | |
184 | instance: Instance, | |
185 | signature: InstanceSignature, | |
186 | }, | |
187 | } |
giterated-models/src/model/discovery.rs
@@ -0,0 +1,15 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | use crate::model::{instance::Instance, repository::Repository}; | |
4 | ||
5 | #[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] | |
6 | pub enum DiscoveryItem { | |
7 | Instance { | |
8 | instance: Instance, | |
9 | signature: Vec<u8>, | |
10 | }, | |
11 | Repository { | |
12 | repository: Repository, | |
13 | signature: Vec<u8>, | |
14 | }, | |
15 | } |
giterated-models/src/model/instance.rs
@@ -0,0 +1,51 @@ | ||
1 | use std::str::FromStr; | |
2 | ||
3 | use serde::{Deserialize, Serialize}; | |
4 | use thiserror::Error; | |
5 | ||
6 | pub struct InstanceMeta { | |
7 | pub url: String, | |
8 | pub public_key: String, | |
9 | } | |
10 | ||
11 | /// An instance, defined by the URL it can be reached at. | |
12 | /// | |
13 | /// # Textual Format | |
14 | /// An instance's textual format is its URL. | |
15 | /// | |
16 | /// ## Examples | |
17 | /// For the instance `giterated.dev`, the following [`Instance`] initialization | |
18 | /// would be valid: | |
19 | /// | |
20 | /// ``` | |
21 | /// let instance = Instance { | |
22 | /// url: String::from("giterated.dev") | |
23 | /// }; | |
24 | /// | |
25 | /// // This is correct | |
26 | /// assert_eq!(Instance::from_str("giterated.dev").unwrap(), instance); | |
27 | /// ``` | |
28 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] | |
29 | pub struct Instance { | |
30 | pub url: String, | |
31 | } | |
32 | ||
33 | impl ToString for Instance { | |
34 | fn to_string(&self) -> String { | |
35 | self.url.clone() | |
36 | } | |
37 | } | |
38 | ||
39 | impl FromStr for Instance { | |
40 | type Err = InstanceParseError; | |
41 | ||
42 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
43 | Ok(Self { url: s.to_string() }) | |
44 | } | |
45 | } | |
46 | ||
47 | #[derive(Debug, Error)] | |
48 | pub enum InstanceParseError { | |
49 | #[error("invalid format")] | |
50 | InvalidFormat, | |
51 | } |
giterated-models/src/model/mod.rs
@@ -0,0 +1,11 @@ | ||
1 | //! # Giterated Network Data Model Types | |
2 | //! | |
3 | //! All network data model types that are not directly associated with | |
4 | //! individual requests or responses. | |
5 | ||
6 | pub mod authenticated; | |
7 | pub mod discovery; | |
8 | pub mod instance; | |
9 | pub mod repository; | |
10 | pub mod settings; | |
11 | pub mod user; |
giterated-models/src/model/repository.rs
@@ -0,0 +1,197 @@ | ||
1 | use std::fmt::{Display, Formatter}; | |
2 | use std::str::FromStr; | |
3 | ||
4 | use serde::{Deserialize, Serialize}; | |
5 | ||
6 | use super::{instance::Instance, user::User}; | |
7 | ||
8 | /// A repository, defined by the instance it exists on along with | |
9 | /// its owner and name. | |
10 | /// | |
11 | /// # Textual Format | |
12 | /// A repository's textual reference is defined as: | |
13 | /// | |
14 | /// `{owner: User}/{name: String}@{instance: Instance}` | |
15 | /// | |
16 | /// # Examples | |
17 | /// For the repository named `foo` owned by `barson:giterated.dev` on the instance | |
18 | /// `giterated.dev`, the following [`Repository`] initialization would | |
19 | /// be valid: | |
20 | /// | |
21 | /// ``` | |
22 | /// let repository = Repository { | |
23 | /// owner: User::from_str("barson:giterated.dev").unwrap(), | |
24 | /// name: String::from("foo"), | |
25 | /// instance: Instance::from_str("giterated.dev").unwrap() | |
26 | /// }; | |
27 | /// | |
28 | /// // This is correct | |
29 | /// assert_eq!(Repository::from_str("barson:giterated.dev/[email protected]").unwrap(), repository); | |
30 | /// ``` | |
31 | #[derive(Hash, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | |
32 | pub struct Repository { | |
33 | pub owner: User, | |
34 | pub name: String, | |
35 | /// Instance the repository is on | |
36 | pub instance: Instance, | |
37 | } | |
38 | ||
39 | impl ToString for Repository { | |
40 | fn to_string(&self) -> String { | |
41 | format!("{}/{}@{}", self.owner, self.name, self.instance.to_string()) | |
42 | } | |
43 | } | |
44 | ||
45 | impl FromStr for Repository { | |
46 | type Err = (); | |
47 | ||
48 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
49 | let mut by_ampersand = s.split('@'); | |
50 | let mut path_split = by_ampersand.next().unwrap().split('/'); | |
51 | ||
52 | let instance = Instance::from_str(by_ampersand.next().unwrap()).unwrap(); | |
53 | let owner = User::from_str(path_split.next().unwrap()).unwrap(); | |
54 | let name = path_split.next().unwrap().to_string(); | |
55 | ||
56 | Ok(Self { | |
57 | instance, | |
58 | owner, | |
59 | name, | |
60 | }) | |
61 | } | |
62 | } | |
63 | ||
64 | /// Visibility of the repository to the general eye | |
65 | #[derive(PartialEq, Eq, Debug, Hash, Serialize, Deserialize, Clone, sqlx::Type)] | |
66 | #[sqlx(type_name = "visibility", rename_all = "lowercase")] | |
67 | pub enum RepositoryVisibility { | |
68 | Public, | |
69 | Unlisted, | |
70 | Private, | |
71 | } | |
72 | ||
73 | /// Implements [`Display`] for [`RepositoryVisiblity`] using [`Debug`] | |
74 | impl Display for RepositoryVisibility { | |
75 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
76 | write!(f, "{:?}", self) | |
77 | } | |
78 | } | |
79 | ||
80 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
81 | pub struct RepositoryView { | |
82 | /// Name of the repository | |
83 | /// | |
84 | /// This is different than the [`Repository`] name, | |
85 | /// which may be a path. | |
86 | pub name: String, | |
87 | /// Owner of the Repository | |
88 | pub owner: User, | |
89 | /// Repository description | |
90 | pub description: Option<String>, | |
91 | /// Repository visibility | |
92 | pub visibility: RepositoryVisibility, | |
93 | /// Default branch of the repository | |
94 | pub default_branch: String, | |
95 | /// Last commit made to the repository | |
96 | pub latest_commit: Option<Commit>, | |
97 | /// Revision of the displayed tree | |
98 | pub tree_rev: Option<String>, | |
99 | /// Repository tree | |
100 | pub tree: Vec<RepositoryTreeEntry>, | |
101 | } | |
102 | ||
103 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
104 | pub enum RepositoryObjectType { | |
105 | Tree, | |
106 | Blob, | |
107 | } | |
108 | ||
109 | /// Stored info for our tree entries | |
110 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
111 | pub struct RepositoryTreeEntry { | |
112 | /// Name of the tree/blob | |
113 | pub name: String, | |
114 | /// Type of the tree entry | |
115 | pub object_type: RepositoryObjectType, | |
116 | /// Git supplies us with the mode at all times, and people like it displayed. | |
117 | pub mode: i32, | |
118 | /// File size | |
119 | pub size: Option<usize>, | |
120 | /// Last commit made to the tree/blob | |
121 | pub last_commit: Option<Commit>, | |
122 | } | |
123 | ||
124 | impl RepositoryTreeEntry { | |
125 | // I love you Emilia <3 | |
126 | pub fn new(name: &str, object_type: RepositoryObjectType, mode: i32) -> Self { | |
127 | Self { | |
128 | name: name.to_string(), | |
129 | object_type, | |
130 | mode, | |
131 | size: None, | |
132 | last_commit: None, | |
133 | } | |
134 | } | |
135 | } | |
136 | ||
137 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
138 | pub struct RepositoryTreeEntryWithCommit { | |
139 | pub tree_entry: RepositoryTreeEntry, | |
140 | pub commit: Commit, | |
141 | } | |
142 | ||
143 | /// Info about a git commit | |
144 | #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] | |
145 | pub struct Commit { | |
146 | /// Unique commit ID | |
147 | pub oid: String, | |
148 | /// Full commit message | |
149 | pub message: Option<String>, | |
150 | /// Who created the commit | |
151 | pub author: CommitSignature, | |
152 | /// Who committed the commit | |
153 | pub committer: CommitSignature, | |
154 | /// Time when the commit happened | |
155 | pub time: chrono::NaiveDateTime, | |
156 | } | |
157 | ||
158 | /// Gets all info from [`git2::Commit`] for easy use | |
159 | impl From<git2::Commit<'_>> for Commit { | |
160 | fn from(commit: git2::Commit<'_>) -> Self { | |
161 | Self { | |
162 | oid: commit.id().to_string(), | |
163 | message: commit.message().map(|message| message.to_string()), | |
164 | author: commit.author().into(), | |
165 | committer: commit.committer().into(), | |
166 | time: chrono::NaiveDateTime::from_timestamp_opt(commit.time().seconds(), 0).unwrap(), | |
167 | } | |
168 | } | |
169 | } | |
170 | ||
171 | /// Git commit signature | |
172 | #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] | |
173 | pub struct CommitSignature { | |
174 | pub name: Option<String>, | |
175 | pub email: Option<String>, | |
176 | pub time: chrono::NaiveDateTime, | |
177 | } | |
178 | ||
179 | /// Converts the signature from git2 into something usable without explicit lifetimes. | |
180 | impl From<git2::Signature<'_>> for CommitSignature { | |
181 | fn from(signature: git2::Signature<'_>) -> Self { | |
182 | Self { | |
183 | name: signature.name().map(|name| name.to_string()), | |
184 | email: signature.email().map(|email| email.to_string()), | |
185 | time: chrono::NaiveDateTime::from_timestamp_opt(signature.when().seconds(), 0).unwrap(), | |
186 | } | |
187 | } | |
188 | } | |
189 | ||
190 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
191 | pub struct RepositorySummary { | |
192 | pub repository: Repository, | |
193 | pub owner: User, | |
194 | pub visibility: RepositoryVisibility, | |
195 | pub description: Option<String>, | |
196 | pub last_commit: Option<Commit>, | |
197 | } |
giterated-models/src/model/settings.rs
@@ -0,0 +1,23 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | pub trait Setting: Serialize { | |
4 | fn name(&self) -> &'static str; | |
5 | } | |
6 | ||
7 | #[derive(Debug, Serialize, Deserialize)] | |
8 | pub struct UserBio(pub String); | |
9 | ||
10 | impl Setting for UserBio { | |
11 | fn name(&self) -> &'static str { | |
12 | "Bio" | |
13 | } | |
14 | } | |
15 | ||
16 | #[derive(Debug, Serialize, Deserialize)] | |
17 | pub struct UserDisplayName(pub String); | |
18 | ||
19 | impl Setting for UserDisplayName { | |
20 | fn name(&self) -> &'static str { | |
21 | "Display Name" | |
22 | } | |
23 | } |
giterated-models/src/model/user.rs
@@ -0,0 +1,56 @@ | ||
1 | use std::fmt::{Display, Formatter}; | |
2 | use std::str::FromStr; | |
3 | ||
4 | use serde::{Deserialize, Serialize}; | |
5 | ||
6 | use super::instance::Instance; | |
7 | ||
8 | /// A user, defined by its username and instance. | |
9 | /// | |
10 | /// # Textual Format | |
11 | /// A user's textual reference is defined as: | |
12 | /// | |
13 | /// `{username: String}:{instance: Instance}` | |
14 | /// | |
15 | /// # Examples | |
16 | /// For the user with the username `barson` and the instance `giterated.dev`, | |
17 | /// the following [`User`] initialization would be valid: | |
18 | /// | |
19 | /// ``` | |
20 | /// let user = User { | |
21 | /// username: String::from("barson"), | |
22 | /// instance: Instance::from_str("giterated.dev").unwrap() | |
23 | /// }; | |
24 | /// | |
25 | /// // This is correct | |
26 | /// assert_eq!(User::from_str("barson:giterated.dev").unwrap(), user); | |
27 | /// ``` | |
28 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] | |
29 | pub struct User { | |
30 | pub username: String, | |
31 | pub instance: Instance, | |
32 | } | |
33 | ||
34 | impl Display for User { | |
35 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
36 | write!(f, "{}:{}", self.username, self.instance.url) | |
37 | } | |
38 | } | |
39 | ||
40 | impl From<String> for User { | |
41 | fn from(user_string: String) -> Self { | |
42 | User::from_str(&user_string).unwrap() | |
43 | } | |
44 | } | |
45 | ||
46 | impl FromStr for User { | |
47 | type Err = (); | |
48 | ||
49 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
50 | let mut colon_split = s.split(':'); | |
51 | let username = colon_split.next().unwrap().to_string(); | |
52 | let instance = Instance::from_str(colon_split.next().unwrap()).unwrap(); | |
53 | ||
54 | Ok(Self { username, instance }) | |
55 | } | |
56 | } |