4 Commits

Author SHA1 Message Date
3bedadf394 Fix ci 2025-03-14 02:44:17 +02:00
a0f83db19a Fix ci 2025-03-14 02:30:34 +02:00
c01eb48451 Added macos build 2025-03-14 02:27:40 +02:00
c3575b013f Added basic auth support 2025-03-14 02:14:15 +02:00
5 changed files with 87 additions and 28 deletions

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest, macos-latest]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
build_target: x86_64-unknown-linux-musl build_target: x86_64-unknown-linux-musl
@ -23,6 +23,9 @@ jobs:
- os: windows-latest - os: windows-latest
build_target: x86_64-pc-windows-msvc build_target: x86_64-pc-windows-msvc
platform_name: windows-amd64 platform_name: windows-amd64
- os: macos-latest
build_target: aarch64-apple-darwin
platform_name: macos-arm64
permissions: permissions:
contents: write contents: write
steps: steps:
@ -67,8 +70,12 @@ jobs:
with: with:
args: cargo build --target ${{ matrix.build_target }} --release args: cargo build --target ${{ matrix.build_target }} --release
- name: Build MacOS
if: matrix.os == 'macos-latest'
run: cargo build --target ${{ matrix.build_target }} --release
- name: Build Windows - name: Build Windows
if: matrix.os != 'ubuntu-latest' if: matrix.os == 'windows-latest'
run: cargo build --target ${{ matrix.build_target }} --release run: cargo build --target ${{ matrix.build_target }} --release
- name: Upload artifact - name: Upload artifact
@ -87,16 +94,13 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Create Release - uses: ncipollo/release-action@v1
id: create_release id: create_release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }} allowUpdates: true
release_name: Release ${{ github.ref }} #artifacts: "release.tar.gz,foo/*.txt"
draft: false
prerelease: false
upload: upload:
name: Upload Release Assets name: Upload Release Assets
@ -104,12 +108,14 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest, macos-latest]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
platform_name: linux-amd64 platform_name: linux-amd64
- os: windows-latest - os: windows-latest
platform_name: windows-amd64 platform_name: windows-amd64
- os: macos-latest
platform_name: macos-arm64
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

5
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "actix-codec" name = "actix-codec"
@ -1053,9 +1053,10 @@ dependencies = [
[[package]] [[package]]
name = "khm" name = "khm"
version = "0.2.0" version = "0.2.2"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"base64 0.21.7",
"chrono", "chrono",
"clap", "clap",
"env_logger", "env_logger",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "khm" name = "khm"
version = "0.2.2" version = "0.3.0"
edition = "2021" edition = "2021"
authors = ["AB <ab@hexor.cy>", "ChatGPT-4o"] authors = ["AB <ab@hexor.cy>", "ChatGPT-4o"]
@ -11,6 +11,7 @@ serde_json = "1.0"
env_logger = "0.11.3" env_logger = "0.11.3"
log = "0.4" log = "0.4"
regex = "1.10.5" regex = "1.10.5"
base64 = "0.21"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }

View File

@ -1,4 +1,6 @@
use base64::{engine::general_purpose, Engine as _};
use log::{error, info}; use log::{error, info};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs::File; use std::fs::File;
@ -48,10 +50,39 @@ fn write_known_hosts(file_path: &str, keys: &[SshKey]) -> io::Result<()> {
Ok(()) Ok(())
} }
async fn send_keys_to_server(host: &str, keys: Vec<SshKey>) -> Result<(), reqwest::Error> { async fn send_keys_to_server(
host: &str,
keys: Vec<SshKey>,
auth_string: &str,
) -> Result<(), reqwest::Error> {
let client = Client::new(); let client = Client::new();
let url = format!("{}/keys", host); let url = format!("{}/keys", host);
let response = client.post(&url).json(&keys).send().await?; info!("URL: {} ", url);
let mut headers = HeaderMap::new();
if !auth_string.is_empty() {
let parts: Vec<&str> = auth_string.splitn(2, ':').collect();
if parts.len() == 2 {
let username = parts[0];
let password = parts[1];
let auth_header_value = format!("{}:{}", username, password);
let encoded_auth = general_purpose::STANDARD.encode(auth_header_value);
let auth_header = format!("Basic {}", encoded_auth);
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header).unwrap());
} else {
error!("Invalid auth string format. Expected 'username:password'");
}
}
let response = client
.post(&url)
.headers(headers)
.json(&keys)
.send()
.await?;
if response.status().is_success() { if response.status().is_success() {
info!("Keys successfully sent to server."); info!("Keys successfully sent to server.");
@ -65,22 +96,38 @@ async fn send_keys_to_server(host: &str, keys: Vec<SshKey>) -> Result<(), reqwes
Ok(()) Ok(())
} }
async fn get_keys_from_server(host: &str) -> Result<Vec<SshKey>, reqwest::Error> { async fn get_keys_from_server(
host: &str,
auth_string: &str,
) -> Result<Vec<SshKey>, reqwest::Error> {
let client = Client::new(); let client = Client::new();
let url = format!("{}/keys", host); let url = format!("{}/keys", host);
let response = client.get(&url).send().await?;
if response.status().is_success() { let mut headers = HeaderMap::new();
if !auth_string.is_empty() {
let parts: Vec<&str> = auth_string.splitn(2, ':').collect();
if parts.len() == 2 {
let username = parts[0];
let password = parts[1];
let auth_header_value = format!("{}:{}", username, password);
let encoded_auth = general_purpose::STANDARD.encode(auth_header_value);
let auth_header = format!("Basic {}", encoded_auth);
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header).unwrap());
} else {
error!("Invalid auth string format. Expected 'username:password'");
}
}
let response = client.get(&url).headers(headers).send().await?;
let response = response.error_for_status()?;
let keys: Vec<SshKey> = response.json().await?; let keys: Vec<SshKey> = response.json().await?;
info!("Received {} keys from server", keys.len()); info!("Received {} keys from server", keys.len());
Ok(keys) Ok(keys)
} else {
error!(
"Failed to get keys from server. Status: {}",
response.status()
);
Ok(vec![])
}
} }
pub async fn run_client(args: crate::Args) -> std::io::Result<()> { pub async fn run_client(args: crate::Args) -> std::io::Result<()> {
@ -89,13 +136,13 @@ pub async fn run_client(args: crate::Args) -> std::io::Result<()> {
let host = args.host.expect("host is required in client mode"); let host = args.host.expect("host is required in client mode");
info!("Client mode: Sending keys to server at {}", host); info!("Client mode: Sending keys to server at {}", host);
send_keys_to_server(&host, keys) send_keys_to_server(&host, keys, &args.basic_auth)
.await .await
.expect("Failed to send keys to server"); .expect("Failed to send keys to server");
if args.in_place { if args.in_place {
info!("Client mode: In-place update is enabled. Fetching keys from server."); info!("Client mode: In-place update is enabled. Fetching keys from server.");
let server_keys = get_keys_from_server(&host) let server_keys = get_keys_from_server(&host, &args.basic_auth)
.await .await
.expect("Failed to get keys from server"); .expect("Failed to get keys from server");

View File

@ -105,6 +105,10 @@ struct Args {
help = "Client mode: Path to the known_hosts file" help = "Client mode: Path to the known_hosts file"
)] )]
known_hosts: String, known_hosts: String,
/// Basic auth string for client mode. Format: user:pass
#[arg(long, default_value = "", help = "Client mode: Basic Auth credentials")]
basic_auth: String,
} }
#[actix_web::main] #[actix_web::main]