Compare commits
8 changed files with 7 additions and 2181 deletions
|
|
@ -1,7 +1,4 @@
|
||||||
# integrate this into your own .envrc file with `source_env .envrc.dist`
|
# integrate this into your own .envrc file with `source_env .envrc.dist`
|
||||||
if has nix; then
|
|
||||||
use flake
|
|
||||||
fi
|
|
||||||
|
|
||||||
watch_file api/pyproject.toml \
|
watch_file api/pyproject.toml \
|
||||||
api/uv.lock
|
api/uv.lock
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, UTC, timedelta
|
from datetime import datetime, UTC, timedelta
|
||||||
from fastapi import Request, Depends, Response
|
from fastapi import Request, Depends, Response, Header
|
||||||
from fastapi.security import APIKeyHeader
|
from fastapi.security import APIKeyHeader
|
||||||
from simple_openid_connect.data import TokenSuccessResponse
|
from simple_openid_connect.data import TokenSuccessResponse
|
||||||
from simple_openid_connect.client import OpenidClient
|
from simple_openid_connect.client import OpenidClient
|
||||||
|
|
@ -14,12 +14,7 @@ from dooris_api.ccujack import CCUJackClient
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
api_key_security_scheme = APIKeyHeader(
|
api_key_security_scheme = APIKeyHeader(name="Authorization", scheme_name="Static-Token", auto_error=False)
|
||||||
name="authorization",
|
|
||||||
scheme_name="Static-Token",
|
|
||||||
description="Set the Authorization header to 'Static-Token foobar123' with a token that is statically configured at application parameter",
|
|
||||||
auto_error=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_oidc_client(req: Request) -> OpenidClient:
|
async def get_oidc_client(req: Request) -> OpenidClient:
|
||||||
|
|
@ -139,17 +134,14 @@ def clear_oidc_auth_state(resp: Response):
|
||||||
|
|
||||||
|
|
||||||
def get_logged_in_token_user(req: Request, token: Optional[str]):
|
def get_logged_in_token_user(req: Request, token: Optional[str]):
|
||||||
print(req.headers)
|
|
||||||
print(token)
|
|
||||||
|
|
||||||
if not token or not token.startswith("Static-Token "):
|
if not token or not token.startswith("Static-Token "):
|
||||||
logger.debug("No static API-Token was part of the request")
|
logger.debug("No static API-Token was part of the request")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
token = token.removeprefix("Static-Token ")
|
token = token.removeprefix("Static-Token ")
|
||||||
valid_tokens = app_config.get().static_api_tokens
|
valid_tokens = app_config.get().static_api_tokens
|
||||||
|
|
||||||
if any((i == token for i in valid_tokens)):
|
if any((i == token for i in valid_tokens)):
|
||||||
logger.debug("Successfully authenticated a static API-Token")
|
logger.debug("Successfully authenticated a static API-Token")
|
||||||
return models.ApiUser(
|
return models.ApiUser(
|
||||||
is_anonymous=False,
|
is_anonymous=False,
|
||||||
|
|
@ -165,10 +157,7 @@ def get_logged_in_token_user(req: Request, token: Optional[str]):
|
||||||
|
|
||||||
|
|
||||||
async def get_api_user(
|
async def get_api_user(
|
||||||
req: Request,
|
req: Request, resp: Response, oidc_client: OpenidClient, token: Annotated[Optional[str], Depends(api_key_security_scheme)] = None
|
||||||
resp: Response,
|
|
||||||
oidc_client: OpenidClient,
|
|
||||||
token: Annotated[Optional[str], Depends(api_key_security_scheme)] = None,
|
|
||||||
) -> models.ApiUser:
|
) -> models.ApiUser:
|
||||||
oidc_user = await get_logged_in_oidc_user(req, resp, oidc_client)
|
oidc_user = await get_logged_in_oidc_user(req, resp, oidc_client)
|
||||||
token_user = get_logged_in_token_user(req, token)
|
token_user = get_logged_in_token_user(req, token)
|
||||||
|
|
@ -193,12 +182,9 @@ ApiUser = Annotated[models.ApiUser, Depends(get_api_user)]
|
||||||
|
|
||||||
|
|
||||||
async def get_authenticated_user(
|
async def get_authenticated_user(
|
||||||
req: Request,
|
req: Request, resp: Response, oidc_client: OpenidClient
|
||||||
resp: Response,
|
|
||||||
oidc_client: OpenidClient,
|
|
||||||
token: Annotated[Optional[str], Depends(api_key_security_scheme)] = None,
|
|
||||||
) -> models.ApiUser:
|
) -> models.ApiUser:
|
||||||
user = await get_api_user(req, resp, oidc_client, token)
|
user = await get_api_user(req, resp, oidc_client)
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
raise exceptions.HttpProblemException.unauthorized(req.url)
|
raise exceptions.HttpProblemException.unauthorized(req.url)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
61
flake.lock
generated
61
flake.lock
generated
|
|
@ -1,61 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1784356753,
|
|
||||||
"narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "61b7c44c4073f0b827768aff0049561b5110ea5a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
36
flake.nix
36
flake.nix
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
description = "dooris";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
flake-utils,
|
|
||||||
}:
|
|
||||||
flake-utils.lib.eachDefaultSystem (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { system = system; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
uv
|
|
||||||
pre-commit
|
|
||||||
ruff
|
|
||||||
python3
|
|
||||||
pnpm
|
|
||||||
rustc
|
|
||||||
cargo
|
|
||||||
rust-analyzer
|
|
||||||
rustfmt
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
1863
ssh-cli/Cargo.lock
generated
1863
ssh-cli/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +0,0 @@
|
||||||
[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"
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
//! 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…
Reference in a new issue