summaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/authorized_keys.rs17
-rw-r--r--server/src/git_root.rs142
-rw-r--r--server/src/main.rs64
-rw-r--r--server/src/tests.rs8
4 files changed, 215 insertions, 16 deletions
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..01cc2b5 100644
--- a/server/src/git_root.rs
+++ b/server/src/git_root.rs
@@ -1,4 +1,5 @@
use futures::{future::TryFutureExt, stream::TryStreamExt};
+use log::{error, trace};
use rmp_serde::{decode, Serializer};
use rocket::fairing::{self, AdHoc};
use rocket::serde::ser::Serialize;
@@ -7,11 +8,15 @@ 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,22 @@ 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 +157,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 +195,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 +206,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 +214,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 +245,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 +281,20 @@ 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 +320,7 @@ async fn git_process_posthook(
));
}
Err(e) => {
+ error!("{e:?}");
messages.push(format!("{branch}: Error {e}",));
}
};
@@ -319,6 +352,7 @@ async fn git_process_posthook(
updated.push(id);
}
Err(e) => {
+ error!("{e:?}");
messages.push(format!("{branch}: Error {e}",));
}
}
@@ -328,6 +362,7 @@ async fn git_process_posthook(
}
},
Err(e) => {
+ error!("{e:?}");
messages.push(format!("{branch}: Error {e}",));
}
}
@@ -402,38 +437,120 @@ 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 +561,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 +589,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..973febe 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 {
+ // Realistically 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,19 @@ 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 +212,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 +223,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 +246,15 @@ 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 +317,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 +1102,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 +1137,7 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> {
basepath,
// Remember to update openapi paths when you add something here.
routes![
+ healthcheck,
projects,
project,
project_new,
diff --git a/server/src/tests.rs b/server/src/tests.rs
index 1b42485..cc71bf3 100644
--- a/server/src/tests.rs
+++ b/server/src/tests.rs
@@ -202,6 +202,7 @@ async fn new_project(client: &Client) -> api_model::Project {
title: Some("foo"),
description: Some("bar"),
remote: None,
+ remote_key: None,
main_branch: Some("zod"),
}),
)
@@ -334,6 +335,7 @@ async fn test_project_new() {
assert_eq!(project.title, "foo");
assert_eq!(project.description, "bar");
assert_eq!(project.remote, "");
+ assert_eq!(project.remote_key_abbrev, "");
assert_eq!(project.main_branch, "zod");
assert_eq!(project.users.len(), 1);
let user = project.users.get(0).unwrap();
@@ -367,6 +369,7 @@ async fn test_project_new_duplicate() {
title: Some("foo"),
description: Some("bar"),
remote: None,
+ remote_key: None,
main_branch: Some("zod"),
})
.header(&FAKE_IP)
@@ -389,6 +392,7 @@ async fn test_project_update() {
title: Some("foo"),
description: None,
remote: None,
+ remote_key: None,
main_branch: Some("fum"),
},
))
@@ -402,6 +406,7 @@ async fn test_project_update() {
title: None,
description: Some("bar"),
remote: None,
+ remote_key: None,
main_branch: Some("zod"),
})
.header(&FAKE_IP)
@@ -413,6 +418,7 @@ async fn test_project_update() {
assert_eq!(updated_project.title, project.title);
assert_eq!(updated_project.description, "bar");
assert_eq!(updated_project.remote, project.remote);
+ assert_eq!(updated_project.remote_key_abbrev, project.remote_key_abbrev);
assert_eq!(updated_project.main_branch, "zod");
}
@@ -428,6 +434,7 @@ async fn test_project_update_unknown() {
title: Some("foo"),
description: Some("bar"),
remote: None,
+ remote_key: None,
main_branch: Some("zod"),
})
.header(&FAKE_IP)
@@ -660,6 +667,7 @@ async fn test_project_check_maintainer() {
title: None,
description: Some("fool"),
remote: None,
+ remote_key: None,
main_branch: None,
})
.header(&FAKE_IP)