Tunnels are now always stored in the handler
If tunnel.domain is set that means the tunnel is actually open. This should make it possible to retry a failed tunnel in the future.
This commit is contained in:
parent
da9dc7700c
commit
4fa885843f
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3221,7 +3221,6 @@ dependencies = [
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"indexmap",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
|
@ -15,7 +15,6 @@ futures = "0.3.31"
|
||||||
http-body-util = { version = "0.1.3", features = ["full"] }
|
http-body-util = { version = "0.1.3", features = ["full"] }
|
||||||
hyper = { version = "1.6.0", features = ["full"] }
|
hyper = { version = "1.6.0", features = ["full"] }
|
||||||
hyper-util = { version = "0.1.11", features = ["full"] }
|
hyper-util = { version = "0.1.11", features = ["full"] }
|
||||||
indexmap = "2.9.0"
|
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] }
|
ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] }
|
||||||
reqwest = { version = "0.12.15", features = ["rustls-tls"] }
|
reqwest = { version = "0.12.15", features = ["rustls-tls"] }
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{io::Write, iter::once};
|
use std::{io::Write, iter::once};
|
||||||
|
|
||||||
use clap::Parser as _;
|
use clap::Parser as _;
|
||||||
use indexmap::IndexMap;
|
|
||||||
use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend};
|
use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend};
|
||||||
use russh::{
|
use russh::{
|
||||||
ChannelId,
|
ChannelId,
|
||||||
|
@ -19,7 +18,7 @@ use crate::{
|
||||||
|
|
||||||
pub struct Handler {
|
pub struct Handler {
|
||||||
all_tunnels: Tunnels,
|
all_tunnels: Tunnels,
|
||||||
tunnels: IndexMap<String, Option<Tunnel>>,
|
tunnels: Vec<Tunnel>,
|
||||||
|
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
pty_channel: Option<ChannelId>,
|
pty_channel: Option<ChannelId>,
|
||||||
|
@ -33,7 +32,7 @@ impl Handler {
|
||||||
pub fn new(all_tunnels: Tunnels) -> Self {
|
pub fn new(all_tunnels: Tunnels) -> Self {
|
||||||
Self {
|
Self {
|
||||||
all_tunnels,
|
all_tunnels,
|
||||||
tunnels: IndexMap::new(),
|
tunnels: Default::default(),
|
||||||
user: None,
|
user: None,
|
||||||
pty_channel: None,
|
pty_channel: None,
|
||||||
terminal: None,
|
terminal: None,
|
||||||
|
@ -43,10 +42,8 @@ impl Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_access_all(&mut self, access: TunnelAccess) {
|
async fn set_access_all(&mut self, access: TunnelAccess) {
|
||||||
for (_address, tunnel) in &self.tunnels {
|
for tunnel in &self.tunnels {
|
||||||
if let Some(tunnel) = tunnel {
|
tunnel.set_access(access.clone()).await;
|
||||||
tunnel.set_access(access.clone()).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +89,7 @@ impl Handler {
|
||||||
|
|
||||||
async fn set_access_selection(&mut self, access: TunnelAccess) {
|
async fn set_access_selection(&mut self, access: TunnelAccess) {
|
||||||
if let Some(selected) = self.selected {
|
if let Some(selected) = self.selected {
|
||||||
if let Some((_, Some(tunnel))) = self.tunnels.get_index_mut(selected) {
|
if let Some(tunnel) = self.tunnels.get_mut(selected) {
|
||||||
tunnel.set_access(access).await;
|
tunnel.set_access(access).await;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -267,18 +264,16 @@ impl russh::server::Handler for Handler {
|
||||||
return Err(russh::Error::Inconsistent);
|
return Err(russh::Error::Inconsistent);
|
||||||
};
|
};
|
||||||
|
|
||||||
let tunnel = Tunnel::new(
|
let tunnel = self
|
||||||
session.handle(),
|
.all_tunnels
|
||||||
address,
|
.add_tunnel(session.handle(), address, *port, user)
|
||||||
*port,
|
.await;
|
||||||
TunnelAccess::Private(user),
|
|
||||||
);
|
|
||||||
let (success, address) = self.all_tunnels.add_tunnel(address, tunnel.clone()).await;
|
|
||||||
|
|
||||||
let tunnel = if success { Some(tunnel) } else { None };
|
self.tunnels.push(tunnel);
|
||||||
self.tunnels.insert(address, tunnel);
|
|
||||||
|
|
||||||
Ok(success)
|
// Technically forwarding has failed if tunnel.domain = None, however by lying to the ssh
|
||||||
|
// client we can retry in the future
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn window_change_request(
|
async fn window_change_request(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use indexmap::IndexMap;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Rect},
|
||||||
|
@ -25,12 +24,8 @@ fn command<'c>(key: &'c str, text: &'c str) -> Vec<Span<'c>> {
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
// NOTE: This needs to be a separate function as the render functions can not be async
|
// NOTE: This needs to be a separate function as the render functions can not be async
|
||||||
pub async fn update(
|
pub async fn update(&mut self, tunnels: &[Tunnel], index: Option<usize>) {
|
||||||
&mut self,
|
self.table_rows = futures::stream::iter(tunnels)
|
||||||
tunnels: &IndexMap<String, Option<Tunnel>>,
|
|
||||||
index: Option<usize>,
|
|
||||||
) {
|
|
||||||
self.table_rows = futures::stream::iter(tunnels.iter())
|
|
||||||
.then(tunnel::tui::to_row)
|
.then(tunnel::tui::to_row)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -8,8 +8,12 @@ use hyper::{
|
||||||
service::Service,
|
service::Service,
|
||||||
};
|
};
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
use indexmap::IndexMap;
|
use std::{
|
||||||
use std::{collections::HashMap, ops::Deref, pin::Pin, sync::Arc};
|
collections::{HashMap, hash_map::Entry},
|
||||||
|
ops::Deref,
|
||||||
|
pin::Pin,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use russh::{
|
use russh::{
|
||||||
|
@ -37,20 +41,12 @@ pub enum TunnelAccess {
|
||||||
pub struct Tunnel {
|
pub struct Tunnel {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
name: String,
|
name: String,
|
||||||
|
domain: Option<String>,
|
||||||
port: u32,
|
port: u32,
|
||||||
access: Arc<RwLock<TunnelAccess>>,
|
access: Arc<RwLock<TunnelAccess>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tunnel {
|
impl Tunnel {
|
||||||
pub fn new(handle: Handle, name: impl Into<String>, port: u32, access: TunnelAccess) -> Self {
|
|
||||||
Self {
|
|
||||||
handle,
|
|
||||||
name: name.into(),
|
|
||||||
port,
|
|
||||||
access: Arc::new(RwLock::new(access)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open_tunnel(&self) -> Result<Channel<Msg>, russh::Error> {
|
pub async fn open_tunnel(&self) -> Result<Channel<Msg>, russh::Error> {
|
||||||
trace!(tunnel = self.name, "Opening tunnel");
|
trace!(tunnel = self.name, "Opening tunnel");
|
||||||
self.handle
|
self.handle
|
||||||
|
@ -65,6 +61,12 @@ impl Tunnel {
|
||||||
pub async fn is_public(&self) -> bool {
|
pub async fn is_public(&self) -> bool {
|
||||||
matches!(*self.access.read().await, TunnelAccess::Public)
|
matches!(*self.access.read().await, TunnelAccess::Public)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_address(&self) -> Option<String> {
|
||||||
|
self.domain
|
||||||
|
.clone()
|
||||||
|
.map(|domain| format!("{}.{domain}", self.name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -83,40 +85,57 @@ impl Tunnels {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_tunnel(&mut self, address: &str, tunnel: Tunnel) -> (bool, String) {
|
pub async fn add_tunnel(
|
||||||
let mut all_tunnels = self.tunnels.write().await;
|
&mut self,
|
||||||
|
handle: Handle,
|
||||||
|
name: impl Into<String>,
|
||||||
|
port: u32,
|
||||||
|
user: impl Into<String>,
|
||||||
|
) -> Tunnel {
|
||||||
|
let mut tunnel = Tunnel {
|
||||||
|
handle,
|
||||||
|
name: name.into(),
|
||||||
|
domain: Some(self.domain.clone()),
|
||||||
|
port,
|
||||||
|
access: Arc::new(RwLock::new(TunnelAccess::Private(user.into()))),
|
||||||
|
};
|
||||||
|
|
||||||
let address = if address == "localhost" {
|
if tunnel.name == "localhost" {
|
||||||
// NOTE: It is technically possible to become stuck in this loop.
|
// NOTE: It is technically possible to become stuck in this loop.
|
||||||
// However, that really only becomes a concern if a (very) high
|
// However, that really only becomes a concern if a (very) high
|
||||||
// number of tunnels is open at the same time.
|
// number of tunnels is open at the same time.
|
||||||
loop {
|
loop {
|
||||||
let address = get_animal_name();
|
tunnel.name = get_animal_name().into();
|
||||||
let address = format!("{address}.{}", self.domain);
|
if !self
|
||||||
if !all_tunnels.contains_key(&address) {
|
.tunnels
|
||||||
break address;
|
.read()
|
||||||
|
.await
|
||||||
|
.contains_key(&tunnel.get_address().expect("domain is set"))
|
||||||
|
{
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
trace!(tunnel = tunnel.name, "Already in use, picking new name");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let address = format!("{address}.{}", self.domain);
|
|
||||||
if all_tunnels.contains_key(&address) {
|
|
||||||
return (false, address);
|
|
||||||
}
|
|
||||||
address
|
|
||||||
};
|
};
|
||||||
|
let address = tunnel.get_address().expect("domain is set");
|
||||||
|
|
||||||
trace!(tunnel = address, "Adding tunnel");
|
if let Entry::Vacant(e) = self.tunnels.write().await.entry(address) {
|
||||||
all_tunnels.insert(address.clone(), tunnel);
|
trace!(tunnel = tunnel.name, "Adding tunnel");
|
||||||
|
e.insert(tunnel.clone());
|
||||||
|
} else {
|
||||||
|
trace!("Address already in use");
|
||||||
|
tunnel.domain = None
|
||||||
|
}
|
||||||
|
|
||||||
(true, address)
|
tunnel
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_tunnels(&mut self, tunnels: &IndexMap<String, Option<Tunnel>>) {
|
pub async fn remove_tunnels(&mut self, tunnels: &[Tunnel]) {
|
||||||
let mut all_tunnels = self.tunnels.write().await;
|
let mut all_tunnels = self.tunnels.write().await;
|
||||||
for (address, tunnel) in tunnels {
|
for tunnel in tunnels {
|
||||||
if tunnel.is_some() {
|
if let Some(address) = tunnel.get_address() {
|
||||||
trace!(address, "Removing tunnel");
|
trace!(tunnel.name, "Removing tunnel");
|
||||||
all_tunnels.remove(address);
|
all_tunnels.remove(&address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,22 +6,30 @@ use ratatui::text::Span;
|
||||||
use super::{Tunnel, TunnelAccess};
|
use super::{Tunnel, TunnelAccess};
|
||||||
|
|
||||||
pub fn header() -> Vec<Span<'static>> {
|
pub fn header() -> Vec<Span<'static>> {
|
||||||
vec!["Access".into(), "Port".into(), "Address".into()]
|
vec![
|
||||||
|
"Name".into(),
|
||||||
|
"Access".into(),
|
||||||
|
"Port".into(),
|
||||||
|
"Address".into(),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn to_row((address, tunnel): (&String, &Option<Tunnel>)) -> Vec<Span<'static>> {
|
pub async fn to_row(tunnel: &Tunnel) -> Vec<Span<'static>> {
|
||||||
let (access, port) = if let Some(tunnel) = tunnel {
|
let access = match tunnel.access.read().await.deref() {
|
||||||
let access = match tunnel.access.read().await.deref() {
|
TunnelAccess::Private(owner) => owner.clone().yellow(),
|
||||||
TunnelAccess::Private(owner) => owner.clone().yellow(),
|
TunnelAccess::Protected => "PROTECTED".blue(),
|
||||||
TunnelAccess::Protected => "PROTECTED".blue(),
|
TunnelAccess::Public => "PUBLIC".green(),
|
||||||
TunnelAccess::Public => "PUBLIC".green(),
|
|
||||||
};
|
|
||||||
|
|
||||||
(access, tunnel.port.to_string().into())
|
|
||||||
} else {
|
|
||||||
("FAILED".red(), "".into())
|
|
||||||
};
|
};
|
||||||
let address = format!("http://{address}").into();
|
|
||||||
|
|
||||||
vec![access, port, address]
|
let address = tunnel
|
||||||
|
.get_address()
|
||||||
|
.map(|address| format!("http://{address}").into())
|
||||||
|
.unwrap_or("FAILED".red());
|
||||||
|
|
||||||
|
vec![
|
||||||
|
tunnel.name.clone().into(),
|
||||||
|
access,
|
||||||
|
tunnel.port.to_string().into(),
|
||||||
|
address,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user