pub mod get_device_custom_data;
pub mod get_device_info;
pub mod get_fan_setting;
pub(crate) mod get_log_download;
pub mod get_miner_history;
pub mod get_miner_setting;
pub mod get_miner_status;
pub mod get_system_setting;
pub mod set_device_custom_data;
pub mod set_fan_poweroff_cool;
pub mod set_fan_temp_offset;
pub mod set_fan_zero_speed;
pub mod set_log_upload;
pub mod set_miner_cointype;
pub mod set_miner_fast_hash;
pub mod set_miner_fastboot;
pub mod set_miner_heat_mode;
pub mod set_miner_pools;
pub mod set_miner_power;
pub mod set_miner_power_limit;
pub mod set_miner_power_mode;
pub mod set_miner_power_percent;
pub mod set_miner_report;
pub mod set_miner_restore_setting;
pub mod set_miner_service;
pub mod set_miner_target_freq;
pub mod set_miner_upfreq_speed;
pub mod set_system_factory_reset;
pub mod set_system_hostname;
pub mod set_system_led;
pub mod set_system_net_config;
pub mod set_system_ntp_server;
pub mod set_system_reboot;
pub mod set_system_time_randomized;
pub mod set_system_timezone;
pub(crate) mod set_system_update_firmware;
pub mod set_system_webpools;
pub mod set_user_change_passwd;
pub mod set_user_permission;
#[cfg(doc)]
use crate::command::set_miner_fastboot::SetMinerFastboot;
#[cfg(doc)]
use crate::response::Response;
use crate::permission::PermissionLevel;
use crate::request::Request;
use crate::{
auth_data::AuthData,
error::{Error, Result},
redacted::Redacted,
};
use core::str;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandLevel {
Public,
Secured,
Encrypted,
}
impl CommandLevel {
pub const fn requires_auth(self) -> bool {
!matches!(self, Self::Public)
}
pub const fn requires_encryption(self) -> bool {
matches!(self, Self::Encrypted)
}
}
pub trait Command {
type Response: serde::de::DeserializeOwned;
const CMD_NAME: &'static str;
const LEVEL: CommandLevel;
const MINIMAL_PERMISSIONS: PermissionLevel = PermissionLevel::ReadWrite;
fn params(&self) -> Result<Option<impl serde::Serialize>>;
fn encode(&self, auth: Option<AuthData>) -> Result<Redacted<Vec<u8>>> {
let parameter = self.params()?;
let bytes = match (parameter, auth) {
(None, auth_data) => {
let req = Request::new(Self::CMD_NAME, auth_data, None::<()>);
serde_json::to_vec(&req)?
}
(Some(param), auth_data) => {
if Self::LEVEL.requires_encryption() {
let Some(auth_data) = auth_data else {
return Err(Error::CommandShouldHaveAuthData(Self::CMD_NAME));
};
let param_json = zeroize::Zeroizing::new(serde_json::to_vec(¶m)?);
let encrypted = auth_data.encrypt(¶m_json)?;
let req = Request::new(Self::CMD_NAME, Some(auth_data), Some(encrypted));
serde_json::to_vec(&req)?
} else {
let req = Request::new(Self::CMD_NAME, auth_data, Some(param));
serde_json::to_vec(&req)?
}
}
};
Ok(Redacted(bytes))
}
fn is_secured(&self) -> bool {
Self::LEVEL.requires_auth()
}
fn is_encrypted(&self) -> bool {
Self::LEVEL.requires_encryption()
}
fn cmd_name(&self) -> &'static str {
Self::CMD_NAME
}
fn level(&self) -> CommandLevel {
Self::LEVEL
}
fn parse_response(raw: &[u8]) -> Result<crate::response::Response<Self::Response>> {
crate::response::parse_response(raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::response::ResponseCode;
struct DummyCommand;
impl Command for DummyCommand {
type Response = String;
const CMD_NAME: &'static str = "dummy.cmd";
const LEVEL: CommandLevel = CommandLevel::Public;
fn params(&self) -> Result<Option<impl serde::Serialize>> {
Ok(None::<()>)
}
}
#[test]
fn parse_response_success() {
let raw = br#"{"code":0,"when":12345,"msg":"ok","desc":"dummy.cmd"}"#;
let resp = DummyCommand::parse_response(raw).unwrap();
assert_eq!(resp.code, ResponseCode::Success);
assert_eq!(resp.when, 12345);
assert_eq!(resp.msg, "ok");
assert_eq!(resp.desc, "dummy.cmd");
}
#[test]
fn parse_response_device_error_fail() {
let raw = br#"{"code":-1,"when":12345,"msg":"operation failed","desc":"dummy.cmd"}"#;
let err = DummyCommand::parse_response(raw).unwrap_err();
match err {
Error::Device { code, msg, desc } => {
assert_eq!(code, ResponseCode::Fail);
assert_eq!(msg, "operation failed");
assert_eq!(desc, "dummy.cmd");
}
other => panic!("expected Error::Device, got: {other:?}"),
}
}
#[test]
fn parse_response_device_error_no_permission() {
let raw = br#"{"code":-4,"when":12345,"msg":"no permission","desc":"dummy.cmd"}"#;
let err = DummyCommand::parse_response(raw).unwrap_err();
match err {
Error::Device { code, msg, desc } => {
assert_eq!(code, ResponseCode::NoPermission);
assert_eq!(msg, "no permission");
assert_eq!(desc, "dummy.cmd");
}
other => panic!("expected Error::Device, got: {other:?}"),
}
}
#[test]
fn parse_response_invalid_utf8() {
let raw = &[0xFF, 0xFE, 0xFD];
let err = DummyCommand::parse_response(raw).unwrap_err();
assert!(matches!(err, Error::InvalidUtf8));
}
#[test]
fn parse_response_invalid_json() {
let raw = b"not a json";
let err = DummyCommand::parse_response(raw).unwrap_err();
assert!(matches!(err, Error::Json(_)));
}
#[derive(Debug)]
struct SecuredCmd(u32);
impl Command for SecuredCmd {
type Response = String;
const CMD_NAME: &'static str = "set.dummy";
const LEVEL: CommandLevel = CommandLevel::Secured;
fn params(&self) -> Result<Option<impl serde::Serialize>> {
Ok(Some(self.0))
}
}
#[derive(Debug)]
struct EncryptedCmd(String);
impl Command for EncryptedCmd {
type Response = String;
const CMD_NAME: &'static str = "set.secret";
const LEVEL: CommandLevel = CommandLevel::Encrypted;
fn params(&self) -> Result<Option<impl serde::Serialize>> {
Ok(Some(&self.0))
}
}
#[test]
fn encode_public_command() {
let cmd = DummyCommand;
let encoded = cmd.encode(None).unwrap();
let v: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(v["cmd"], "dummy.cmd");
assert!(v.get("param").is_none());
assert!(v.get("token").is_none());
}
#[test]
fn encode_secured_command() {
use crate::account::Account;
use crate::password::Password;
let auth =
AuthData::new::<SecuredCmd>(Account::Super, &Password::Super, "dummy_salt").unwrap();
let cmd = SecuredCmd(42);
let encoded = cmd.encode(Some(auth)).unwrap();
let v: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(v["cmd"], "set.dummy");
assert_eq!(v["param"], 42);
assert_eq!(v["account"], "super");
assert!(v.get("token").is_some());
}
#[test]
fn encode_encrypted_command_requires_auth() {
let cmd = EncryptedCmd("secret_payload".to_string());
let err = cmd.encode(None).unwrap_err();
assert!(matches!(
err,
Error::CommandShouldHaveAuthData("set.secret")
));
}
#[test]
fn encode_encrypted_command_with_auth() {
use crate::account::Account;
use crate::password::Password;
let auth =
AuthData::new::<EncryptedCmd>(Account::Super, &Password::Super, "dummy_salt").unwrap();
let cmd = EncryptedCmd("secret_payload".to_string());
let encoded = cmd.encode(Some(auth)).unwrap();
let v: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
use aes::cipher::{BlockDecryptMut, KeyInit, block_padding::Pkcs7};
use sha2::{Digest, Sha256};
let expected_key =
Sha256::digest(format!("set.secretsuperdummy_salt{}", v["ts"]).as_bytes());
let mut ciphertext = base64_light::base64_decode(v["param"].as_str().unwrap());
let cipher = aes::Aes256::new_from_slice(&expected_key).unwrap();
let cleartext = cipher.decrypt_padded_mut::<Pkcs7>(&mut ciphertext).unwrap();
assert_eq!(cleartext, br#""secret_payload""#);
assert_eq!(v["cmd"], "set.secret");
}
}