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:
2025-07-15 23:44:50 -06:00
parent 5391d6c6dd
commit 0edf7c60c1
8 changed files with 724 additions and 176 deletions
+5 -2
View File
@@ -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"] }
+115
View File
@@ -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))
}
}
+5 -4
View File
@@ -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,
)?)
}
+54 -3
View File
@@ -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)
}
+158
View File
@@ -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)?)
}
+18 -4
View File
@@ -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(())
}
+141 -145
View File
@@ -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(&params.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 = &params.host;
let port = params.port;
println!("Paired with server {host}:{port} successfully!");
Ok(())
}
Ok(())
}