use std::{
collections::BTreeMap,
net::{Ipv4Addr, SocketAddr},
time::Instant,
};
use gled_core::{
engine::{ClientId, EngineConnector},
paths,
runtime::persistent_state::{PersistentState, RemoteAccessSettings},
};
use gled_server::{HttpMode, SessionEvent, WebServer, WebServerConfig};
use kanal::Receiver;
use tracing::{error, info};
#[derive(Debug)]
pub struct RemoteAccess {
engine: EngineConnector,
runtime: Option<tokio::runtime::Runtime>,
server: Option<RunningServer>,
applied: RemoteAccessSettings,
error: Option<String>,
}
#[derive(Debug)]
struct RunningServer {
server: WebServer,
session_events: Option<Receiver<SessionEvent>>,
clients: BTreeMap<ClientId, ConnectedClient>,
}
#[derive(Debug, Clone)]
pub struct ConnectedClient {
pub peer: SocketAddr,
pub app_version: String,
pub since: Instant,
}
impl RemoteAccess {
pub fn new(engine: EngineConnector) -> Self {
let settings = PersistentState::default().remote_access().clone();
let settings = &settings;
let mut remote_access = Self {
engine,
runtime: None,
server: None,
applied: RemoteAccessSettings {
enabled: false,
..settings.clone()
},
error: None,
};
remote_access.apply(settings);
remote_access
}
pub fn settings(&self) -> RemoteAccessSettings {
PersistentState::default().remote_access().clone()
}
pub fn change(&mut self, settings: RemoteAccessSettings) {
let mut state = PersistentState::default();
if *state.remote_access() != settings {
state.set_remote_access(settings.clone());
state.save();
}
self.apply(&settings);
}
pub fn apply(&mut self, settings: &RemoteAccessSettings) {
if *settings == self.applied && (self.server.is_some() || self.error.is_some()) {
return;
}
self.stop();
self.applied = settings.clone();
self.error = None;
if settings.enabled
&& let Err(err) = self.start(settings)
{
error!(%err, "could not start remote access");
self.error = Some(err);
}
}
fn start(&mut self, settings: &RemoteAccessSettings) -> Result<(), String> {
if self.runtime.is_none() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_name("gled:remote")
.enable_all()
.build()
.map_err(|err| format!("could not start the network runtime: {err}"))?;
self.runtime = Some(runtime);
}
let Some(runtime) = &self.runtime else {
return Err("the network runtime is missing".to_owned());
};
let config = WebServerConfig {
https_listen: SocketAddr::from((Ipv4Addr::UNSPECIFIED, settings.https_port)),
http_listen: settings
.http_redirect_port
.map(|port| SocketAddr::from((Ipv4Addr::UNSPECIFIED, port))),
http_mode: HttpMode::Redirect,
tls_dir: paths::tls_dir(),
};
let mut server = runtime
.block_on(WebServer::start(config, self.engine.clone()))
.map_err(|err| err.to_string())?;
info!(https = %server.https_addr(), "remote access on");
self.server = Some(RunningServer {
session_events: server.take_session_events(),
server,
clients: BTreeMap::new(),
});
Ok(())
}
fn stop(&mut self) {
if let (Some(running), Some(runtime)) = (self.server.take(), &self.runtime) {
runtime.spawn(running.server.stop());
info!("remote access off");
}
}
pub fn update(&mut self) {
let Some(running) = &mut self.server else {
return;
};
let Some(events) = &running.session_events else {
return;
};
while let Ok(Some(event)) = events.try_recv() {
match event {
SessionEvent::Connected {
id,
peer,
app_version,
} => {
running.clients.insert(
id,
ConnectedClient {
peer,
app_version,
since: Instant::now(),
},
);
}
SessionEvent::Disconnected { id } => {
running.clients.remove(&id);
}
}
}
}
pub fn https_port(&self) -> Option<u16> {
self.server
.as_ref()
.map(|running| running.server.https_addr().port())
}
pub fn fingerprint(&self) -> Option<&str> {
self.server
.as_ref()
.map(|running| running.server.fingerprint())
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn clients(&self) -> impl Iterator<Item = &ConnectedClient> {
self.server
.iter()
.flat_map(|running| running.clients.values())
}
}
impl Drop for RemoteAccess {
fn drop(&mut self) {
if let (Some(running), Some(runtime)) = (self.server.take(), &self.runtime) {
runtime.block_on(running.server.stop());
}
}
}