mod commands;
mod inputs;
mod settings;
use super::rand_data;
use inputs::Input;
pub use settings::{expected_slider_info, LIST_LEN};
use http::Response;
use rand::{
distributions::{Distribution, Standard},
Rng,
};
use serde_json::Value;
use warp::{filters::BoxedFilter, Filter, Reply};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
enum Result {
Success,
InvalidParameter,
ChallengeIncorrect,
Blocked,
}
impl ToString for Result {
fn to_string(&self) -> String {
match self {
Self::Success => "SUCCESS",
Self::InvalidParameter => "INVALID_PARAMETER",
Self::Blocked => "BLOCKED",
Self::ChallengeIncorrect => "CHALLENGE_INCORRECT",
}
.to_string()
}
}
pub enum PortOption {
Port9000,
Port7345,
Random,
}
impl From<PortOption> for u16 {
fn from(option: PortOption) -> Self {
match option {
PortOption::Port7345 => 7345,
PortOption::Port9000 => 9000,
PortOption::Random => PortOption::into(rand::random()),
}
}
}
impl Distribution<PortOption> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PortOption {
match rng.gen_range(0..2) {
0 => PortOption::Port9000,
1 => PortOption::Port7345,
_ => panic!("Rand Port - Bad Range"),
}
}
}
pub enum DeviceType {
TV,
SoundBar,
Random,
}
impl DeviceType {
fn settings_root(self) -> String {
match self {
Self::TV => "tv_settings".into(),
Self::SoundBar => "audio_settings".into(),
Self::Random => Self::settings_root(rand::random()),
}
}
}
impl Distribution<DeviceType> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> DeviceType {
match rng.gen_range(0..2) {
0 => DeviceType::TV,
1 => DeviceType::SoundBar,
_ => panic!("Rand Device Type - Bad Range"),
}
}
}
#[derive(Debug)]
pub enum CodeSet {
Default,
Secondary,
Random,
}
impl CodeSet {
fn choose(self) -> Self {
match self {
Self::Random => rand::random(),
not_random => not_random,
}
}
fn hashmap(self) -> HashMap<u32, Vec<u32>> {
if matches!(self, Self::Random) {
return self.choose().hashmap();
}
let mut hash = HashMap::new();
hash.insert(2, vec![0, 1, 2, 3]);
match self {
Self::Default => {
hash.insert(3, vec![0, 1, 8, 7, 2]);
}
Self::Secondary => {
hash.insert(3, vec![0, 4, 3, 5, 2]);
}
_ => panic!("CodeSet not chosen"),
}
hash.insert(4, vec![0, 3, 4, 6, 8, 15]);
hash.insert(5, vec![0, 1, 2, 3, 4]);
hash.insert(6, vec![0, 2]);
hash.insert(7, vec![1]);
hash.insert(8, vec![0, 1, 2]);
hash.insert(9, vec![0]);
hash.insert(11, vec![0, 1, 2]);
hash
}
}
impl Distribution<CodeSet> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> CodeSet {
match rng.gen_range(0..2) {
0 => CodeSet::Default,
1 => CodeSet::Secondary,
_ => panic!("Rand CodeSet - Bad Range"),
}
}
}
#[derive(Debug, PartialEq)]
enum State {
Ready,
Pairing {
challenge: u32,
pair_token: u32,
client_name: String,
client_id: String,
},
}
#[derive(Debug, Clone)]
pub struct SimulatedDevice {
inner: Arc<SimulatedDeviceRef>,
}
impl SimulatedDevice {
pub fn new(port: PortOption, device_type: DeviceType, code_set: CodeSet) -> Self {
let name = "Simulated Device".to_string();
let model = rand_data::string(6);
let settings_root = device_type.settings_root();
let port = port.into();
let uuid = rand_data::uuid();
let input_list = inputs::generate();
let current_input = input_list.values().next().unwrap().name.clone();
let cert = rcgen::generate_simple_self_signed(vec![
"127.0.0.1".to_string(),
"localhost".to_string(),
])
.unwrap();
let pkey = cert.serialize_private_key_pem();
let cert = cert.serialize_pem().unwrap();
Self {
inner: Arc::new(SimulatedDeviceRef {
name,
model,
settings_root,
port,
uuid,
code_set: code_set.hashmap(),
state: RwLock::new(State::Ready),
powered_on: RwLock::new(false),
input_list,
current_input: RwLock::new(current_input),
cert,
pkey,
}),
}
}
pub fn serve(&self) {
tokio::spawn(warp::serve(self.description()).run(([127, 0, 0, 1], 8008)));
log::info!(target: "test::simulated_device::serve", "Starting Description server");
tokio::spawn(
warp::serve(self.api())
.tls()
.key(self.inner.pkey.clone())
.cert(self.inner.cert.clone())
.run(([127, 0, 0, 1], self.inner.port)),
);
log::info!(target: "test::simulated_device::serve", "Starting API server");
}
fn description(&self) -> BoxedFilter<(impl Reply,)> {
warp::path("ssdp")
.and(warp::path("device-desc.xml"))
.and(warp::path::end())
.and(warp::get())
.map({
let desc_xml = device_desc!(self.inner.name, self.inner.model, self.inner.uuid);
move || {
let desc_xml = desc_xml.clone();
Response::builder()
.header("Application-URL", "http//127.0.0.1:8008/apps/")
.header("Content-Length", desc_xml.len())
.header("Content-Type", "application/xml")
.body(desc_xml)
.unwrap()
}
})
.with(warp::log("test::simulated_device::description"))
.boxed()
}
fn api(&self) -> BoxedFilter<(impl Reply,)> {
self.pairing()
.or(self.power_state())
.or(self.inputs())
.or(self.device_info())
.or(self.settings())
.or(self.virtual_remote())
.or(self.uri_not_found())
.with(warp::log("test::simulated_device::api"))
.boxed()
}
fn expected_put(&self) -> BoxedFilter<(impl Reply,)> {
warp::get()
.map(|| {
warp::reply::json::<Value>(
&serde_json::from_str(&format!("{{{}}}", status!("EXPECTED_PUT"))).unwrap(),
)
})
.boxed()
}
fn expected_get(&self) -> BoxedFilter<(impl Reply,)> {
warp::put()
.map(|| {
warp::reply::json::<Value>(
&serde_json::from_str(&format!("{{{}}}", status!("EXPECTED_GET"))).unwrap(),
)
})
.boxed()
}
fn uri_not_found(&self) -> BoxedFilter<(impl Reply,)> {
warp::any()
.map(|| {
warp::reply::json::<Value>(
&serde_json::from_str(&format!("{{{}}}", status!("uri_not_found"))).unwrap(),
)
})
.boxed()
}
fn pairing(&self) -> BoxedFilter<(impl Reply,)> {
warp::path("pairing")
.and(
warp::put()
.and(warp::path::param())
.and(warp::path::end())
.and(warp::body::json())
.map({
let device = self.clone();
move |ep: String, val: Value| match ep.as_str() {
"start" => commands::pair_start(val, device.clone()),
"pair" => commands::pair_finish(val, device.clone()),
"cancel" => commands::pair_cancel(val, device.clone()),
_ => warp::reply::json::<Value>(
&serde_json::from_str(&format!("{{{}}}", status!("uri_not_found")))
.unwrap(),
),
}
})
.or(self.expected_put()),
)
.boxed()
}
fn inputs(&self) -> BoxedFilter<(impl Reply,)> {
warp::path!("menu_native" / "dynamic" / ..)
.and(warp::path(self.inner.settings_root.clone()))
.and(warp::path("devices"))
.and(
warp::path("name_input")
.and(warp::path::end())
.and(
warp::get()
.map({
let device = self.clone();
move || commands::list_inputs(device.clone())
})
.or(self.expected_get()),
)
.or(warp::path("current_input")
.and(warp::path::end())
.and(warp::get().map({
let device = self.clone();
move || commands::current_input(device.clone())
}))
.or(warp::put().and(warp::body::json()).map({
let device = self.clone();
move |val: Value| commands::change_input(val, device.clone())
}))),
)
.boxed()
}
fn power_state(&self) -> BoxedFilter<(impl Reply,)> {
warp::path!("state" / "device" / "power_mode")
.and(
warp::get()
.map({
let device = self.clone();
move || commands::power_state(device.clone())
})
.or(self.expected_put()),
)
.boxed()
}
fn device_info(&self) -> BoxedFilter<(impl Reply,)> {
warp::path!("state" / "device" / "deviceinfo")
.and(
warp::get()
.map({
let device = self.clone();
move || commands::device_info(device.clone())
})
.or(self.expected_get()),
)
.boxed()
}
fn settings(&self) -> BoxedFilter<(impl Reply,)> {
warp::path("menu_native")
.and(settings::generate(self.inner.settings_root.clone()))
.boxed()
}
fn virtual_remote(&self) -> BoxedFilter<(impl Reply,)> {
warp::path("key_command")
.and(
warp::put()
.and(warp::body::json())
.map({
let device = self.clone();
move |val: Value| commands::virtual_remote(val, device.clone())
})
.or(self.expected_get()),
)
.boxed()
}
}
#[derive(Debug)]
struct SimulatedDeviceRef {
name: String,
model: String,
settings_root: String,
port: u16,
uuid: String,
code_set: HashMap<u32, Vec<u32>>,
state: RwLock<State>,
powered_on: RwLock<bool>,
input_list: HashMap<String, Input>,
current_input: RwLock<String>,
cert: String,
pkey: String,
}