Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ pub struct Config {
pub base_dir: PathBuf,
pub log: LogConfig,
pub database: DbConfig,
pub ssh: SshConfig,
pub storage: StorageConfig,
pub monorepo: MonoConfig,
pub pack: PackConfig,
Expand Down Expand Up @@ -188,19 +187,6 @@ impl Default for DbConfig {
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SshConfig {
pub ssh_key_path: PathBuf,
}

impl Default for SshConfig {
fn default() -> Self {
Self {
ssh_key_path: PathBuf::from("/tmp/.mega/ssh"),
}
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StorageConfig {
pub obs_access_key: String,
Expand All @@ -223,12 +209,14 @@ impl Default for StorageConfig {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MonoConfig {
pub import_dir: PathBuf,
pub disable_http_push: bool,
}

impl Default for MonoConfig {
fn default() -> Self {
Self {
import_dir: PathBuf::from("/third-part"),
disable_http_push: false,
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions docker/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ min_connection = 8
sqlx_logging = false


[ssh]
ssh_key_path = "${base_dir}/etc/ssh"

[storage]

obs_access_key = ""
Expand All @@ -56,6 +53,7 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
## Mega treats files under this directory as import repo and other directories as monorepo
import_dir = "/third-part"

disable_http_push = false

[pack]
# The maximum memory used by decode, Unit is GB
Expand Down
2 changes: 1 addition & 1 deletion docker/mono-pg-dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ EXPOSE 5432

# Add the database initialization script to the container
# When the container starts, PostgreSQL will automatically execute all .sql files in the docker-entrypoint-initdb.d/ directory
COPY ./sql/postgres/pg_20240910__init.sql /docker-entrypoint-initdb.d/
COPY ./sql/postgres/pg_20240912__init.sql /docker-entrypoint-initdb.d/

CMD ["postgres"]
4 changes: 1 addition & 3 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,6 @@ min_connection = 16
sqlx_logging = false


[ssh]
ssh_key_path = "${base_dir}/ssh"

[storage]

obs_access_key = ""
Expand All @@ -293,6 +290,7 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
## Mega treats files under this directory as import repo and other directories as monorepo
import_dir = "/third-part"

disable_http_push = false

[pack]
# The maximum memory used by decode, Unit is GB
Expand Down
2 changes: 2 additions & 0 deletions jupiter/callisto/src/ssh_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub struct Model {
pub id: i64,
pub user_id: i64,
#[sea_orm(column_type = "Text")]
pub title: String,
#[sea_orm(column_type = "Text")]
pub ssh_key: String,
#[sea_orm(column_type = "Text")]
pub finger: String,
Expand Down
2 changes: 1 addition & 1 deletion jupiter/src/storage/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async fn setup_sql(conn: &DatabaseConnection) -> Result<(), TransactionError<DbE
let backend = txn.get_database_backend();

// `include_str!` will expand the file while compiling, so `.sql` is not needed after that
const SETUP_SQL: &str = include_str!("../../../sql/sqlite/sqlite_20240910_init.sql");
const SETUP_SQL: &str = include_str!("../../../sql/sqlite/sqlite_20240912_init.sql");
txn.execute(Statement::from_string(backend, SETUP_SQL)).await?;
Ok(())
})
Expand Down
2 changes: 2 additions & 0 deletions jupiter/src/storage/user_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ impl UserStorage {
pub async fn save_ssh_key(
&self,
user_id: i64,
title: &str,
ssh_key: &str,
finger: &str,
) -> Result<(), MegaError> {
let model = ssh_keys::Model {
id: generate_id(),
user_id,
title: title.to_owned(),
ssh_key: ssh_key.to_owned(),
finger: finger.to_owned(),
created_at: chrono::Utc::now().naive_utc(),
Expand Down
4 changes: 1 addition & 3 deletions mega/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ min_connection = 16
sqlx_logging = false


[ssh]
ssh_key_path = "${base_dir}/ssh"

[storage]

obs_access_key = ""
Expand All @@ -56,6 +53,7 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
## Mega treats files under this directory as import repo and other directories as monorepo
import_dir = "/third-part"

disable_http_push = false

[pack]
# The maximum memory used by decode, Unit is GB
Expand Down
1 change: 1 addition & 0 deletions mono/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ callisto = { workspace = true }
jupiter = { workspace = true }
ceres = { workspace = true }
taurus = { workspace = true }
vault = { workspace = true }

anyhow = { workspace = true }
axum = { workspace = true }
Expand Down
4 changes: 1 addition & 3 deletions mono/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ min_connection = 16
sqlx_logging = false


[ssh]
ssh_key_path = "${base_dir}/ssh"

[storage]

obs_access_key = ""
Expand All @@ -56,6 +53,7 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
## Mega treats files under this directory as import repo and other directories as monorepo
import_dir = "/third-part"

disable_http_push = false

[pack]
# The maximum memory used by decode, Unit is GB
Expand Down
3 changes: 3 additions & 0 deletions mono/src/api/user/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ use sea_orm::entity::prelude::*;

#[derive(Debug, Deserialize)]
pub struct AddSSHKey {
pub title: String,
pub ssh_key: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ListSSHKey {
pub id: i64,
pub title: String,
pub ssh_key: String,
pub finger: String,
pub created_at: DateTime,
Expand All @@ -19,6 +21,7 @@ impl From<ssh_keys::Model> for ListSSHKey {
fn from(value: ssh_keys::Model) -> Self {
Self {
id: value.id,
title: value.title,
ssh_key: value.ssh_key,
finger: value.finger,
created_at: value.created_at,
Expand Down
24 changes: 14 additions & 10 deletions mono/src/api/user/user_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use russh_keys::parse_public_key_base64;

use common::model::CommonResult;

use crate::api::{error::ApiError, oauth::model::LoginUser};
use crate::api::user::model::AddSSHKey;
use crate::api::user::model::ListSSHKey;
use crate::api::MonoApiServiceState;
use crate::api::{error::ApiError, oauth::model::LoginUser};

pub fn routers() -> Router<MonoApiServiceState> {
Router::new()
Expand All @@ -32,20 +32,24 @@ async fn add_key(
state: State<MonoApiServiceState>,
Json(json): Json<AddSSHKey>,
) -> Result<Json<CommonResult<String>>, ApiError> {
let key_data = json
.ssh_key
.split_whitespace()
.nth(1)
.ok_or("Invalid key format")
.unwrap();

let key = parse_public_key_base64(key_data)?;
let ssh_key: Vec<&str> = json.ssh_key.split_whitespace().collect();
let key = parse_public_key_base64(ssh_key.get(1).ok_or("Invalid key format").unwrap())?;
let title = if !json.title.is_empty() {
json.title
} else {
ssh_key
.get(2)
.ok_or("Invalid key format")
.unwrap()
.to_owned()
.to_owned()
};

let res = state
.context
.services
.user_storage
.save_ssh_key(user.user_id, &json.ssh_key, &key.fingerprint())
.save_ssh_key(user.user_id, &title, &json.ssh_key, &key.fingerprint())
.await;
let res = match res {
Ok(_) => CommonResult::success(None),
Expand Down
9 changes: 7 additions & 2 deletions mono/src/server/https_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,7 @@ pub async fn app(config: Config, host: String, port: u16, common: CommonOptions)
}

lazy_static! {
/// The following regular expressions are used to match the LFS server discovery protocol.
/// Git Protocol
/// The following regular expressions are used to match the Git server protocol.
static ref INFO_REFS_REGEX: Regex = Regex::new(r"/info/refs$").unwrap();
static ref REGEX_GIT_UPLOAD_PACK: Regex = Regex::new(r"/git-upload-pack$").unwrap();
static ref REGEX_GIT_RECEIVE_PACK: Regex = Regex::new(r"/git-receive-pack$").unwrap();
Expand Down Expand Up @@ -186,6 +185,12 @@ pub async fn post_method_router(
pack_protocol.service_type = Some(ServiceType::UploadPack);
crate::git_protocol::http::git_upload_pack(req, pack_protocol).await
} else if REGEX_GIT_RECEIVE_PACK.is_match(uri.path()) {
if state.context.config.monorepo.disable_http_push {
return Err((
StatusCode::FORBIDDEN,
String::from("HTTP Push Has Been Disabled"),
))
}
let mut pack_protocol = SmartProtocol::new(
remove_git_suffix(uri.clone(), "/git-receive-pack"),
state.context.clone(),
Expand Down
83 changes: 23 additions & 60 deletions mono/src/server/ssh_server.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::io::Read;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;

use anyhow::Result;
use bytes::BytesMut;
use clap::Args;

use common::config::Config;
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
use ed25519_dalek::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey};
use ed25519_dalek::pkcs8::{DecodePrivateKey, EncodePrivateKey};
use ed25519_dalek::SigningKey;
use russh::server::Server;
use russh_keys::key::KeyPair;

use common::model::CommonOptions;
use jupiter::context::Context;
use tokio::sync::Mutex;
use vault::vault::{read_secret, write_secret};

use crate::git_protocol::ssh::SshServer;

Expand All @@ -36,18 +33,12 @@ pub struct SshOptions {
pub struct SshCustom {
#[arg(long, default_value_t = 2222)]
ssh_port: u16,

#[arg(long, value_name = "FILE")]
ssh_key_path: Option<PathBuf>,

#[arg(long, value_name = "FILE")]
ssh_cert_path: Option<PathBuf>,
}

/// start a ssh server
pub async fn start_server(config: Config, command: &SshOptions) {
// we need to persist the key to prevent key expired after server restart.
let client_key = load_key(config.ssh.ssh_key_path.clone()).unwrap();
let client_key = load_key();
let client_pubkey = Arc::new(client_key.clone_public_key().unwrap());

let mut ru_config = russh::server::Config {
Expand All @@ -65,12 +56,7 @@ pub async fn start_server(config: Config, command: &SshOptions) {

let SshOptions {
common: CommonOptions { host, .. },
custom:
SshCustom {
ssh_port,
ssh_key_path: _,
ssh_cert_path: _,
},
custom: SshCustom { ssh_port },
} = command;
let context = Context::new(config).await;
let mut ssh_server = SshServer {
Expand All @@ -86,51 +72,28 @@ pub async fn start_server(config: Config, command: &SshOptions) {
ssh_server.run_on_address(ru_config, addr).await.unwrap();
}

/// # Loads an SSH keypair.
///
/// This function follows the following steps:
/// 1. It retrieves the root directory for the SSH key from path.
/// 2. It constructs the path to the SSH private key file by joining the root directory with the filename "id_rsa" using PathBuf.
/// 3. It checks if the key file exists. If it doesn't, it generates a new Ed25519 keypair using KeyPair::generate_ed25519.
/// - The generated keypair is then written to the key file.
/// 4. If the key file exists, it reads the keypair from the file.
/// - The keypair is loaded from the file and returned.
///
/// # Returns
///
/// An asynchronous Result containing the loaded SSH keypair if successful, or an error if any of the steps fail.
pub fn load_key(key_root: PathBuf) -> Result<KeyPair> {
let key_path = PathBuf::from(&key_root).join("id_rsa");

if !Path::new(&key_root).exists() {
// create ssh directory if not exists
create_dir_all(&key_root).unwrap();
}

if !key_path.exists() {
pub fn load_key() -> KeyPair {
let ssh_key = read_secret("ssh_server_key").unwrap();
if let Some(ssh_key) = ssh_key {
// load the keypair from the vault
let data = ssh_key.data.unwrap();
let secret_key = data["secret_key"].as_str().unwrap().to_string();
let keypair = SigningKey::from_pkcs8_pem(&secret_key).expect("parsing key err");
KeyPair::Ed25519(keypair)
} else {
// generate a keypair if not exists
let keys = KeyPair::generate_ed25519().unwrap();

if let KeyPair::Ed25519(inner_pair) = &keys {
// Handle other variants or provide a default behavior
inner_pair
.write_pkcs8_pem_file(key_path, LineEnding::CR)
.unwrap();
inner_pair
.verifying_key()
.write_public_key_pem_file(
PathBuf::from(&key_root).join("id_rsa.pub"),
LineEnding::CR,
)
.unwrap();
let secret = serde_json::json!({
"secret_key": *inner_pair.to_pkcs8_pem(LineEnding::CR).unwrap()
})
.as_object()
.unwrap()
.clone();
write_secret("ssh_server_key", Some(secret)).unwrap_or_else(|e| {
panic!("Failed to write ssh_server_key: {:?}", e);
});
}
Ok(keys)
} else {
// load the keypair from the file
let mut file = File::open(&key_path)?;
let mut pem_str = String::new();
file.read_to_string(&mut pem_str).unwrap();
let keypair = SigningKey::from_pkcs8_pem(&pem_str)?;
Ok(KeyPair::Ed25519(keypair))
keys
}
}
Loading