diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-02-20 22:53:27 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-02-20 22:53:27 +0100 |
| commit | e940d84f69e3fd627731d5d3f698d6f838797862 (patch) | |
| tree | 779eefcde993e22c0a69c18a3cb6e1cb9d17aad3 | |
| parent | bf025b4977543a371df9dbdddfe9cc2f02f2a8d0 (diff) | |
WIPWIP
20 files changed, 312 insertions, 47 deletions
diff --git a/docker/integration_test/docker-compose.yaml b/docker/integration_test/docker-compose.yaml index 628973f..442d2ce 100644 --- a/docker/integration_test/docker-compose.yaml +++ b/docker/integration_test/docker-compose.yaml @@ -40,7 +40,8 @@ services: environment: - SSH_AUTH_METHODS=publickey depends_on: - - web + web: + condition: service_healthy ports: - '10022:22' volumes: diff --git a/docker/integration_test/web/Dockerfile b/docker/integration_test/web/Dockerfile index 2ba7a5d..5056fa1 100644 --- a/docker/integration_test/web/Dockerfile +++ b/docker/integration_test/web/Dockerfile @@ -1,6 +1,6 @@ FROM archlinux:base -RUN pacman -Suy --noconfirm && pacman -S openssl git --noconfirm +RUN pacman -Suy --noconfirm && pacman -S openssh openssl git --noconfirm # Docker still have this really stupied idea that all files must be relative "context", # so context is set to ../.. relative the docker-compose.yaml @@ -8,8 +8,27 @@ COPY server/target/x86_64-unknown-linux-musl/debug/eyeballs-githook /app/eyeball COPY server/target/debug/eyeballs /app/eyeballs COPY docker/integration_test/web/setup.sh /app/setup.sh +RUN mkdir -p /git/auth /git/repos + +# git image runs as default git user, with uid 1000 gid 1000. +# we need the same, but it can't be named git (as package git installs a git user) +# so add another user with 1000 gid 1000 and make sure that shared files +# (/git/auth and /git/repos) are owned by that user and not root. +RUN useradd --no-create-home --uid 1000 --user-group -s /usr/bin/nologin alf + +RUN chown alf:alf /app +RUN chown alf:alf /git/auth +RUN chown alf:alf /git/repos +VOLUME /git/auth +VOLUME /git/repos + +USER alf:alf + RUN mkdir -p -m 0700 /app/.ssh COPY docker/integration_test/web/gitkey /app/.ssh/id_rsa WORKDIR /app ENTRYPOINT /app/setup.sh + +HEALTHCHECK --start-interval=1s --timeout=1s CMD curl -f http://localhost:8000/api/v1/healthcheck + diff --git a/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json b/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json index 97d107d..13a9155 100644 --- a/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json +++ b/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json @@ -1,6 +1,6 @@ { "db_name": "MySQL", - "query": "SELECT id,remote,main_branch FROM projects", + "query": "SELECT id,remote,remote_key,main_branch FROM projects", "describe": { "columns": [ { @@ -25,6 +25,16 @@ }, { "ordinal": 2, + "name": "remote_key", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 32768 + } + }, + { + "ordinal": 3, "name": "main_branch", "type_info": { "type": "VarString", @@ -40,8 +50,9 @@ "nullable": [ false, false, + false, false ] }, - "hash": "2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252" + "hash": "11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8" } diff --git a/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json b/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json index 942c054..3379f2a 100644 --- a/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json +++ b/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json @@ -1,6 +1,6 @@ { "db_name": "MySQL", - "query": "SELECT id,title,description,remote,main_branch FROM projects WHERE id=?", + "query": "SELECT id,title,description,remote,remote_key,main_branch FROM projects WHERE id=?", "describe": { "columns": [ { @@ -45,6 +45,16 @@ }, { "ordinal": 4, + "name": "remote_key", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 32768 + } + }, + { + "ordinal": 5, "name": "main_branch", "type_info": { "type": "VarString", @@ -62,8 +72,9 @@ false, false, false, + false, false ] }, - "hash": "51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197" + "hash": "dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445" } diff --git a/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json b/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json index 6f761ee..92f794c 100644 --- a/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json +++ b/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json @@ -1,12 +1,12 @@ { "db_name": "MySQL", - "query": "INSERT INTO projects (id, title, description, remote, main_branch) VALUES (?, ?, ?, ?, ?)", + "query": "INSERT INTO projects (id, title, description, remote, remote_key, main_branch) VALUES (?, ?, ?, ?, ?, ?)", "describe": { "columns": [], "parameters": { - "Right": 5 + "Right": 6 }, "nullable": [] }, - "hash": "a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f" + "hash": "fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961" } diff --git a/server/Cargo.lock b/server/Cargo.lock index e44bc77..9394da3 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.12" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "shlex", ] @@ -582,6 +582,7 @@ dependencies = [ "eyeballs-common", "futures", "ldap3", + "log", "reqwest", "rmp-serde", "rocket", @@ -593,6 +594,7 @@ dependencies = [ "testdir", "time", "tokio", + "url", "utoipa", "utoipa-swagger-ui", ] @@ -610,6 +612,7 @@ name = "eyeballs-common" version = "0.1.0" dependencies = [ "futures", + "log", "pathdiff", "serde", "testdir", @@ -621,6 +624,7 @@ name = "eyeballs-githook" version = "0.1.0" dependencies = [ "eyeballs-common", + "log", "rmp-serde", "serde", "tokio", @@ -1461,9 +1465,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", ] @@ -1618,9 +1622,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "opaque-debug" @@ -3041,9 +3045,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -3692,9 +3696,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" dependencies = [ "memchr", ] diff --git a/server/Cargo.toml b/server/Cargo.toml index b9d0488..f8bab3c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,6 +11,7 @@ resolver = "2" [workspace.dependencies] futures = "0.3.31" +log = { version = "0.4.25", features = ["release_max_level_warn"] } rmp-serde = "1.3" serde = { version = "1.0", features = ["derive"] } testdir = "0.9.3" @@ -23,6 +24,7 @@ eyeballs-api = { path = "api" } eyeballs-common = { path = "common" } futures.workspace = true ldap3 = { version = "0.11.5", default-features = false, features = [ "native-tls", "tls", "tls-native", "tokio-native-tls" ] } +log.workspace = true rmp-serde.workspace = true rocket = { version = "0.5.1", features = ["json", "secrets"] } rocket_db_pools = { version = "0.2.0", features = ["sqlx_mysql"] } @@ -30,8 +32,9 @@ serde.workspace = true sqlx = { version = "0.7.0", default-features = false, features = ["macros", "migrate"] } time = "0.3.34" tokio = { workspace = true, features = ["process"] } +url = "2.5.4" utoipa = { workspace = true, features = ["rocket_extras"] } -utoipa-swagger-ui = { version = "9", features = ["rocket", "vendored"], default-features = false } +utoipa-swagger-ui = { version = "9", features = ["debug-embed", "rocket", "vendored"], default-features = false } [dev-dependencies] reqwest = { version = "0.12.12", features = ["cookies", "json"], default-features = false } diff --git a/server/api/src/api_model.rs b/server/api/src/api_model.rs index 3760f9e..d769a98 100644 --- a/server/api/src/api_model.rs +++ b/server/api/src/api_model.rs @@ -178,6 +178,8 @@ pub struct Project { pub description: String, #[schema(example = "ssh://git.example.org/srv/git/")] pub remote: String, + #[schema(example = "b3BlbNNz...AQIDBA==")] + pub remote_key_abbrev: String, #[schema(example = "main")] pub main_branch: String, pub users: Vec<ProjectUserEntry>, @@ -191,6 +193,8 @@ pub struct ProjectData<'r> { pub description: Option<&'r str>, #[schema(example = "ssh://git.example.org/srv/git/")] pub remote: Option<&'r str>, + #[schema(example = "b3BlbNNz...AQIDBA==")] + pub remote_key: Option<String>, #[schema(example = "main")] pub main_branch: Option<&'r str>, } @@ -244,7 +248,7 @@ pub struct UserKeyData<'r> { #[schema(example = "ssh-rsa")] pub kind: &'r str, #[schema(example = "AAAAfoobar==")] - pub data: &'r str, + pub data: String, #[schema(example = "user@host 1970-01-01")] pub comment: Option<&'r str>, } diff --git a/server/common/Cargo.toml b/server/common/Cargo.toml index aa358b2..fdabffa 100644 --- a/server/common/Cargo.toml +++ b/server/common/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] futures.workspace = true +log.workspace = true pathdiff = "0.2.3" serde.workspace = true tokio = { workspace = true, features = ["fs", "macros", "process", "rt", "sync"] } diff --git a/server/common/src/git.rs b/server/common/src/git.rs index ac0bdb3..02a9c0a 100644 --- a/server/common/src/git.rs +++ b/server/common/src/git.rs @@ -49,6 +49,7 @@ pub struct Repository { project_id: Option<String>, socket: Option<PathBuf>, githook: Option<PathBuf>, + ssh_config: Option<PathBuf>, // Lock for any repo task, 90% of all tasks are readers but there are some writers // where nothing else may be done. @@ -138,6 +139,12 @@ impl RepoData { self.config_set(repo, "eyeballs.socket", relative.to_str().unwrap()) .await?; } + if let Some(ssh_config) = repo.ssh_config() { + let relative = diff_paths(ssh_config, repo.path()).unwrap(); + self.config_set(repo, "core.sshcommand", + format!("ssh -F {}", relative.to_str().unwrap()).as_str()) + .await?; + } // Handled by pre-receive hook, allow fast forwards for reviews that expect it. self.config_set(repo, "receive.denyNonFastForwards", "false") @@ -454,10 +461,12 @@ impl Repository { remote: Option<impl Into<String>>, project_id: Option<impl Into<String>>, githook: Option<impl Into<PathBuf>>, + ssh_config: Option<impl Into<PathBuf>>, ) -> Self { let path = path.into(); let project_id = project_id.map(|x| x.into()); let githook = githook.map(|x| x.into()); + let ssh_config = ssh_config.map(|x| x.into()); let socket: Option<PathBuf>; if let Some(project_id) = &project_id { socket = Some( @@ -475,6 +484,7 @@ impl Repository { path, socket, githook, + ssh_config, bare, lock: RwLock::new(RepoData::new()), } @@ -500,6 +510,10 @@ impl Repository { self.githook.as_deref() } + pub fn ssh_config(&self) -> Option<&Path> { + self.ssh_config.as_deref() + } + pub fn is_bare(&self) -> bool { self.bare } diff --git a/server/common/src/tests.rs b/server/common/src/tests.rs index 41f44fe..5a02119 100644 --- a/server/common/src/tests.rs +++ b/server/common/src/tests.rs @@ -59,6 +59,7 @@ async fn git_setup(bare: bool) -> git::Repository { None::<String>, None::<String>, None::<PathBuf>, + None::<PathBuf>, ); assert_eq!(repo.remote(), None); assert_eq!(repo.project_id(), None); @@ -212,6 +213,7 @@ async fn git_fetch(bare: bool) -> git::Repository { Some(remote.as_str()), None::<String>, None::<PathBuf>, + None::<PathBuf>, ); assert_eq!(repo.remote(), Some(remote.as_str())); repo.setup().await.unwrap(); diff --git a/server/hook/Cargo.toml b/server/hook/Cargo.toml index 2a298b7..29663e4 100644 --- a/server/hook/Cargo.toml +++ b/server/hook/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] eyeballs-common = { path = "../common" } +log.workspace = true rmp-serde.workspace = true serde.workspace = true tokio = { workspace = true, features = ["full"] } diff --git a/server/hook/src/githook.rs b/server/hook/src/githook.rs index a9cb898..3a27e2c 100644 --- a/server/hook/src/githook.rs +++ b/server/hook/src/githook.rs @@ -57,6 +57,7 @@ async fn main() -> Result<(), Box<dyn Error>> { None::<String>, None::<String>, None::<PathBuf>, + None::<PathBuf>, ); while let Some(line) = lines.next_line().await? { diff --git a/server/migrations/1_initial_eyeballs.sql b/server/migrations/1_initial_eyeballs.sql index 728c329..a39202d 100644 --- a/server/migrations/1_initial_eyeballs.sql +++ b/server/migrations/1_initial_eyeballs.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS projects ( title VARCHAR(1024) NOT NULL, description MEDIUMTEXT NOT NULL DEFAULT '', remote VARCHAR(2048) NOT NULL, + remote_key VARCHAR(8192) NOT NULL, main_branch VARCHAR(1024) NOT NULL ); diff --git a/server/src/authorized_keys.rs b/server/src/authorized_keys.rs index 21651ef..81885b3 100644 --- a/server/src/authorized_keys.rs +++ b/server/src/authorized_keys.rs @@ -1,9 +1,11 @@ use futures::stream::TryStreamExt; +use log::{error, info}; use rocket::fairing::{self, AdHoc}; use rocket::serde::Deserialize; use rocket::{Build, Rocket}; use rocket_db_pools::{sqlx, Database}; use std::borrow::Cow; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; use tokio::fs; @@ -86,8 +88,18 @@ impl AuthorizedKeys { let tmp = path.with_extension("new"); fs::write(&tmp, content.as_bytes()).await?; + if let Ok(metadata) = fs::metadata(path).await { + // Try to replicate ownership and permissions of original file + fs::set_permissions(&tmp, metadata.permissions()) + .await + .unwrap_or(()); + std::os::unix::fs::chown(&tmp, Some(metadata.uid()), Some(metadata.gid())) + .unwrap_or(()); + } fs::rename(tmp, path).await?; + info!("Updated {path:?}, {} keys", keys.len()); + Ok(()) } } @@ -127,7 +139,10 @@ async fn setup_users(rocket: Rocket<Build>) -> fairing::Result { Some(roots) => match Db::fetch(&rocket) { Some(db) => match setup_users_keys(roots, config, db).await { Ok(_) => Ok(rocket), - Err(_) => Err(rocket), + Err(e) => { + error!("{e:?}"); + Err(rocket) + } }, None => Err(rocket), }, diff --git a/server/src/git_root.rs b/server/src/git_root.rs index f818495..1df831c 100644 --- a/server/src/git_root.rs +++ b/server/src/git_root.rs @@ -1,17 +1,22 @@ use futures::{future::TryFutureExt, stream::TryStreamExt}; +use log::{trace, error}; use rmp_serde::{decode, Serializer}; use rocket::fairing::{self, AdHoc}; -use rocket::serde::ser::Serialize; use rocket::serde::Deserialize; +use rocket::serde::ser::Serialize; use rocket::{Build, Rocket}; use rocket_db_pools::{sqlx, Database, Pool}; use std::borrow::Cow; use std::collections::HashMap; +use std::fs::Permissions; use std::ops::Deref; -use std::path::PathBuf; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use tokio::fs; use tokio::net::{UnixListener, UnixStream}; use tokio::task; +use url::Url; use crate::api_model; use crate::fs_utils; @@ -43,14 +48,16 @@ impl Roots { db: &Db, project_id: &str, remote: &str, + remote_key: &str, main_branch: &str, - ) -> Result<(), git::Error> { + ) -> Result<(), anyhow::Error> { let project_id = project_id.to_string(); let repo = setup_project_root( config, db, &project_id, remote.to_string(), + remote_key.to_string(), main_branch.to_string(), ) .await?; @@ -115,13 +122,17 @@ async fn git_process_prehook( let mut errors: Vec<String> = Vec::new(); for row in receive { + trace!("prehook {} {} {}", row.old_value, row.new_value, row.reference); + if !valid_branch_name(row.reference.as_str()) { if row.reference.starts_with("refs/heads/") { + error!("{}: Bad branch name", row.reference); errors.push(format!( "{}: Bad branch name", row.reference.strip_prefix("refs/heads/").unwrap() )); } else { + error!("{}: Only branches are allowed", row.reference); errors.push(format!("{}: Only branches are allowed", row.reference)); } continue; @@ -141,11 +152,13 @@ async fn git_process_prehook( { Ok(_) => {} Err(e) => { + error!("{e:?}"); errors.push(e.message); continue; } }, None => { + error!("{branch}: Missing commiter"); errors.push(format!("{branch}: Missing commiter")); continue; } @@ -177,6 +190,7 @@ async fn git_process_prehook( // All branches should be connected to a branch, but in case of errors this might // be relevant. if result.is_ok() { + error!("{branch}: Not allowed to delete branch, delete review instead."); errors.push(format!( "{branch}: Not allowed to delete branch, delete review instead." )); @@ -187,6 +201,7 @@ async fn git_process_prehook( let (state, rewrite) = match result { Ok(data) => data, Err(e) => { + error!("{e:?}"); errors.push(e.message); continue; } @@ -194,10 +209,12 @@ async fn git_process_prehook( match state { api_model::ReviewState::Dropped => { + error!("{branch}: Review is dropped, no pushes allowed"); errors.push(format!("{branch}: Review is dropped, no pushes allowed")); continue; } api_model::ReviewState::Closed => { + error!("{branch}: Review is closed, no pushes allowed"); errors.push(format!("{branch}: Review is closed, no pushes allowed")); continue; } @@ -223,10 +240,12 @@ async fn git_process_prehook( if equal_content { continue; } + error!("{branch}: History rewrite not allowed"); errors.push(format!("{}: History rewrite not allowed as final result does not match. Please check locally with `git diff {} {}`", branch, row.old_value, row.new_value)); } api_model::Rewrite::Rebase => {} api_model::Rewrite::Disabled => { + error!("{}: Non fast-forward not allowed", row.reference); errors.push(format!( "Non fast-forward not allowed for review: {}", row.reference @@ -257,12 +276,15 @@ async fn git_process_posthook( let mut updated: Vec<u64> = Vec::new(); for row in receive { + trace!("posthook {} {} {}", row.old_value, row.new_value, row.reference); + let branch = row.reference.strip_prefix("refs/heads/").unwrap(); if row.old_value == git::EMPTY { let commiter = match repo.get_commiter(row.reference.as_str()).await { Ok(user) => user, Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: {e}")); continue; } @@ -288,6 +310,7 @@ async fn git_process_posthook( )); } Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } }; @@ -319,6 +342,7 @@ async fn git_process_posthook( updated.push(id); } Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } } @@ -328,6 +352,7 @@ async fn git_process_posthook( } }, Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } } @@ -402,38 +427,114 @@ async fn git_socket_listen( } } +fn get_host(url: &str) -> String { + match Url::parse(url) { + Ok(u) => match u.host_str() { + Some(h) => h.to_string(), + None => String::new(), + }, + Err(_) => String::new(), + } +} + +async fn write_private_file<P: AsRef<Path>, D: AsRef<[u8]>>(path: P, data: D) -> anyhow::Result<()> { + fs::write(&path, data).await?; + let permissions = Permissions::from_mode(0o600); + fs::set_permissions(&path, permissions).await?; + Ok(()) +} + +async fn write_ssh_config(ssh_config: &Path, host: &str, remote_key: &str) -> anyhow::Result<()> { + let host_pattern = if host.is_empty() { + "*" + } else { + host + }; + let basedir = ssh_config.parent().unwrap(); + let identity_file = basedir.join("ssh_identity"); + let known_hosts = basedir.join("ssh_known_hosts"); + + let mut identity_data = String::from("-----BEGIN OPENSSH PRIVATE KEY-----\n"); + { + let mut left = remote_key; + while left.len() > 70 { + let (a, b) = left.split_at(70); + identity_data.push_str(a); + identity_data.push('\n'); + left = b; + } + identity_data.push_str(left); + identity_data.push('\n'); + } + identity_data.push_str("-----END OPENSSH PRIVATE KEY-----\n"); + + let config_data = format!("Host {host_pattern} +IdentityFile ./{} +PasswordAuthentication no +StrictHostKeyChecking accept-new +UpdateHostKeys yes +UserKnownHostsFile ./{} +", identity_file.file_name().unwrap().to_str().unwrap(), known_hosts.file_name().unwrap().to_str().unwrap()); + + let (a, b, c) = tokio::join!( + write_private_file(identity_file, identity_data), + fs::write(known_hosts, "\n"), + fs::write(ssh_config, config_data), + ); + a?; + b?; + c?; + Ok(()) +} + async fn setup_project_root( config: &Config<'_>, db: &Db, project_id: &String, remote: String, + remote_key: String, main_branch: String, -) -> Result<Arc<git::Repository>, git::Error> { +) -> Result<Arc<git::Repository>, anyhow::Error> { let mut path = PathBuf::from(config.git_server_root.to_string()); path.push(project_id); + info!("{project_id}: Setup repo at {path:?}"); let githook = PathBuf::from(config.git_hook.to_string()); + let ssh_config = path.join("ssh_config"); let repo = Arc::new(git::Repository::new( path, true, Some(remote), Some(project_id), Some(githook), + Some(ssh_config), )); repo.setup().await?; + write_ssh_config(repo.ssh_config().unwrap(), + get_host(repo.remote().unwrap()).as_str(), + remote_key.as_str()).await?; + if !repo.remote().unwrap().is_empty() && !main_branch.is_empty() { let bg_repo = repo.clone(); - tokio::spawn(async move { bg_repo.fetch(main_branch).await }); + let bg_project_id = project_id.clone(); + tokio::spawn(async move { + match bg_repo.fetch(&main_branch).await { + Ok(()) => {} + Err(e) => { + error!("{bg_project_id}: fetch {main_branch} returned {e:?}"); + } + } + }); } let socket_repo = repo.clone(); let socket_db = db.deref().clone(); + let bg_project_id = project_id.clone(); tokio::spawn(async move { match git_socket_listen(socket_repo, socket_db).await { Ok(()) => {} Err(e) => { - // TODO: Log - print!("git_socket_listen returned {:?}", e) + error!("{bg_project_id}: git_socket_listen returned {e:?}"); } } }); @@ -444,17 +545,17 @@ async fn setup_project_root( async fn setup_projects_roots(roots: &Roots, config: &Config<'_>, db: &Db) -> anyhow::Result<()> { fs_utils::create_dir_allow_existing(PathBuf::from(config.git_server_root.to_string())).await?; - let projects = sqlx::query!("SELECT id,remote,main_branch FROM projects") + let projects = sqlx::query!("SELECT id,remote,remote_key,main_branch FROM projects") .fetch(&**db) - .map_ok(|r| (r.id, r.remote, r.main_branch)) + .map_ok(|r| (r.id, r.remote, r.remote_key, r.main_branch)) .try_collect::<Vec<_>>() .await .unwrap(); let mut project_repo: HashMap<String, Arc<git::Repository>> = HashMap::new(); - for (id, remote, main_branch) in projects { - let repo = setup_project_root(config, db, &id, remote, main_branch).await?; + for (id, remote, remote_key, main_branch) in projects { + let repo = setup_project_root(config, db, &id, remote, remote_key, main_branch).await?; project_repo.insert(id, repo); } @@ -472,7 +573,10 @@ async fn setup_projects(rocket: Rocket<Build>) -> fairing::Result { Some(roots) => match Db::fetch(&rocket) { Some(db) => match setup_projects_roots(roots, config, db).await { Ok(_) => Ok(rocket), - Err(_) => Err(rocket), + Err(e) => { + error!("{e:?}"); + Err(rocket) + }, }, None => Err(rocket), }, diff --git a/server/src/main.rs b/server/src/main.rs index 9bdfeaf..a2a9165 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -36,6 +36,7 @@ struct Db(sqlx::MySqlPool); #[derive(OpenApi)] #[openapi( paths( + healthcheck, projects, project, project_new, @@ -107,6 +108,18 @@ async fn projects( }) } +fn abbrivate_key(key: &str) -> String { + let len = key.len(); + if len <= 24 { + // Realisticly this will only happen for empty keys, + // but make sure to NEVER send the whole key if it is really short. + return String::new(); + } + let start = &key[0..8]; + let end = &key[len - 8..len]; + return format!("{start}...{end}") +} + async fn get_project( db: &mut <<Db as Database>::Pool as Pool>::Connection, projectid: &str, @@ -129,7 +142,7 @@ async fn get_project( .unwrap(); let project = sqlx::query!( - "SELECT id,title,description,remote,main_branch FROM projects WHERE id=?", + "SELECT id,title,description,remote,remote_key,main_branch FROM projects WHERE id=?", projectid ) .fetch_one(&mut **db) @@ -138,6 +151,7 @@ async fn get_project( title: r.title, description: r.description, remote: r.remote, + remote_key_abbrev: abbrivate_key(r.remote_key.as_str()), main_branch: r.main_branch, users, }) @@ -166,6 +180,20 @@ async fn project( get_project(&mut conn, projectid).await } +// Remove linebreaks and potential openssh wrapper +fn cleanup_key(key: &str) -> String { + let mut ret = String::with_capacity(key.len()); + let mut lines = key.lines(); + while let Some(line) = lines.next() { + match line { + "-----BEGIN OPENSSH PRIVATE KEY-----" | + "-----END OPENSSH PRIVATE KEY-----" => {}, + _ => ret.push_str(line) + } + } + return ret +} + #[utoipa::path( responses( (status = 200, description = "Project created", body = api_model::Project), @@ -185,6 +213,10 @@ async fn project_new( data: Json<api_model::ProjectData<'_>>, ) -> Result<Json<api_model::Project>, Custom<String>> { let remote = data.remote.unwrap_or(""); + let remote_key = match &data.remote_key { + Some(data) => cleanup_key(data.as_str()), + None => String::new(), + }; let main_branch = data.main_branch.unwrap_or("main"); let mut conn = db.get().await.unwrap(); @@ -192,11 +224,12 @@ async fn project_new( let mut tx = conn.begin().await.unwrap(); sqlx::query!( - "INSERT INTO projects (id, title, description, remote, main_branch) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO projects (id, title, description, remote, remote_key, main_branch) VALUES (?, ?, ?, ?, ?, ?)", projectid, data.title.unwrap_or("Unnamed"), data.description.unwrap_or(""), remote, + remote_key, main_branch, ) .execute(&mut *tx) @@ -214,8 +247,8 @@ async fn project_new( } roots_state - .new_project(git_roots_config, db, projectid, remote, main_branch) - .map_err(|e| Custom(Status::InternalServerError, e.message)) + .new_project(git_roots_config, db, projectid, remote, remote_key.as_str(), main_branch) + .map_err(|e| Custom(Status::InternalServerError, format!("{e}"))) .await?; Ok(get_project(&mut conn, projectid).await.unwrap()) @@ -278,6 +311,9 @@ async fn project_update( if let Some(remote) = &data.remote { update_builder.set("remote", remote); } + if let Some(remote_key) = &data.remote_key { + update_builder.set("remote_key", cleanup_key(remote_key)); + } if let Some(main_branch) = &data.main_branch { update_builder.set("main_branch", main_branch); } @@ -1060,6 +1096,19 @@ async fn user_keys( }) } +#[utoipa::path( + responses( + (status = 200, description = "All good"), + ), + security( + (), + ), +)] +#[get("/healthcheck")] +fn healthcheck() -> &'static str { + "" +} + async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result { match Db::fetch(&rocket) { Some(db) => match sqlx::migrate!().run(&**db).await { @@ -1082,6 +1131,7 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> { basepath, // Remember to update openapi paths when you add something here. routes