use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rmux_proto::{
CreateWebShareRequest, Request, Response, WebShareRequest, WebShareResponse, WebShareScope,
WebShareUrlOptions, WebTerminalPalette, WebTerminalTheme,
};
use crate::handles::{Pane, Session};
use crate::transport::TransportClient;
use crate::{Result, RmuxError};
use super::{require_web_share, unexpected_response, WebShareHandle};
pub struct WebShareBuilder<'a> {
transport: &'a TransportClient,
scope: WebShareScope,
frontend_url: Option<String>,
public_base_url: Option<String>,
tunnel_provider: Option<String>,
ttl_seconds: Option<u64>,
expires_at_unix: Option<u64>,
max_operators: Option<u16>,
max_spectators: Option<u16>,
url_options: WebShareUrlOptions,
require_pin: bool,
operator_pin: Option<String>,
spectator_pin: Option<String>,
terminal_theme: Option<WebTerminalTheme>,
terminal_palette: Option<WebTerminalPalette>,
operator: bool,
spectator: bool,
kill_session_on_expire: bool,
}
impl<'a> WebShareBuilder<'a> {
pub(crate) fn new(transport: &'a TransportClient, scope: WebShareScope) -> Self {
Self {
transport,
scope,
frontend_url: None,
public_base_url: None,
tunnel_provider: None,
ttl_seconds: None,
expires_at_unix: None,
max_operators: None,
max_spectators: None,
url_options: WebShareUrlOptions::default(),
require_pin: true,
operator_pin: None,
spectator_pin: None,
terminal_theme: None,
terminal_palette: None,
operator: true,
spectator: true,
kill_session_on_expire: false,
}
}
#[must_use]
pub fn ttl(mut self, duration: Duration) -> Self {
self.ttl_seconds = Some(whole_seconds_ceil(duration));
self.expires_at_unix = None;
self
}
pub fn expires_at(mut self, deadline: SystemTime) -> Result<Self> {
self.expires_at_unix = Some(system_time_to_unix(deadline)?);
self.ttl_seconds = None;
Ok(self)
}
#[must_use]
pub const fn max_spectators(mut self, max_spectators: u16) -> Self {
self.max_spectators = Some(max_spectators);
self
}
#[must_use]
pub const fn max_operators(mut self, max_operators: u16) -> Self {
self.max_operators = Some(max_operators);
self
}
#[must_use]
pub fn frontend_url(mut self, url: impl Into<String>) -> Self {
self.frontend_url = Some(url.into());
self
}
#[must_use]
pub fn tunnel_url(mut self, url: impl Into<String>) -> Self {
self.public_base_url = Some(url.into());
self.tunnel_provider = None;
self
}
#[must_use]
pub fn tunnel_provider(mut self, provider: impl Into<String>) -> Self {
self.tunnel_provider = Some(provider.into());
self.public_base_url = None;
self
}
#[must_use]
pub const fn no_navbar(mut self) -> Self {
self.url_options.no_navbar = true;
self
}
#[must_use]
pub const fn no_disclaimer(mut self) -> Self {
self.url_options.no_disclaimer = true;
self
}
#[must_use]
pub const fn hide_viewers(mut self) -> Self {
self.url_options.show_viewers = false;
self
}
#[must_use]
pub const fn show_viewers(mut self) -> Self {
self.url_options.show_viewers = true;
self
}
#[must_use]
pub const fn show_viewer_count(self) -> Self {
self.show_viewers()
}
#[must_use]
pub const fn no_pin(mut self) -> Self {
self.require_pin = false;
self
}
#[must_use]
pub const fn pin(mut self) -> Self {
self.require_pin = true;
self
}
#[must_use]
pub const fn pairing_code(self) -> Self {
self.pin()
}
#[must_use]
pub fn operator_pin(mut self, pin: impl Into<String>) -> Self {
self.operator_pin = Some(pin.into());
self
}
#[must_use]
pub fn spectator_pin(mut self, pin: impl Into<String>) -> Self {
self.spectator_pin = Some(pin.into());
self
}
#[must_use]
pub const fn theme(mut self, theme: WebTerminalTheme) -> Self {
self.terminal_theme = Some(theme);
self
}
#[must_use]
pub const fn terminal_theme(self, theme: WebTerminalTheme) -> Self {
self.theme(theme)
}
#[must_use]
pub const fn user_theme(self) -> Self {
self.theme(WebTerminalTheme::User)
}
#[must_use]
pub const fn light_theme(self) -> Self {
self.theme(WebTerminalTheme::Light)
}
#[must_use]
pub const fn dark_theme(self) -> Self {
self.theme(WebTerminalTheme::Dark)
}
#[must_use]
pub fn terminal_palette(mut self, palette: WebTerminalPalette) -> Self {
self.terminal_palette = Some(palette);
self
}
#[must_use]
pub const fn operator_only(mut self) -> Self {
self.operator = true;
self.spectator = false;
self
}
#[must_use]
pub const fn spectator_only(mut self) -> Self {
self.operator = false;
self.spectator = true;
self
}
#[must_use]
pub const fn kill_session_on_expire(mut self, enabled: bool) -> Self {
self.kill_session_on_expire = enabled;
self
}
async fn run(self) -> Result<WebShareHandle> {
require_web_share(self.transport).await?;
let controls = self.operator && matches!(&self.scope, WebShareScope::Session(_));
let response = self
.transport
.request(Request::WebShare(WebShareRequest::Create(
CreateWebShareRequest {
scope: self.scope,
public_base_url: self.public_base_url,
tunnel_provider: self.tunnel_provider,
frontend_url: self.frontend_url,
ttl_seconds: self.ttl_seconds,
expires_at_unix: self.expires_at_unix,
max_spectators: self.max_spectators,
max_operators: self.max_operators,
url_options: WebShareUrlOptions {
terminal_theme: self.terminal_theme,
..self.url_options
},
require_pin: self.require_pin,
operator_pin: self.operator_pin,
spectator_pin: self.spectator_pin,
terminal_palette: self.terminal_palette.map(Box::new),
operator: self.operator,
spectator: self.spectator,
controls,
kill_session_on_expire: self.kill_session_on_expire,
},
)))
.await?;
match response {
Response::WebShare(WebShareResponse::Created(created)) => {
Ok(WebShareHandle::new(self.transport.clone(), created))
}
Response::Error(error) => Err(error.into()),
response => Err(unexpected_response("web-share create", response)),
}
}
}
impl<'a> IntoFuture for WebShareBuilder<'a> {
type Output = Result<WebShareHandle>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.run())
}
}
impl Session {
#[must_use]
pub fn share(&self) -> WebShareBuilder<'_> {
WebShareBuilder::new(
self.transport(),
WebShareScope::Session(self.name().clone()),
)
}
}
impl Pane {
#[must_use]
pub fn share(&self) -> WebShareBuilder<'_> {
WebShareBuilder::new(
self.transport(),
WebShareScope::Pane(self.proto_target_ref()),
)
}
}
fn whole_seconds_ceil(duration: Duration) -> u64 {
if duration.is_zero() {
0
} else {
duration
.as_secs()
.saturating_add(u64::from(duration.subsec_nanos() > 0))
}
}
fn system_time_to_unix(value: SystemTime) -> Result<u64> {
value
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.map_err(|_| {
RmuxError::protocol(rmux_proto::RmuxError::Server(
"web-share expiration must not be before the Unix epoch".to_owned(),
))
})
}
#[cfg(test)]
mod tests {
use super::{system_time_to_unix, whole_seconds_ceil, WebShareBuilder};
use crate::transport::TransportClient;
use rmux_proto::{SessionName, WebShareScope};
use std::time::{Duration, UNIX_EPOCH};
#[test]
fn ttl_ceil_rejects_only_explicit_zero_later() {
assert_eq!(whole_seconds_ceil(Duration::ZERO), 0);
assert_eq!(whole_seconds_ceil(Duration::from_millis(1)), 1);
assert_eq!(whole_seconds_ceil(Duration::from_secs(3)), 3);
assert_eq!(whole_seconds_ceil(Duration::new(3, 1)), 4);
}
#[test]
fn system_time_to_unix_returns_seconds() {
assert_eq!(
system_time_to_unix(UNIX_EPOCH + Duration::from_secs(42)).expect("valid deadline"),
42
);
}
#[test]
fn system_time_to_unix_rejects_pre_epoch_deadlines() {
let error = system_time_to_unix(UNIX_EPOCH - Duration::from_secs(1))
.expect_err("pre-epoch deadline must be rejected locally");
assert!(error
.to_string()
.contains("web-share expiration must not be before the Unix epoch"));
}
#[tokio::test]
async fn positive_compat_aliases_restore_default_web_share_choices() {
let (client, _server) = tokio::io::duplex(64);
let transport = TransportClient::spawn(client);
let scope = WebShareScope::Session(SessionName::new("alpha").expect("valid session"));
let builder = WebShareBuilder::new(&transport, scope)
.hide_viewers()
.show_viewer_count()
.no_pin()
.pairing_code();
assert!(builder.url_options.show_viewers);
assert!(builder.require_pin);
}
}