meta: cleanup and added apps endpoint
- Added (working) apps endpoint - Added config file (actually a state file) wrapper to handle shared mutability - Refactored base url and http get shared code into common - Added tracing based logging and converted debug statemets to it - More things that I forgot
This commit is contained in:
@@ -16,9 +16,12 @@ reqwest = { version = "0.12.20", features = [
|
||||
"rustls-tls",
|
||||
"native-tls",
|
||||
], default-features = false }
|
||||
salvo = { version = "0.79.0", features = ["oapi"] }
|
||||
salvo = { version = "0.80.0", features = ["oapi", "craft", "logging"] }
|
||||
serde = { version = "1.0.219", features = ["serde_derive"] }
|
||||
serde-xml-rs = "0.8.1"
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url-constructor = "0.1.0"
|
||||
uuid = { version = "1.17.0", features = ["v4"] }
|
||||
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use salvo::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{common::AppResult, config::ConfigReader};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AppListRespApp {
|
||||
#[serde(rename = "AppTitle")]
|
||||
app_title: String,
|
||||
#[serde(rename = "UUID")]
|
||||
uuid: uuid::Uuid,
|
||||
#[serde(rename = "IsHdrSupported")]
|
||||
is_hdr_supported: bool,
|
||||
#[serde(rename = "ID")]
|
||||
id: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AppListResp {
|
||||
#[serde(rename = "App")]
|
||||
apps: Vec<AppListRespApp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
struct App {
|
||||
title: String,
|
||||
id: u64,
|
||||
hdr_supported: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
struct GetAppsResponse {
|
||||
apps: HashMap<String, Vec<App>>,
|
||||
}
|
||||
|
||||
#[craft]
|
||||
impl crate::config::ConfigFile {
|
||||
#[craft(endpoint(status_codes(StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR)))]
|
||||
pub async fn get_apps(self: ::std::sync::Arc<Self>) -> AppResult<Json<GetAppsResponse>> {
|
||||
let standard_error = Err(crate::common::AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "failed to get available apps".to_string(),
|
||||
});
|
||||
|
||||
let reader = self.read().await;
|
||||
let unique_id = match reader.unique_id() {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
error!("could not get unique id: {e}");
|
||||
return standard_error;
|
||||
}
|
||||
};
|
||||
|
||||
let servers = match reader.servers() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("could not get servers: {e}");
|
||||
return standard_error;
|
||||
}
|
||||
};
|
||||
|
||||
let mut get_apps_resp = GetAppsResponse {
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
for (_, server) in servers.into_iter() {
|
||||
let mut base_url = crate::common::base_url(
|
||||
"https",
|
||||
&server.host,
|
||||
server.https_port(),
|
||||
&unique_id,
|
||||
"applist",
|
||||
None,
|
||||
);
|
||||
|
||||
let resp = match crate::common::get_url(&mut base_url, true).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("could not get applist from server {}: {}", server.name, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
debug!(resp);
|
||||
|
||||
let applist_resp: AppListResp = match serde_xml_rs::from_str(&resp) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"could not parse applist response from server {}: {}",
|
||||
server.name, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let resp_vec = applist_resp
|
||||
.apps
|
||||
.into_iter()
|
||||
.map(|a| App {
|
||||
title: a.app_title,
|
||||
hdr_supported: a.is_hdr_supported,
|
||||
id: a.id,
|
||||
})
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
get_apps_resp.apps.insert(server.name, resp_vec);
|
||||
}
|
||||
|
||||
Ok(Json(get_apps_resp))
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ pub fn save_cert_and_key_to_disk(cert: &X509, key: &PKey<Private>) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn http_client_with_identity() -> Result<reqwest::Client> {
|
||||
pub fn identity() -> Result<reqwest::tls::Identity> {
|
||||
let cert_dir = get_and_create_cert_dir()?;
|
||||
let cert_filepath = cert_dir.join("cert");
|
||||
let key_filepath = cert_dir.join("key");
|
||||
@@ -118,7 +118,8 @@ pub fn http_client_with_identity() -> Result<reqwest::Client> {
|
||||
let cert_bytes = fs::read(cert_filepath)?;
|
||||
let key_bytes = fs::read(key_filepath)?;
|
||||
|
||||
let identity = reqwest::tls::Identity::from_pkcs8_pem(&cert_bytes, &key_bytes)?;
|
||||
|
||||
Ok(reqwest::Client::builder().identity(identity).build()?)
|
||||
Ok(reqwest::tls::Identity::from_pkcs8_pem(
|
||||
&cert_bytes,
|
||||
&key_bytes,
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use serde::Serialize;
|
||||
//use salvo::http::{StatusCode, StatusError};
|
||||
use anyhow::Result;
|
||||
use salvo::oapi::{self, EndpointOutRegister, ToSchema};
|
||||
use salvo::prelude::*;
|
||||
use serde::Serialize;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
struct ApiError {
|
||||
@@ -14,7 +15,7 @@ pub struct AppError {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
pub type AppResult<T> = anyhow::Result<T, AppError>;
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
|
||||
#[async_trait]
|
||||
impl Writer for AppError {
|
||||
@@ -43,3 +44,53 @@ impl EndpointOutRegister for AppError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_url(
|
||||
scheme: &str,
|
||||
host: &String,
|
||||
base_port: u16,
|
||||
unique_id: &String,
|
||||
path: &str,
|
||||
params: Option<Vec<(&str, &str)>>,
|
||||
) -> url_constructor::UrlConstructor {
|
||||
let mut base_url = url_constructor::UrlConstructor::new();
|
||||
base_url
|
||||
.scheme(scheme)
|
||||
.host(host)
|
||||
.port(base_port)
|
||||
.subdir(path)
|
||||
.param("uniqueid", unique_id);
|
||||
|
||||
if let Some(p) = params {
|
||||
for (k, v) in p.into_iter() {
|
||||
base_url.param(k, v);
|
||||
}
|
||||
}
|
||||
base_url
|
||||
}
|
||||
|
||||
pub async fn get_url(
|
||||
base_url: &mut url_constructor::UrlConstructor,
|
||||
with_identity: bool,
|
||||
) -> Result<String> {
|
||||
let mut uuidv2 = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut uuidv2)?;
|
||||
let uuidv2_hex = hex::encode(uuidv2);
|
||||
|
||||
let url = base_url.param("uuid", uuidv2_hex).build();
|
||||
debug!("Getting url: {url}");
|
||||
|
||||
let mut http_builder = reqwest::Client::builder();
|
||||
http_builder = http_builder.user_agent("Mozilla/5.0");
|
||||
http_builder = http_builder.danger_accept_invalid_certs(true);
|
||||
if with_identity {
|
||||
let identity = crate::certs::identity()?;
|
||||
http_builder = http_builder.identity(identity);
|
||||
}
|
||||
|
||||
let client = http_builder.build()?;
|
||||
|
||||
let resp = client.get(url).send().await?;
|
||||
let text = resp.text().await?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Server {
|
||||
pub name: String,
|
||||
pub host: String,
|
||||
pub base_port: u16,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn http_port(&self) -> u16 {
|
||||
self.base_port + 189
|
||||
}
|
||||
pub fn https_port(&self) -> u16 {
|
||||
self.base_port + 184
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Config {
|
||||
servers: HashMap<String, Server>,
|
||||
unique_id: String,
|
||||
}
|
||||
|
||||
pub struct ConfigFile {
|
||||
lock: tokio::sync::RwLock<()>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
pub trait ConfigReader {
|
||||
fn servers(&self) -> Result<HashMap<String, Server>>;
|
||||
fn unique_id(&self) -> Result<String>;
|
||||
}
|
||||
|
||||
pub trait ConfigWriter {
|
||||
fn add_server(&self, server: Server) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct ConfigReadAccess<'a> {
|
||||
_guard: RwLockReadGuard<'a, ()>,
|
||||
config: &'a ConfigFile,
|
||||
}
|
||||
|
||||
pub struct ConfigWriteAccess<'a> {
|
||||
_guard: RwLockWriteGuard<'a, ()>,
|
||||
config: &'a ConfigFile,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
fn load_config(&self) -> Result<Config> {
|
||||
tracing::debug!("parsing config file");
|
||||
|
||||
let config_file = File::open(&self.path)?;
|
||||
Ok(serde_json::from_reader(config_file)?)
|
||||
}
|
||||
|
||||
fn write_config(&self, config: Config) -> Result<()> {
|
||||
tracing::debug!("serializing config file");
|
||||
let config_file = File::create(&self.path)?;
|
||||
Ok(serde_json::to_writer_pretty(config_file, &config)?)
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self> {
|
||||
let project_dirs =
|
||||
directories::ProjectDirs::from("xyz", "ohea", "gamestream-webtransport-proxy")
|
||||
.ok_or(anyhow::anyhow!("Could not get project dirs"))?;
|
||||
|
||||
let data_dir = project_dirs.data_dir();
|
||||
fs::create_dir_all(data_dir)?;
|
||||
|
||||
let state_path = data_dir.join("state.json");
|
||||
|
||||
if let Err(e) = File::open(&state_path) {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
write_default_config_to_path(&state_path)?;
|
||||
} else {
|
||||
return Err(anyhow!(e));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ConfigFile {
|
||||
lock: tokio::sync::RwLock::new(()),
|
||||
path: state_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
pub async fn read(&self) -> ConfigReadAccess {
|
||||
ConfigReadAccess {
|
||||
_guard: self.lock.read().await,
|
||||
config: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write(&self) -> ConfigWriteAccess {
|
||||
ConfigWriteAccess {
|
||||
_guard: self.lock.write().await,
|
||||
config: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ConfigReader for ConfigReadAccess<'a> {
|
||||
fn servers(&self) -> Result<HashMap<String, Server>> {
|
||||
let config = self.config.load_config()?;
|
||||
Ok(config.servers)
|
||||
}
|
||||
|
||||
fn unique_id(&self) -> Result<String> {
|
||||
let config = self.config.load_config()?;
|
||||
Ok(config.unique_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ConfigWriter for ConfigWriteAccess<'a> {
|
||||
fn add_server(&self, server: Server) -> Result<()> {
|
||||
let mut config = self.config.load_config()?;
|
||||
|
||||
if config.servers.contains_key(&server.name) {
|
||||
return Err(anyhow!(
|
||||
"cannot add duplicate server with name: {}",
|
||||
server.name
|
||||
));
|
||||
}
|
||||
|
||||
config.servers.insert(server.name.clone(), server);
|
||||
|
||||
self.config.write_config(config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_unique_id() -> Result<String> {
|
||||
let mut bytes = [0u8; 8];
|
||||
openssl::rand::rand_bytes(&mut bytes)?;
|
||||
Ok(hex::encode(bytes))
|
||||
}
|
||||
|
||||
fn write_default_config_to_path(path: &PathBuf) -> Result<()> {
|
||||
let default_config = Config {
|
||||
servers: HashMap::new(),
|
||||
unique_id: get_unique_id()?,
|
||||
};
|
||||
|
||||
let config_file = File::create(path)?;
|
||||
|
||||
Ok(serde_json::to_writer_pretty(config_file, &default_config)?)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use salvo::logging::Logger;
|
||||
use salvo::prelude::*;
|
||||
use std::ffi::CString;
|
||||
|
||||
@@ -6,8 +7,10 @@ use moonlight_common_c_sys::{
|
||||
CONNECTION_LISTENER_CALLBACKS, ENCFLG_NONE, SCM_H264, STREAM_CFG_LOCAL, VIDEO_FORMAT_H264,
|
||||
};
|
||||
|
||||
mod apps;
|
||||
mod certs;
|
||||
mod common;
|
||||
mod config;
|
||||
mod pair;
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -105,15 +108,26 @@ fn get_listener_callbacks() -> CONNECTION_LISTENER_CALLBACKS {
|
||||
//}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.init();
|
||||
|
||||
let config = config::ConfigFile::new()?;
|
||||
let config_arc = std::sync::Arc::new(config);
|
||||
|
||||
let router = Router::new()
|
||||
.push(Router::with_path("pair").post(pair::post_pair))
|
||||
.push(Router::with_path("apps").get(apps::get_apps));
|
||||
.push(Router::with_path("pair").post(config_arc.post_pair()))
|
||||
.push(Router::with_path("apps").get(config_arc.get_apps()));
|
||||
let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
|
||||
let router = router
|
||||
.unshift(doc.into_router("/api-doc/openapi.json"))
|
||||
.unshift(SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui"));
|
||||
let service = Service::new(router).hoop(Logger::new());
|
||||
|
||||
let listener = TcpListener::new("0.0.0.0:3001");
|
||||
let acceptor = listener.join(TcpListener::new("0.0.0.0:3000")).bind().await;
|
||||
salvo::Server::new(acceptor).serve(router).await;
|
||||
salvo::Server::new(acceptor).serve(service).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ use openssl::x509::X509;
|
||||
use rand::Rng;
|
||||
use salvo::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::common::{AppError, AppResult};
|
||||
use crate::common::{AppError, AppResult, get_url};
|
||||
use crate::config::{ConfigReader, ConfigWriter};
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct PostPairParams {
|
||||
name: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
#[allow(unused)]
|
||||
pair_endpoint: Option<String>,
|
||||
base_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
@@ -56,23 +57,6 @@ struct ServerPairingSecret {
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn get_url(base_url: &mut url_constructor::UrlConstructor) -> Result<String> {
|
||||
let mut uuidv2 = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut uuidv2)?;
|
||||
let uuidv2_hex = hex::encode(uuidv2);
|
||||
|
||||
let url = base_url.param("uuid", uuidv2_hex).build();
|
||||
//println!("Getting url: {url}");
|
||||
|
||||
let mut http_builder = reqwest::Client::builder();
|
||||
http_builder = http_builder.user_agent("Mozilla/5.0");
|
||||
let client = http_builder.build().unwrap();
|
||||
|
||||
let resp = client.get(url).send().await?;
|
||||
let text = resp.text().await?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
async fn get_server_cert(
|
||||
mut base_url: url_constructor::UrlConstructor,
|
||||
salt_hex: String,
|
||||
@@ -84,7 +68,7 @@ async fn get_server_cert(
|
||||
.param("salt", &salt_hex)
|
||||
.param("clientcert", &cert_hex);
|
||||
|
||||
let text = get_url(url).await?;
|
||||
let text = get_url(url, false).await?;
|
||||
let server_cert: ServerCertResponse = serde_xml_rs::from_str(&text)?;
|
||||
|
||||
let server_cert_bytes = hex::decode(server_cert.plaincert)?;
|
||||
@@ -116,7 +100,7 @@ async fn get_server_challenge(
|
||||
let challenge_hex = hex::encode(challenge_enc);
|
||||
|
||||
let url = base_url.param("clientchallenge", challenge_hex);
|
||||
let text = get_url(url).await?;
|
||||
let text = get_url(url, false).await?;
|
||||
|
||||
Ok(serde_xml_rs::from_str(&text)?)
|
||||
}
|
||||
@@ -151,8 +135,8 @@ fn generate_challenge_response(
|
||||
)?;
|
||||
cipher_ctx.cipher_final(&mut client_challenge_response_data)?;
|
||||
|
||||
//let client_challenge_response_data_hex = hex::encode(&client_challenge_response_data);
|
||||
//println!("client_challenge_response_data_hex: {client_challenge_response_data_hex}");
|
||||
let client_challenge_response_data_hex = hex::encode(&client_challenge_response_data);
|
||||
debug!("client_challenge_response_data_hex: {client_challenge_response_data_hex}");
|
||||
|
||||
// Extract ASN.1 signature from certificate
|
||||
let asn_signature = cert.signature();
|
||||
@@ -166,8 +150,8 @@ fn generate_challenge_response(
|
||||
challenge_response.extend_from_slice(signature_data);
|
||||
challenge_response.extend_from_slice(client_secret_data);
|
||||
|
||||
//let challenge_response_hex = hex::encode(&challenge_response);
|
||||
//println!("challenge_response_hex: {challenge_response_hex}");
|
||||
let challenge_response_hex = hex::encode(&challenge_response);
|
||||
debug!("challenge_response_hex: {challenge_response_hex}");
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&challenge_response);
|
||||
@@ -196,7 +180,7 @@ async fn send_server_challenge_response(
|
||||
) -> Result<ServerChallengeResponseResponse> {
|
||||
let url = base_url.param("serverchallengeresp", server_challenge_response);
|
||||
|
||||
let text = get_url(url).await?;
|
||||
let text = get_url(url, false).await?;
|
||||
Ok(serde_xml_rs::from_str(&text)?)
|
||||
}
|
||||
|
||||
@@ -218,11 +202,11 @@ async fn do_challenge(
|
||||
cert: &X509,
|
||||
) -> Result<ServerPairingSecret> {
|
||||
let aes_key = generate_aes_key(salt, pin);
|
||||
//let aes_hex = hex::encode(&aes_key);
|
||||
//println!("aes_hex: {aes_hex}");
|
||||
let aes_hex = hex::encode(&aes_key);
|
||||
debug!("aes_hex: {aes_hex}");
|
||||
|
||||
let client_challenge_response = get_server_challenge(base_url.clone(), &aes_key).await?;
|
||||
//println!("{client_challenge_response:?}");
|
||||
debug!("{client_challenge_response:?}");
|
||||
|
||||
let challenge_response = generate_challenge_response(
|
||||
client_challenge_response.challengeresponse,
|
||||
@@ -241,32 +225,16 @@ async fn do_challenge(
|
||||
})
|
||||
}
|
||||
|
||||
fn get_base_url(host: &String, port: u16, unique_id: String) -> url_constructor::UrlConstructor {
|
||||
let mut base_url = url_constructor::UrlConstructor::new();
|
||||
base_url
|
||||
.scheme("http")
|
||||
.host(host)
|
||||
.port(port)
|
||||
.subdir("pair")
|
||||
.param("uniqueid", unique_id)
|
||||
.param("devicename", "roth") // TODO: what is this roth thing?
|
||||
.param("updateState", "1");
|
||||
base_url
|
||||
}
|
||||
|
||||
fn generate_pin() -> [u8; 4] {
|
||||
let mut pin = [0u8; 4];
|
||||
{
|
||||
print!("pairing pin: ");
|
||||
|
||||
// TODO: reenable real RNG
|
||||
let mut rng = rand::rng();
|
||||
for i in 0..pin.len() {
|
||||
// Generate ascii number 0-9
|
||||
pin[i] = rng.random_range(48..58);
|
||||
print!("{}", pin[i] as char);
|
||||
}
|
||||
println!("");
|
||||
let pin_string: String = pin.iter().map(|&b| b as char).collect();
|
||||
info!("pairing pin: {}", pin_string);
|
||||
}
|
||||
pin
|
||||
}
|
||||
@@ -317,133 +285,161 @@ async fn send_client_pairing_secret(
|
||||
|
||||
let url = base_url.param("clientpairingsecret", client_secret_hex);
|
||||
|
||||
let text = get_url(url).await?;
|
||||
let text = get_url(url, false).await?;
|
||||
|
||||
Ok(serde_xml_rs::from_str(&text)?)
|
||||
}
|
||||
|
||||
fn get_unique_id() -> Result<String> {
|
||||
let mut bytes = [0u8; 8];
|
||||
openssl::rand::rand_bytes(&mut bytes)?;
|
||||
Ok(hex::encode(bytes))
|
||||
}
|
||||
#[craft]
|
||||
impl crate::config::ConfigFile {
|
||||
#[craft(endpoint(status_codes(StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR)))]
|
||||
pub async fn post_pair(
|
||||
self: ::std::sync::Arc<Self>,
|
||||
body: salvo::oapi::extract::JsonBody<PostPairParams>,
|
||||
) -> AppResult<()> {
|
||||
let params = body.into_inner();
|
||||
|
||||
#[salvo::oapi::endpoint(status_codes(StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR))]
|
||||
pub async fn post_pair(body: salvo::oapi::extract::JsonBody<PostPairParams>) -> AppResult<()> {
|
||||
let params = body.into_inner();
|
||||
let server = crate::config::Server {
|
||||
host: params.host,
|
||||
base_port: params.base_port,
|
||||
name: params.name,
|
||||
};
|
||||
|
||||
let unique_id = match get_unique_id() {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
println!("Could not generate unique id: {e}");
|
||||
let unique_id = match self.read().await.unique_id() {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
error!("Could not get unique id: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let base_url = crate::common::base_url(
|
||||
"http",
|
||||
&server.host,
|
||||
server.http_port(),
|
||||
&unique_id,
|
||||
"pair",
|
||||
Some(vec![("devicename", "roth"), ("updateState", "1")]),
|
||||
);
|
||||
|
||||
let pin = generate_pin();
|
||||
|
||||
let mut client_secret_data = [0u8; 16];
|
||||
if let Err(e) = openssl::rand::rand_bytes(&mut client_secret_data) {
|
||||
error!("Could not generate client secret data: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let base_url = get_base_url(¶ms.host, params.port, unique_id);
|
||||
// Get or generate cert / private key
|
||||
let (cert, private_key) = match crate::certs::get_cert_and_key() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Could not generate certs: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let pin = generate_pin();
|
||||
// Convert to hex
|
||||
let cert_pem = cert.to_pem().unwrap();
|
||||
let cert_hex = hex::encode(&cert_pem);
|
||||
|
||||
let mut client_secret_data = [0u8; 16];
|
||||
if let Err(e) = openssl::rand::rand_bytes(&mut client_secret_data) {
|
||||
println!("Could not generate client secret data: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
// Generate salt and convert to hex
|
||||
let mut salt = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut salt).unwrap();
|
||||
let salt_hex = hex::encode(salt);
|
||||
|
||||
// Get or generate cert / private key
|
||||
let (cert, private_key) = match crate::certs::get_cert_and_key() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Could not generate certs: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Convert to hex
|
||||
let cert_pem = cert.to_pem().unwrap();
|
||||
let cert_hex = hex::encode(&cert_pem);
|
||||
|
||||
// Generate salt and convert to hex
|
||||
let mut salt = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut salt).unwrap();
|
||||
let salt_hex = hex::encode(salt);
|
||||
|
||||
// Get the server certificate and start the pairing process
|
||||
// This returns once the user has submitted the pin to the
|
||||
// server out of band.
|
||||
let server_cert = match get_server_cert(base_url.clone(), salt_hex, cert_hex).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("Could not get server cert: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
//println!("{server_cert:?}");
|
||||
|
||||
// Do the challenge response process
|
||||
// This returns the pairing secret
|
||||
let server_pairing_secret =
|
||||
match do_challenge(base_url.clone(), &client_secret_data, pin, salt, &cert).await {
|
||||
// Get the server certificate and start the pairing process
|
||||
// This returns once the user has submitted the pin to the
|
||||
// server out of band.
|
||||
let server_cert = match get_server_cert(base_url.clone(), salt_hex, cert_hex).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("Could not do challenge: {e}");
|
||||
error!("Could not get server cert: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
//println!("{server_pairing_secret:?}");
|
||||
let server_cert_hex = hex::encode(&server_cert.cert);
|
||||
debug!("server_cert_hex: {server_cert_hex:?}");
|
||||
|
||||
// Verify the pairing_secret signature
|
||||
if let Err(e) = verify_signature(
|
||||
server_pairing_secret.pairing_secret,
|
||||
server_pairing_secret.signature,
|
||||
server_cert.cert,
|
||||
) {
|
||||
println!("Could not verify signature: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
// Do the challenge response process
|
||||
// This returns the pairing secret
|
||||
let server_pairing_secret =
|
||||
match do_challenge(base_url.clone(), &client_secret_data, pin, salt, &cert).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Could not do challenge: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let server_pairing_secret_hex = hex::encode(&server_pairing_secret.pairing_secret);
|
||||
debug!("server_pairing_secret_hex: {server_pairing_secret_hex:?}");
|
||||
|
||||
let client_pairing_secret_response =
|
||||
match send_client_pairing_secret(base_url.clone(), &client_secret_data, &private_key).await
|
||||
{
|
||||
Ok(p) => p,
|
||||
// Verify the pairing_secret signature
|
||||
if let Err(e) = verify_signature(
|
||||
server_pairing_secret.pairing_secret,
|
||||
server_pairing_secret.signature,
|
||||
server_cert.cert,
|
||||
) {
|
||||
error!("Could not verify signature: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let client_pairing_secret_response =
|
||||
match send_client_pairing_secret(base_url.clone(), &client_secret_data, &private_key)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("Could not send client pairing secret: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if client_pairing_secret_response.paired != 1 {
|
||||
error!("Failed to pair with server");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
} else {
|
||||
info!(
|
||||
"Paired with server {}:{} successfully!",
|
||||
server.host, server.base_port
|
||||
);
|
||||
}
|
||||
|
||||
match self.write().await.add_server(server) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!("Could not send client pairing secret: {e}");
|
||||
error!("Could not write to config file: {e}");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if client_pairing_secret_response.paired != 1 {
|
||||
println!("Failed to pair with server");
|
||||
return Err(AppError {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
description: "Pairing failed".to_string(),
|
||||
});
|
||||
} else {
|
||||
let host = ¶ms.host;
|
||||
let port = params.port;
|
||||
println!("Paired with server {host}:{port} successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user