From 6825f1fff68c54f2d4c2bf7d8a94954202a65597 Mon Sep 17 00:00:00 2001 From: Alexandr Bogomiakov Date: Thu, 24 Jul 2025 03:11:53 +0300 Subject: [PATCH] Fix Release action --- .github/workflows/main.yml | 115 +++++++++++++----- Cargo.toml | 13 +- Dockerfile | 4 +- src/main.rs | 240 ------------------------------------- src/server.rs | 82 +++++++------ 5 files changed, 147 insertions(+), 307 deletions(-) delete mode 100644 src/main.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a010079..987ae42 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,8 @@ on: env: CARGO_TERM_COLOR: always - BINARY_NAME: khm + CLI_BINARY_NAME: khm + DESKTOP_BINARY_NAME: khm-desktop jobs: build: @@ -25,6 +26,10 @@ jobs: build_target: x86_64-unknown-linux-gnu platform_name: linux-amd64 build_type: dynamic + - os: ubuntu-latest + build_target: aarch64-unknown-linux-gnu + platform_name: linux-arm64 + build_type: dynamic - os: windows-latest build_target: x86_64-pc-windows-msvc platform_name: windows-amd64 @@ -67,15 +72,34 @@ jobs: - name: Install rust targets run: rustup target add ${{ matrix.build_target }} - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'dynamic' + - name: Install Linux x86_64 dependencies + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'dynamic' && matrix.build_target == 'x86_64-unknown-linux-gnu' run: | sudo apt-get update sudo apt-get install -y libssl-dev pkg-config libgtk-3-dev libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libxdo-dev - - name: Build Linux Dynamic - if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'dynamic' - run: cargo build --target ${{ matrix.build_target }} --release + - name: Install Linux ARM64 cross-compilation dependencies + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'dynamic' && matrix.build_target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu pkg-config libssl-dev + sudo dpkg --add-architecture arm64 + sudo apt-get update + sudo apt-get install -y libssl-dev:arm64 libgtk-3-dev:arm64 libglib2.0-dev:arm64 libcairo2-dev:arm64 libpango1.0-dev:arm64 libatk1.0-dev:arm64 libgdk-pixbuf2.0-dev:arm64 libxdo-dev:arm64 + + - name: Build Linux x86_64 + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'dynamic' && matrix.build_target == 'x86_64-unknown-linux-gnu' + run: cargo build --target ${{ matrix.build_target }} --release --bins + + - name: Build Linux ARM64 + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'dynamic' && matrix.build_target == 'aarch64-unknown-linux-gnu' + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc + CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++ + PKG_CONFIG_ALLOW_CROSS: 1 + PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig + run: cargo build --target ${{ matrix.build_target }} --release --bins # - name: Build Linux MUSL (no GUI) # if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'musl' @@ -90,18 +114,26 @@ jobs: - name: Build MacOS if: matrix.os == 'macos-latest' - run: cargo build --target ${{ matrix.build_target }} --release + run: cargo build --target ${{ matrix.build_target }} --release --bins - name: Build Windows if: matrix.os == 'windows-latest' - run: cargo build --target ${{ matrix.build_target }} --release + run: cargo build --target ${{ matrix.build_target }} --release --bins - - name: Upload artifact + - name: Upload CLI artifact uses: actions/upload-artifact@v4 with: - name: ${{ env.BINARY_NAME }}_${{ matrix.platform_name }} + name: ${{ env.CLI_BINARY_NAME }}_${{ matrix.platform_name }} path: | - target/${{ matrix.build_target }}/release/${{ env.BINARY_NAME }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} + target/${{ matrix.build_target }}/release/${{ env.CLI_BINARY_NAME }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} + + - name: Upload Desktop artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.DESKTOP_BINARY_NAME }}_${{ matrix.platform_name }} + path: | + target/${{ matrix.build_target }}/release/${{ env.DESKTOP_BINARY_NAME }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} + continue-on-error: true # Don't fail if desktop binary doesn't build on some platforms release: name: Create Release and Upload Assets @@ -124,22 +156,38 @@ jobs: # Copy files with proper naming from each artifact directory for artifact_dir in artifacts/*/; do if [[ -d "$artifact_dir" ]]; then - platform=$(basename "$artifact_dir" | sed 's|khm_||') - echo "Processing platform: $platform" + artifact_name=$(basename "$artifact_dir") + echo "Processing artifact: $artifact_name" + + # Extract binary type and platform from artifact name + if [[ "$artifact_name" =~ ^khm-desktop_(.*)$ ]]; then + binary_type="desktop" + platform="${BASH_REMATCH[1]}" + binary_name="${{ env.DESKTOP_BINARY_NAME }}" + elif [[ "$artifact_name" =~ ^khm_(.*)$ ]]; then + binary_type="cli" + platform="${BASH_REMATCH[1]}" + binary_name="${{ env.CLI_BINARY_NAME }}" + else + echo "Unknown artifact format: $artifact_name" + continue + fi + + echo "Binary type: $binary_type, Platform: $platform, Binary name: $binary_name" # For Windows, look for .exe file specifically if [[ "$platform" == "windows-amd64" ]]; then - exe_file=$(find "$artifact_dir" -name "${{ env.BINARY_NAME }}.exe" -type f | head -1) + exe_file=$(find "$artifact_dir" -name "${binary_name}.exe" -type f | head -1) if [[ -n "$exe_file" ]]; then - cp "$exe_file" "release-assets/${{ env.BINARY_NAME }}_${platform}.exe" - echo "Copied: $exe_file -> release-assets/${{ env.BINARY_NAME }}_${platform}.exe" + cp "$exe_file" "release-assets/${binary_name}_${platform}.exe" + echo "Copied: $exe_file -> release-assets/${binary_name}_${platform}.exe" fi else # For Linux/macOS, look for binary without extension - binary_file=$(find "$artifact_dir" -name "${{ env.BINARY_NAME }}" -type f | head -1) + binary_file=$(find "$artifact_dir" -name "${binary_name}" -type f | head -1) if [[ -n "$binary_file" ]]; then - cp "$binary_file" "release-assets/${{ env.BINARY_NAME }}_${platform}" - echo "Copied: $binary_file -> release-assets/${{ env.BINARY_NAME }}_${platform}" + cp "$binary_file" "release-assets/${binary_name}_${platform}" + echo "Copied: $binary_file -> release-assets/${binary_name}_${platform}" fi fi fi @@ -165,15 +213,26 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - name: Download Linux artifact + - name: Download Linux AMD64 CLI artifact + uses: actions/download-artifact@v4 with: - name: ${{ env.BINARY_NAME }}_linux-amd64 - path: . + name: ${{ env.CLI_BINARY_NAME }}_linux-amd64 + path: amd64/ - - name: List downloaded files + - name: Download Linux ARM64 CLI artifact + uses: actions/download-artifact@v4 + with: + name: ${{ env.CLI_BINARY_NAME }}_linux-arm64 + path: arm64/ + + - name: Prepare binaries for multi-arch build run: | - ls -lah + mkdir -p bin/linux_amd64 bin/linux_arm64 + cp amd64/${{ env.CLI_BINARY_NAME }} bin/linux_amd64/${{ env.CLI_BINARY_NAME }} + cp arm64/${{ env.CLI_BINARY_NAME }} bin/linux_arm64/${{ env.CLI_BINARY_NAME }} + chmod +x bin/linux_amd64/${{ env.CLI_BINARY_NAME }} + chmod +x bin/linux_arm64/${{ env.CLI_BINARY_NAME }} + ls -la bin/*/ - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -187,10 +246,6 @@ jobs: username: ultradesu password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set exec flag - run: | - chmod +x ${{ env.BINARY_NAME }} - - name: Set outputs id: get_tag run: | @@ -202,5 +257,5 @@ jobs: context: . platforms: linux/amd64,linux/arm64 push: true - tags: ultradesu/${{ env.BINARY_NAME }}:latest,ultradesu/${{ env.BINARY_NAME }}:${{ steps.get_tag.outputs.tag }} + tags: ultradesu/${{ env.CLI_BINARY_NAME }}:latest,ultradesu/${{ env.CLI_BINARY_NAME }}:${{ steps.get_tag.outputs.tag }} diff --git a/Cargo.toml b/Cargo.toml index 2d3d135..d7bc120 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,14 @@ license = "WTFPL" keywords = ["ssh", "known-hosts", "security", "system-admin", "automation"] categories = ["command-line-utilities", "network-programming"] +[[bin]] +name = "khm" +path = "src/bin/cli.rs" + +[[bin]] +name = "khm-desktop" +path = "src/bin/desktop.rs" + [dependencies] actix-web = "4" serde = { version = "1.0", features = ["derive"] } @@ -38,7 +46,10 @@ env_logger = "0.11" urlencoding = "2.1" [features] -default = ["gui"] +default = ["server", "web", "gui"] +cli = ["server", "web"] +desktop = ["gui"] gui = ["tray-icon", "eframe", "egui", "winit", "notify", "notify-debouncer-mini"] server = [] +web = [] diff --git a/Dockerfile b/Dockerfile index 4ab3a9f..3a24b69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,9 @@ RUN apt-get update && apt-get install -y \ libxdo3 \ && rm -rf /var/lib/apt/lists/* -COPY khm /usr/local/bin/khm +# Copy the appropriate binary based on the target architecture +ARG TARGETARCH +COPY bin/linux_${TARGETARCH}/khm /usr/local/bin/khm RUN chmod +x /usr/local/bin/khm ENTRYPOINT ["/usr/local/bin/khm"] diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 4659cae..0000000 --- a/src/main.rs +++ /dev/null @@ -1,240 +0,0 @@ -mod client; -mod db; -mod gui; -mod server; -mod web; - -use clap::Parser; -use env_logger; -use log::{error, info}; - -/// This application manages SSH keys and flows, either as a server or client. -/// In server mode, it stores keys and flows in a PostgreSQL database. -/// In client mode, it sends keys to the server and can update the known_hosts file with keys from the server. -#[derive(Parser, Debug, Clone)] -#[command( - author = env!("CARGO_PKG_AUTHORS"), - version = env!("CARGO_PKG_VERSION"), - about = "SSH Host Key Manager", - long_about = None, - after_help = "Examples:\n\ - \n\ - Running in GUI tray mode (default):\n\ - khm\n\ - \n\ - Running in GUI tray mode with background daemon:\n\ - khm --daemon\n\ - \n\ - Running in server mode:\n\ - khm --server --ip 0.0.0.0 --port 1337 --db-host psql.psql.svc --db-name khm --db-user admin --db-password --flows work,home\n\ - \n\ - Running in client mode to send diff and sync ~/.ssh/known_hosts with remote flow `work` in place:\n\ - khm --host https://khm.example.com --flow work --known-hosts ~/.ssh/known_hosts --in-place\n\ - \n\ - Running settings window:\n\ - khm --settings-ui\n\ - \n\ - " -)] -pub struct Args { - /// Run in server mode (default: false) - #[arg(long, help = "Run in server mode")] - pub server: bool, - - /// Hide console window and run in background (default: auto when no arguments) - #[arg(long, help = "Hide console window and run in background")] - pub daemon: bool, - - /// Run settings UI window - #[arg(long, help = "Run settings UI window")] - pub settings_ui: bool, - - /// Update the known_hosts file with keys from the server after sending keys (default: false) - #[arg( - long, - help = "Server mode: Sync the known_hosts file with keys from the server" - )] - pub in_place: bool, - - /// Comma-separated list of flows to manage (default: default) - #[arg(long, default_value = "default", value_parser, num_args = 1.., value_delimiter = ',', help = "Server mode: Comma-separated list of flows to manage")] - pub flows: Vec, - - /// IP address to bind the server or client to (default: 127.0.0.1) - #[arg( - short, - long, - default_value = "127.0.0.1", - help = "Server mode: IP address to bind the server to" - )] - pub ip: String, - - /// Port to bind the server or client to (default: 8080) - #[arg( - short, - long, - default_value = "8080", - help = "Server mode: Port to bind the server to" - )] - pub port: u16, - - /// Hostname or IP address of the PostgreSQL database (default: 127.0.0.1) - #[arg( - long, - default_value = "127.0.0.1", - help = "Server mode: Hostname or IP address of the PostgreSQL database" - )] - pub db_host: String, - - /// Name of the PostgreSQL database (default: khm) - #[arg( - long, - default_value = "khm", - help = "Server mode: Name of the PostgreSQL database" - )] - pub db_name: String, - - /// Username for the PostgreSQL database (required in server mode) - #[arg( - long, - required_if_eq("server", "true"), - help = "Server mode: Username for the PostgreSQL database" - )] - pub db_user: Option, - - /// Password for the PostgreSQL database (required in server mode) - #[arg( - long, - required_if_eq("server", "true"), - help = "Server mode: Password for the PostgreSQL database" - )] - pub db_password: Option, - - /// Host address of the server to connect to in client mode (required in client mode) - #[arg( - long, - required_if_eq("server", "false"), - help = "Client mode: Full host address of the server to connect to. Like https://khm.example.com" - )] - pub host: Option, - - /// Flow name to use on the server - #[arg( - long, - required_if_eq("server", "false"), - help = "Client mode: Flow name to use on the server" - )] - pub flow: Option, - - /// Path to the known_hosts file (default: ~/.ssh/known_hosts) - #[arg( - long, - default_value = "~/.ssh/known_hosts", - help = "Client mode: Path to the known_hosts file" - )] - pub known_hosts: String, - - /// Basic auth string for client mode. Format: user:pass - #[arg(long, default_value = "", help = "Client mode: Basic Auth credentials")] - pub basic_auth: String, -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - // Configure logging to show only khm logs, filtering out noisy library logs - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Warn) // Default level for all modules - .filter_module("khm", log::LevelFilter::Debug) // Our app logs - .filter_module("actix_web", log::LevelFilter::Info) // Server logs - .filter_module("reqwest", log::LevelFilter::Warn) // HTTP client - .filter_module("winit", log::LevelFilter::Error) // Window management - .filter_module("egui", log::LevelFilter::Error) // GUI framework - .filter_module("eframe", log::LevelFilter::Error) // GUI framework - .filter_module("tray_icon", log::LevelFilter::Error) // Tray icon - .filter_module("wgpu", log::LevelFilter::Error) // Graphics - .filter_module("naga", log::LevelFilter::Error) // Graphics - .filter_module("glow", log::LevelFilter::Error) // Graphics - .filter_module("tracing", log::LevelFilter::Error) // Tracing spans - .init(); - - info!("Starting SSH Key Manager"); - - let args = Args::parse(); - - // Hide console on Windows if daemon flag is set - if args.daemon { - #[cfg(target_os = "windows")] - { - extern "system" { - fn FreeConsole() -> i32; - } - unsafe { - FreeConsole(); - } - } - } - - // Settings UI mode - just show settings window and exit - if args.settings_ui { - // Always hide console for settings window - #[cfg(target_os = "windows")] - { - extern "system" { - fn FreeConsole() -> i32; - } - unsafe { - FreeConsole(); - } - } - - #[cfg(feature = "gui")] - { - info!("Running settings UI window"); - gui::run_settings_window(); - return Ok(()); - } - #[cfg(not(feature = "gui"))] - { - error!("GUI features not compiled. Install system dependencies and rebuild with --features gui"); - return Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "GUI features not compiled", - )); - } - } - - // Check if we should run GUI mode (default when no server/client args) - if !args.server && (args.host.is_none() || args.flow.is_none()) { - info!("Running in GUI mode"); - #[cfg(feature = "gui")] - { - if let Err(e) = gui::run_gui().await { - error!("Failed to run GUI: {}", e); - } - } - #[cfg(not(feature = "gui"))] - { - error!("GUI features not compiled. Install system dependencies and rebuild with --features gui"); - return Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "GUI features not compiled", - )); - } - return Ok(()); - } - - if args.server { - info!("Running in server mode"); - if let Err(e) = server::run_server(args).await { - error!("Failed to run server: {}", e); - } - } else { - info!("Running in client mode"); - if let Err(e) = client::run_client(args).await { - error!("Failed to run client: {}", e); - } - } - - info!("Application has exited"); - Ok(()) -} diff --git a/src/server.rs b/src/server.rs index 3a75e23..d4ff17d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -300,48 +300,60 @@ pub async fn run_server(args: crate::Args) -> std::io::Result<()> { info!("Starting HTTP server on {}:{}", args.ip, args.port); HttpServer::new(move || { - App::new() + let mut app = App::new() .app_data(web::Data::new(flows.clone())) .app_data(web::Data::new(db_client.clone())) .app_data(allowed_flows.clone()) - // API routes - .route("/api/version", web::get().to(crate::web::get_version_api)) - .route("/api/flows", web::get().to(crate::web::get_flows_api)) - .route( - "/{flow_id}/scan-dns", - web::post().to(crate::web::scan_dns_resolution), - ) - .route( - "/{flow_id}/bulk-deprecate", - web::post().to(crate::web::bulk_deprecate_servers), - ) - .route( - "/{flow_id}/bulk-restore", - web::post().to(crate::web::bulk_restore_servers), - ) - .route( - "/{flow_id}/keys/{server}", - web::delete().to(crate::web::delete_key_by_server), - ) - .route( - "/{flow_id}/keys/{server}/restore", - web::post().to(crate::web::restore_key_by_server), - ) - .route( - "/{flow_id}/keys/{server}/delete", - web::delete().to(crate::web::permanently_delete_key_by_server), - ) // Original API routes .route("/{flow_id}/keys", web::get().to(get_keys)) - .route("/{flow_id}/keys", web::post().to(add_keys)) - // Web interface routes - .route("/", web::get().to(crate::web::serve_web_interface)) - .route( - "/static/{filename:.*}", - web::get().to(crate::web::serve_static_file), - ) + .route("/{flow_id}/keys", web::post().to(add_keys)); + + #[cfg(feature = "web")] + { + app = app.configure(configure_web_routes); + } + + app }) .bind((args.ip.as_str(), args.port))? .run() .await } + +#[cfg(feature = "web")] +fn configure_web_routes(cfg: &mut web::ServiceConfig) { + cfg + // API routes + .route("/api/version", web::get().to(crate::web::get_version_api)) + .route("/api/flows", web::get().to(crate::web::get_flows_api)) + .route( + "/{flow_id}/scan-dns", + web::post().to(crate::web::scan_dns_resolution), + ) + .route( + "/{flow_id}/bulk-deprecate", + web::post().to(crate::web::bulk_deprecate_servers), + ) + .route( + "/{flow_id}/bulk-restore", + web::post().to(crate::web::bulk_restore_servers), + ) + .route( + "/{flow_id}/keys/{server}", + web::delete().to(crate::web::delete_key_by_server), + ) + .route( + "/{flow_id}/keys/{server}/restore", + web::post().to(crate::web::restore_key_by_server), + ) + .route( + "/{flow_id}/keys/{server}/delete", + web::delete().to(crate::web::permanently_delete_key_by_server), + ) + // Web interface routes + .route("/", web::get().to(crate::web::serve_web_interface)) + .route( + "/static/{filename:.*}", + web::get().to(crate::web::serve_static_file), + ); +}