init small ssh-cli to accept commands over SSH and operate local locks
All checks were successful
Build Container / Build Container (push) Successful in 1m42s
All checks were successful
Build Container / Build Container (push) Successful in 1m42s
This commit is contained in:
parent
a7692dc894
commit
504d64ea57
5 changed files with 2062 additions and 0 deletions
|
|
@ -27,6 +27,8 @@
|
||||||
pnpm
|
pnpm
|
||||||
rustc
|
rustc
|
||||||
cargo
|
cargo
|
||||||
|
rust-analyzer
|
||||||
|
rustfmt
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1863
ssh-cli/Cargo.lock
generated
Normal file
1863
ssh-cli/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
ssh-cli/Cargo.toml
Normal file
12
ssh-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "ssh-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre = "0.6.5"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
reqwest = { version = "0.13.4", features = ["blocking", "json"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tracing-subscriber = "0.3.23"
|
||||||
63
ssh-cli/src/api_models.rs
Normal file
63
ssh-cli/src/api_models.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
pub struct ApiLock {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub status: ApiLockStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
pub struct ApiLockStatus {
|
||||||
|
pub is_unreachable: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_low_batteray: bool,
|
||||||
|
pub is_error_jammed: bool,
|
||||||
|
pub lock_target_level: String,
|
||||||
|
pub lock_state: String,
|
||||||
|
pub activity_state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LockTargetLevel {
|
||||||
|
LOCKED,
|
||||||
|
UNLOCKED,
|
||||||
|
OPEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LockState {
|
||||||
|
UNKNOWN,
|
||||||
|
LOCKED,
|
||||||
|
UNLOCKED,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LockActivityState {
|
||||||
|
UNKNOWN,
|
||||||
|
LOCKING,
|
||||||
|
UNLOCKING,
|
||||||
|
STABLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Serialize)]
|
||||||
|
pub struct LockOperationRequest {
|
||||||
|
pub desired_state: DesiredLockState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DesiredLockState {
|
||||||
|
OPEN,
|
||||||
|
CLOSED,
|
||||||
|
}
|
||||||
122
ssh-cli/src/main.rs
Normal file
122
ssh-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
//! This is a small CLI intended to be run on a host that accepts dooris commands via SSH
|
||||||
|
//!
|
||||||
|
//! The SSH server should be configured to force execution of this command via the following server config snippet
|
||||||
|
//!
|
||||||
|
//! ```sshd_config
|
||||||
|
//! Match User dooris
|
||||||
|
//! AuthorizedKeysFile /path/to/api/state/authorized_keys
|
||||||
|
//! ForceCommand /path/to/ssh-cli
|
||||||
|
//! SetEnv DOORIS_SERVER_URL=https://dooris.ccchh.net/ DOORIS_API_TOKEN=foobar123
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod api_models;
|
||||||
|
|
||||||
|
use eyre::{Context, OptionExt, eyre};
|
||||||
|
use reqwest::{Url, header::HeaderValue};
|
||||||
|
use std::{env::VarError, process::exit};
|
||||||
|
|
||||||
|
use crate::api_models::LockOperationRequest;
|
||||||
|
|
||||||
|
fn main() -> eyre::Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
color_eyre::install()?;
|
||||||
|
|
||||||
|
let doors = fetch_doors()?;
|
||||||
|
let args = get_args()?;
|
||||||
|
let args = args.split(" ").collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if args.len() == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "" {
|
||||||
|
println!("Help is not yet available. Come back later >~<");
|
||||||
|
exit(3);
|
||||||
|
} else if args[0] == "lock" || args[0] == "unlock" {
|
||||||
|
let cli_target_door = args
|
||||||
|
.get(1)
|
||||||
|
.ok_or_eyre("'unlock' and 'lock' cli requires the door name as second argument")?;
|
||||||
|
|
||||||
|
let api_target_door = doors
|
||||||
|
.iter()
|
||||||
|
.find(|i_door| i_door.name.eq_ignore_ascii_case(cli_target_door))
|
||||||
|
.ok_or({
|
||||||
|
let door_names = doors
|
||||||
|
.iter()
|
||||||
|
.map(|i_door| i_door.name.to_owned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
eyre!(
|
||||||
|
"No door named {cli_target_door:?} exists. Available doors are {door_names:?}.",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Lock {} ({:?}) identified!",
|
||||||
|
api_target_door.id, api_target_door.name
|
||||||
|
);
|
||||||
|
operate_lock(
|
||||||
|
&api_target_door.id,
|
||||||
|
match args[0] {
|
||||||
|
"lock" => api_models::DesiredLockState::CLOSED,
|
||||||
|
"unlock" => api_models::DesiredLockState::OPEN,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
println!("Lock has been turned. Enjoy :3");
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
println!("Unknown commandline. See --help for more info.");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get passed arguments from SSH_ORIGINAL_COMMAND environment variable but falling back to direct CLI args
|
||||||
|
fn get_args() -> eyre::Result<String> {
|
||||||
|
match std::env::var("SSH_ORIGINAL_COMMAND") {
|
||||||
|
Ok(original_command) => Ok(original_command),
|
||||||
|
Err(VarError::NotUnicode(_)) => {
|
||||||
|
Err(eyre!("Your command is not very unicoded. Refusing >~<"))
|
||||||
|
}
|
||||||
|
Err(VarError::NotPresent) => Ok(std::env::args().skip(1).collect::<Vec<_>>().join(" ")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http_client() -> eyre::Result<reqwest::blocking::Client> {
|
||||||
|
return reqwest::blocking::ClientBuilder::new()
|
||||||
|
.user_agent("dooris-ssh-cli")
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.http1_title_case_headers()
|
||||||
|
.build()
|
||||||
|
.context("Could not construct http client even though all values are statically known to be valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_token() -> eyre::Result<HeaderValue> {
|
||||||
|
let server_token = std::env::var("DOORIS_API_TOKEN").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_API_TOKEN environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
||||||
|
format!("Static-Token {server_token}")
|
||||||
|
.try_into()
|
||||||
|
.context("Could not convert HTTP API Token to a valid HTTP header")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_url() -> eyre::Result<Url> {
|
||||||
|
let server_url = std::env::var("DOORIS_SERVER_URL").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_SERVER_URL environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
||||||
|
Url::parse(&server_url).context("Configured DOORIS_SERVER_URL is not a valid URL. Contact your nearest server maid to fix the dooris setup")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_doors() -> eyre::Result<Vec<api_models::ApiLock>> {
|
||||||
|
http_client()?
|
||||||
|
.get(server_url()?.join("api/locks/")?)
|
||||||
|
.header(reqwest::header::AUTHORIZATION, api_token()?)
|
||||||
|
.send()?
|
||||||
|
.error_for_status()
|
||||||
|
.context("Could not get list of locks from the dooris API")?
|
||||||
|
.json()
|
||||||
|
.context("Could not parse list-locks response from dooris API")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate_lock(lock_id: &str, desired_state: api_models::DesiredLockState) -> eyre::Result<()> {
|
||||||
|
println!("Turning Lock, please stand by…");
|
||||||
|
http_client()?
|
||||||
|
.patch(server_url()?.join(&format!("api/locks/{lock_id}"))?)
|
||||||
|
.header(reqwest::header::AUTHORIZATION, api_token()?)
|
||||||
|
.json(&LockOperationRequest { desired_state })
|
||||||
|
.send()?
|
||||||
|
.error_for_status()
|
||||||
|
.context("Could not operate lock")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue