JavaScript is disabled, refresh for a better experience. ambee/giterated

ambee/giterated

Git repository hosting, collaboration, and discovery for the Fediverse.

Completely refactor project structure

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨ae8ff44

Showing ⁨⁨74⁩ changed files⁩ with ⁨⁨3405⁩ insertions⁩ and ⁨⁨3297⁩ deletions⁩

Cargo.lock

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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/lib.rs

View file
@@ -0,0 +1,2 @@
1 pub mod messages;
2 pub mod model;

giterated-models/src/messages/authentication.rs

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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 }