2019-03-04 07:24:56 +00:00
|
|
|
// Copyright (c) 2019 Fabian Freyer <fabian.freyer@physik.tu-berlin.de>.
|
|
|
|
|
// All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
// Redistribution and use in source and binary forms, with or without
|
|
|
|
|
// modification, are permitted provided that the following conditions are met:
|
|
|
|
|
//
|
|
|
|
|
// 1. Redistributions of source code must retain the above copyright notice,
|
|
|
|
|
// this list of conditions and the following disclaimer.
|
|
|
|
|
//
|
|
|
|
|
// 2: Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
|
// this list of conditions and the following disclaimer in the documentation
|
|
|
|
|
// and/or other materials provided with the distribution.
|
|
|
|
|
//
|
|
|
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors
|
|
|
|
|
// may be used to endorse or promote products derived from this software
|
|
|
|
|
// without specific prior written permission.
|
|
|
|
|
//
|
|
|
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
|
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
2019-01-03 17:15:07 +00:00
|
|
|
extern crate actix;
|
|
|
|
|
extern crate actix_web;
|
|
|
|
|
extern crate futures;
|
2019-01-03 17:15:07 +00:00
|
|
|
extern crate libc;
|
|
|
|
|
extern crate serde;
|
|
|
|
|
extern crate serde_json;
|
|
|
|
|
extern crate tokio;
|
2019-01-03 17:15:07 +00:00
|
|
|
extern crate tokio_codec;
|
|
|
|
|
extern crate tokio_io;
|
|
|
|
|
extern crate tokio_pty_process;
|
|
|
|
|
#[macro_use]
|
|
|
|
|
extern crate log;
|
|
|
|
|
extern crate pretty_env_logger;
|
|
|
|
|
|
|
|
|
|
use actix::*;
|
2019-03-29 13:48:31 +00:00
|
|
|
use actix_web::{ws, App};
|
2019-01-03 17:15:07 +00:00
|
|
|
|
2019-01-03 17:15:07 +00:00
|
|
|
use futures::prelude::*;
|
|
|
|
|
|
2019-01-03 17:15:07 +00:00
|
|
|
use std::io::Write;
|
2019-01-03 17:15:07 +00:00
|
|
|
use std::process::Command;
|
|
|
|
|
use std::time::{Duration, Instant};
|
2019-01-03 17:15:07 +00:00
|
|
|
|
|
|
|
|
use tokio_codec::{BytesCodec, Decoder, FramedRead};
|
2019-03-23 04:49:09 +00:00
|
|
|
use tokio_pty_process::{AsyncPtyMaster, AsyncPtyMasterWriteHalf, Child, CommandExt};
|
2019-01-03 17:15:07 +00:00
|
|
|
|
|
|
|
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
|
|
|
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
mod event;
|
2019-01-03 17:15:07 +00:00
|
|
|
mod terminado;
|
2019-01-03 17:15:07 +00:00
|
|
|
|
2019-03-25 19:46:07 +00:00
|
|
|
/// Actix WebSocket actor
|
|
|
|
|
pub struct Websocket {
|
2019-03-22 14:13:00 +00:00
|
|
|
cons: Option<Addr<Terminal>>,
|
2019-01-03 17:15:07 +00:00
|
|
|
hb: Instant,
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
impl Actor for Websocket {
|
2019-01-03 17:15:07 +00:00
|
|
|
type Context = ws::WebsocketContext<Self>;
|
|
|
|
|
|
|
|
|
|
fn started(&mut self, ctx: &mut Self::Context) {
|
|
|
|
|
// Start heartbeat
|
|
|
|
|
self.hb(ctx);
|
|
|
|
|
|
|
|
|
|
// Start PTY
|
2019-03-22 14:13:00 +00:00
|
|
|
self.cons = Some(Terminal::new(ctx.address()).start());
|
2019-01-03 17:15:07 +00:00
|
|
|
|
|
|
|
|
trace!("Started WebSocket");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
|
|
|
|
trace!("Stopping WebSocket");
|
|
|
|
|
|
|
|
|
|
Running::Stop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
|
|
|
|
trace!("Stopped WebSocket");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
impl Handler<event::IO> for Websocket {
|
2019-01-03 17:15:07 +00:00
|
|
|
type Result = ();
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
fn handle(&mut self, msg: event::IO, ctx: &mut <Self as Actor>::Context) {
|
2019-03-22 14:13:00 +00:00
|
|
|
trace!("Websocket <- Terminal : {:?}", msg);
|
2019-01-03 17:15:07 +00:00
|
|
|
ctx.binary(msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
impl Handler<event::TerminadoMessage> for Websocket {
|
2019-01-03 17:15:07 +00:00
|
|
|
type Result = ();
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
fn handle(&mut self, msg: event::TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
|
2019-03-22 14:13:00 +00:00
|
|
|
trace!("Websocket <- Terminal : {:?}", msg);
|
2019-01-03 17:15:07 +00:00
|
|
|
match msg {
|
2019-03-23 04:49:09 +00:00
|
|
|
event::TerminadoMessage::Stdout(_) => {
|
2019-01-03 17:15:07 +00:00
|
|
|
let json = serde_json::to_string(&msg);
|
|
|
|
|
|
|
|
|
|
if let Ok(json) = json {
|
|
|
|
|
ctx.text(json);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-03-23 04:49:09 +00:00
|
|
|
_ => error!(r#"Invalid event::TerminadoMessage to Websocket: only "stdout" supported"#),
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
impl Websocket {
|
2019-01-03 17:15:07 +00:00
|
|
|
pub fn new() -> Self {
|
2019-01-03 17:15:07 +00:00
|
|
|
Self {
|
|
|
|
|
hb: Instant::now(),
|
|
|
|
|
cons: None,
|
|
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.");
|
|
|
|
|
ctx.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.ping("");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
impl StreamHandler<ws::Message, ws::ProtocolError> for Websocket {
|
2019-01-03 17:15:07 +00:00
|
|
|
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
2019-03-22 14:13:00 +00:00
|
|
|
let cons: &mut Addr<Terminal> = match self.cons {
|
2019-01-03 17:15:07 +00:00
|
|
|
Some(ref mut c) => c,
|
|
|
|
|
None => {
|
2019-03-22 14:13:00 +00:00
|
|
|
error!("Terminalole died, closing websocket.");
|
2019-01-03 17:15:07 +00:00
|
|
|
ctx.stop();
|
|
|
|
|
return;
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match msg {
|
|
|
|
|
ws::Message::Ping(msg) => {
|
|
|
|
|
self.hb = Instant::now();
|
|
|
|
|
ctx.pong(&msg);
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
ws::Message::Pong(_) => self.hb = Instant::now(),
|
2019-01-03 17:15:07 +00:00
|
|
|
ws::Message::Text(t) => {
|
|
|
|
|
// Attempt to parse the message as JSON.
|
2019-03-23 04:49:09 +00:00
|
|
|
if let Ok(tmsg) = event::TerminadoMessage::from_json(&t) {
|
2019-01-03 17:15:07 +00:00
|
|
|
cons.do_send(tmsg);
|
|
|
|
|
} else {
|
|
|
|
|
// Otherwise, it's just byte data.
|
2019-03-23 04:49:09 +00:00
|
|
|
cons.do_send(event::IO::from(t));
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-03-23 04:49:09 +00:00
|
|
|
ws::Message::Binary(b) => cons.do_send(event::IO::from(b)),
|
2019-01-03 17:15:07 +00:00
|
|
|
ws::Message::Close(_) => ctx.stop(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-25 19:46:07 +00:00
|
|
|
/// Represents a PTY backenActix WebSocket actor.d with attached child
|
|
|
|
|
pub struct Terminal {
|
2019-01-03 17:15:07 +00:00
|
|
|
pty_write: Option<AsyncPtyMasterWriteHalf>,
|
2019-01-03 17:15:07 +00:00
|
|
|
child: Option<Child>,
|
2019-03-22 14:13:00 +00:00
|
|
|
ws: Addr<Websocket>,
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
impl Terminal {
|
|
|
|
|
pub fn new(ws: Addr<Websocket>) -> Self {
|
2019-01-03 17:15:07 +00:00
|
|
|
Self {
|
|
|
|
|
pty_write: None,
|
|
|
|
|
child: None,
|
|
|
|
|
ws,
|
|
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
impl StreamHandler<<BytesCodec as Decoder>::Item, <BytesCodec as Decoder>::Error> for Terminal {
|
2019-01-03 17:15:07 +00:00
|
|
|
fn handle(&mut self, msg: <BytesCodec as Decoder>::Item, _ctx: &mut Self::Context) {
|
2019-03-23 04:49:36 +00:00
|
|
|
self.ws
|
|
|
|
|
.do_send(event::TerminadoMessage::Stdout(event::IO(msg)));
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
impl Actor for Terminal {
|
2019-01-03 17:15:07 +00:00
|
|
|
type Context = Context<Self>;
|
|
|
|
|
|
|
|
|
|
fn started(&mut self, ctx: &mut Self::Context) {
|
2019-03-22 14:13:00 +00:00
|
|
|
info!("Started Terminal");
|
2019-01-03 17:15:07 +00:00
|
|
|
let pty = match AsyncPtyMaster::open() {
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Unable to open PTY: {:?}", e);
|
|
|
|
|
ctx.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Ok(pty) => pty,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let child = match Command::new("/bin/sh").spawn_pty_async(&pty) {
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Unable to spawn child: {:?}", e);
|
|
|
|
|
ctx.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Ok(child) => child,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
info!("Spawned new child process with PID {}", child.id());
|
|
|
|
|
|
|
|
|
|
let (pty_read, pty_write) = pty.split();
|
|
|
|
|
|
|
|
|
|
self.pty_write = Some(pty_write);
|
|
|
|
|
self.child = Some(child);
|
|
|
|
|
|
|
|
|
|
Self::add_stream(FramedRead::new(pty_read, BytesCodec::new()), ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
2019-03-22 14:13:00 +00:00
|
|
|
info!("Stopping Terminal");
|
2019-01-03 17:15:07 +00:00
|
|
|
|
|
|
|
|
let child = self.child.take();
|
|
|
|
|
|
|
|
|
|
if child.is_none() {
|
|
|
|
|
// Great, child is already dead!
|
|
|
|
|
return Running::Stop;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Running::Stop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
2019-03-22 14:13:00 +00:00
|
|
|
info!("Stopped Terminal");
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
impl Handler<event::IO> for Terminal {
|
2019-01-03 17:15:07 +00:00
|
|
|
type Result = ();
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
fn handle(&mut self, msg: event::IO, ctx: &mut <Self as Actor>::Context) {
|
2019-01-03 17:15:07 +00:00
|
|
|
let pty = match self.pty_write {
|
|
|
|
|
Some(ref mut p) => p,
|
|
|
|
|
None => {
|
2019-03-22 14:13:00 +00:00
|
|
|
error!("Write half of PTY died, stopping Terminal.");
|
2019-01-03 17:15:07 +00:00
|
|
|
ctx.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-03-22 14:04:12 +00:00
|
|
|
if let Err(e) = pty.write(msg.as_ref()) {
|
|
|
|
|
error!("Could not write to PTY: {}", e);
|
|
|
|
|
ctx.stop();
|
|
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
trace!("Websocket -> Terminal : {:?}", msg);
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
impl Handler<event::TerminadoMessage> for Terminal {
|
2019-01-03 17:15:07 +00:00
|
|
|
type Result = ();
|
|
|
|
|
|
2019-03-23 04:49:09 +00:00
|
|
|
fn handle(&mut self, msg: event::TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
|
2019-01-03 17:15:07 +00:00
|
|
|
let pty = match self.pty_write {
|
|
|
|
|
Some(ref mut p) => p,
|
|
|
|
|
None => {
|
2019-03-22 14:13:00 +00:00
|
|
|
error!("Write half of PTY died, stopping Terminal.");
|
2019-01-03 17:15:07 +00:00
|
|
|
ctx.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-03-22 14:13:00 +00:00
|
|
|
trace!("Websocket -> Terminal : {:?}", msg);
|
2019-01-03 17:15:07 +00:00
|
|
|
match msg {
|
2019-03-23 04:49:09 +00:00
|
|
|
event::TerminadoMessage::Stdin(io) => {
|
2019-03-22 14:04:12 +00:00
|
|
|
if let Err(e) = pty.write(io.as_ref()) {
|
|
|
|
|
error!("Could not write to PTY: {}", e);
|
|
|
|
|
ctx.stop();
|
|
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
2019-03-29 13:48:31 +00:00
|
|
|
event::TerminadoMessage::Resize { rows, cols } => {
|
2019-01-03 17:15:07 +00:00
|
|
|
info!("Resize: cols = {}, rows = {}", cols, rows);
|
2019-03-29 14:42:51 +00:00
|
|
|
if let Err(e) = event::Resize::new(pty, rows, cols).wait() {
|
2019-01-03 17:15:07 +00:00
|
|
|
error!("Resize failed: {}", e);
|
2019-03-22 14:04:12 +00:00
|
|
|
ctx.stop();
|
|
|
|
|
}
|
2019-01-03 17:15:07 +00:00
|
|
|
}
|
2019-03-23 04:49:09 +00:00
|
|
|
event::TerminadoMessage::Stdout(_) => {
|
2019-01-03 17:15:07 +00:00
|
|
|
error!("Invalid Terminado Message: Stdin cannot go to PTY")
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-25 19:46:07 +00:00
|
|
|
/// Trait to extend an [actix_web::App] by serving a web terminal.
|
|
|
|
|
pub trait WebTermExt {
|
|
|
|
|
/// Serve the websocket for the webterm
|
2019-03-29 13:42:50 +00:00
|
|
|
fn webterm_socket(self: Self, endpoint: &str) -> Self;
|
2019-03-25 19:46:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WebTermExt for App<()> {
|
2019-03-29 13:42:50 +00:00
|
|
|
fn webterm_socket(self: Self, endpoint: &str) -> Self {
|
|
|
|
|
self.resource(endpoint, |r| r.f(|req| ws::start(req, Websocket::new())))
|
2019-03-25 19:46:07 +00:00
|
|
|
}
|
|
|
|
|
}
|