Public Access
Add port forwarding: local, remote and dynamic, per terminal session
A host can now carry port forwards (-L, -R, -D), saved inside its encrypted payload as RelayTicketRequest.PortForwardId already assumed, so they sync and merge per forward like tags and pinned paths. The host schema moves to version 8 only for hosts that carry forwards. ISshConnection gains StartForwardAsync over SSH.NET's forwarded ports. Every listener binds loopback and there is no bind-address field: a forward is something any member of a shared keychain can edit. A remote forward's target is resolved here, IPv4 first, rather than by SSH.NET's constructor, which takes the first DNS answer and can send ::1. Forwards start with every terminal opened to the host and stop with its connection; a reconnect starts them again. The session sidebar lists them with state, failure reason and an on/off switch, the status bar counts the listening ones, and the host editor and detail pane gain a PORT FORWARDS section. A forward that cannot start fails alone. The phone does not start forwards yet: it has no surface showing what is listening, and its More screen now says so. No byte counts, since SSH.NET keeps none.
This commit is contained in:
1 parent
c8848ec1e3
commit
f05deb4fbc
36 files changed
+2776
-42
No files matched your search
@@ -1,6 +1,7 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:dom="using:DodoSSH.Client.Domain"
|
||||
xmlns:views="using:DodoSSH.Client.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.HostDrawer"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
@@ -385,6 +386,33 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
============ FORWARDS (read-only) ============
|
||||
QUICK ACCESS's shape, and hidden when empty for the same reason: a card with nothing under it
|
||||
answers a question nobody asked here.
|
||||
-->
|
||||
<Border Classes="section" IsVisible="{Binding SelectedHost.HasForwards}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="sectionhead" Text="Port forwards" Margin="0,0,0,2" />
|
||||
|
||||
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="13"
|
||||
Foreground="{StaticResource TextFaint}" Width="16" VerticalAlignment="Top"
|
||||
Margin="0,3,0,0" Text="" />
|
||||
<ItemsControl Grid.Column="1" ItemsSource="{Binding SelectedHost.Host.PortForwards}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="dom:PortForwardRule">
|
||||
<TextBlock Classes="mono" Text="{Binding Notation}" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis" Margin="0,0,0,2" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Double-clicking the card does the same as CONNECT. The terminal opens as a tab in the strip above and stays there while you look at anything else." />
|
||||
|
||||
@@ -740,6 +768,84 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
============ PORT FORWARDS ============
|
||||
Staged on VaultViewModel.EditorPortForwards the way QUICK ACCESS stages its pins, and saved
|
||||
with the host. Each row is drawn in OpenSSH's own notation, because that is what anybody who has
|
||||
used one reads fastest; the add row spells out which end the listening port is on instead,
|
||||
because -L and -R are the two flags people get backwards.
|
||||
|
||||
No bind-address field, and that is not an omission: every listener binds loopback. A forward is
|
||||
a field every member of a shared keychain can fill in, and one bound to 0.0.0.0 would hand the
|
||||
remote's network to anybody who can reach this machine. See SshPortForwardSpec.
|
||||
-->
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="sectionlabel" Text="PORT FORWARDS" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding EditorPortForwards}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Spacing="6" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="dom:PortForwardRule">
|
||||
<Border Height="33" CornerRadius="9" Background="{StaticResource Field}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="8,0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="14"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Notation}" FontSize="12.5"
|
||||
Margin="8,0" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" ToolTip.Tip="{Binding Notation}" />
|
||||
<Button Grid.Column="2" Classes="flat paneicon" Content="✕" Width="22" Height="22"
|
||||
FontSize="11"
|
||||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).RemoveEditorForwardCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="Removes this forward" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<ComboBox SelectedIndex="{Binding EditorNewForwardKind}" HorizontalAlignment="Stretch"
|
||||
Height="36">
|
||||
<ComboBoxItem Content="Local -L" />
|
||||
<ComboBoxItem Content="Remote -R" />
|
||||
<ComboBoxItem Content="Dynamic -D (SOCKS)" />
|
||||
</ComboBox>
|
||||
|
||||
<!-- Which end each port is on, for the kind chosen: the thing -L and -R get backwards. -->
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="{Binding EditorNewForwardKindHint}" />
|
||||
|
||||
<Grid ColumnDefinitions="104,6,*,6,Auto">
|
||||
<TextBox Grid.Column="0" FontFamily="{StaticResource MonoFont}"
|
||||
Text="{Binding EditorNewForwardBindPort}"
|
||||
PlaceholderText="{Binding EditorNewForwardBindHint}" Height="36"
|
||||
ToolTip.Tip="{Binding EditorNewForwardBindHint}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding AddEditorForwardCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBox Grid.Column="2" FontFamily="{StaticResource MonoFont}"
|
||||
Text="{Binding EditorNewForwardTarget}"
|
||||
IsVisible="{Binding EditorNewForwardHasTarget}"
|
||||
PlaceholderText="host:port" Height="36">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding AddEditorForwardCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="4" Classes="ghost" Content="+" Width="36" Height="36" FontSize="15"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding AddEditorForwardCommand}" ToolTip.Tip="Adds this forward" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="Forwards start with every terminal opened to this host and stop when it closes. Everything listens on localhost only. A change here applies from the next connection." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE GROUP EDITOR ============ -->
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
a sidebar naming no host would be furniture with nothing to say. Collapsing frees the
|
||||
"Auto" column it sits in, so the pane column takes the width back.
|
||||
-->
|
||||
<views:SessionSidebar Grid.Column="1" ShowsSnips="True"
|
||||
<views:SessionSidebar Grid.Column="1" ShowsSnips="True" ShowsForwards="True"
|
||||
IsVisible="{Binding ShowsQuickAccessSidebar, FallbackValue=False}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -39,6 +39,22 @@
|
||||
is showing without knowing anything about the state — and the pane beside it grows into what is freed.
|
||||
-->
|
||||
|
||||
<!--
|
||||
A forward's state word: green only while it is listening, which is the one status Live is for, and the
|
||||
danger text colour for a forward that could not start. OFF and STARTING stay faint.
|
||||
-->
|
||||
<UserControl.Styles>
|
||||
<Style Selector="TextBlock.fwdstate">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.fwdstate.live">
|
||||
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.fwdstate.failed">
|
||||
<Setter Property="Foreground" Value="{StaticResource DangerText}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Panel>
|
||||
|
||||
<!-- ============ THE RAIL, WHEN THE COLUMN IS CLOSED ============ -->
|
||||
@@ -142,6 +158,64 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- ============ FORWARDS (the terminal surface only) ============ -->
|
||||
<!--
|
||||
The selected tab's forwards: the host's saved ones, as they run over this session. Each row is
|
||||
its own switch, and a failed one says why under its notation — a port already taken, most often
|
||||
by a second tab to the same host running the same forwards. Nothing here edits a forward; the add
|
||||
row goes to the host's editor, where they are saved, as Pin folder does for QUICK ACCESS.
|
||||
|
||||
No byte counts, which the design draws. SSH.NET keeps no count of what a forward carries, so a
|
||||
number here would be invented.
|
||||
-->
|
||||
<StackPanel Spacing="6" Margin="0,16,0,0" IsVisible="{Binding #Root.ShowsForwards}">
|
||||
|
||||
<TextBlock Classes="label" Text="FORWARDS" FontSize="10" Margin="8,0" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding SelectedTab.Forwards}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PortForwardViewModel">
|
||||
<!--
|
||||
The reason sits under the row rather than inside it: a sidebar row is a fixed 33 pixels, and
|
||||
a refusal is a sentence. Indented to the notation, so it reads as belonging to the row.
|
||||
-->
|
||||
<StackPanel>
|
||||
<Button Classes="sidebarrow" Command="{Binding ToggleCommand}"
|
||||
ToolTip.Tip="{Binding Route}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text=""
|
||||
FontFamily="{StaticResource IconFont}" FontSize="15"
|
||||
Foreground="{StaticResource AccentText}" VerticalAlignment="Center"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1" FontFamily="{StaticResource MonoFont}" FontSize="12.5"
|
||||
Text="{Binding Notation}" TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Classes="fwdstate" FontFamily="Montserrat"
|
||||
FontWeight="SemiBold" FontSize="9" LetterSpacing="1"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"
|
||||
Text="{Binding StateText}"
|
||||
Classes.live="{Binding IsListening}" Classes.failed="{Binding IsFailed}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<TextBlock FontSize="11" Foreground="{StaticResource TextFaint}" TextWrapping="Wrap"
|
||||
Margin="37,0,12,4" Text="{Binding Detail}"
|
||||
IsVisible="{Binding Detail, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Button Classes="sidebaradd" Command="{Binding AddForwardFromSidebarCommand}"
|
||||
ToolTip.Tip="Opens the active tab's host for editing, where its forwards are saved. A new forward starts with the next connection.">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="14"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="Add forward" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ SNIPS (the terminal surface only) ============ -->
|
||||
<StackPanel Spacing="6" Margin="0,16,0,0" IsVisible="{Binding #Root.ShowsSnips}">
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ internal sealed partial class SessionSidebar : UserControl
|
||||
internal static readonly StyledProperty<bool> ShowsSnipsProperty =
|
||||
AvaloniaProperty.Register<SessionSidebar, bool>(nameof(ShowsSnips));
|
||||
|
||||
/// <summary>Whether the FORWARDS section draws. The terminal's own, for the reason SNIPS is.</summary>
|
||||
internal static readonly StyledProperty<bool> ShowsForwardsProperty =
|
||||
AvaloniaProperty.Register<SessionSidebar, bool>(nameof(ShowsForwards));
|
||||
|
||||
public SessionSidebar() => InitializeComponent();
|
||||
|
||||
internal bool ShowsSnips
|
||||
@@ -26,4 +30,10 @@ internal sealed partial class SessionSidebar : UserControl
|
||||
get => GetValue(ShowsSnipsProperty);
|
||||
set => SetValue(ShowsSnipsProperty, value);
|
||||
}
|
||||
|
||||
internal bool ShowsForwards
|
||||
{
|
||||
get => GetValue(ShowsForwardsProperty);
|
||||
set => SetValue(ShowsForwardsProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,13 @@
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="18" VerticalAlignment="Center">
|
||||
<!--
|
||||
How many of the selected tab's forwards are listening, and nothing when none are. The terminal's
|
||||
own, for the reason UTF-8 is: an SFTP connection runs no forwards.
|
||||
-->
|
||||
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
|
||||
VerticalAlignment="Center" Text="{Binding SelectedTab.ForwardsSummary}"
|
||||
IsVisible="{Binding #Root.ShowsEncoding}" />
|
||||
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
|
||||
VerticalAlignment="Center" Text="{Binding SessionElapsedText}"
|
||||
IsVisible="{Binding SessionElapsedText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
|
||||
The design puts five things here: the active host and the bastion it is reached through, the open port
|
||||
forwards, the SFTP transfer count, an auto-lock countdown, and the last sync time. Four of those five
|
||||
have nothing behind them — there is no jump-host routing, no port forwarding, no file transfer and no
|
||||
idle auto-lock — so what is left is the active session, the vault's last message, and the shortcut hint.
|
||||
had nothing behind them when this was written — there was no jump-host routing, no port forwarding, no
|
||||
file transfer and no idle auto-lock — so what is left is the active session, the vault's last message,
|
||||
and the shortcut hint. Forwards have since been built, and are counted on SessionStatusBar instead.
|
||||
|
||||
The last message is the load-bearing one, and it is why this bar is worth having at all: the vault's
|
||||
status line used to be a cramped label in the account bar, and everything the application has to say
|
||||
|
||||
Reference in new issue
Block a user