forked from github-starred/komodo
container terminal over connection
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2754,7 +2754,6 @@ dependencies = [
|
||||
"slack_client_rs",
|
||||
"svi",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.27.0",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"toml_pretty",
|
||||
@@ -3657,10 +3656,8 @@ dependencies = [
|
||||
"resolver_api",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serror",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.27.0",
|
||||
"tracing",
|
||||
"transport",
|
||||
"uuid",
|
||||
|
||||
@@ -39,7 +39,6 @@ slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
aws-credential-types.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
openidconnect.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
|
||||
@@ -112,7 +112,7 @@ async fn refresh_server_cache(ts: i64) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
periphery_client::connection::manage_outbound_connections(&servers)
|
||||
periphery_client::connection::manage_client_connections(&servers)
|
||||
.await;
|
||||
let futures = servers.into_iter().map(|server| async move {
|
||||
update_cache_for_server(&server, false).await;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::ws::{CloseFrame, Message, Utf8Bytes, WebSocket},
|
||||
extract::ws::{Message, WebSocket},
|
||||
routing::get,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
@@ -14,13 +14,10 @@ use komodo_client::{
|
||||
entities::{server::Server, user::User},
|
||||
ws::WsLoginMessage,
|
||||
};
|
||||
use tokio::{
|
||||
net::TcpStream,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use tokio_tungstenite::{
|
||||
MaybeTlsStream, WebSocketStream, tungstenite,
|
||||
use periphery_client::{
|
||||
PeripheryClient, api::terminal::DisconnectTerminal,
|
||||
};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use transport::{
|
||||
MessageState,
|
||||
@@ -155,115 +152,39 @@ async fn handle_container_terminal(
|
||||
|
||||
trace!("connecting to periphery container exec websocket");
|
||||
|
||||
let periphery_socket = match periphery
|
||||
.connect_container_exec(container, shell)
|
||||
.await
|
||||
{
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed connect to periphery container exec websocket | {e:#}"
|
||||
);
|
||||
let _ = client_socket
|
||||
.send(Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (periphery_connection_id, periphery_sender, periphery_receiver) =
|
||||
match periphery.connect_container_exec(container, shell).await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed connect to periphery container exec websocket | {e:#}"
|
||||
);
|
||||
let _ = client_socket
|
||||
.send(Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
trace!("connected to periphery container exec websocket");
|
||||
|
||||
core_periphery_forward_ws(client_socket, periphery_socket).await
|
||||
forward_ws_channel(
|
||||
periphery,
|
||||
client_socket,
|
||||
periphery_connection_id,
|
||||
periphery_sender,
|
||||
periphery_receiver,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn core_periphery_forward_ws(
|
||||
client_socket: axum::extract::ws::WebSocket,
|
||||
periphery_socket: WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
) {
|
||||
let (mut periphery_send, mut periphery_receive) =
|
||||
periphery_socket.split();
|
||||
let (mut core_send, mut core_receive) = client_socket.split();
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
trace!("starting ws exchange");
|
||||
|
||||
let core_to_periphery = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = core_receive.next() => res,
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("core to periphery read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(msg)) => {
|
||||
if let Err(e) =
|
||||
periphery_send.send(axum_to_tungstenite(msg)).await
|
||||
{
|
||||
debug!("Failed to send terminal message | {e:?}",);
|
||||
cancel.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(Err(_e)) => {
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let periphery_to_core = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = periphery_receive.next() => res,
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("periphery to core read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(msg)) => {
|
||||
if let Err(e) =
|
||||
core_send.send(tungstenite_to_axum(msg)).await
|
||||
{
|
||||
debug!("{e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
let _ = core_send
|
||||
.send(Message::text(format!(
|
||||
"ERROR: Failed to receive message from periphery | {e:?}"
|
||||
)))
|
||||
.await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
let _ = core_send.send(Message::text("STREAM EOF")).await;
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::join!(core_to_periphery, periphery_to_core);
|
||||
}
|
||||
|
||||
async fn core_periphery_forward_ws_channel(
|
||||
async fn forward_ws_channel(
|
||||
periphery: PeripheryClient,
|
||||
client_socket: axum::extract::ws::WebSocket,
|
||||
periphery_connection_id: Uuid,
|
||||
periphery_send: Sender<Bytes>,
|
||||
mut periphery_receive: Receiver<Bytes>,
|
||||
periphery_sender: Sender<Bytes>,
|
||||
mut periphery_receiver: Receiver<Bytes>,
|
||||
) {
|
||||
let (mut core_send, mut core_receive) = client_socket.split();
|
||||
let cancel = CancellationToken::new();
|
||||
@@ -281,7 +202,7 @@ async fn core_periphery_forward_ws_channel(
|
||||
};
|
||||
match res {
|
||||
Some(Ok(Message::Binary(data))) => {
|
||||
if let Err(e) = periphery_send
|
||||
if let Err(e) = periphery_sender
|
||||
.send(to_transport_bytes(
|
||||
data.into(),
|
||||
periphery_connection_id,
|
||||
@@ -296,7 +217,7 @@ async fn core_periphery_forward_ws_channel(
|
||||
}
|
||||
Some(Ok(Message::Text(data))) => {
|
||||
let data: Bytes = data.into();
|
||||
if let Err(e) = periphery_send
|
||||
if let Err(e) = periphery_sender
|
||||
.send(to_transport_bytes(
|
||||
data.into(),
|
||||
periphery_connection_id,
|
||||
@@ -331,7 +252,7 @@ async fn core_periphery_forward_ws_channel(
|
||||
let periphery_to_core = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = periphery_receive.recv() => res.map(data_from_transport_bytes),
|
||||
res = periphery_receiver.recv() => res.map(data_from_transport_bytes),
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("periphery to core read: cancelled from inside");
|
||||
break;
|
||||
@@ -358,44 +279,15 @@ async fn core_periphery_forward_ws_channel(
|
||||
};
|
||||
|
||||
tokio::join!(core_to_periphery, periphery_to_core);
|
||||
}
|
||||
|
||||
fn axum_to_tungstenite(msg: Message) -> tungstenite::Message {
|
||||
match msg {
|
||||
Message::Text(text) => tungstenite::Message::Text(
|
||||
// TODO: improve this conversion cost from axum ws library
|
||||
tungstenite::Utf8Bytes::from(text.to_string()),
|
||||
),
|
||||
Message::Binary(bytes) => tungstenite::Message::Binary(bytes),
|
||||
Message::Ping(bytes) => tungstenite::Message::Ping(bytes),
|
||||
Message::Pong(bytes) => tungstenite::Message::Pong(bytes),
|
||||
Message::Close(close_frame) => {
|
||||
tungstenite::Message::Close(close_frame.map(|cf| {
|
||||
tungstenite::protocol::CloseFrame {
|
||||
code: cf.code.into(),
|
||||
reason: tungstenite::Utf8Bytes::from(cf.reason.to_string()),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tungstenite_to_axum(msg: tungstenite::Message) -> Message {
|
||||
match msg {
|
||||
tungstenite::Message::Text(text) => {
|
||||
Message::Text(Utf8Bytes::from(text.to_string()))
|
||||
}
|
||||
tungstenite::Message::Binary(bytes) => Message::Binary(bytes),
|
||||
tungstenite::Message::Ping(bytes) => Message::Ping(bytes),
|
||||
tungstenite::Message::Pong(bytes) => Message::Pong(bytes),
|
||||
tungstenite::Message::Close(close_frame) => {
|
||||
Message::Close(close_frame.map(|cf| CloseFrame {
|
||||
code: cf.code.into(),
|
||||
reason: Utf8Bytes::from(cf.reason.to_string()),
|
||||
}))
|
||||
}
|
||||
tungstenite::Message::Frame(_) => {
|
||||
unreachable!()
|
||||
}
|
||||
if let Err(e) = periphery
|
||||
.request(DisconnectTerminal {
|
||||
id: periphery_connection_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to disconnect Periphery terminal forwarding | {e:#}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use komodo_client::{
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, permission::get_check_permissions,
|
||||
ws::core_periphery_forward_ws_channel,
|
||||
ws::forward_ws_channel,
|
||||
};
|
||||
|
||||
#[instrument(name = "ConnectTerminal", skip(ws))]
|
||||
@@ -77,7 +77,8 @@ pub async fn handler(
|
||||
|
||||
trace!("connected to periphery terminal websocket");
|
||||
|
||||
core_periphery_forward_ws_channel(
|
||||
forward_ws_channel(
|
||||
periphery,
|
||||
client_socket,
|
||||
periphery_connection_id,
|
||||
periphery_sender,
|
||||
|
||||
@@ -18,8 +18,11 @@ use periphery_client::api::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{config::periphery_config, docker::docker_login};
|
||||
use crate::{
|
||||
api::Args, config::periphery_config, docker::docker_login,
|
||||
};
|
||||
|
||||
use super::docker_compose;
|
||||
|
||||
@@ -149,6 +152,9 @@ pub async fn pull_or_clone_stack(
|
||||
|
||||
let git_token = crate::helpers::git_token(git_token, &args)?;
|
||||
|
||||
let req_args = Args {
|
||||
req_id: Uuid::new_v4(),
|
||||
};
|
||||
PullOrCloneRepo {
|
||||
args,
|
||||
git_token,
|
||||
@@ -162,7 +168,7 @@ pub async fn pull_or_clone_stack(
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: Default::default(),
|
||||
}
|
||||
.resolve(&crate::api::Args)
|
||||
.resolve(&req_args)
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ use periphery_client::api::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{config::periphery_config, helpers};
|
||||
use crate::{api::Args, config::periphery_config, helpers};
|
||||
|
||||
pub trait WriteStackRes {
|
||||
fn logs(&mut self) -> &mut Vec<Log>;
|
||||
@@ -151,6 +152,9 @@ async fn write_stack_linked_repo<'a>(
|
||||
let on_pull = (!repo.config.on_pull.is_none())
|
||||
.then_some(repo.config.on_pull.clone());
|
||||
|
||||
let req_args = Args {
|
||||
req_id: Uuid::new_v4(),
|
||||
};
|
||||
let clone_res = if stack.config.reclone {
|
||||
CloneRepo {
|
||||
args,
|
||||
@@ -162,7 +166,7 @@ async fn write_stack_linked_repo<'a>(
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers,
|
||||
}
|
||||
.resolve(&crate::api::Args)
|
||||
.resolve(&req_args)
|
||||
.await
|
||||
.map_err(|e| e.error)?
|
||||
} else {
|
||||
@@ -176,7 +180,7 @@ async fn write_stack_linked_repo<'a>(
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers,
|
||||
}
|
||||
.resolve(&crate::api::Args)
|
||||
.resolve(&req_args)
|
||||
.await
|
||||
.map_err(|e| e.error)?
|
||||
};
|
||||
@@ -236,6 +240,9 @@ async fn write_stack_inline_repo(
|
||||
|
||||
let git_token = stack_git_token(git_token, &args, &mut res)?;
|
||||
|
||||
let req_args = Args {
|
||||
req_id: Uuid::new_v4(),
|
||||
};
|
||||
let clone_res = if stack.config.reclone {
|
||||
CloneRepo {
|
||||
args,
|
||||
@@ -247,7 +254,7 @@ async fn write_stack_inline_repo(
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: Default::default(),
|
||||
}
|
||||
.resolve(&crate::api::Args)
|
||||
.resolve(&req_args)
|
||||
.await
|
||||
.map_err(|e| e.error)?
|
||||
} else {
|
||||
@@ -261,7 +268,7 @@ async fn write_stack_inline_repo(
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: Default::default(),
|
||||
}
|
||||
.resolve(&crate::api::Args)
|
||||
.resolve(&req_args)
|
||||
.await
|
||||
.map_err(|e| e.error)?
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Resolve<super::Args> for Deploy {
|
||||
stop_time = self.stop_time,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
|
||||
async fn resolve(self, args: &super::Args) -> serror::Result<Log> {
|
||||
let Deploy {
|
||||
mut deployment,
|
||||
stop_signal,
|
||||
@@ -87,7 +87,7 @@ impl Resolve<super::Args> for Deploy {
|
||||
signal: stop_signal,
|
||||
time: stop_time,
|
||||
})
|
||||
.resolve(&super::Args)
|
||||
.resolve(args)
|
||||
.await;
|
||||
debug!("container stopped and removed");
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use periphery_client::api::{
|
||||
use resolver_api::Resolve;
|
||||
use response::JsonBytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{config::periphery_config, docker::docker_client};
|
||||
|
||||
@@ -29,7 +30,9 @@ mod network;
|
||||
mod stats;
|
||||
mod volume;
|
||||
|
||||
pub struct Args;
|
||||
pub struct Args {
|
||||
pub req_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
|
||||
@@ -143,6 +146,7 @@ pub enum PeripheryRequest {
|
||||
ListTerminals(ListTerminals),
|
||||
CreateTerminal(CreateTerminal),
|
||||
ConnectTerminal(ConnectTerminal),
|
||||
ConnectContainerExec(ConnectContainerExec),
|
||||
DisconnectTerminal(DisconnectTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
@@ -220,7 +224,7 @@ impl Resolve<Args> for GetDockerLists {
|
||||
#[instrument(name = "GetDockerLists", level = "debug", skip_all)]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &Args,
|
||||
args: &Args,
|
||||
) -> serror::Result<GetDockerListsResponse> {
|
||||
let docker = docker_client();
|
||||
let containers =
|
||||
@@ -235,7 +239,7 @@ impl Resolve<Args> for GetDockerLists {
|
||||
docker.list_images(_containers).map_err(Into::into),
|
||||
docker.list_volumes(_containers).map_err(Into::into),
|
||||
ListComposeProjects {}
|
||||
.resolve(&Args)
|
||||
.resolve(args)
|
||||
.map_err(|e| e.error.into())
|
||||
);
|
||||
Ok(GetDockerListsResponse {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
extract::{
|
||||
@@ -73,169 +75,56 @@ impl Resolve<super::Args> for DeleteAllTerminals {
|
||||
impl Resolve<super::Args> for ConnectTerminal {
|
||||
#[instrument(name = "ConnectTerminal", level = "debug")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<Uuid> {
|
||||
let id = Uuid::new_v4();
|
||||
let ws_sender = ws_sender();
|
||||
let (sender, mut ws_receiver) = channel(1000);
|
||||
let cancel = CancellationToken::new();
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
terminal_channels()
|
||||
.insert(id, (sender, cancel.clone()))
|
||||
.await;
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
clean_up_terminals().await;
|
||||
let terminal = get_terminal(&self.terminal).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let init_res = async {
|
||||
let (a, b) = terminal.history.bytes_parts();
|
||||
if !a.is_empty() {
|
||||
ws_sender
|
||||
.send(to_transport_bytes(
|
||||
a.into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await
|
||||
.context("Failed to send history part a")?;
|
||||
}
|
||||
if !b.is_empty() {
|
||||
ws_sender
|
||||
.send(to_transport_bytes(
|
||||
b.into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await
|
||||
.context("Failed to send history part b")?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
tokio::spawn(handle_terminal_forwarding(id, terminal));
|
||||
|
||||
if let Err(e) = init_res {
|
||||
// TODO: Handle error
|
||||
warn!("Failed to init terminal | {e:#}");
|
||||
return;
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
let ws_read = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = ws_receiver.recv() => res,
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws read: cancelled from outside");
|
||||
break
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("ws read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(bytes) if bytes.first() == Some(&0x00) => {
|
||||
// println!("Got ws read bytes - for stdin");
|
||||
if let Err(e) = terminal
|
||||
.stdin
|
||||
.send(StdinMsg::Bytes(Bytes::copy_from_slice(
|
||||
&bytes[1..],
|
||||
)))
|
||||
.await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(bytes) if bytes.first() == Some(&0xFF) => {
|
||||
// println!("Got ws read bytes - for resize");
|
||||
if let Ok(dimensions) = serde_json::from_slice::<
|
||||
ResizeDimensions,
|
||||
>(&bytes[1..])
|
||||
&& let Err(e) = terminal
|
||||
.stdin
|
||||
.send(StdinMsg::Resize(dimensions))
|
||||
.await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(bytes) => {
|
||||
trace!("Got ws read text");
|
||||
if let Err(e) =
|
||||
terminal.stdin.send(StdinMsg::Bytes(bytes)).await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:?}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
None => {
|
||||
debug!("Got ws read none");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
impl Resolve<super::Args> for ConnectContainerExec {
|
||||
#[instrument(name = "ConnectContainerExec", level = "debug")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<Uuid> {
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let ws_write = async {
|
||||
let mut stdout = terminal.stdout.resubscribe();
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = stdout.recv() => res.context("Failed to get message over stdout receiver"),
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws write: cancelled from outside");
|
||||
// let _ = ws_sender.send("PTY KILLED")).await;
|
||||
// if let Err(e) = ws_write.close().await {
|
||||
// debug!("Failed to close ws: {e:?}");
|
||||
// };
|
||||
break
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
// let _ = ws_write.send(Message::Text(Utf8Bytes::from_static("WS KILLED"))).await;
|
||||
// if let Err(e) = ws_write.close().await {
|
||||
// debug!("Failed to close ws: {e:?}");
|
||||
// };
|
||||
break
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = ws_sender
|
||||
.send(to_transport_bytes(
|
||||
bytes.into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await
|
||||
{
|
||||
debug!("Failed to send to WS: {e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("PTY -> WS channel read error: {e:?}");
|
||||
let _ = ws_sender
|
||||
.send(to_transport_bytes(
|
||||
format!("ERROR: {e:#}").into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await;
|
||||
terminal.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if periphery_config().disable_container_exec {
|
||||
return Err(
|
||||
anyhow!("Container exec is disabled in the periphery config")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
tokio::join!(ws_read, ws_write);
|
||||
let ConnectContainerExec { container, shell } = self;
|
||||
|
||||
clean_up_terminals().await;
|
||||
});
|
||||
if container.contains("&&") || shell.contains("&&") {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"The use of '&&' is forbidden in the container name or shell"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
// Create (recreate if shell changed)
|
||||
let terminal = create_terminal(
|
||||
container.clone(),
|
||||
format!("docker exec -it {container} {shell}"),
|
||||
TerminalRecreateMode::DifferentCommand,
|
||||
)
|
||||
.await
|
||||
.context("Failed to create terminal for container exec")?;
|
||||
|
||||
tokio::spawn(handle_terminal_forwarding(id, terminal));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
@@ -244,10 +133,13 @@ impl Resolve<super::Args> for ConnectTerminal {
|
||||
impl Resolve<super::Args> for DisconnectTerminal {
|
||||
#[instrument(name = "DisconnectTerminal", level = "debug")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {
|
||||
// TODO: Remove logs
|
||||
info!("Disconnect called for {}", self.id);
|
||||
if let Some((_, cancel)) =
|
||||
terminal_channels().remove(&self.uuid).await
|
||||
terminal_channels().remove(&self.id).await
|
||||
{
|
||||
cancel.cancel();
|
||||
info!("Cancel called for {}", self.id);
|
||||
}
|
||||
Ok(NoData {})
|
||||
}
|
||||
@@ -265,17 +157,164 @@ impl Resolve<super::Args> for CreateTerminalAuthToken {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_terminal(
|
||||
Query(query): Query<ConnectTerminalQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> serror::Result<Response> {
|
||||
if periphery_config().disable_terminals {
|
||||
return Err(
|
||||
anyhow!("Terminals are disabled in the periphery config")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
async fn handle_terminal_forwarding(
|
||||
id: Uuid,
|
||||
terminal: Arc<Terminal>,
|
||||
) {
|
||||
let ws_sender = ws_sender();
|
||||
let (sender, mut ws_receiver) = channel(1000);
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
terminal_channels()
|
||||
.insert(id, (sender, cancel.clone()))
|
||||
.await;
|
||||
|
||||
let init_res = async {
|
||||
let (a, b) = terminal.history.bytes_parts();
|
||||
if !a.is_empty() {
|
||||
ws_sender
|
||||
.send(to_transport_bytes(
|
||||
a.into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await
|
||||
.context("Failed to send history part a")?;
|
||||
}
|
||||
if !b.is_empty() {
|
||||
ws_sender
|
||||
.send(to_transport_bytes(
|
||||
b.into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await
|
||||
.context("Failed to send history part b")?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
handle_terminal_websocket(query, ws).await
|
||||
.await;
|
||||
|
||||
if let Err(e) = init_res {
|
||||
// TODO: Handle error
|
||||
warn!("Failed to init terminal | {e:#}");
|
||||
return;
|
||||
}
|
||||
|
||||
let ws_read = async {
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = ws_receiver.recv() => res,
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws read: cancelled from outside");
|
||||
break
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
trace!("ws read: cancelled from inside");
|
||||
break;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(bytes) if bytes.first() == Some(&0x00) => {
|
||||
// println!("Got ws read bytes - for stdin");
|
||||
if let Err(e) = terminal
|
||||
.stdin
|
||||
.send(StdinMsg::Bytes(Bytes::copy_from_slice(
|
||||
&bytes[1..],
|
||||
)))
|
||||
.await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
Some(bytes) if bytes.first() == Some(&0xFF) => {
|
||||
// println!("Got ws read bytes - for resize");
|
||||
if let Ok(dimensions) =
|
||||
serde_json::from_slice::<ResizeDimensions>(&bytes[1..])
|
||||
&& let Err(e) =
|
||||
terminal.stdin.send(StdinMsg::Resize(dimensions)).await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(bytes) => {
|
||||
trace!("Got ws read text");
|
||||
if let Err(e) =
|
||||
terminal.stdin.send(StdinMsg::Bytes(bytes)).await
|
||||
{
|
||||
debug!("WS -> PTY channel send error: {e:?}");
|
||||
terminal.cancel();
|
||||
break;
|
||||
};
|
||||
}
|
||||
None => {
|
||||
debug!("Got ws read none");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ws_write = async {
|
||||
let mut stdout = terminal.stdout.resubscribe();
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = stdout.recv() => res.context("Failed to get message over stdout receiver"),
|
||||
_ = terminal.cancel.cancelled() => {
|
||||
trace!("ws write: cancelled from outside");
|
||||
// let _ = ws_sender.send("PTY KILLED")).await;
|
||||
// if let Err(e) = ws_write.close().await {
|
||||
// debug!("Failed to close ws: {e:?}");
|
||||
// };
|
||||
break
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
// let _ = ws_write.send(Message::Text(Utf8Bytes::from_static("WS KILLED"))).await;
|
||||
// if let Err(e) = ws_write.close().await {
|
||||
// debug!("Failed to close ws: {e:?}");
|
||||
// };
|
||||
break
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = ws_sender
|
||||
.send(to_transport_bytes(
|
||||
bytes.into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await
|
||||
{
|
||||
debug!("Failed to send to WS: {e:?}");
|
||||
cancel.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("PTY -> WS channel read error: {e:?}");
|
||||
let _ = ws_sender
|
||||
.send(to_transport_bytes(
|
||||
format!("ERROR: {e:#}").into(),
|
||||
id,
|
||||
MessageState::Terminal,
|
||||
))
|
||||
.await;
|
||||
terminal.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::join!(ws_read, ws_write);
|
||||
|
||||
clean_up_terminals().await;
|
||||
}
|
||||
|
||||
pub async fn connect_container_exec(
|
||||
|
||||
@@ -18,7 +18,7 @@ use transport::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::PeripheryRequest;
|
||||
use crate::api::{Args, PeripheryRequest};
|
||||
|
||||
static WS_SENDER: OnceLock<Sender<Bytes>> = OnceLock::new();
|
||||
pub fn ws_sender() -> &'static Sender<Bytes> {
|
||||
@@ -51,7 +51,7 @@ pub fn init_response_channel() {
|
||||
pub async fn inbound_connection(
|
||||
ws: WebSocketUpgrade,
|
||||
) -> serror::Result<Response> {
|
||||
transport::server::inbound_connection(
|
||||
transport::server::handle_server_connection(
|
||||
ws,
|
||||
PeripheryTransportHandler,
|
||||
ws_receiver(),
|
||||
@@ -84,7 +84,7 @@ impl TransportHandler for PeripheryTransportHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(id: Uuid, bytes: Bytes) {
|
||||
fn handle_request(req_id: Uuid, bytes: Bytes) {
|
||||
tokio::spawn(async move {
|
||||
let request = match data_from_transport_bytes(bytes) {
|
||||
Ok(req) if !req.is_empty() => req,
|
||||
@@ -105,7 +105,7 @@ fn handle_request(id: Uuid, bytes: Bytes) {
|
||||
|
||||
let resolve_response = async {
|
||||
let (state, data) =
|
||||
match request.resolve(&crate::api::Args).await {
|
||||
match request.resolve(&Args { req_id }).await {
|
||||
Ok(JsonBytes::Ok(res)) => (MessageState::Successful, res),
|
||||
Ok(JsonBytes::Err(e)) => (
|
||||
MessageState::Failed,
|
||||
@@ -118,8 +118,9 @@ fn handle_request(id: Uuid, bytes: Bytes) {
|
||||
(MessageState::Failed, serialize_error_bytes(&e.error))
|
||||
}
|
||||
};
|
||||
if let Err(e) =
|
||||
ws_sender().send(to_transport_bytes(data, id, state)).await
|
||||
if let Err(e) = ws_sender()
|
||||
.send(to_transport_bytes(data, req_id, state))
|
||||
.await
|
||||
{
|
||||
error!("Failed to send response over channel | {e:?}");
|
||||
}
|
||||
@@ -131,7 +132,7 @@ fn handle_request(id: Uuid, bytes: Bytes) {
|
||||
if let Err(e) = ws_sender()
|
||||
.send(to_transport_bytes(
|
||||
Vec::new(),
|
||||
id,
|
||||
req_id,
|
||||
MessageState::InProgress,
|
||||
))
|
||||
.await
|
||||
@@ -150,6 +151,7 @@ fn handle_request(id: Uuid, bytes: Bytes) {
|
||||
|
||||
pub type TerminalChannels =
|
||||
CloneCache<Uuid, (Sender<Bytes>, CancellationToken)>;
|
||||
|
||||
pub fn terminal_channels() -> &'static TerminalChannels {
|
||||
static TERMINAL_CHANNELS: OnceLock<TerminalChannels> =
|
||||
OnceLock::new();
|
||||
|
||||
@@ -15,16 +15,10 @@ use crate::config::periphery_config;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
// .merge(
|
||||
// Router::new()
|
||||
// .route("/", post(handler))
|
||||
// .layer(middleware::from_fn(guard_request_by_passkey)),
|
||||
// )
|
||||
.route("/", get(crate::connection::inbound_connection))
|
||||
.nest(
|
||||
"/terminal",
|
||||
Router::new()
|
||||
.route("/", get(crate::api::terminal::connect_terminal))
|
||||
.route(
|
||||
"/container",
|
||||
get(crate::api::terminal::connect_container_exec),
|
||||
@@ -43,41 +37,6 @@ pub fn router() -> Router {
|
||||
.layer(middleware::from_fn(guard_request_by_ip))
|
||||
}
|
||||
|
||||
// async fn handler(
|
||||
// Json(request): Json<crate::api::PeripheryRequest>,
|
||||
// ) -> serror::Result<axum::response::Response> {
|
||||
// let req_id = Uuid::new_v4();
|
||||
|
||||
// let res = tokio::spawn(task(req_id, request))
|
||||
// .await
|
||||
// .context("task handler spawn error");
|
||||
|
||||
// if let Err(e) = &res {
|
||||
// warn!("request {req_id} spawn error: {e:#}");
|
||||
// }
|
||||
|
||||
// res?
|
||||
// }
|
||||
|
||||
// async fn task(
|
||||
// req_id: Uuid,
|
||||
// request: crate::api::PeripheryRequest,
|
||||
// ) -> serror::Result<axum::response::Response> {
|
||||
// let variant = request.extract_variant();
|
||||
|
||||
// // let res = request.resolve(&crate::api::Args).await.map(|res| res.0);
|
||||
|
||||
// // if let Err(e) = &res {
|
||||
// // warn!(
|
||||
// // "request {req_id} | type: {variant:?} | error: {:#}",
|
||||
// // e.error
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // res
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn create_terminal(
|
||||
name: String,
|
||||
command: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> anyhow::Result<Arc<Terminal>> {
|
||||
trace!(
|
||||
"CreateTerminal: {name} | command: {command} | recreate: {recreate:?}"
|
||||
);
|
||||
@@ -40,7 +40,7 @@ pub async fn create_terminal(
|
||||
&& let Some(terminal) = terminals.get(&name)
|
||||
{
|
||||
if terminal.command == command {
|
||||
return Ok(());
|
||||
return Ok(terminal.clone());
|
||||
} else if matches!(recreate, Never) {
|
||||
return Err(anyhow!(
|
||||
"Terminal {name} already exists, but has command {} instead of {command}",
|
||||
@@ -48,16 +48,15 @@ pub async fn create_terminal(
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(prev) = terminals.insert(
|
||||
name,
|
||||
let terminal = Arc::new(
|
||||
Terminal::new(command)
|
||||
.await
|
||||
.context("Failed to init terminal")?
|
||||
.into(),
|
||||
) {
|
||||
.context("Failed to init terminal")?,
|
||||
);
|
||||
if let Some(prev) = terminals.insert(name, terminal.clone()) {
|
||||
prev.cancel();
|
||||
}
|
||||
Ok(())
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
pub async fn delete_terminal(name: &str) {
|
||||
|
||||
@@ -18,9 +18,7 @@ cache.workspace = true
|
||||
resolver_api.workspace = true
|
||||
serror.workspace = true
|
||||
# external
|
||||
tokio-tungstenite.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_qs.workspace = true
|
||||
reqwest.workspace = true
|
||||
tracing.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -44,12 +44,26 @@ pub struct ConnectTerminal {
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(Uuid)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ConnectContainerExec {
|
||||
/// The name of the container to connect to.
|
||||
pub container: String,
|
||||
/// The shell to start inside container.
|
||||
/// Default: `sh`
|
||||
#[serde(default = "default_container_shell")]
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(NoData)]
|
||||
#[error(serror::Error)]
|
||||
pub struct DisconnectTerminal {
|
||||
/// The connection of the terminal to disconnect from
|
||||
pub uuid: Uuid,
|
||||
/// The connection id of the terminal to disconnect from
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -6,24 +6,17 @@ use std::{
|
||||
use bytes::Bytes;
|
||||
use cache::CloneCache;
|
||||
use komodo_client::entities::server::Server;
|
||||
use tokio::sync::mpsc::{self, Sender};
|
||||
use tokio::sync::mpsc::{Sender, error::SendError};
|
||||
use tracing::warn;
|
||||
use transport::{
|
||||
TransportHandler,
|
||||
bytes::id_from_transport_bytes,
|
||||
channel::{BufferedReceiver, buffered_channel},
|
||||
client::{ClientConnection, fix_ws_address},
|
||||
client::ClientConnection,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Server id => Channel sender map
|
||||
pub type ResponseChannels =
|
||||
CloneCache<String, Arc<CloneCache<Uuid, mpsc::Sender<Bytes>>>>;
|
||||
pub fn periphery_response_channels() -> &'static ResponseChannels {
|
||||
static RESPONSE_CHANNELS: OnceLock<ResponseChannels> =
|
||||
OnceLock::new();
|
||||
RESPONSE_CHANNELS.get_or_init(Default::default)
|
||||
}
|
||||
use crate::periphery_response_channels;
|
||||
|
||||
pub struct CoreTransportHandler {
|
||||
response_channels: Arc<CloneCache<Uuid, Sender<Bytes>>>,
|
||||
@@ -61,8 +54,25 @@ impl TransportHandler for CoreTransportHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// - Fixes server addresses:
|
||||
/// - `server.domain` => `wss://server.domain`
|
||||
/// - `http://server.domain` => `ws://server.domain`
|
||||
/// - `https://server.domain` => `wss://server.domain`
|
||||
fn fix_ws_address(address: &str) -> String {
|
||||
if address.starts_with("ws://") || address.starts_with("wss://") {
|
||||
return address.to_string();
|
||||
}
|
||||
if address.starts_with("http://") {
|
||||
return address.replace("http://", "ws://");
|
||||
}
|
||||
if address.starts_with("https://") {
|
||||
return address.replace("https://", "wss://");
|
||||
}
|
||||
format!("wss://{address}")
|
||||
}
|
||||
|
||||
/// Managed connections to exactly those specified by specs (ServerId -> Address)
|
||||
pub async fn manage_outbound_connections(servers: &[Server]) {
|
||||
pub async fn manage_client_connections(servers: &[Server]) {
|
||||
let periphery_connections = periphery_connections();
|
||||
let periphery_response_channels = periphery_response_channels();
|
||||
|
||||
@@ -77,7 +87,7 @@ pub async fn manage_outbound_connections(servers: &[Server]) {
|
||||
periphery_connections.get_entries().await
|
||||
{
|
||||
if !specs.contains_key(&server_id) {
|
||||
connection.client.cancel();
|
||||
connection.connection.cancel();
|
||||
periphery_connections.remove(&server_id).await;
|
||||
periphery_response_channels.remove(&server_id).await;
|
||||
}
|
||||
@@ -92,7 +102,7 @@ pub async fn manage_outbound_connections(servers: &[Server]) {
|
||||
// All other cases re-spawn connection
|
||||
_ => {
|
||||
if let Err(e) =
|
||||
spawn_outbound_connection(server_id.clone(), address).await
|
||||
spawn_client_connection(server_id.clone(), address).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to spawn new connnection for {server_id} | {e:#}"
|
||||
@@ -104,7 +114,7 @@ pub async fn manage_outbound_connections(servers: &[Server]) {
|
||||
}
|
||||
|
||||
// Assumes address already wss formatted
|
||||
async fn spawn_outbound_connection(
|
||||
async fn spawn_client_connection(
|
||||
server_id: String,
|
||||
address: String,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -112,17 +122,18 @@ async fn spawn_outbound_connection(
|
||||
|
||||
let (connection, mut request_receiver) =
|
||||
PeripheryConnection::new(address.clone());
|
||||
|
||||
if let Some(existing_connection) = periphery_connections()
|
||||
.insert(server_id, connection.clone())
|
||||
.await
|
||||
{
|
||||
existing_connection.client.cancel();
|
||||
existing_connection.connection.cancel();
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
transport::client::handle_reconnecting_websocket(
|
||||
transport::client::handle_client_connection(
|
||||
&address,
|
||||
&connection.client,
|
||||
&connection.connection,
|
||||
&transport,
|
||||
&mut request_receiver,
|
||||
)
|
||||
@@ -135,6 +146,7 @@ async fn spawn_outbound_connection(
|
||||
/// server id => connection
|
||||
pub type PeripheryConnections =
|
||||
CloneCache<String, Arc<PeripheryConnection>>;
|
||||
|
||||
pub fn periphery_connections() -> &'static PeripheryConnections {
|
||||
static CONNECTIONS: OnceLock<PeripheryConnections> =
|
||||
OnceLock::new();
|
||||
@@ -143,9 +155,9 @@ pub fn periphery_connections() -> &'static PeripheryConnections {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PeripheryConnection {
|
||||
address: String,
|
||||
pub request_sender: mpsc::Sender<Bytes>,
|
||||
pub client: ClientConnection,
|
||||
pub address: String,
|
||||
pub request_sender: Sender<Bytes>,
|
||||
pub connection: ClientConnection,
|
||||
}
|
||||
|
||||
impl PeripheryConnection {
|
||||
@@ -157,7 +169,7 @@ impl PeripheryConnection {
|
||||
PeripheryConnection {
|
||||
address,
|
||||
request_sender,
|
||||
client: ClientConnection::new().into(),
|
||||
connection: ClientConnection::new().into(),
|
||||
}
|
||||
.into(),
|
||||
request_receiver,
|
||||
@@ -167,23 +179,23 @@ impl PeripheryConnection {
|
||||
pub async fn send(
|
||||
&self,
|
||||
value: Bytes,
|
||||
) -> Result<(), mpsc::error::SendError<Bytes>> {
|
||||
) -> Result<(), SendError<Bytes>> {
|
||||
self.request_sender.send(value).await
|
||||
}
|
||||
|
||||
pub fn connected(&self) -> bool {
|
||||
self.client.connected()
|
||||
self.connection.connected()
|
||||
}
|
||||
|
||||
pub async fn error(&self) -> Option<serror::Serror> {
|
||||
self.client.error().await
|
||||
self.connection.error().await
|
||||
}
|
||||
|
||||
pub async fn set_error(&self, e: anyhow::Error) {
|
||||
self.client.set_error(e).await
|
||||
self.connection.set_error(e).await
|
||||
}
|
||||
|
||||
pub async fn clear_error(&self) {
|
||||
self.client.clear_error().await
|
||||
self.connection.clear_error().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use bytes::Bytes;
|
||||
use cache::CloneCache;
|
||||
use resolver_api::HasResponse;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
@@ -10,6 +12,18 @@ mod request;
|
||||
mod terminal;
|
||||
|
||||
pub use request::request;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Server id => Channel sender map
|
||||
pub type ResponseChannels =
|
||||
CloneCache<String, Arc<CloneCache<Uuid, Sender<Bytes>>>>;
|
||||
|
||||
pub fn periphery_response_channels() -> &'static ResponseChannels {
|
||||
static RESPONSE_CHANNELS: OnceLock<ResponseChannels> =
|
||||
OnceLock::new();
|
||||
RESPONSE_CHANNELS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
fn periphery_http_client() -> &'static reqwest::Client {
|
||||
static PERIPHERY_HTTP_CLIENT: OnceLock<reqwest::Client> =
|
||||
|
||||
@@ -11,8 +11,8 @@ use transport::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::connection::{
|
||||
periphery_connections, periphery_response_channels,
|
||||
use crate::{
|
||||
connection::periphery_connections, periphery_response_channels,
|
||||
};
|
||||
|
||||
#[tracing::instrument(name = "PeripheryRequest", level = "debug")]
|
||||
|
||||
@@ -2,22 +2,15 @@ use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use komodo_client::terminal::TerminalStreamResponse;
|
||||
use reqwest::RequestBuilder;
|
||||
use tokio::{
|
||||
net::TcpStream,
|
||||
sync::mpsc::{Receiver, Sender, channel},
|
||||
};
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use tokio::sync::mpsc::{Receiver, Sender, channel};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
PeripheryClient,
|
||||
api::terminal::*,
|
||||
connection::{periphery_connections, periphery_response_channels},
|
||||
PeripheryClient, api::terminal::*,
|
||||
connection::periphery_connections, periphery_response_channels,
|
||||
};
|
||||
|
||||
impl PeripheryClient {
|
||||
/// Handles ws connect and login.
|
||||
/// Does not handle reconnect.
|
||||
pub async fn connect_terminal(
|
||||
&self,
|
||||
terminal: String,
|
||||
@@ -34,7 +27,35 @@ impl PeripheryClient {
|
||||
let id = self
|
||||
.request(ConnectTerminal { terminal })
|
||||
.await
|
||||
.context("Failed to create terminal connectionn")?;
|
||||
.context("Failed to create terminal connection")?;
|
||||
|
||||
let response_channels = periphery_response_channels()
|
||||
.get_or_insert_default(&self.id)
|
||||
.await;
|
||||
let (response_sender, response_receiever) = channel(1000);
|
||||
response_channels.insert(id, response_sender).await;
|
||||
|
||||
Ok((id, connection.request_sender.clone(), response_receiever))
|
||||
}
|
||||
|
||||
pub async fn connect_container_exec(
|
||||
&self,
|
||||
container: String,
|
||||
shell: String,
|
||||
) -> anyhow::Result<(Uuid, Sender<Bytes>, Receiver<Bytes>)> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectContainerExec | container name: {container} | shell: {shell}",
|
||||
);
|
||||
|
||||
let connection =
|
||||
periphery_connections().get(&self.id).await.with_context(
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let id = self
|
||||
.request(ConnectContainerExec { container, shell })
|
||||
.await
|
||||
.context("Failed to create conntainer exec connection")?;
|
||||
|
||||
let response_channels = periphery_response_channels()
|
||||
.get_or_insert_default(&self.id)
|
||||
@@ -74,37 +95,6 @@ impl PeripheryClient {
|
||||
terminal_stream_response(req).await
|
||||
}
|
||||
|
||||
/// Handles ws connect and login.
|
||||
/// Does not handle reconnect.
|
||||
pub async fn connect_container_exec(
|
||||
&self,
|
||||
container: String,
|
||||
shell: String,
|
||||
) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectContainerExec | container name: {container} | shell: {shell}",
|
||||
);
|
||||
|
||||
let token = self
|
||||
.request(CreateTerminalAuthToken {})
|
||||
.await
|
||||
.context("Failed to create terminal auth token")?;
|
||||
|
||||
let query_str = serde_qs::to_string(&ConnectContainerExecQuery {
|
||||
token: token.token,
|
||||
container,
|
||||
shell,
|
||||
})
|
||||
.context("Failed to serialize query string")?;
|
||||
|
||||
let url = format!(
|
||||
"{}/terminal/container?{query_str}",
|
||||
self.address.replacen("http", "ws", 1)
|
||||
);
|
||||
|
||||
transport::client::connect_websocket(&url).await
|
||||
}
|
||||
|
||||
/// Executes command on specified container,
|
||||
/// and streams the response ending in [KOMODO_EXIT_CODE][komodo_client::entities::KOMODO_EXIT_CODE]
|
||||
/// sentinal value as the expected final line of the stream.
|
||||
|
||||
@@ -19,24 +19,8 @@ use tracing::{info, warn};
|
||||
|
||||
use crate::{TransportHandler, channel::BufferedReceiver};
|
||||
|
||||
/// Fixes server addresses:
|
||||
/// server.domain => wss://server.domain
|
||||
/// http://server.domain => ws://server.domain
|
||||
/// https://server.domain => wss://server.domain
|
||||
pub fn fix_ws_address(address: &str) -> String {
|
||||
if address.starts_with("ws://") || address.starts_with("wss://") {
|
||||
return address.to_string();
|
||||
}
|
||||
if address.starts_with("http://") {
|
||||
return address.replace("http://", "ws://");
|
||||
}
|
||||
if address.starts_with("https://") {
|
||||
return address.replace("https://", "wss://");
|
||||
}
|
||||
format!("wss://{address}")
|
||||
}
|
||||
|
||||
pub async fn handle_reconnecting_websocket<
|
||||
/// Handles client side / outbound connection
|
||||
pub async fn handle_client_connection<
|
||||
T: TransportHandler + Send + Sync + 'static,
|
||||
>(
|
||||
address: &str,
|
||||
|
||||
@@ -20,3 +20,5 @@ pub trait TransportHandler {
|
||||
bytes: Bytes,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ use tracing::{error, warn};
|
||||
|
||||
use crate::{TransportHandler, channel::BufferedReceiver};
|
||||
|
||||
pub fn inbound_connection<
|
||||
/// Handles server side / inbound connection
|
||||
pub fn handle_server_connection<
|
||||
T: TransportHandler + Send + Sync + 'static,
|
||||
>(
|
||||
ws: WebSocketUpgrade,
|
||||
|
||||
Reference in New Issue
Block a user