b90f48e
use std::net::SocketAddr;
use std::sync::Arc;
use russh::keys::PrivateKey;
use russh::server::Server as _;
use russh::{MethodKind, MethodSet, SshId, server};
use tokio::sync::Semaphore;
use config::git::GitSshConfig;
use crate::ssh::error::{SshError, SshResult};
use crate::ssh::handler::GitSshHandler;
use crate::ssh::service::GitSshService;
pub struct GitSshServer {
service: Arc<GitSshService>,
session_slots: Arc<Semaphore>,
}
impl GitSshServer {
pub fn new(service: Arc<GitSshService>) -> Self {
let max = service.config.max_sessions.max(1);
Self {
service,
session_slots: Arc::new(Semaphore::new(max)),
}
}
}
impl server::Server for GitSshServer {
type Handler = GitSshHandler;
fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler {
let permit = self.session_slots.clone().try_acquire_owned().ok();
if permit.is_none() {
tracing::warn!(?peer_addr, "ssh session limit reached; rejecting client");
} else {
tracing::info!(?peer_addr, "new ssh client connected");
}
GitSshHandler::new(self.service.clone(), peer_addr, permit)
}
fn handle_session_error(&mut self, error: russh::Error) {
tracing::debug!("ssh session error: {error}");
}
}
pub fn load_host_key(path: &str) -> SshResult<PrivateKey> {
let data = std::fs::read_to_string(path)
.map_err(|e| SshError::KeyLoadFailed(format!("cannot read {path}: {e}")))?;
let key = PrivateKey::from_openssh(&data)
.map_err(|e| SshError::KeyLoadFailed(format!("openssh decode failed for {path}: {e}")))?;
tracing::debug!(path, "ssh host key loaded");
Ok(key)
}
pub fn build_server_config(config: &GitSshConfig) -> SshResult<server::Config> {
let host_key = load_host_key(&config.host_key_path)?;
let mut methods = MethodSet::empty();
methods.push(MethodKind::PublicKey);
Ok(server::Config {
server_id: SshId::Standard(format!("SSH-2.0-gitdata-{}", env!("CARGO_PKG_VERSION")).into()),
methods,
keys: vec![host_key],
max_auth_attempts: 10,
auth_rejection_time: config.auth_rejection_time,
inactivity_timeout: config.inactivity_timeout,
keepalive_interval: Some(config.keepalive_interval),
keepalive_max: config.keepalive_max as usize,
..Default::default()
})
}
pub async fn run_ssh(service: GitSshService) -> SshResult<()> {
let bind_addr = service.config.bind_addr.clone();
let port = service.config.port;
let max_sessions = service.config.max_sessions;
let config = Arc::new(build_server_config(&service.config)?);
let mut server = GitSshServer::new(Arc::new(service));
tracing::info!(%bind_addr, port, max_sessions, "starting git ssh server");
server
.run_on_address(config, format!("{bind_addr}:{port}"))
.await
.map_err(SshError::Io)?;
tracing::info!(%bind_addr, port, "git ssh server stopped");
Ok(())
}
pub async fn serve_on_listener(
service: GitSshService,
listener: tokio::net::TcpListener,
) -> SshResult<()> {
let config = Arc::new(build_server_config(&service.config)?);
let mut server = GitSshServer::new(Arc::new(service));
tracing::info!(local_addr = ?listener.local_addr().ok(), "serving git ssh on listener");
let running = server.run_on_socket(config, &listener);
running.await.map_err(SshError::Io)?;
Ok(())
}