- modernize packages and rust edition. - increase timeout for rollout -
increment cargo version
This commit is contained in:
+8
-45
@@ -1,71 +1,34 @@
|
||||
use actix::Message;
|
||||
use futures::{Future, Poll};
|
||||
use libc::c_ushort;
|
||||
use tokio_pty_process::PtyMaster;
|
||||
use bytes::Bytes;
|
||||
|
||||
pub use crate::terminado::TerminadoMessage;
|
||||
|
||||
use tokio_codec::{BytesCodec, Decoder};
|
||||
type BytesMut = <BytesCodec as Decoder>::Item;
|
||||
|
||||
pub struct Resize<T: PtyMaster> {
|
||||
pty: T,
|
||||
rows: c_ushort,
|
||||
cols: c_ushort,
|
||||
}
|
||||
|
||||
impl<T: PtyMaster> Resize<T> {
|
||||
pub fn new(pty: T, rows: c_ushort, cols: c_ushort) -> Self {
|
||||
Self { pty, rows, cols }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PtyMaster> Future for Resize<T> {
|
||||
type Item = ();
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
self.pty.resize(self.rows, self.cols)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct IO(pub BytesMut);
|
||||
pub struct IO(pub Bytes);
|
||||
|
||||
impl Message for IO {
|
||||
type Result = ();
|
||||
}
|
||||
|
||||
impl Into<actix_web::web::Bytes> for IO {
|
||||
fn into(self) -> actix_web::web::Bytes {
|
||||
self.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for IO {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<actix_web::web::Bytes> for IO {
|
||||
fn from(b: actix_web::web::Bytes) -> Self {
|
||||
Self(b.as_ref().into())
|
||||
impl From<Bytes> for IO {
|
||||
fn from(b: Bytes) -> Self {
|
||||
Self(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for IO {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s.into())
|
||||
Self(Bytes::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for IO {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.into())
|
||||
Self(Bytes::from(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChildDied();
|
||||
|
||||
impl Message for ChildDied {
|
||||
|
||||
+196
-149
@@ -1,4 +1,5 @@
|
||||
// Copyright (c) 2019 Fabian Freyer <fabian.freyer@physik.tu-berlin.de>.
|
||||
// Copyright (c) 2024 Jason Witty <jasonpwitty+socktop@proton.me>.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
@@ -27,24 +28,19 @@
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix::{Actor, StreamHandler};
|
||||
use actix_web::{web, App, HttpRequest, HttpResponse};
|
||||
use actix_web_actors::ws;
|
||||
|
||||
use std::io::Write;
|
||||
use std::io::{Read, Write};
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio_codec::{BytesCodec, Decoder, FramedRead};
|
||||
use tokio_pty_process::{AsyncPtyMaster, AsyncPtyMasterWriteHalf, Child, CommandExt};
|
||||
|
||||
use bytes::Bytes;
|
||||
use handlebars::Handlebars;
|
||||
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
|
||||
use serde_json::json;
|
||||
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -54,6 +50,8 @@ const IDLE_CHECK_INTERVAL: Duration = Duration::from_secs(30); // Check every 30
|
||||
mod event;
|
||||
mod terminado;
|
||||
|
||||
use event::{ChildDied, TerminadoMessage, IO};
|
||||
|
||||
/// Actix WebSocket actor
|
||||
pub struct Websocket {
|
||||
cons: Option<Addr<Terminal>>,
|
||||
@@ -76,51 +74,49 @@ impl Actor for Websocket {
|
||||
// Start PTY
|
||||
self.cons = Some(Terminal::new(ctx.address(), command).start());
|
||||
|
||||
trace!("Started WebSocket");
|
||||
log::trace!("Started WebSocket");
|
||||
}
|
||||
|
||||
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
||||
trace!("Stopping WebSocket");
|
||||
log::trace!("Stopping WebSocket");
|
||||
|
||||
// When the WebSocket disconnects, the Terminal's idle timeout will
|
||||
// automatically clean up the PTY session after IDLE_TIMEOUT (5 minutes).
|
||||
// This prevents "grey goo" accumulation of orphaned terminal processes
|
||||
// while giving reconnecting clients a grace period.
|
||||
if let Some(_cons) = self.cons.take() {
|
||||
info!("WebSocket disconnecting, Terminal will timeout if idle");
|
||||
log::info!("WebSocket disconnecting, Terminal will timeout if idle");
|
||||
}
|
||||
|
||||
Running::Stop
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
trace!("Stopped WebSocket");
|
||||
log::trace!("Stopped WebSocket");
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<event::IO> for Websocket {
|
||||
impl Handler<IO> for Websocket {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: event::IO, ctx: &mut <Self as Actor>::Context) {
|
||||
trace!("Websocket <- Terminal : {:?}", msg);
|
||||
ctx.binary(msg);
|
||||
fn handle(&mut self, msg: IO, ctx: &mut <Self as Actor>::Context) {
|
||||
log::trace!("Websocket <- Terminal : {:?}", msg);
|
||||
ctx.binary(msg.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<event::TerminadoMessage> for Websocket {
|
||||
impl Handler<TerminadoMessage> for Websocket {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: event::TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
|
||||
trace!("Websocket <- Terminal : {:?}", msg);
|
||||
fn handle(&mut self, msg: TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
|
||||
log::trace!("Websocket <- Terminal : {:?}", msg);
|
||||
match msg {
|
||||
event::TerminadoMessage::Stdout(_) => {
|
||||
TerminadoMessage::Stdout(_) => {
|
||||
let json = serde_json::to_string(&msg);
|
||||
|
||||
if let Ok(json) = json {
|
||||
ctx.text(json);
|
||||
}
|
||||
}
|
||||
_ => error!(r#"Invalid event::TerminadoMessage to Websocket: only "stdout" supported"#),
|
||||
_ => log::error!(
|
||||
r#"Invalid event::TerminadoMessage to Websocket: only "stdout" supported"#
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,22 +133,31 @@ impl Websocket {
|
||||
fn hb(&self, ctx: &mut <Self as Actor>::Context) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||
warn!("Client heartbeat timeout, disconnecting.");
|
||||
log::warn!("Client heartbeat timeout, disconnecting.");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping("");
|
||||
ctx.ping(b"");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for Websocket {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Websocket {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
let cons: &mut Addr<Terminal> = match self.cons {
|
||||
Some(ref mut c) => c,
|
||||
None => {
|
||||
error!("Terminalole died, closing websocket.");
|
||||
log::error!("Terminal died, closing websocket.");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let msg = match msg {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
log::error!("WebSocket protocol error: {}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
@@ -166,34 +171,35 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for Websocket {
|
||||
ws::Message::Pong(_) => self.hb = Instant::now(),
|
||||
ws::Message::Text(t) => {
|
||||
// Attempt to parse the message as JSON.
|
||||
if let Ok(tmsg) = event::TerminadoMessage::from_json(&t) {
|
||||
if let Ok(tmsg) = TerminadoMessage::from_json(t.as_ref()) {
|
||||
cons.do_send(tmsg);
|
||||
} else {
|
||||
// Otherwise, it's just byte data.
|
||||
cons.do_send(event::IO::from(t));
|
||||
cons.do_send(IO::from(t.to_string()));
|
||||
}
|
||||
}
|
||||
ws::Message::Binary(b) => cons.do_send(event::IO::from(b)),
|
||||
ws::Message::Binary(b) => cons.do_send(IO::from(b)),
|
||||
ws::Message::Close(_) => ctx.stop(),
|
||||
ws::Message::Nop => {}
|
||||
ws::Message::Nop | ws::Message::Continuation(_) => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<event::ChildDied> for Websocket {
|
||||
impl Handler<ChildDied> for Websocket {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, _msg: event::ChildDied, ctx: &mut <Self as Actor>::Context) {
|
||||
trace!("Websocket <- ChildDied");
|
||||
fn handle(&mut self, _msg: ChildDied, ctx: &mut <Self as Actor>::Context) {
|
||||
log::trace!("Websocket <- ChildDied");
|
||||
ctx.close(None);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a PTY backenActix WebSocket actor.d with attached child
|
||||
/// Represents a PTY backend with attached child
|
||||
pub struct Terminal {
|
||||
pty_write: Option<AsyncPtyMasterWriteHalf>,
|
||||
child: Option<Child>,
|
||||
pty_master: Option<Box<dyn portable_pty::MasterPty + Send>>,
|
||||
pty_writer: Option<Box<dyn Write + Send>>,
|
||||
child: Option<Box<dyn portable_pty::Child + Send>>,
|
||||
ws: Addr<Websocket>,
|
||||
command: Command,
|
||||
last_activity: Instant,
|
||||
@@ -203,7 +209,8 @@ pub struct Terminal {
|
||||
impl Terminal {
|
||||
pub fn new(ws: Addr<Websocket>, command: Command) -> Self {
|
||||
Self {
|
||||
pty_write: None,
|
||||
pty_master: None,
|
||||
pty_writer: None,
|
||||
child: None,
|
||||
ws,
|
||||
command,
|
||||
@@ -213,58 +220,101 @@ impl Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<<BytesCodec as Decoder>::Item, <BytesCodec as Decoder>::Error> for Terminal {
|
||||
fn handle(&mut self, msg: <BytesCodec as Decoder>::Item, _ctx: &mut Self::Context) {
|
||||
self.ws
|
||||
.do_send(event::TerminadoMessage::Stdout(event::IO(msg)));
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for Terminal {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
info!("Started Terminal");
|
||||
let pty = match AsyncPtyMaster::open() {
|
||||
log::info!("Started Terminal");
|
||||
|
||||
let pty_system = native_pty_system();
|
||||
|
||||
let pty_pair = match pty_system.openpty(PtySize {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
}) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
error!("Unable to open PTY: {:?}", e);
|
||||
log::error!("Unable to open PTY: {:?}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
Ok(pty) => pty,
|
||||
};
|
||||
|
||||
let child = match self.command.spawn_pty_async(&pty) {
|
||||
Err(e) => {
|
||||
error!("Unable to spawn child: {:?}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
let mut cmd_builder = CommandBuilder::new(self.command.get_program());
|
||||
for arg in self.command.get_args() {
|
||||
cmd_builder.arg(arg);
|
||||
}
|
||||
for (key, val) in self.command.get_envs() {
|
||||
if let Some(val) = val {
|
||||
cmd_builder.env(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
let child = match pty_pair.slave.spawn_command(cmd_builder) {
|
||||
Ok(child) => child,
|
||||
Err(e) => {
|
||||
log::error!("Unable to spawn child: {:?}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Spawned new child process with PID {}", child.id());
|
||||
log::info!("Spawned new child process");
|
||||
|
||||
let (pty_read, mut pty_write) = pty.split();
|
||||
// Get reader and writer
|
||||
let reader = match pty_pair.master.try_clone_reader() {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::error!("Unable to clone reader: {:?}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Set a sensible default PTY size immediately after splitting the PTY.
|
||||
// This avoids sending an initial 0x0 resize to the backend which can
|
||||
// cause panics in terminal UI libraries like ratatui.
|
||||
//
|
||||
// We use the Resize helper which accepts a mutable reference to the
|
||||
// write-half of the PTY and block until the resize completes.
|
||||
let _ = event::Resize::new(&mut pty_write, 24, 80).wait();
|
||||
let writer = match pty_pair.master.take_writer() {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
log::error!("Unable to get writer: {:?}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.pty_write = Some(pty_write);
|
||||
self.pty_master = Some(pty_pair.master);
|
||||
self.pty_writer = Some(writer);
|
||||
self.child = Some(child);
|
||||
|
||||
Self::add_stream(FramedRead::new(pty_read, BytesCodec::new()), ctx);
|
||||
// Spawn blocking thread to read from PTY
|
||||
let ws = self.ws.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut reader = reader;
|
||||
let mut buf = [0u8; 8192];
|
||||
|
||||
loop {
|
||||
match reader.read(&mut buf) {
|
||||
Ok(0) => {
|
||||
log::info!("PTY reader reached EOF");
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
let data = Bytes::copy_from_slice(&buf[..n]);
|
||||
ws.do_send(TerminadoMessage::Stdout(IO(data)));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error reading from PTY: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start idle timeout checker
|
||||
ctx.run_interval(IDLE_CHECK_INTERVAL, |act, ctx| {
|
||||
let idle_duration = Instant::now().duration_since(act.last_activity);
|
||||
if idle_duration >= act.idle_timeout {
|
||||
info!(
|
||||
log::info!(
|
||||
"Terminal idle timeout reached ({:?} idle), stopping session",
|
||||
idle_duration
|
||||
);
|
||||
@@ -274,93 +324,81 @@ impl Actor for Terminal {
|
||||
}
|
||||
|
||||
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
||||
info!("Stopping Terminal");
|
||||
log::info!("Stopping Terminal");
|
||||
|
||||
let child = self.child.take();
|
||||
|
||||
if child.is_none() {
|
||||
// Great, child is already dead!
|
||||
return Running::Stop;
|
||||
if let Some(mut child) = self.child.take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
let mut child = child.unwrap();
|
||||
|
||||
match child.kill() {
|
||||
Ok(()) => match child.wait() {
|
||||
Ok(exit) => info!("Child died: {:?}", exit),
|
||||
Err(e) => error!("Child wouldn't die: {}", e),
|
||||
},
|
||||
Err(e) => error!("Could not kill child with PID {}: {}", child.id(), e),
|
||||
};
|
||||
|
||||
// Notify the websocket that the child died.
|
||||
self.ws.do_send(event::ChildDied());
|
||||
self.ws.do_send(ChildDied());
|
||||
|
||||
Running::Stop
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
info!("Stopped Terminal");
|
||||
log::info!("Stopped Terminal");
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<event::IO> for Terminal {
|
||||
impl Handler<IO> for Terminal {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: event::IO, ctx: &mut <Self as Actor>::Context) {
|
||||
fn handle(&mut self, msg: IO, ctx: &mut <Self as Actor>::Context) {
|
||||
// Reset idle timer on activity
|
||||
self.last_activity = Instant::now();
|
||||
|
||||
let pty = match self.pty_write {
|
||||
Some(ref mut p) => p,
|
||||
let writer = match &mut self.pty_writer {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
error!("Write half of PTY died, stopping Terminal.");
|
||||
log::error!("PTY writer died, stopping Terminal.");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = pty.write(msg.as_ref()) {
|
||||
error!("Could not write to PTY: {}", e);
|
||||
if let Err(e) = writer.write_all(&msg.0) {
|
||||
log::error!("Could not write to PTY: {}", e);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Websocket -> Terminal : {:?}", msg);
|
||||
log::trace!("Websocket -> Terminal : {:?}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<event::TerminadoMessage> for Terminal {
|
||||
impl Handler<TerminadoMessage> for Terminal {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: event::TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
|
||||
let pty = match self.pty_write {
|
||||
Some(ref mut p) => p,
|
||||
None => {
|
||||
error!("Write half of PTY died, stopping Terminal.");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("Websocket -> Terminal : {:?}", msg);
|
||||
fn handle(&mut self, msg: TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
|
||||
log::trace!("Websocket -> Terminal : {:?}", msg);
|
||||
match msg {
|
||||
event::TerminadoMessage::Stdin(io) => {
|
||||
TerminadoMessage::Stdin(io) => {
|
||||
// Reset idle timer on user input
|
||||
self.last_activity = Instant::now();
|
||||
|
||||
if let Err(e) = pty.write(io.as_ref()) {
|
||||
error!("Could not write to PTY: {}", e);
|
||||
let writer = match &mut self.pty_writer {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
log::error!("PTY writer died, stopping Terminal.");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = writer.write_all(&io.0) {
|
||||
log::error!("Could not write to PTY: {}", e);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
event::TerminadoMessage::Resize { rows, cols } => {
|
||||
TerminadoMessage::Resize { rows, cols } => {
|
||||
// Reset idle timer on resize (user interaction)
|
||||
self.last_activity = Instant::now();
|
||||
|
||||
// Ignore zero-sized resizes which can cause panics in backends
|
||||
// such as ratatui when they receive a Rect with width or height 0.
|
||||
// Ignore zero-sized resizes
|
||||
if rows == 0 || cols == 0 {
|
||||
trace!(
|
||||
log::trace!(
|
||||
"Ignoring zero-sized resize: cols = {}, rows = {}",
|
||||
cols,
|
||||
rows
|
||||
@@ -368,14 +406,29 @@ impl Handler<event::TerminadoMessage> for Terminal {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Resize: cols = {}, rows = {}", cols, rows);
|
||||
if let Err(e) = event::Resize::new(pty, rows, cols).wait() {
|
||||
error!("Resize failed: {}", e);
|
||||
log::info!("Resize: cols = {}, rows = {}", cols, rows);
|
||||
|
||||
let pty = match &mut self.pty_master {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
log::error!("PTY died, stopping Terminal.");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = pty.resize(PtySize {
|
||||
rows,
|
||||
cols,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
}) {
|
||||
log::error!("Resize failed: {}", e);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
event::TerminadoMessage::Stdout(_) => {
|
||||
error!("Invalid Terminado Message: Stdin cannot go to PTY")
|
||||
TerminadoMessage::Stdout(_) => {
|
||||
log::error!("Invalid Terminado Message: Stdout cannot go to PTY")
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -384,63 +437,57 @@ impl Handler<event::TerminadoMessage> for Terminal {
|
||||
/// Trait to extend an [actix_web::App] by serving a web terminal.
|
||||
pub trait WebTermExt {
|
||||
/// Serve the websocket for the webterm
|
||||
fn webterm_socket<F>(self: Self, endpoint: &str, handler: F) -> Self
|
||||
fn webterm_socket<F>(self, endpoint: &str, handler: F) -> Self
|
||||
where
|
||||
F: Clone + Fn(&actix_web::HttpRequest) -> Command + 'static;
|
||||
|
||||
fn webterm_ui(
|
||||
self: Self,
|
||||
endpoint: &str,
|
||||
webterm_socket_endpoint: &str,
|
||||
static_path: &str,
|
||||
) -> Self;
|
||||
fn webterm_ui(self, endpoint: &str, webterm_socket_endpoint: &str, static_path: &str) -> Self;
|
||||
}
|
||||
|
||||
impl<T, B> WebTermExt for App<T, B>
|
||||
impl<T> WebTermExt for App<T>
|
||||
where
|
||||
B: actix_web::body::MessageBody,
|
||||
T: actix_service::NewService<
|
||||
T: actix_web::dev::ServiceFactory<
|
||||
actix_web::dev::ServiceRequest,
|
||||
Config = (),
|
||||
Request = actix_web::dev::ServiceRequest,
|
||||
Response = actix_web::dev::ServiceResponse<B>,
|
||||
Error = actix_web::Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
fn webterm_socket<F>(self: Self, endpoint: &str, handler: F) -> Self
|
||||
fn webterm_socket<F>(self, endpoint: &str, handler: F) -> Self
|
||||
where
|
||||
F: Clone + Fn(&actix_web::HttpRequest) -> Command + 'static,
|
||||
{
|
||||
self.route(
|
||||
endpoint,
|
||||
web::get().to(move |req: HttpRequest, stream: web::Payload| {
|
||||
ws::start(Websocket::new(handler(&req)), &req, stream)
|
||||
let cmd = handler(&req);
|
||||
async move { ws::start(Websocket::new(cmd), &req, stream) }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn webterm_ui(
|
||||
self: Self,
|
||||
endpoint: &str,
|
||||
webterm_socket_endpoint: &str,
|
||||
static_path: &str,
|
||||
) -> Self {
|
||||
fn webterm_ui(self, endpoint: &str, webterm_socket_endpoint: &str, static_path: &str) -> Self {
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars
|
||||
.register_templates_directory(".html", "./templates")
|
||||
.register_template_file("term", "./templates/term.html")
|
||||
.unwrap();
|
||||
let handlebars_ref = web::Data::new(handlebars);
|
||||
let static_path = static_path.to_owned();
|
||||
let webterm_socket_endpoint = webterm_socket_endpoint.to_owned();
|
||||
self.register_data(handlebars_ref.clone()).route(
|
||||
|
||||
self.app_data(handlebars_ref.clone()).route(
|
||||
endpoint,
|
||||
web::get().to(move |hb: web::Data<Handlebars>| {
|
||||
let data = json!({
|
||||
"websocket_path": webterm_socket_endpoint,
|
||||
"static_path": static_path,
|
||||
});
|
||||
let body = hb.render("term", &data).unwrap();
|
||||
HttpResponse::Ok().body(body)
|
||||
web::get().to(move |hb: web::Data<Handlebars<'static>>| {
|
||||
let websocket_path = webterm_socket_endpoint.clone();
|
||||
let static_path_clone = static_path.clone();
|
||||
async move {
|
||||
let data = json!({
|
||||
"websocket_path": websocket_path,
|
||||
"static_path": static_path_clone,
|
||||
});
|
||||
let body = hb.render("term", &data).unwrap();
|
||||
HttpResponse::Ok().body(body)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
+26
-56
@@ -1,90 +1,60 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use actix_files;
|
||||
use actix_web::{App, HttpServer};
|
||||
use structopt::StructOpt;
|
||||
use clap::Parser;
|
||||
use webterm::WebTermExt;
|
||||
|
||||
use std::net::TcpListener;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "webterm-server")]
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "webterm-server")]
|
||||
#[command(about = "Web terminal server based on xterm.js")]
|
||||
struct Opt {
|
||||
/// The port to listen on
|
||||
#[structopt(short, long, default_value = "8082")]
|
||||
#[arg(short, long, default_value = "8082")]
|
||||
port: u16,
|
||||
|
||||
/// The host or IP to listen on
|
||||
#[structopt(short, long, default_value = "localhost")]
|
||||
#[arg(short = 'H', long, default_value = "localhost")]
|
||||
host: String,
|
||||
|
||||
/// The command to execute
|
||||
#[structopt(short, long, default_value = "/bin/sh")]
|
||||
#[arg(short, long, default_value = "/bin/sh")]
|
||||
command: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref OPT: Opt = Opt::from_args();
|
||||
}
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
let opt = Opt::parse();
|
||||
|
||||
// Normalize common hostnames that sometimes resolve to IPv6-only addresses
|
||||
// which can cause platform-specific bind failures. Mapping `localhost` to
|
||||
// 127.0.0.1 makes behavior predictable on systems where `::1` would otherwise
|
||||
// be selected.
|
||||
let host = if OPT.host == "localhost" {
|
||||
let host = if opt.host == "localhost" {
|
||||
"127.0.0.1".to_string()
|
||||
} else {
|
||||
OPT.host.clone()
|
||||
opt.host.clone()
|
||||
};
|
||||
|
||||
let bind_addr = format!("{}:{}", host, OPT.port);
|
||||
let bind_addr = format!("{}:{}", host, opt.port);
|
||||
println!("Starting webterm server on http://{}", bind_addr);
|
||||
|
||||
// Single factory closure variable that we reuse for HttpServer::new.
|
||||
// The closure does not capture any stack variables (it references the static
|
||||
// `OPT`), so it can act as a simple, repeated factory for the server.
|
||||
let factory = || {
|
||||
let command = opt.command.clone();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cmd = command.clone();
|
||||
App::new()
|
||||
.service(actix_files::Files::new("/assets", "./static"))
|
||||
.service(actix_files::Files::new("/static", "./node_modules"))
|
||||
.webterm_socket("/websocket", |_req| {
|
||||
// Use the static OPT inside the handler; this does not make the
|
||||
// outer `factory` closure capture stack variables, so factory
|
||||
// remains a zero-capture closure (a function item/type).
|
||||
let mut cmd = Command::new(OPT.command.clone());
|
||||
cmd.env("TERM", "xterm");
|
||||
cmd
|
||||
.webterm_socket("/websocket", move |_req| {
|
||||
let mut command = Command::new(&cmd);
|
||||
command.env("TERM", "xterm");
|
||||
command
|
||||
})
|
||||
.webterm_ui("/", "/websocket", "/static")
|
||||
};
|
||||
|
||||
// Bind a std::net::TcpListener ourselves and hand it to actix via `listen`.
|
||||
// This avoids actix's address parser producing EINVAL on some platforms.
|
||||
let listener = match TcpListener::bind(&bind_addr) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to bind TcpListener to {}: {}", bind_addr, e);
|
||||
eprintln!("Try `--host 0.0.0.0` or `--host 127.0.0.1` to bind explicitly.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let server = HttpServer::new(factory)
|
||||
.listen(listener)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Failed to listen on {}: {}", bind_addr, e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
println!("Listening on http://{}", bind_addr);
|
||||
|
||||
if let Err(e) = server.run() {
|
||||
eprintln!("Server run failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
})
|
||||
.bind(&bind_addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,5 @@
|
||||
use actix::Message;
|
||||
use log::error;
|
||||
|
||||
use libc::c_ushort;
|
||||
|
||||
@@ -6,7 +7,6 @@ use std::convert::TryFrom;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json;
|
||||
|
||||
use crate::event::IO;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user