Files
desktop-edge-win/DesktopEdge/MainWindow.xaml.cs
T
Christopher Britton 2d96b0f2d6 Show the UI when an identity is added by URL or needs MFA (#1042)
* Show UI on add-by-URL without tracking pending-identity state or auto-starting MFA setup
2026-07-21 15:30:18 -04:00

2658 lines
119 KiB
C#

//#define DEBUG_DUMP
/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.IO;
using System.ServiceProcess;
using System.Linq;
using System.Diagnostics;
using System.Windows.Controls;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Animation;
using System.Web;
using Microsoft.Toolkit.Uwp.Notifications;
using ZitiDesktopEdge.Models;
using ZitiDesktopEdge.DataStructures;
using ZitiDesktopEdge.ServiceClient;
using ZitiDesktopEdge.Utility;
using NLog;
using NLog.Config;
using NLog.Targets;
using Microsoft.Win32;
using Ziti.Desktop.Edge.Models;
using Ziti.Desktop.Edge;
using System.Text;
using Newtonsoft.Json;
using System.ComponentModel;
using static ZitiDesktopEdge.CommonDelegates;
using Ziti.Desktop.Edge.Utils;
namespace ZitiDesktopEdge {
public partial class MainWindow : Window, ZitiDesktopEdge.Tray.ITrayHost {
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public string RECOVER = "RECOVER";
public System.Windows.Forms.NotifyIcon notifyIcon;
public string Position = "Bottom";
private DateTime _startDate;
private System.Windows.Forms.Timer _tunnelUptimeTimer;
private DataClient serviceClient = null;
MonitorClient monitorClient = null;
private bool _isAttached = true;
private bool _isServiceInError = false;
private NotificationThrottle _notificationThrottle;
private int _right = 75;
private int _left = 75;
private int _top = 30;
private int defaultHeight = 600;
public int NotificationsShownCount = 0;
private double _maxHeight = 805d;
public string CurrentIcon = "white";
private string[] suffixes = { "Bps", "kBps", "mBps", "gBps", "tBps", "pBps" };
private string _blurbUrl = "";
private DateTime NextNotificationTime;
private static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
static System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
public static string ThisAssemblyName;
public static string ExecutionDirectory;
public static string ExpectedLogPathRoot;
public static string ExpectedLogPathUI;
public static string ExpectedLogPathServices;
private static ZDEWViewState state;
public static UIElement MouseDownControl;
// Global MouseDown for all controls inside the window
private void Window_GlobalMouseDown(object sender, MouseButtonEventArgs e) {
Console.WriteLine("MOUSE DOWN ON: " + e.OriginalSource);
MouseDownControl = e.OriginalSource as UIElement;
}
static MainWindow() {
asm = System.Reflection.Assembly.GetExecutingAssembly();
ThisAssemblyName = asm.GetName().Name;
state = (ZDEWViewState)Application.Current.Properties["ZDEWViewState"];
#if DEBUG
ExecutionDirectory = @"C:\Program Files (x86)\NetFoundry Inc\Ziti Desktop Edge";
#else
ExecutionDirectory = Path.GetDirectoryName(asm.Location);
#endif
ExpectedLogPathRoot = Path.Combine(ExecutionDirectory, "logs");
ExpectedLogPathUI = Path.Combine(ExpectedLogPathRoot, "UI", $"{ThisAssemblyName}.log");
ExpectedLogPathServices = Path.Combine(ExpectedLogPathRoot, "service", $"ziti-tunneler.log");
}
private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) {
LoadIdentities(true);
}
private List<ZitiIdentity> identities {
get {
return (List<ZitiIdentity>)Application.Current.Properties["Identities"];
}
}
/// <summary>
/// The MFA Toggle was toggled
/// </summary>
/// <param name="isOn">True if the toggle was on</param>
private async void MFAToggled(ZitiIdentity id, bool isOn) {
if (isOn) {
ShowLoad("Generating MFA", "MFA Setup Commencing, please wait");
await serviceClient.EnableMFA(this.IdentityMenu.Identity.Identifier);
} else {
this.ShowMFA(IdentityMenu.Identity, 3);
}
HideLoad();
}
/// <summary>
/// When a Service Client is ready to setup the MFA Authorization
/// </summary>
/// <param name="sender">The service client</param>
/// <param name="e">The MFA Event</param>
private void ServiceClient_OnMfaEvent(object sender, MfaEvent mfa) {
HideLoad();
this.Dispatcher.Invoke(async () => {
if (mfa.Action == "enrollment_challenge") {
var found = identities.Find(id => id.Identifier == mfa.Identifier);
if (mfa.Successful) {
string url = HttpUtility.UrlDecode(mfa.ProvisioningUrl);
string secret = HttpUtility.ParseQueryString(url)["secret"];
found.RecoveryCodes = mfa?.RecoveryCodes?.ToArray();
SetupMFA(found, url, secret);
} else {
// setup never started: make sure the toggle reflects IsMFAEnabled
if (IdentityMenu.Identity != null && IdentityMenu.Identity.Identifier == mfa.Identifier) {
IdentityMenu.IdentityMFA.IsOn = IdentityMenu.Identity.IsMFAEnabled;
}
await ShowBlurbAsync("MFA setup failed to start", "");
}
} else if (mfa.Action == "auth_challenge") {
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == mfa.Identifier) {
identities[i].WasNotified = false;
identities[i].WasFullNotified = false;
identities[i].IsMFANeeded = true;
identities[i].IsTimingOut = false;
QueueMfaNotification(identities[i]);
break;
}
}
} else if (mfa.Action == "enrollment_verification") {
if (mfa.Successful) {
var found = identities.Find(id => id.Identifier == mfa.Identifier);
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == mfa.Identifier) {
identities[i].WasNotified = false;
identities[i].WasFullNotified = false;
identities[i].IsMFANeeded = false;
identities[i].IsMFAEnabled = true;
identities[i].IsTimingOut = false;
identities[i].LastUpdatedTime = DateTime.Now;
for (int j = 0; j < identities[i].Services.Count; j++) {
identities[i].Services[j].TimeUpdated = DateTime.Now;
identities[i].Services[j].TimeoutRemaining = identities[i].Services[j].Timeout;
}
found = identities[i];
found.IsMFAEnabled = true;
break;
}
}
if (this.IdentityMenu.Identity != null && this.IdentityMenu.Identity.Identifier == mfa.Identifier) this.IdentityMenu.Identity = found;
ShowMFARecoveryCodes(found);
} else {
await ShowBlurbAsync("Provided code could not be verified", "");
}
} else if (mfa.Action == "enrollment_remove") {
if (mfa.Successful) {
var found = identities.Find(id => id.Identifier == mfa.Identifier);
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == mfa.Identifier) {
identities[i].WasNotified = false;
identities[i].WasFullNotified = false;
identities[i].IsMFAEnabled = false;
identities[i].IsMFANeeded = false;
identities[i].LastUpdatedTime = DateTime.Now;
identities[i].IsTimingOut = false;
for (int j = 0; j < identities[i].Services.Count; j++) {
identities[i].Services[j].TimeUpdated = DateTime.Now;
identities[i].Services[j].TimeoutRemaining = 0;
}
found = identities[i];
break;
}
}
if (this.IdentityMenu.Identity != null && this.IdentityMenu.Identity.Identifier == mfa.Identifier) this.IdentityMenu.Identity = found;
await ShowBlurbAsync("MFA disabled, access may be limited", "");
} else {
await ShowBlurbAsync("MFA Removal Failed", "");
}
} else if (mfa.Action == "mfa_auth_status") {
var found = identities.Find(id => id.Identifier == mfa.Identifier);
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == mfa.Identifier) {
identities[i].WasNotified = false;
identities[i].WasFullNotified = false;
identities[i].IsTimingOut = false;
identities[i].IsMFANeeded = !mfa.Successful;
identities[i].LastUpdatedTime = DateTime.Now;
for (int j = 0; j < identities[i].Services.Count; j++) {
identities[i].Services[j].TimeUpdated = DateTime.Now;
identities[i].Services[j].TimeoutRemaining = identities[i].Services[j].Timeout;
}
found = identities[i];
break;
}
}
if (this.IdentityMenu.Identity != null && this.IdentityMenu.Identity.Identifier == mfa.Identifier) this.IdentityMenu.Identity = found;
if (mfa.Successful) {
_notificationThrottle.Remove(mfa.Identifier);
}
} else if (mfa.Action == "enrollment_required") {
logger.Debug("MFA enrollment required for identity {0}", mfa.Identifier);
BringWindowForward();
} else {
await ShowBlurbAsync("Unexpected error when processing MFA", "");
logger.Error("unexpected action: " + mfa.Action);
}
LoadIdentities(true);
});
}
/// <summary>
/// Show the MFA Setup Modal
/// </summary>
/// <param name="identity">The Ziti Identity to Setup</param>
public void SetupMFA(ZitiIdentity identity, string url, string secret) {
MFASetup.Opacity = 0;
MFASetup.Visibility = Visibility.Visible;
MFASetup.Margin = new Thickness(0, 0, 0, 0);
MFASetup.BeginAnimation(Grid.OpacityProperty, new DoubleAnimation(1, TimeSpan.FromSeconds(.3)));
MFASetup.BeginAnimation(Grid.MarginProperty, new ThicknessAnimation(new Thickness(30, 30, 30, 30), TimeSpan.FromSeconds(.3)));
MFASetup.ShowSetup(identity, url, secret);
ShowModal();
}
/// <summary>
/// Show the MFA Authentication Screen when it is time to authenticate
/// </summary>
/// <param name="identity">The Ziti Identity to Authenticate</param>
public void MFAAuthenticate(ZitiIdentity identity) {
this.ShowMFA(identity, 1);
}
/// <summary>
/// Show MFA for the identity and set the type of screen to show
/// </summary>
/// <param name="identity">The Identity that is currently active</param>
/// <param name="type">The type of screen to show - 1 Setup, 2 Authenticate, 3 Remove MFA, 4 Regenerate Codes</param>
private void ShowMFA(ZitiIdentity identity, int type) {
if (identity.IsEnabled) {
MFASetup.Opacity = 0;
MFASetup.Visibility = Visibility.Visible;
MFASetup.Margin = new Thickness(0, 0, 0, 0);
DoubleAnimation animatin = new DoubleAnimation(1, TimeSpan.FromSeconds(.3));
animatin.Completed += Animatin_Completed;
MFASetup.BeginAnimation(Grid.OpacityProperty, animatin);
MFASetup.BeginAnimation(Grid.MarginProperty, new ThicknessAnimation(new Thickness(30, 30, 30, 30), TimeSpan.FromSeconds(.3)));
MFASetup.ShowMFA(identity, type);
ShowModal();
} else {
MainMenu_OnShowBlurb("Identity disabled, MFA cannot continue.");
}
}
private void ShowJoinByUrl() {
AnimateDialogIn(AddIdentityByURL);
ShowModal();
AddIdentityByURL.PrepareForEntry();
}
private void AnimateDialogIn(UserControl dialog) {
dialog.Opacity = 0;
dialog.Visibility = Visibility.Visible;
dialog.Margin = new Thickness(0, 0, 0, 0);
dialog.BeginAnimation(Grid.OpacityProperty, new DoubleAnimation(1, TimeSpan.FromSeconds(.3)));
dialog.BeginAnimation(Grid.MarginProperty, new ThicknessAnimation(new Thickness(30, 30, 30, 30), TimeSpan.FromSeconds(.3)));
}
private void AnimateDialogOut(UserControl dialog) {
DoubleAnimation fade = new DoubleAnimation(0, TimeSpan.FromSeconds(.3));
fade.Completed += (s, e) => dialog.Visibility = Visibility.Collapsed;
dialog.BeginAnimation(Grid.OpacityProperty, fade);
dialog.BeginAnimation(Grid.MarginProperty, new ThicknessAnimation(new Thickness(0, 0, 0, 0), TimeSpan.FromSeconds(.3)));
}
private void ShowJoinWith3rdPartyCA() {
AnimateDialogIn(AddIdentityBy3rdPartyCA);
ShowModal();
}
private void Animatin_Completed(object sender, EventArgs e) {
MFASetup.AuthCode.Focusable = true;
MFASetup.AuthCode.Focus();
}
/// <summary>
/// Show the MFA Recovery Codes
/// </summary>
/// <param name="identity">The Ziti Identity to Authenticate</param>
async public void ShowMFARecoveryCodes(ZitiIdentity identity) {
if (identity.IsMFAEnabled) {
if (identity.IsMFAEnabled && identity.RecoveryCodes != null) {
MFASetup.Opacity = 0;
MFASetup.Visibility = Visibility.Visible;
MFASetup.Margin = new Thickness(0, 0, 0, 0);
MFASetup.BeginAnimation(Grid.OpacityProperty, new DoubleAnimation(1, TimeSpan.FromSeconds(.3)));
MFASetup.BeginAnimation(Grid.MarginProperty, new ThicknessAnimation(new Thickness(30, 30, 30, 30), TimeSpan.FromSeconds(.3)));
MFASetup.ShowRecovery(identity.RecoveryCodes, identity);
ShowModal();
} else {
this.ShowMFA(IdentityMenu.Identity, 2);
}
} else {
await ShowBlurbAsync("MFA is not setup on this Identity", "");
}
}
/// <summary>
/// Show the modal, aniimating opacity
/// </summary>
private void ShowModal() {
ModalBg.Visibility = Visibility.Visible;
ModalBg.Opacity = 0;
DoubleAnimation animation = new DoubleAnimation(.8, TimeSpan.FromSeconds(.3));
ModalBg.BeginAnimation(Grid.OpacityProperty, animation);
}
/// <summary>
/// Hide the modal animating the opacity
/// </summary>
private void HideModal() {
DoubleAnimation animation = new DoubleAnimation(0, TimeSpan.FromSeconds(.3));
animation.Completed += ModalHideComplete;
ModalBg.BeginAnimation(Grid.OpacityProperty, animation);
}
/// <summary>
/// When the animation completes, set the visibility to avoid UI object conflicts
/// </summary>
/// <param name="sender">The animation</param>
/// <param name="e">The event</param>
private void ModalHideComplete(object sender, EventArgs e) {
ModalBg.Visibility = Visibility.Collapsed;
}
private void CloseJoinByUrl(bool isComplete, UserControl sender) {
AnimateDialogOut(sender);
HideModal();
}
/// <summary>
/// Close the MFA Screen with animation
/// </summary>
private void DoClose(bool isComplete, UserControl sender) {
DoubleAnimation animation = new DoubleAnimation(0, TimeSpan.FromSeconds(.3));
ThicknessAnimation animateThick = new ThicknessAnimation(new Thickness(0, 0, 0, 0), TimeSpan.FromSeconds(.3));
animation.Completed += (s, e) => {
sender.Visibility = Visibility.Collapsed;
};
sender.BeginAnimation(Grid.OpacityProperty, animation);
sender.BeginAnimation(Grid.MarginProperty, animateThick);
HideModal();
if (isComplete) {
if (MFASetup.Type == 1) {
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == MFASetup.Identity.Identifier) {
identities[i] = MFASetup.Identity;
identities[i].LastUpdatedTime = DateTime.Now;
}
}
}
}
if (IdentityMenu.IsVisible) {
if (isComplete) {
if (MFASetup.Type == 2) {
ShowRecovery(IdentityMenu.Identity);
} else if (MFASetup.Type == 3) {
} else if (MFASetup.Type == 4) {
ShowRecovery(IdentityMenu.Identity);
}
}
IdentityMenu.UpdateView();
}
LoadIdentities(true);
}
private void AddIdentity(ZitiIdentity id) {
semaphoreSlim.Wait();
if (!identities.Any(i => id.Identifier == i.Identifier)) {
identities.Add(id);
}
semaphoreSlim.Release();
}
// System-tray context menu + its dynamic sections. Lives in ZitiDesktopEdge.Tray;
// calls back into MainWindow through ITrayHost for actions that touch the rest of
// the app.
private ZitiDesktopEdge.Tray.TrayMenuController tray;
private System.ComponentModel.IContainer components;
private MainViewModel props = null;
public MainWindow() {
InitializeComponent();
props = new MainViewModel();
DataContext = props;
NextNotificationTime = DateTime.Now;
_notificationThrottle = new NotificationThrottle(ShowToast, "Authorization Required", "{0} identities require authorization.");
SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;
string nlogFile = Path.Combine(ExecutionDirectory, ThisAssemblyName + "-log.config");
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompat_OnActivated;
bool byFile = false;
if (File.Exists(nlogFile)) {
LogManager.Configuration = new XmlLoggingConfiguration(nlogFile);
byFile = true;
} else {
var config = new LoggingConfiguration();
// Targets where to log to: File and Console
var logfile = new FileTarget("logfile") {
FileName = ExpectedLogPathUI,
ArchiveEvery = FileArchivePeriod.Day,
ArchiveSuffixFormat = "_{0:00}",
MaxArchiveFiles = 7,
AutoFlush = true,
Layout = "[${date:format=yyyy-MM-ddTHH:mm:ss.fff}Z] ${level:uppercase=true:padding=5}\t${logger}\t${message}\t${exception:format=tostring}",
};
var logconsole = new ConsoleTarget("logconsole");
// Rules for mapping loggers to targets
config.AddRule(LogLevel.Debug, LogLevel.Fatal, logconsole);
config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);
// Apply config
LogManager.Configuration = config;
}
logger.Info("============================== UI started ==============================");
logger.Info("logger initialized");
logger.Info(" - version : {0}", asm.GetName().Version.ToString());
logger.Info(" - using file: {0}", byFile);
logger.Info(" - file: {0}", nlogFile);
logger.Info("========================================================================");
App.Current.MainWindow.WindowState = WindowState.Normal;
App.Current.MainWindow.Deactivated += MainWindow_Deactivated;
App.Current.MainWindow.Activated += MainWindow_Activated;
App.Current.Exit += Current_Exit;
App.Current.SessionEnding += Current_SessionEnding;
this.components = new System.ComponentModel.Container();
this.tray = new ZitiDesktopEdge.Tray.TrayMenuController(this, this.Dispatcher);
notifyIcon = new System.Windows.Forms.NotifyIcon();
notifyIcon.Visible = true;
notifyIcon.BalloonTipClosed += NotifyIcon_BalloonTipClosed;
// Only subscribe MouseClick (button-aware). The plain Click event fires for both
// mouse buttons and races MouseClick, which would steal focus on right-click and
// sometimes eat the first activation.
notifyIcon.MouseClick += NotifyIcon_MouseClick;
notifyIcon.ContextMenuStrip = this.tray.Menu;
IdentityMenu.OnDetach += HandleDetached;
IdentityMenu.OnAttach += HandleAttach;
MainMenu.OnDetach += HandleDetached;
this.MainMenu.MainWindow = this;
this.IdentityMenu.MainWindow = this;
SetNotifyIcon("white");
this.PreviewKeyDown += KeyPressed;
MFASetup.OnLoad += MFASetup_OnLoad;
MFASetup.OnError += MFASetup_OnError;
}
async private void MFASetup_OnError(string message) {
await ShowBlurbAsync(message, "", "error");
}
private static ToastButton feedbackToastButton = new ToastButton()
.SetContent("Click here to collect logs")
.AddArgument("action", "feedback");
private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivatedEventArgsCompat e) {
this.Dispatcher.Invoke(() => {
if (e.Argument != null && e.Argument.Length > 0) {
string[] items = e.Argument.Split(';');
if (items.Length > 0) {
string[] values = items[0].Split('=');
if (values.Length == 2) {
string identifier = values[1];
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == identifier) {
ShowMFA(identities[i], 1);
break;
}
}
}
}
}
ToastArguments args = ToastArguments.Parse(e.Argument);
string value = null;
if (args.TryGetValue("action", out value)) {
if (value == "feedback") {
this.Dispatcher.Invoke(() => {
MainMenu.CollectFeedbackLogs(e, null);
});
} else if (value == "ext-auth") {
string extAuthIdentifier = null;
args.TryGetValue("identifier", out extAuthIdentifier);
if (extAuthIdentifier != null) {
var found = identities.Find(i => i.Identifier == extAuthIdentifier);
if (found != null) {
this.Dispatcher.Invoke(async () => {
await StartExtAuth(found);
});
}
}
} else if (value == "mfa-auth") {
string mfaIdentifier = null;
args.TryGetValue("identifier", out mfaIdentifier);
if (mfaIdentifier != null) {
var found = identities.Find(i => i.Identifier == mfaIdentifier);
if (found != null) {
this.Dispatcher.Invoke(() => {
MFAAuthenticate(found);
});
}
}
} else if (value == "mfa-setup") {
string setupIdentifier = null;
args.TryGetValue("identifier", out setupIdentifier);
if (setupIdentifier != null) {
var found = identities.Find(i => i.Identifier == setupIdentifier);
if (found != null) {
this.Dispatcher.Invoke(() => {
IdItem_EnableMFA(found);
});
}
}
}
}
this.Show();
this.Activate();
});
}
private void KeyPressed(object sender, KeyEventArgs e) {
if (e.Key == Key.Escape) {
if (IdentityMenu.Visibility == Visibility.Visible) IdentityMenu.Visibility = Visibility.Collapsed;
else if (MainMenu.Visibility == Visibility.Visible) MainMenu.Visibility = Visibility.Collapsed;
}
// Ctrl+Shift+T : open the dev-only tunneler instance picker
if (e.Key == Key.T
&& (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
&& (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) {
OpenTunnelInstancePicker();
e.Handled = true;
}
}
private void OpenTunnelInstancePicker() {
// If there's only one ziti-edge-tunnel instance running, there's
// nothing useful to switch to — suppress the picker entirely.
_ = OpenTunnelInstancePickerAsync();
}
private async Task OpenTunnelInstancePickerAsync() {
try {
var instances = await TunnelInstanceDiscovery.EnumerateAsync();
// Fire whenever a non-default tunneler exists, regardless of count. Covers the
// "only the alternate is running" case where there's still a meaningful switch
// (back to default). The picker synthesizes the default row when needed.
bool hasNonDefault = instances.Any(i => !string.IsNullOrEmpty(i.Discriminator));
if (!hasNonDefault) {
logger.Debug("Ctrl+Shift+T ignored — no non-default tunneler instances ({0} total)", instances.Count);
return;
}
} catch (Exception ex) {
logger.Debug(ex, "enumeration failed when opening picker; showing it anyway");
}
bool picked = false;
string chosen = null;
this.Dispatcher.Invoke(() => {
string tmp;
picked = PromptForInstance(out tmp);
chosen = tmp;
});
if (picked) {
await SwitchToInstanceAsync(chosen);
}
}
/// <summary>
/// Shows the picker modally. Returns true if the user chose an instance
/// (populating <paramref name="discriminator"/> with null for default
/// or the picked name); false if the user dismissed the dialog.
/// </summary>
private bool PromptForInstance(out string discriminator) {
discriminator = null;
try {
var picker = new TunnelInstancePickerWindow(serviceClient?.Discriminator);
if (this.IsLoaded) picker.Owner = this;
bool? dialog = picker.ShowDialog();
if (dialog == true && picker.InstanceSelected) {
discriminator = picker.SelectedDiscriminator;
return true;
}
} catch (Exception ex) {
logger.Error(ex, "failed to open tunnel instance picker");
}
return false;
}
/// <summary>
/// Startup: connect to the default ziti-edge-tunnel instance. If it
/// isn't running, show the not-started banner and let the reconnect
/// loop poll for it. Non-default instances are reached exclusively via
/// the Ctrl+Shift+T picker — startup never auto-routes to them.
/// </summary>
private async Task ChooseAndConnectOnStartupAsync() {
try {
await serviceClient.ConnectAsync();
await serviceClient.WaitForConnectionAsync();
} catch /*ignored for now (Exception ex) */
{
ShowServiceNotStarted();
serviceClient.Reconnect();
}
}
private async Task SwitchToInstanceAsync(string discriminator) {
logger.Info("switching tunneler instance to '{0}'", discriminator ?? "default");
this.Dispatcher.Invoke(() => {
ResetTunnelerScopedState();
ShowLoad("Switching tunneler", "Connecting to '" + (discriminator ?? "default") + "'…");
});
try {
await serviceClient.SwitchInstanceAsync(discriminator);
this.Dispatcher.Invoke(() => HideLoad());
} catch (Exception ex) {
logger.Error(ex, "failed to switch tunneler instance to '{0}'", discriminator ?? "default");
this.Dispatcher.Invoke(() => {
HideLoad();
ShowError("Switch failed", "Could not connect to '" + (discriminator ?? "default") + "': " + ex.Message);
ShowServiceNotStarted();
});
}
}
/// <summary>
/// True when the UI is currently viewing the default ziti-edge-tunnel
/// instance. The big start/stop/force-terminate button always targets
/// the default Windows service regardless; this is purely for
/// cosmetic signals (e.g. the alternate-tunnel footer).
/// </summary>
private bool IsViewingDefaultInstance {
get { return string.IsNullOrEmpty(serviceClient?.Discriminator); }
}
/// <summary>
/// Show/hide the "you are viewing an alternate tunneler" footer based
/// on the current <see cref="DataClient.Discriminator"/>. Safe to call
/// from any thread — bounces to the Dispatcher.
/// </summary>
private void UpdateAlternateTunnelFooter() {
this.Dispatcher.Invoke(() => {
if (AlternateTunnelFooter == null) return;
string disc = serviceClient?.Discriminator;
if (string.IsNullOrEmpty(disc)) {
AlternateTunnelFooter.Visibility = Visibility.Collapsed;
AlternateTunnelFooterText.Text = "";
} else {
AlternateTunnelFooter.Visibility = Visibility.Visible;
AlternateTunnelFooterText.Text =
"viewing alternate tunneler: ziti-edge-tunnel.sock." + disc;
}
});
}
private void MFASetup_OnLoad(bool isComplete, string title, string message) {
if (isComplete) HideLoad();
else ShowLoad(title, message);
}
private void Current_SessionEnding(object sender, SessionEndingCancelEventArgs e) {
if (notifyIcon != null) {
notifyIcon.Visible = false;
notifyIcon.Icon.Dispose();
notifyIcon.Dispose();
notifyIcon = null;
}
Application.Current.Shutdown();
}
private void Current_Exit(object sender, ExitEventArgs e) {
if (notifyIcon != null) {
notifyIcon.Visible = false;
if (notifyIcon.Icon != null) {
notifyIcon.Icon.Dispose();
}
notifyIcon.Dispose();
notifyIcon = null;
}
}
// -----------------------------------------------------------------------------------
// ITrayHost: callbacks the TrayMenuController invokes for menu actions. Kept as a
// small surface so the controller doesn't depend on MainWindow directly.
// -----------------------------------------------------------------------------------
public void BringWindowForward() {
try {
this.Show();
this.Activate();
} catch (Exception ex) {
logger.Warn(ex, "could not bring window forward from tray");
}
}
public void ShowAddIdentityByJwt() {
AddIdentity_Click(this, new RoutedEventArgs());
}
public void ShowAddIdentityByUrl() {
ShowJoinByUrl();
}
/// <summary>
/// Reopen the welcome screen and expand the window to fit it. Bypasses the
/// session-dismissal flag in the VM.
/// </summary>
public void ShowWelcomeScreen() {
// Clear any in-flight close-animation clock so our imperative Height write sticks.
this.BeginAnimation(HeightProperty, null);
this.Height = defaultHeight + GetStartedExtraHeight;
GetStartedScreen.ViewModel.Show();
}
public void OpenIdentityDetails(ZitiIdentity identity) {
IdentityMenu.Identity = identity;
}
private void HelpCircle_MouseUp(object sender, MouseButtonEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
ShowWelcomeScreen();
}
public Task<bool> SetLogLevelAsync(string level) {
return LogLevelChanged(level);
}
public Task SwitchTunnelerAsync(string discriminator) {
return SwitchToInstanceAsync(discriminator);
}
public void StartFeedbackCapture() {
MainMenu.CollectFeedbackLogs(this, null);
}
private void NotifyIcon_MouseClick(object sender, System.Windows.Forms.MouseEventArgs e) {
if (e.Button == System.Windows.Forms.MouseButtons.Left) {
BringWindowForward();
//Do the awesome left clickness
} else if (e.Button == System.Windows.Forms.MouseButtons.Right) {
//Do the wickedy right clickness
} else {
//Some other button from the enum :)
}
}
private void NotifyIcon_BalloonTipClosed(object sender, EventArgs e) {
var thisIcon = (System.Windows.Forms.NotifyIcon)sender;
thisIcon.Visible = false;
thisIcon.Dispose();
}
System.Windows.Point positionAtDetatch;
private void Window_MouseDown(object sender, MouseButtonEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
positionAtDetatch = new System.Windows.Point(App.Current.MainWindow.Left, App.Current.MainWindow.Top);
HandleDetached(e);
}
private void HandleAttach(object sender, MouseButtonEventArgs e) {
if (!UIUtils.MouseUpForMouseDown(e)) return;
if (e.ChangedButton == MouseButton.Right) {
_isAttached = true;
MainMenu.Retach();
}
}
private const double GetStartedExtraHeight = 120;
private void GetStartedScreen_AddJwt(object sender, EventArgs e) {
AddIdentity_Click(sender, new RoutedEventArgs());
}
private void GetStartedScreen_AddUrl(object sender, EventArgs e) {
ShowJoinByUrl();
}
private void GetStartedScreen_ClosedByUser(object sender, EventArgs e) {
var anim = new DoubleAnimation(defaultHeight, TimeSpan.FromSeconds(0.22)) {
FillBehavior = FillBehavior.Stop,
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
anim.Completed += (s, ev) => {
// Release the animation clock so future imperative Height writes take effect,
// and lock in the final value to avoid a one-frame snap.
this.BeginAnimation(HeightProperty, null);
this.Height = defaultHeight;
};
this.BeginAnimation(HeightProperty, anim);
}
/// <summary>
/// Detach + start a window drag, invoked from inside another control's MouseDown handler
/// (e.g. the welcome screen logo). Equivalent to a left-click on the main Z image.
/// </summary>
internal void BeginDetachDrag() {
try {
_isAttached = false;
MainMenu.Detach();
this.DragMove();
} catch {
logger.Trace("error when BeginDetachDrag was called?");
}
}
/// <summary>
/// Reattach the window, invoked from inside another control's MouseUp handler
/// (e.g. the welcome screen logo). Equivalent to a right-click on the main Z image.
/// </summary>
internal void Reattach() {
_isAttached = true;
MainMenu.Retach();
}
private void HandleDetached(MouseButtonEventArgs e) {
try {
if (e.ChangedButton == MouseButton.Left) {
_isAttached = false;
MainMenu.Detach();
this.DragMove();
}
}catch {
// just ignore any errors
logger.Trace("error when HandleDetached was called?");
}
}
private void MainWindow_Activated(object sender, EventArgs e) {
Placement();
this.Show();
this.Visibility = Visibility.Visible;
this.Opacity = 1;
_notificationThrottle.Suppress = true;
}
private void MainWindow_Deactivated(object sender, EventArgs e) {
_notificationThrottle.Suppress = false;
if (this._isAttached) {
this.Visibility = Visibility.Hidden;
}
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
if (notifyIcon != null) {
notifyIcon.Visible = false;
notifyIcon.Icon.Dispose();
notifyIcon.Dispose();
notifyIcon = null;
}
Application.Current.Shutdown();
}
private void SetCantDisplay(string title, string detailMessage, Visibility closeButtonVisibility) {
this.Dispatcher.Invoke(() => {
NoServiceView.Visibility = Visibility.Visible;
CloseErrorButton.IsEnabled = true;
CloseErrorButton.Visibility = closeButtonVisibility;
ErrorMsg.Content = title;
ErrorMsgDetail.Content = detailMessage;
SetNotifyIcon("red");
_isServiceInError = true;
UpdateServiceView();
});
}
private void UpdateServiceView() {
if (_isServiceInError) {
AddIdAreaButton.Opacity = 0.1;
AddIdAreaButton.IsEnabled = false;
AddIdButton.Opacity = 0.1;
AddIdButton.IsEnabled = false;
ConnectButton.Opacity = 0.1;
StatArea.Opacity = 0.1;
} else {
AddIdAreaButton.Opacity = 1.0;
AddIdAreaButton.IsEnabled = true;
AddIdButton.Opacity = 1.0;
AddIdButton.IsEnabled = true;
StatArea.Opacity = 1.0;
ConnectButton.Opacity = 1.0;
}
TunnelConnected(!_isServiceInError);
}
private void App_ReceiveString(string obj) {
Console.WriteLine(obj);
BringWindowForward();
}
async private void MainWindow_Loaded(object sender, RoutedEventArgs e) {
Window window = Window.GetWindow(App.Current.MainWindow);
ZitiDesktopEdge.App app = (ZitiDesktopEdge.App)App.Current;
app.ReceiveString += App_ReceiveString;
// add a new service client
serviceClient = new DataClient("UI-DataClient");
serviceClient.OnClientConnected += ServiceClient_OnClientConnected;
serviceClient.OnClientDisconnected += ServiceClient_OnClientDisconnected;
serviceClient.OnIdentityEvent += ServiceClient_OnIdentityEvent;
serviceClient.OnMetricsEvent += ServiceClient_OnMetricsEvent;
serviceClient.OnServiceEvent += ServiceClient_OnServiceEvent;
serviceClient.OnTunnelStatusEvent += ServiceClient_OnTunnelStatusEvent;
serviceClient.OnMfaEvent += ServiceClient_OnMfaEvent;
serviceClient.OnLogLevelEvent += ServiceClient_OnLogLevelEvent;
serviceClient.OnBulkServiceEvent += ServiceClient_OnBulkServiceEvent;
serviceClient.OnNotificationEvent += ServiceClient_OnNotificationEvent;
serviceClient.OnControllerEvent += ServiceClient_OnControllerEvent;
serviceClient.OnAuthenticationEvent += ServiceClient_OnAuthenticationEvent;
serviceClient.OnCommunicationError += ServiceClient_OnCommunicationError;
Application.Current.Properties.Add("ServiceClient", serviceClient);
monitorClient = new MonitorClient("UI-MonitorClient");
monitorClient.OnClientConnected += MonitorClient_OnClientConnected;
monitorClient.OnNotificationEvent += MonitorClient_OnInstallationNotificationEvent;
monitorClient.OnServiceStatusEvent += MonitorClient_OnServiceStatusEvent;
monitorClient.OnCaptureFeedbackProgressEvent += MonitorClient_OnCaptureFeedbackProgressEvent;
monitorClient.OnShutdownEvent += MonitorClient_OnShutdownEvent;
monitorClient.OnCommunicationError += MonitorClient_OnCommunicationError;
monitorClient.OnReconnectFailure += MonitorClient_OnReconnectFailure;
Application.Current.Properties.Add("MonitorClient", monitorClient);
Application.Current.Properties.Add("Identities", new List<ZitiIdentity>());
MainMenu.OnAttachmentChange += AttachmentChanged;
MainMenu.OnLogLevelChanged += LogLevelChanged;
MainMenu.OnShowBlurb += MainMenu_OnShowBlurb;
IdentityMenu.OnError += IdentityMenu_OnError;
await ChooseAndConnectOnStartupAsync();
try {
await monitorClient.ConnectAsync();
await monitorClient.WaitForConnectionAsync();
} catch /*ignored for now (Exception ex) */
{
monitorClient.Reconnect();
}
IdentityMenu.OnForgot += IdentityForgotten;
Placement();
}
private async void ServiceClient_OnAuthenticationEvent(object sender, AuthenticationEvent e) {
ZitiIdentity found = identities.Find(i => i.Identifier == e.Identifier);
if (found != null) {
if (e.Action == "error") {
found.AuthInProgress = false;
await Dispatcher.BeginInvoke(new Action(async () => {
if (_notificationThrottle.Suppress) {
await ShowBlurbAsync("Authentication Failed", "External Auth Failed");
} else {
string displayName = string.IsNullOrEmpty(found.Name) ? found.Identifier : found.Name;
ShowToast("Authentication Failed", $"{displayName} failed to authenticate externally.", null);
}
}));
}
}
}
private void ServiceClient_OnCommunicationError(object sender, Exception e) {
serviceClient.Reconnect();
string msg = "Operation Timed Out";
ShowError(msg, e.Message);
}
private void MonitorClient_OnCommunicationError(object sender, Exception e) {
string msg = "Communication Error with monitor?";
ShowError(msg, e.Message);
}
private void MainMenu_OnShowBlurb(string message) {
_ = ShowBlurbAsync(message, "", "info");
}
private void ServiceClient_OnBulkServiceEvent(object sender, BulkServiceEvent e) {
var found = identities.Find(id => id.Identifier == e.Identifier);
if (found == null) {
logger.Warn($"{e.Action} service event for {e.Identifier} but the provided identity identifier was not found!");
return;
} else {
if (e.RemovedServices != null) {
foreach (var removed in e.RemovedServices) {
removeService(found, removed);
}
}
if (e.AddedServices != null) {
foreach (var added in e.AddedServices) {
addService(found, added);
}
}
LoadIdentities(true);
this.Dispatcher.Invoke(() => {
IdentityDetails deets = ((MainWindow)Application.Current.MainWindow).IdentityMenu;
if (deets.IsVisible) {
deets.UpdateView();
}
});
}
}
private void ServiceClient_OnNotificationEvent(object sender, NotificationEvent e) {
var displayMFARequired = false;
var displayMFATimeout = false;
foreach (var notification in e.Notification) {
var found = identities.Find(id => id.Identifier == notification.Identifier);
if (found == null) {
logger.Warn($"{e.Op} event for {notification.Identifier} but the provided identity identifier was not found!");
continue;
} else {
found.TimeoutMessage = notification.Message;
found.MaxTimeout = notification.MfaMaximumTimeout;
found.MinTimeout = notification.MfaMinimumTimeout;
if (notification.MfaMinimumTimeout == 0) {
// display mfa token icon
displayMFARequired = true;
} else {
displayMFATimeout = true;
}
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == found.Identifier) {
identities[i] = found;
break;
}
}
}
}
// we may need to display mfa icon, based on the timer in UI, remove found.MFAInfo.ShowMFA setting in this function.
// the below function can show mfa icon even after user authenticates successfully, in race conditions
if (displayMFARequired || displayMFATimeout) {
this.Dispatcher.Invoke(() => {
IdentityDetails deets = ((MainWindow)Application.Current.MainWindow).IdentityMenu;
if (deets.IsVisible) {
deets.UpdateView();
}
});
}
LoadIdentities(true);
}
private void ServiceClient_OnControllerEvent(object sender, ControllerEvent e) {
logger.Debug($"==== ControllerEvent : action:{e.Action} identifier:{e.Identifier}");
// commenting this block, because when it receives the disconnected events, identities are disabled and
// it is not allowing me to click/perform any operation on the identity
// the color of the title is also too dark, and it is not clearly visible, when the identity is disconnected
/* if (e.Action == "connected") {
var found = identities.Find(i => i.Identifier == e.Identifier);
found.IsConnected = true;
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == found.Identifier) {
identities[i] = found;
break;
}
}
LoadIdentities(true);
} else if (e.Action == "disconnected") {
var found = identities.Find(i => i.Identifier == e.Identifier);
found.IsConnected = false;
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == found.Identifier) {
identities[i] = found;
break;
}
}
LoadIdentities(true);
} */
}
string nextVersionStr = null;
private void MonitorClient_OnReconnectFailure(object sender, object e) {
logger.Trace("MonitorClient_OnReconnectFailure triggered");
var policyViewModel = Application.Current.Properties["ManagedSettingsViewModel"] as ZitiDesktopEdge.ViewModels.ManagedSettingsViewModel;
if (policyViewModel != null) policyViewModel.IsMonitorConnected = false;
if (nextVersionStr == null) {
// check for the current version
nextVersionStr = "checking for update";
Version nextVersion = GithubAPI.GetVersion(GithubAPI.GetJson(GithubAPI.ProdUrl));
nextVersionStr = nextVersion.ToString();
Version currentVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; //fetch from ziti?
int compare = currentVersion.CompareTo(nextVersion);
if (compare < 0) {
MainMenu.SetAppUpgradeAvailableText("Upgrade available: " + nextVersionStr);
logger.Info("upgrade is available. Published version: {} is newer than the current version: {}", nextVersion, currentVersion);
//UpgradeAvailable();
} else if (compare > 0) {
logger.Info("the version installed: {0} is newer than the released version: {1}", currentVersion, nextVersion);
MainMenu.SetAppIsNewer("This version is newer than the latest: " + nextVersionStr);
} else {
logger.Info("Current version installed: {0} is the same as the latest released version {1}", currentVersion, nextVersion);
MainMenu.SetAppUpgradeAvailableText("");
}
}
}
private void MonitorClient_OnShutdownEvent(object sender, StatusEvent e) {
logger.Info("The monitor has indicated the application should shut down.");
this.Dispatcher.Invoke(() => {
Application.Current.Shutdown();
});
}
private void MonitorClient_OnServiceStatusEvent(object sender, MonitorServiceStatusEvent evt) {
this.Dispatcher.Invoke(() => {
try {
if (evt.Message?.StartsWith("UpdateProgress:") == true) {
string phase = evt.Message.Substring("UpdateProgress:".Length);
logger.Info("Upgrade progress phase received: {0}", phase);
ShowLoad("Update In Progress", phase);
return;
}
if (evt.Message?.StartsWith("UpdateFailed:") == true) {
string reason = evt.Message.Substring("UpdateFailed:".Length);
logger.Warn("Update failed: {0}", reason);
UpgradeSentinel.StopUpgradeSentinel();
HideLoad();
this.Show();
this.Activate();
ShowError("Update Failed", reason);
return;
}
if (evt.Message?.ToLower() == "upgrading") {
logger.Info("The monitor has indicated an upgrade is in progress. Shutting down the UI");
UpgradeSentinel.StartUpgradeSentinel(true);
App.Current.Exit -= Current_Exit;
logger.Info("Removed Current_Exit handler");
notifyIcon.Visible = false;
notifyIcon.Icon.Dispose();
notifyIcon.Dispose();
Application.Current.Shutdown();
return;
}
SetAutomaticUpdateEnabled(evt.AutomaticUpgradeDisabled, evt.AutomaticUpgradeURL);
var policyViewModel = Application.Current.Properties["ManagedSettingsViewModel"] as ZitiDesktopEdge.ViewModels.ManagedSettingsViewModel;
if (policyViewModel != null) {
logger.Info("MonitorClient_OnServiceStatusEvent policy lock state: " +
"AutomaticUpdatesDisabled={0} (locked={1}), " +
"AutomaticUpdateURL={2} (locked={3})",
evt.AutomaticUpgradeDisabled, evt.AutomaticUpgradeDisabledLocked,
evt.AutomaticUpgradeURL, evt.AutomaticUpgradeURLLocked);
policyViewModel.ApplyFromEvent(evt);
} else {
logger.Warn("MonitorClient_OnServiceStatusEvent: ManagedSettingsViewModel not found in Application.Current.Properties");
}
if (evt.Code != 0) {
logger.Error("CODE: " + evt.Code);
if (MainMenu.ShowUnexpectedFailure) {
ShowToast("The data channel has stopped unexpectedly", $"If this keeps happening please collect logs and report the issue.", feedbackToastButton);
}
}
state.DeferredInstallPending = evt.DeferredInstallPending;
state.DeferToRestartPending = evt.DeferToRestartPending;
state.StagingDownloadPending = evt.StagingDownloadPending;
state.DeferInstallToRestartLocked = evt.DeferInstallToRestartLocked;
MainMenu.ShowUpdateAvailable();
tray.RefreshUpdateState();
logger.Debug("MonitorClient_OnServiceStatusEvent: {0}", evt.Status);
Application.Current.Properties["ReleaseStream"] = evt.ReleaseStream;
ServiceControllerStatus status = (ServiceControllerStatus)Enum.Parse(typeof(ServiceControllerStatus), evt.Status);
switch (status) {
case ServiceControllerStatus.Running:
logger.Info("Service is started");
break;
case ServiceControllerStatus.Stopped:
logger.Info("Service is stopped");
ShowServiceNotStarted();
break;
case ServiceControllerStatus.StopPending:
logger.Info("Service is stopping...");
this.Dispatcher.Invoke(async () => {
ShowLoad("Service is Stopping", "Please wait while the service stops");
try {
await WaitForServiceToStop(DateTime.Now + TimeSpan.FromSeconds(30));
} finally {
// Wipe identity UI before removing the modal so the
// user doesn't briefly see stale identities after the
// modal disappears (ClientDisconnected would clear
// them a moment later, but that's visibly laggy).
// If WaitForServiceToStop hit its timeout it has
// already replaced the modal with the "Service
// Appears Stuck" error (which owns the Force Quit
// button); HideLoad in that case is a harmless no-op
// on the already-hidden modal.
ResetTunnelerScopedState();
HideLoad();
}
});
break;
case ServiceControllerStatus.StartPending:
logger.Info("Service is starting...");
break;
case ServiceControllerStatus.PausePending:
logger.Warn("UNEXPECTED STATUS: PausePending");
break;
case ServiceControllerStatus.Paused:
logger.Warn("UNEXPECTED STATUS: Paused");
break;
default:
logger.Warn("UNEXPECTED STATUS: {0}", evt.Status);
break;
}
} catch (Exception ex) {
logger.Warn(ex, "unexpected exception in MonitorClient_OnServiceStatusEvent? {0}", ex.Message);
}
});
}
private void SetAutomaticUpdateEnabled(string enabled, string url) {
if (enabled == null) {
return;
}
try {
bool yn = bool.Parse(enabled);
state.AutomaticUpdatesDisabled = yn;
state.AutomaticUpdateURL = url;
} catch(Exception e) {
logger.Error("SetAutomaticUpdateEnabled: {0}", e);
}
}
private void MonitorClient_OnCaptureFeedbackProgressEvent(object sender, MonitorServiceStatusEvent evt) {
if (evt == null || string.IsNullOrEmpty(evt.Message)) return;
this.Dispatcher.Invoke(() => {
if (LoadingScreen.Visibility == Visibility.Visible) {
string current = MainMenu?.LogLevel ?? "";
string level = string.IsNullOrEmpty(current) ? "UNKNOWN" : current.ToUpper();
bool isVerbose = current == "trace" || current == "verbose" || current == "debug";
string warning = isVerbose ? " (may take several minutes)" : "";
LoadingDetails.Text = $"Log level: {level}{warning}\n{evt.Message}";
}
});
}
private void MonitorClient_OnInstallationNotificationEvent(object sender, InstallationNotificationEvent evt) {
this.Dispatcher.Invoke(() => {
logger.Debug("MonitorClient_OnInstallationNotificationEvent: {0}", evt.Message);
switch (evt.Message?.ToLower()) {
case "installationupdate":
logger.Debug("Installation Update is available - {0}", evt.ZDEVersion);
var remaining = evt.InstallTime - DateTime.Now;
state.PendingUpdate.Version = evt.ZDEVersion;
state.PendingUpdate.InstallTime = evt.InstallTime;
state.UpdateAvailable = true;
state.DeferredInstallPending = evt.DeferredInstallPending;
state.DeferToRestartPending = evt.DeferToRestartPending;
state.StagingDownloadPending = evt.StagingDownloadPending;
state.DeferInstallToRestartLocked = evt.DeferInstallToRestartLocked;
SetAutomaticUpdateEnabled(evt.AutomaticUpgradeDisabled, evt.AutomaticUpgradeURL);
MainMenu.ShowUpdateAvailable();
tray.RefreshUpdateState();
AlertCanvas.Visibility = Visibility.Visible;
if (isToastEnabled()) {
if (!state.AutomaticUpdatesDisabled) {
if (remaining.TotalSeconds < 60) {
//this is an immediate update - show a different message
ShowToast("Ziti Desktop Edge will initiate auto installation in the next minute!");
} else {
if (DateTime.Now > NextNotificationTime) {
ShowToast($"Update {evt.ZDEVersion} is available for Ziti Desktop Edge and will be automatically installed by " + evt.InstallTime);
NextNotificationTime = DateTime.Now + evt.NotificationDuration;
} else {
logger.Debug("Skipping notification. Time until next notification {} seconds which is at {}", (int)((NextNotificationTime - DateTime.Now).TotalSeconds), NextNotificationTime);
}
}
} else {
ShowToast("New version available", $"Version {evt.ZDEVersion} is available for Ziti Desktop Edge", null);
}
SetNotifyIcon("");
// display a tag in UI and a button for the update software
}
break;
case "configuration changed":
break;
default:
logger.Debug("unexpected event type?");
break;
}
});
}
private bool isToastEnabled() {
bool result;
//only show notifications once if automatic updates are disabled
if (NotificationsShownCount == 0) {
result = true; //regardless - if never notified, always return true
} else {
result = !state.AutomaticUpdatesDisabled;
}
return result;
}
private void ShowToast(string header, string message, ToastButton button) {
try {
logger.Debug("showing toast: {} {}", header, message);
var builder = new ToastContentBuilder()
.AddArgument("notbutton", "click")
.AddText(header)
.AddText(message);
if (button != null) {
builder.AddButton(button);
}
builder.Show();
NotificationsShownCount++;
} catch {
logger.Warn("couldn't show toast: {} {}", header, message);
}
}
private void ShowToast(string message) {
ShowToast("Important Notice", message, null);
}
private void QueueExtAuthNotification(ZitiIdentity identity) {
string displayName = string.IsNullOrEmpty(identity.Name) ? identity.Identifier : identity.Name;
var button = new ToastButton()
.SetContent("Authenticate")
.AddArgument("action", "ext-auth")
.AddArgument("identifier", identity.Identifier);
_notificationThrottle.Queue(identity.Identifier, $"{displayName} requires external authentication to access services.", button);
}
private void QueueMfaNotification(ZitiIdentity identity) {
string displayName = string.IsNullOrEmpty(identity.Name) ? identity.Identifier : identity.Name;
ToastButton button;
string message;
if (identity.IsMFAEnabled) {
button = new ToastButton()
.SetContent("Authenticate")
.AddArgument("action", "mfa-auth")
.AddArgument("identifier", identity.Identifier);
message = $"{displayName} requires MFA to access services.";
} else {
// MFA is required but isn't set up yet: prompt setup, not authentication
button = new ToastButton()
.SetContent("Set Up MFA")
.AddArgument("action", "mfa-setup")
.AddArgument("identifier", identity.Identifier);
message = $"{displayName} requires MFA setup to access services.";
}
_notificationThrottle.Queue(identity.Identifier, message, button);
}
private async Task StartExtAuth(ZitiIdentity identity) {
try {
if (!identity.NeedsExtAuth) return;
string defaultProvider = identity.GetDefaultProvider();
if (defaultProvider != null) {
DataClient client = (DataClient)Application.Current.Properties["ServiceClient"];
await identity.PerformExternalAuthEvent(client, identity.GetDefaultProviderId());
} else if (identity.ExtAuthProviders?.Count == 1) {
DataClient client = (DataClient)Application.Current.Properties["ServiceClient"];
await identity.PerformExternalAuthEvent(client, identity.ExtAuthProviders[0]);
} else {
OpenIdentity(identity);
}
} catch (Exception ex) {
logger.Error("ext auth from toast failed: {}", ex.Message);
ShowError("Authentication Failed", ex.Message);
identity.AuthInProgress = false;
}
}
async private Task WaitForServiceToStop(DateTime until) {
//continually poll for the service to stop. If it is stuck - ask the user if they want to try to force
//close the service
while (DateTime.Now < until) {
await Task.Delay(250);
MonitorServiceStatusEvent resp = await monitorClient.StatusAsync();
if (resp.IsStopped()) {
// good - that's what we are waiting for...
return;
} else {
// bad - not stopped yet...
logger.Debug("Waiting for service to stop... Still not stopped yet. Status: {0}", resp.Status);
}
}
// real bad - means it's stuck probably. Ask the user if they want to try to force it...
logger.Warn("Waiting for service to stop... Service did not reach stopped state in the expected amount of time.");
SetCantDisplay("The Service Appears Stuck", "Would you like to try to force close the service?", Visibility.Visible);
CloseErrorButton.Content = "Force Quit";
CloseErrorButton.Click -= CloseError;
CloseErrorButton.Click += ForceQuitButtonClick;
}
async private void ForceQuitButtonClick(object sender, RoutedEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
// Targets the default Windows-service-managed tunneler regardless
// of which instance is currently being viewed.
MonitorServiceStatusEvent status = await monitorClient.ForceTerminateAsync();
if (status.IsStopped()) {
//good
CloseErrorButton.Click += CloseError; //reset the close button...
CloseErrorButton.Click -= ForceQuitButtonClick;
} else {
//bad...
SetCantDisplay("The Service Is Still Running", "Current status is: " + status.Status, Visibility.Visible);
}
}
async private void StartZitiService(object sender, RoutedEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
// Always targets the default Windows service.
try {
ShowLoad("Starting", "Starting the data service");
logger.Info("StartZitiService");
var r = await monitorClient.StartServiceAsync(TimeSpan.FromSeconds(60));
if (r.Code != 0) {
logger.Debug("ERROR: {0} : {1}", r.Message, r.Error);
} else {
logger.Info("Service started!");
CloseErrorButton.Click -= StartZitiService;
CloseError(null, null);
}
} catch (MonitorServiceException me) {
logger.Warn("the monitor service appears offline. {0}", me);
CloseErrorButton.IsEnabled = true;
HideLoad();
ShowError("Error Starting Service", "The monitor service is offline");
} catch (Exception ex) {
logger.Error(ex, "UNEXPECTED ERROR!");
CloseErrorButton.IsEnabled = true;
HideLoad();
ShowError("Unexpected Error", "Code 2:" + ex.Message);
}
CloseErrorButton.IsEnabled = true;
// HideLoad();
}
private void ShowServiceNotStarted() {
TunnelConnected(false);
LoadIdentities(true);
}
private void MonitorClient_OnClientConnected(object sender, object e) {
logger.Debug("MonitorClient_OnClientConnected");
MainMenu.SetAppUpgradeAvailableText("");
var policyViewModel = Application.Current.Properties["ManagedSettingsViewModel"] as ZitiDesktopEdge.ViewModels.ManagedSettingsViewModel;
if (policyViewModel != null) policyViewModel.IsMonitorConnected = true;
}
async private Task<bool> LogLevelChanged(string level) {
int logsSet = 0;
try {
await serviceClient.SetLogLevelAsync(level);
logsSet++;
await monitorClient.SetLogLevelAsync(level);
logsSet++;
Ziti.Desktop.Edge.Utils.UIUtils.SetLogLevel(level);
return true;
} catch (Exception ex) {
logger.Error(ex, "Unexpected error. logsSet: {0}", logsSet);
if (logsSet > 1) {
await ShowBlurbAsync("Unexpected error setting logs?", "");
} else if (logsSet > 0) {
await ShowBlurbAsync("Failed to set monitor client log level", "");
} else {
await ShowBlurbAsync("Failed to set log levels", "");
}
}
return false;
}
private void IdentityMenu_OnError(string message) {
ShowError("Identity Error", message);
}
private void ServiceClient_OnClientConnected(object sender, object e) {
this.Dispatcher.Invoke(() => {
MainMenu.Connected();
NoServiceView.Visibility = Visibility.Collapsed;
_isServiceInError = false;
UpdateServiceView();
SetNotifyIcon("white");
GetStartedScreen.ViewModel.Hide(); // hide get started until tunnel status returns
LoadIdentities(true);
UpdateAlternateTunnelFooter();
});
}
private void ServiceClient_OnClientDisconnected(object sender, object e) {
this.Dispatcher.Invoke(() => {
ResetTunnelerScopedState();
GetStartedScreen.ViewModel.Hide(); // hide the get started when disconnected
if (e != null) {
logger.Debug(e.ToString());
}
SetNotifyIcon("red");
ShowServiceNotStarted();
// The underlying DataClient has already kicked off its own
// reconnect loop against whatever pipe we were viewing. If the
// user wants to view a different instance they can hit
// Ctrl+Shift+T.
});
}
/// <summary>
/// Wipes all UI and in-memory state scoped to the currently-connected
/// ziti-edge-tunnel instance. Safe to call when nothing is connected.
/// Called by the disconnect handler and (later) by the instance switcher
/// before reconnecting to a different tunneler.
///
/// Must be invoked on the UI thread.
/// </summary>
private void ResetTunnelerScopedState() {
AddIdAreaButton.IsEnabled = false;
IdentityMenu.Visibility = Visibility.Collapsed;
MFASetup.Visibility = Visibility.Collapsed;
HideModal();
MainMenu.Disconnected();
for (int i = 0; i < IdList.Children.Count; i++) {
IdentityItem item = (IdentityItem)IdList.Children[i];
item.StopTimers();
}
IdList.Children.Clear();
identities.Clear();
StopTunnelUptimeTimer();
_startDate = default(DateTime);
ConnectedTime.Content = "00:00:00";
DownloadSpeed.Content = "0.0";
UploadSpeed.Content = "0.0";
_notificationThrottle?.Clear();
NextNotificationTime = DateTime.Now;
var props = Application.Current.Properties;
props.Remove("CurrentTunnelStatus");
props.Remove("ip");
props.Remove("subnet");
props.Remove("mtu");
props.Remove("dns");
props.Remove("dnsenabled");
props.Remove("ApiPageSize");
props.Remove("L2Enabled");
props.Remove("PcapInterface");
// Hide the alternate-tunnel footer while we're not connected; it
// will come back when ClientConnected fires (discriminator
// persists across a reconnect on the same pipe).
if (AlternateTunnelFooter != null) {
AlternateTunnelFooter.Visibility = Visibility.Collapsed;
}
}
private void StopTunnelUptimeTimer() {
if (_tunnelUptimeTimer == null) return;
try {
_tunnelUptimeTimer.Stop();
_tunnelUptimeTimer.Tick -= OnTimedEvent;
_tunnelUptimeTimer.Dispose();
} catch (Exception ex) {
logger.Debug(ex, "error while stopping tunnel uptime timer");
}
_tunnelUptimeTimer = null;
}
/// <summary>
/// If an identity gets added late, execute this.
///
/// Do not update services for identity events
/// </summary>
/// <param name="sender">The sending service</param>
/// <param name="e">The identity event</param>
private void ServiceClient_OnIdentityEvent(object sender, IdentityEvent e) {
if (e == null) return;
ZitiIdentity zid = ZitiIdentity.FromClient(e.Id);
logger.Debug($"==== IdentityEvent : action:{e.Action} identifer:{e.Id.Identifier} name:{e.Id.Name} ");
this.Dispatcher.Invoke(async () => {
if (e.Action == "added" || e.Action == "needs_ext_login") {
var found = identities.Find(i => i.Identifier == e.Id.Identifier);
if (found == null) {
zid.IsMFAEnabled = false; // this is an added identity, it cannot have mfa enabled yet
AddIdentity(zid);
LoadIdentities(true);
if (zid.NeedsExtAuth) {
QueueExtAuthNotification(zid);
}
BringWindowForward();
} else {
var isAdd = e.Action == "added";
var isExtLogin = e.Action == "needs_ext_login";
// identity already exists: handle ext auth state transitions.
// ziti-edge-tunnel sends needs_ext_login immediately when ExternalAuthLogin is called.
// that clears AuthInProgress so the subsequent added event with NeedsExtAuth=false
// is treated as a successful auth in the else branch.
if (found.AuthInProgress) {
// ext auth started: dequeue the authenticate toast and reset state
if (isExtLogin) {
_notificationThrottle.Remove(found.Identifier);
found.AuthInProgress = false;
found.NeedsExtAuth = e.Id.NeedsExtAuth;
}
} else {
// ext auth completed: identity no longer needs ext auth
if (isAdd && found.NeedsExtAuth && !e.Id.NeedsExtAuth) {
logger.Info("Identity: {} successfully authenticated", found.Identifier);
_notificationThrottle.Remove(found.Identifier);
if (!_notificationThrottle.Suppress) {
string displayName = string.IsNullOrEmpty(found.Name) ? found.Identifier : found.Name;
ShowToast("Authentication Successful", $"{displayName} has been authenticated.", null);
}
// identity still needs ext auth and no auth is currently in progress: queue a toast
} else if (isExtLogin && e.Id.NeedsExtAuth) {
QueueExtAuthNotification(found);
}
found.NeedsExtAuth = e.Id.NeedsExtAuth;
}
if (zid.Name != null && zid.Name.Length > 0) found.Name = zid.Name;
if (zid.ControllerUrl != null && zid.ControllerUrl.Length > 0) found.ControllerUrl = zid.ControllerUrl;
if (zid.ContollerVersion != null && zid.ContollerVersion.Length > 0) found.ContollerVersion = zid.ContollerVersion;
found.IsEnabled = zid.IsEnabled;
found.IsMFAEnabled = e.Id.MfaEnabled;
found.IsConnected = true;
found.NeedsExtAuth = e.Id.NeedsExtAuth;
found.ExtAuthProviders = e.Id.ExtAuthProviders;
if (!found.NeedsExtAuth) {
_notificationThrottle.Remove(found.Identifier);
}
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == found.Identifier) {
identities[i] = found;
break;
}
}
LoadIdentities(true);
}
} else if (e.Action == "updated") {
//this indicates that all updates have been sent to the UI... wait for 2 seconds then trigger any ui updates needed
await Task.Delay(2000);
LoadIdentities(true);
} else if (e.Action == "connected") {
var found = identities.Find(i => i.Identifier == e.Id.Identifier);
found.IsConnected = true;
found.IsMFANeeded = e.Id.MfaNeeded;
found.IsMFAEnabled = e.Id.MfaEnabled;
found.NeedsExtAuth = e.Id.NeedsExtAuth;
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == found.Identifier) {
identities[i] = found;
break;
}
}
if (found.IsMFANeeded) {
QueueMfaNotification(found);
}
if (found.NeedsExtAuth) {
QueueExtAuthNotification(found);
}
LoadIdentities(true);
} else if (e.Action == "disconnected") {
var found = identities.Find(i => i.Identifier == e.Id.Identifier);
found.IsConnected = false;
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == found.Identifier) {
identities[i] = found;
break;
}
}
LoadIdentities(true);
} else {
logger.Warn("unexpected action received: {}", e.Action);
IdentityForgotten(ZitiIdentity.FromClient(e.Id));
}
});
logger.Debug($"IDENTITY EVENT. Action: {e.Action} identifier: {zid.Identifier}");
}
private void ServiceClient_OnMetricsEvent(object sender, List<Identity> ids) {
if (ids != null) {
long totalUp = 0;
long totalDown = 0;
foreach (var id in ids) {
//logger.Debug($"==== MetricsEvent : id {id.Name} down: {id.Metrics.Down} up:{id.Metrics.Up}");
if (id?.Metrics != null) {
totalDown += id.Metrics.Down;
totalUp += id.Metrics.Up;
}
}
this.Dispatcher.Invoke(() => {
SetSpeed(totalUp, UploadSpeed, UploadSpeedLabel);
SetSpeed(totalDown, DownloadSpeed, DownloadSpeedLabel);
});
}
}
public void SetSpeed(decimal bytes, Label speed, Label speedLabel) {
int counter = 0;
while (Math.Round(bytes / 1024) >= 1) {
bytes = bytes / 1024;
counter++;
}
speed.Content = bytes.ToString("0.0");
speedLabel.Content = suffixes[counter];
}
private void ServiceClient_OnServiceEvent(object sender, ServiceEvent e) {
if (e == null) return;
logger.Debug($"==== ServiceEvent : action:{e.Action} identifier:{e.Identifier} name:{e.Service.Name} ");
var found = identities.Find(id => id.Identifier == e.Identifier);
if (found == null) {
logger.Debug($"{e.Action} service event for {e.Service.Name} but the provided identity identifier {e.Identifier} is not found!");
return;
}
if (e.Action == "added") {
addService(found, e.Service);
} else {
removeService(found, e.Service);
}
LoadIdentities(true);
this.Dispatcher.Invoke(() => {
IdentityDetails deets = ((MainWindow)Application.Current.MainWindow).IdentityMenu;
if (deets.IsVisible) {
deets.UpdateView();
}
});
}
private void addService(ZitiIdentity found, Service added) {
ZitiService zs = new ZitiService(added);
var svc = found.Services.Find(s => s.Name == zs.Name);
if (svc == null) {
logger.Debug("Service Added: " + zs.Name);
found.Services.Add(zs);
if (zs.HasFailingPostureCheck()) {
found.HasServiceFailingPostureCheck = true;
foreach (var service in found.Services) {
if (service != null && service.PostureChecks != null && service.PostureChecks.Any(p => !p.IsPassing && p.QueryType == "MFA")) {
found.IsMFANeeded = true;
QueueMfaNotification(found);
}
}
}
} else {
logger.Debug("the service named " + zs.Name + " is already accounted for on this identity.");
}
}
private void removeService(ZitiIdentity found, Service removed) {
logger.Debug("removing the service named: {0}", removed.Name);
found.Services.RemoveAll(s => s.Name == removed.Name);
}
private void ServiceClient_OnTunnelStatusEvent(object sender, TunnelStatusEvent e) {
if (e == null) return; //just skip it for now...
logger.Debug($"==== TunnelStatusEvent: ");
Application.Current.Properties.Remove("CurrentTunnelStatus");
Application.Current.Properties.Add("CurrentTunnelStatus", e.Status);
#if DEBUG && DEBUG_DUMP
e.Status.Dump(Console.Out);
#endif
this.Dispatcher.Invoke(() => {
/*if (e.ApiVersion != DataClient.EXPECTED_API_VERSION) {
SetCantDisplay("Version mismatch!", "The version of the Service is not compatible", Visibility.Visible);
return;
}*/
this.MainMenu.LogLevel = e.Status.LogLevel;
Ziti.Desktop.Edge.Utils.UIUtils.SetLogLevel(e.Status.LogLevel);
tray.RefreshLogLevelChecks(e.Status.LogLevel);
InitializeTimer((int)e.Status.Duration);
LoadStatusFromService(e.Status);
LoadIdentities(true);
GetStartedScreen.ViewModel.UpdateForState(serviceClient.Connected, identities.Count);
IdentityDetails deets = ((MainWindow)Application.Current.MainWindow).IdentityMenu;
if (deets.IsVisible) {
deets.UpdateView();
}
});
}
private void ServiceClient_OnLogLevelEvent(object sender, LogLevelEvent e) {
if (e.LogLevel != null) {
SetLogLevel_monitor(e.LogLevel);
this.Dispatcher.Invoke(() => {
this.MainMenu.LogLevel = e.LogLevel;
Ziti.Desktop.Edge.Utils.UIUtils.SetLogLevel(e.LogLevel);
tray.RefreshLogLevelChecks(e.LogLevel);
});
}
}
async private void SetLogLevel_monitor(string loglevel) {
await monitorClient.SetLogLevelAsync(loglevel);
}
private void IdentityForgotten(ZitiIdentity forgotten) {
ZitiIdentity idToRemove = null;
foreach (var id in identities) {
if (id.Identifier == forgotten.Identifier) {
idToRemove = id;
break;
}
}
identities.Remove(idToRemove);
LoadIdentities(false);
}
private void AttachmentChanged(bool attached) {
_isAttached = attached;
if (!_isAttached) {
SetLocation();
}
Placement();
MainMenu.Visibility = Visibility.Collapsed;
}
private void LoadStatusFromService(TunnelStatus status) {
if (status != null) {
_isServiceInError = false;
UpdateServiceView();
NoServiceView.Visibility = Visibility.Collapsed;
SetNotifyIcon("green");
AddIdAreaButton.IsEnabled = true;
if (!Application.Current.Properties.Contains("ip")) {
Application.Current.Properties.Add("ip", status?.IpInfo?.Ip);
} else {
Application.Current.Properties["ip"] = status?.IpInfo?.Ip;
}
if (!Application.Current.Properties.Contains("subnet")) {
Application.Current.Properties.Add("subnet", status?.IpInfo?.Subnet);
} else {
Application.Current.Properties["subnet"] = status?.IpInfo?.Subnet;
}
if (!Application.Current.Properties.Contains("mtu")) {
Application.Current.Properties.Add("mtu", status?.IpInfo?.MTU);
} else {
Application.Current.Properties["mtu"] = status?.IpInfo?.MTU;
}
if (!Application.Current.Properties.Contains("dns")) {
Application.Current.Properties.Add("dns", status?.IpInfo?.DNS);
} else {
Application.Current.Properties["dns"] = status?.IpInfo?.DNS;
}
if (!Application.Current.Properties.Contains("dnsenabled")) {
Application.Current.Properties.Add("dnsenabled", status?.AddDns);
} else {
Application.Current.Properties["dnsenabled"] = status?.AddDns;
}
string key = "ApiPageSize";
if (!Application.Current.Properties.Contains(key)) {
Application.Current.Properties.Add(key, status?.ApiPageSize);
} else {
Application.Current.Properties[key] = status?.ApiPageSize;
}
if (!Application.Current.Properties.Contains("L2Enabled")) {
Application.Current.Properties.Add("L2Enabled", status?.L2Enabled);
} else {
Application.Current.Properties["L2Enabled"] = status?.L2Enabled;
}
if (!Application.Current.Properties.Contains("PcapInterface")) {
Application.Current.Properties.Add("PcapInterface", status?.PcapInterface);
} else {
Application.Current.Properties["PcapInterface"] = status?.PcapInterface;
}
foreach (var id in status.Identities) {
updateViewWithIdentity(id);
}
foreach (var zid in identities) {
if (!zid.IsEnabled) continue;
if (zid.NeedsExtAuth) {
QueueExtAuthNotification(zid);
}
if (zid.IsMFANeeded) {
QueueMfaNotification(zid);
}
}
} else {
ShowServiceNotStarted();
}
}
private void updateViewWithIdentity(Identity id) {
var zid = ZitiIdentity.FromClient(id);
var found = identities.Find(fid => fid.Identifier == id.Identifier);
if(found != null) {
identities.Remove(found);
zid.IsMFAEnabled = found.IsMFAEnabled;
}
identities.Add(zid);
}
private bool IsTimingOut() {
if (identities != null) {
for (int i = 0; i < identities.Count; i++) {
if (identities[i].IsTimingOut) return true;
}
}
return false;
}
private bool IsTimedOut() {
if (identities != null) {
return identities.Any(i => i.IsTimedOut);
}
return false;
}
internal void RefreshNotifyIcon() {
SetNotifyIcon("");
AlertCanvas.Visibility = (state.UpdateAvailable && !state.AutomaticUpdatesDisabled)
? Visibility.Visible : Visibility.Collapsed;
}
private void SetNotifyIcon(string iconPrefix) {
if (iconPrefix != "") CurrentIcon = iconPrefix;
string icon = "pack://application:,,/Assets/Images/ziti-" + CurrentIcon;
if (state.UpdateAvailable && !state.AutomaticUpdatesDisabled) {
icon += "-update";
} else {
if (IsTimedOut()) {
icon += "-mfa";
} else {
if (IsTimingOut()) {
icon += "-timer";
}
}
}
icon += ".ico";
var iconUri = new Uri(icon);
Stream iconStream = Application.GetResourceStream(iconUri).Stream;
notifyIcon.Icon = new Icon(iconStream);
Application.Current.MainWindow.Icon = System.Windows.Media.Imaging.BitmapFrame.Create(iconUri);
}
private void SortByName_Click(object sender, MouseButtonEventArgs e) {
props.SetSort("Name");
LoadIdentities(true);
}
private void SortByServices_Click(object sender, MouseButtonEventArgs e) {
props.SetSort("Services");
LoadIdentities(true);
}
private void SortByStatus_Click(object sender, MouseButtonEventArgs e) {
props.SetSort("Status");
LoadIdentities(true);
}
private ZitiIdentity[] GetSortedIdentities() {
return props.GetSortedIdentities(identities);
}
private void LoadIdentities(Boolean repaint) {
this.Dispatcher.Invoke(() => {
for (int i = 0; i < IdList.Children.Count; i++) {
IdentityItem item = (IdentityItem)IdList.Children[i];
item.StopTimers();
}
int previousIdentityCount = IdList.Children.Count;
IdList.Children.Clear();
var desktopWorkingArea = SystemParameters.WorkArea;
if (_maxHeight > (desktopWorkingArea.Height - 10)) {
_maxHeight = desktopWorkingArea.Height - 10;
}
if (_maxHeight < 100) {
_maxHeight = 100;
}
IdList.MaxHeight = _maxHeight - 480;
ZitiIdentity[] ids = GetSortedIdentities();
MainMenu.SetupIdList(ids);
props.IdentityCount = ids.Length;
MainMenu.ShowWelcomeButton.Visibility = ids.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
HelpCircle.Visibility = ids.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
tray.RebuildIdentities(ids);
if (ids.Length > 0) {
GetStartedScreen.ViewModel.Hide(); // if there's any identities close the get started screen
}
if (ids.Length > 0 && serviceClient.Connected) {
double height = defaultHeight + (ids.Length * 60);
if (height > _maxHeight) {
height = _maxHeight;
}
this.Height = height;
MainMenu.IdentitiesButton.Visibility = Visibility.Visible;
foreach (var id in ids) {
IdentityItem idItem = new IdentityItem();
idItem.ShowError = ShowError;
idItem.ToggleStatus.IsEnabled = id.IsEnabled;
if (id.IsEnabled) idItem.ToggleStatus.Content = "ENABLED";
else idItem.ToggleStatus.Content = "DISABLED";
idItem.AuthenticateTOTP += IdItem_Authenticate;
idItem.EnableMFARequested += IdItem_EnableMFA;
idItem.OnStatusChanged += Id_OnStatusChanged;
idItem.Identity = id;
idItem.IdentityChanged += IdItem_IdentityChanged;
if (repaint) {
idItem.RefreshUI();
}
idItem.CompleteExternalAuth += CompleteExternalAuthEvent;
IdList.Children.Add(idItem);
if (IdentityMenu.Visibility == Visibility.Visible) {
if (id.Identifier == IdentityMenu.Identity.Identifier) IdentityMenu.Identity = id;
}
}
// animate only when the identity count changes, not on every event
if (ids.Length != previousIdentityCount) {
IdList.BeginAnimation(FrameworkElement.HeightProperty, new DoubleAnimation(IdList.ActualHeight, ids.Length * 64, TimeSpan.FromSeconds(.2)));
}
IdListScroller.Visibility = Visibility.Visible;
} else {
// Make room for the welcome screen when it's about to show.
this.Height = defaultHeight + (GetStartedScreen.ViewModel.IsOpen ? GetStartedExtraHeight : 0);
MainMenu.IdentitiesButton.Visibility = Visibility.Collapsed;
IdListScroller.Visibility = Visibility.Collapsed;
}
AddIdButton.Visibility = Visibility.Visible;
AddIdAreaButton.Visibility = Visibility.Visible;
Placement();
SetNotifyIcon("");
});
}
private void IdItem_IdentityChanged(ZitiIdentity identity) {
for (int i = 0; i < identities.Count; i++) {
if (identities[i].Identifier == identity.Identifier) {
identities[i] = identity;
break;
}
}
SetNotifyIcon("");
}
private void IdItem_Authenticate(ZitiIdentity identity) {
ShowAuthenticate(identity);
}
private async void IdItem_EnableMFA(ZitiIdentity identity) {
if (!identity.IsEnabled) {
await ShowBlurbAsync("Identity disabled, MFA cannot continue.", "");
return;
}
ShowLoad("Generating MFA", "MFA Setup Commencing, please wait");
await serviceClient.EnableMFA(identity.Identifier);
HideLoad();
}
private void Id_OnStatusChanged(bool attached) {
for (int i = 0; i < IdList.Children.Count; i++) {
IdentityItem item = IdList.Children[i] as IdentityItem;
if (item.ToggleSwitch.Enabled) break;
}
}
private void TunnelConnected(bool isConnected) {
this.Dispatcher.Invoke(() => {
if (isConnected) {
ConnectButton.Visibility = Visibility.Collapsed;
DisconnectButton.Visibility = Visibility.Visible;
MainMenu.Connected();
HideLoad();
SetNotifyIcon("green");
props.Connected();
} else {
ConnectButton.Visibility = Visibility.Visible;
DisconnectButton.Visibility = Visibility.Collapsed;
IdentityMenu.Visibility = Visibility.Collapsed;
MainMenu.Disconnected();
DownloadSpeed.Content = "0.0";
UploadSpeed.Content = "0.0";
props.Disconnected();
}
});
}
private void SetLocation() {
var desktopWorkingArea = SystemParameters.WorkArea;
double renderedHeight = MainView.ActualHeight;
Rectangle trayRectangle = WinAPI.GetTrayRectangle();
if (trayRectangle.Top < 20) {
this.Position = "Top";
this.Top = desktopWorkingArea.Top + _top;
this.Left = desktopWorkingArea.Right - this.Width - _right;
} else if (trayRectangle.Left < 20) {
this.Position = "Left";
this.Left = _left;
this.Top = desktopWorkingArea.Bottom - this.ActualHeight - 75;
} else if (desktopWorkingArea.Right == (double)trayRectangle.Left) {
this.Position = "Right";
this.Left = desktopWorkingArea.Right - this.Width - 20;
this.Top = desktopWorkingArea.Bottom - renderedHeight - 75;
} else {
this.Position = "Bottom";
this.Left = desktopWorkingArea.Right - this.Width - 75;
this.Top = desktopWorkingArea.Bottom - renderedHeight;
}
}
public void Placement() {
if (_isAttached) {
SetLocation();
}
}
private void OpenIdentity(ZitiIdentity identity) {
IdentityMenu.Identity = identity;
}
private void ShowMenu(object sender, MouseButtonEventArgs e) {
MainMenu.Visibility = Visibility.Visible;
}
async private Task AddId(EnrollIdentifierPayload payload) {
#if DEBUG
Console.WriteLine("AddId.JwtContent\t: " + payload.JwtContent);
Console.WriteLine("AddId.IdentityFilename\t: " + payload.IdentityFilename);
Console.WriteLine("AddId.ControllerURL\t: " + payload.ControllerURL);
Console.WriteLine("AddId.Certificate\t: " + payload.Certificate);
Console.WriteLine("AddId.Key\t\t: " + payload.Key);
Console.WriteLine("AddId.UseKeychain\t: " + payload.UseKeychain);
#endif
try {
Identity createdId = await serviceClient.AddIdentityAsync(payload);
if (createdId != null) {
// External-auth enrollment: zet returns an auth URL instead of a full identity.
// Launch the browser; zet will emit a normal identity event once the user signs in.
if (!string.IsNullOrEmpty(createdId.Url)) {
System.Diagnostics.Process.Start(createdId.Url);
return;
}
var zid = ZitiIdentity.FromClient(createdId);
AddIdentity(zid);
LoadIdentities(true);
await serviceClient.IdentityOnOffAsync(createdId.Identifier, true);
} else {
// this never returns a value...
}
} catch (ServiceException e) {
HideLoad();
await ShowBlurbAsync("Unexpected error when adding identity!", e.Message);
} catch (Exception e) {
ZdewLink linkControl = new ZdewLink {
NavigateUri = new Uri("https://openziti.discourse.group/"),
Text = "Visit our support forum",
};
ShowError("Unexpected Error!",
$"Please review the logs and consider providing a feedback bundle to the support forum.\nError: {e.Message}",
linkControl
);
}
}
private static string PadBase64(string base64) {
int padding = 4 - (base64.Length % 4);
if (padding < 4) {
base64 += new string('=', padding);
}
return base64;
}
private async void AddIdentity_Click(object sender, RoutedEventArgs e) {
UIModel.HideOnLostFocus = false;
OpenFileDialog jwtDialog = new OpenFileDialog();
UIModel.HideOnLostFocus = true;
jwtDialog.DefaultExt = ".jwt";
jwtDialog.Filter = "Ziti Identities (*.jwt)|*.jwt";
if (jwtDialog.ShowDialog() == true) {
ShowLoad("Adding Identity", "Please wait while the identity is added");
string fileContent = File.ReadAllText(jwtDialog.FileName);
EnrollIdentifierPayload payload = new EnrollIdentifierPayload();
payload.UseKeychain = false;
string jwtFile = Path.GetFileName(jwtDialog.FileName);
payload.IdentityFilename = Path.GetFileNameWithoutExtension(jwtFile);
payload.JwtContent = fileContent.Trim();
string[] jwtParts = fileContent?.Split('.');
if (jwtParts != null && jwtParts.Length > 1) {
string jsonString = @"{
""iss"": ""invalid"",
""sub"": ""invalid"",
""jti"": ""invalid"",
""aud"": [""invalid""],
""em"": ""invalid""
}";
try {
jsonString = Encoding.UTF8.GetString(Convert.FromBase64String(PadBase64(jwtParts[1])));
// Deserialize JSON into a dynamic object
} catch {
//any exceptions means this is not json... set the jsonString to something that represents the failure
}
dynamic jsonObj = JsonConvert.DeserializeObject(jsonString);
#if DEBUG
Console.WriteLine(jsonString);
// Access properties dynamically
Console.WriteLine($"ISS: {jsonObj.iss}");
Console.WriteLine($"SUB: {jsonObj.sub}");
Console.WriteLine($"JTI: {jsonObj.jti}");
Console.WriteLine($"AUD: {string.Join(", ", jsonObj.aud)}");
Console.WriteLine($"EM: {jsonObj.em}");
#endif
try {
switch ($"{jsonObj.em}") {
case "ottca":
With3rdPartyCA_Click(sender, e);
break;
case "network":
await AddId(payload);
break;
case "ott":
await AddId(payload);
break;
case "ca":
AddIdentityBy3rdPartyCA.Payload = payload;
ShowJoinWith3rdPartyCA();
break;
default:
logger.Error("JWT is invalid? {}", fileContent);
ShowError("JWT Invalid", "The file selected is not a valid JWT");
break;
}
} catch (ServiceException se) {
ShowError(se.Message, se.AdditionalInfo);
} catch (Exception ex) {
ShowError("Unexpected Error", "Code 2:" + ex.Message);
}
} else {
// invalid jwt
logger.Error("JWT is invalid? {}", fileContent);
}
} else {
logger.Debug("user closed jwt dialog without selecting a file. nbd.");
}
HideLoad();
}
private void OnTimedEvent(object sender, EventArgs e) {
TimeSpan span = (DateTime.Now - _startDate);
int hours = span.Hours;
int minutes = span.Minutes;
int seconds = span.Seconds;
var hoursString = (hours > 9) ? hours.ToString() : "0" + hours;
var minutesString = (minutes > 9) ? minutes.ToString() : "0" + minutes;
var secondsString = (seconds > 9) ? seconds.ToString() : "0" + seconds;
ConnectedTime.Content = hoursString + ":" + minutesString + ":" + secondsString;
}
private void InitializeTimer(int millisAgoStarted) {
StopTunnelUptimeTimer();
_startDate = DateTime.Now.Subtract(new TimeSpan(0, 0, 0, 0, millisAgoStarted));
_tunnelUptimeTimer = new System.Windows.Forms.Timer();
_tunnelUptimeTimer.Interval = 100;
_tunnelUptimeTimer.Tick += OnTimedEvent;
_tunnelUptimeTimer.Enabled = true;
_tunnelUptimeTimer.Start();
}
async private void Disconnect(object sender, RoutedEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
// Always targets the default Windows service.
try {
ShowLoad("Disabling Service", "Please wait for the service to stop.");
var r = await monitorClient.StopServiceAsync();
if (r.Code != 0) {
logger.Warn("ERROR: Error:{0}, Message:{1}", r.Error, r.Message);
} else {
logger.Info("Service stopped!");
SetNotifyIcon("white");
}
} catch (MonitorServiceException me) {
logger.Warn("the monitor service appears offline. {0}", me);
ShowError("Error Disabling Service", "The monitor service is offline");
} catch (Exception ex) {
logger.Error(ex, "unexpected error: {0}", ex.Message);
ShowError("Error Disabling Service", "An error occurred while trying to disable the data service. Is the monitor service running?");
}
HideLoad();
}
internal void ShowLoad(string title, string msg) {
this.Dispatcher.Invoke(() => {
LoadingDetails.Text = msg;
LoadingTitle.Content = title;
LoadProgress.IsIndeterminate = true;
LoadingScreen.Visibility = Visibility.Visible;
UpdateLayout();
});
}
internal void HideLoad() {
this.Dispatcher.Invoke(() => {
LoadingScreen.Visibility = Visibility.Collapsed;
LoadProgress.IsIndeterminate = false;
});
}
private void FormFadeOut_Completed(object sender, EventArgs e) {
closeCompleted = true;
}
private bool closeCompleted = false;
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
if (!closeCompleted) {
FormFadeOut.Begin();
e.Cancel = true;
}
}
public void ShowError(string title, string message) {
ShowError(title, message, null);
}
public void ShowError(string title, string message, UIElement additionalElements) {
this.Dispatcher.Invoke(() => {
ErrorTitle.Text = title;
ErrorDetails.Text = message;
if (additionalElements != null) {
ErrorPanel.Children.Add(additionalElements);
}
ErrorView.Visibility = Visibility.Visible;
});
}
private void CloseError(object sender, RoutedEventArgs e) {
this.Dispatcher.Invoke(() => {
ErrorView.Visibility = Visibility.Collapsed;
NoServiceView.Visibility = Visibility.Collapsed;
CloseErrorButton.IsEnabled = true;
});
}
public void ShowKeychainOverrideDialog(EnrollIdentifierPayload payload, string title, string description) {
this.Dispatcher.Invoke(() => {
OverrideKeychain.Title = title;
OverrideKeychain.Description = description;
OverrideKeychain.OkFunc = new ActionCommand(async () => {
HideModal();
OverrideKeychain.Visibility = Visibility.Collapsed;
payload.UseKeychain = false;
await AddId(payload);
});
OverrideKeychain.CancelFunc = new ActionCommand(() => {
HideModal();
OverrideKeychain.Visibility = Visibility.Collapsed;
});
ShowModal();
OverrideKeychain.Visibility = Visibility.Visible;
});
}
private void CloseApp(object sender, RoutedEventArgs e) {
Application.Current.Shutdown();
}
private void MainUI_Deactivated(object sender, EventArgs e) {
if (this._isAttached) {
#if DEBUG
logger.Debug("debug is enabled - windows pinned");
#else
this.Visibility = Visibility.Collapsed;
#endif
}
}
int cur = 0;
LogLevelEnum[] levels = new LogLevelEnum[] { LogLevelEnum.FATAL, LogLevelEnum.ERROR, LogLevelEnum.WARN, LogLevelEnum.INFO, LogLevelEnum.DEBUG, LogLevelEnum.TRACE, LogLevelEnum.VERBOSE };
public LogLevelEnum NextLevel() {
cur++;
if (cur > 6) {
cur = 0;
}
return levels[cur];
}
private void IdList_LayoutUpdated(object sender, EventArgs e) {
Placement();
}
async private void CollectLogFileClick(object sender, RoutedEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
await CollectLogFiles();
}
async private Task CollectLogFiles() {
MonitorServiceStatusEvent resp = await monitorClient.CaptureLogsAsync();
if (resp != null) {
logger.Info("response: {0}", resp.Message);
} else {
ShowError("Error Collecting Feedback", "An error occurred while trying to gather feedback. Is the monitor service running?");
}
}
/// <summary>
/// Show the blurb as a growler notification
/// </summary>
/// <param name="message">The message to show</param>
/// <param name="url">The url or action name to execute</param>
public async Task ShowBlurbAsync(string message, string url, string level = "error") {
try {
RedBlurb.Visibility = Visibility.Collapsed;
InfoBlurb.Visibility = Visibility.Collapsed;
if (level == "error") {
RedBlurb.Visibility = Visibility.Visible;
} else {
InfoBlurb.Visibility = Visibility.Visible;
}
Blurb.Content = message;
_blurbUrl = url;
BlurbArea.Visibility = Visibility.Visible;
BlurbArea.Opacity = 0;
BlurbArea.Margin = new Thickness(0, 0, 0, 0);
DoubleAnimation animation = new DoubleAnimation(1, TimeSpan.FromSeconds(.3));
ThicknessAnimation animateThick = new ThicknessAnimation(new Thickness(15, 0, 15, 15), TimeSpan.FromSeconds(.3));
BlurbArea.BeginAnimation(Grid.OpacityProperty, animation);
BlurbArea.BeginAnimation(Grid.MarginProperty, animateThick);
await Task.Delay(2500);
HideBlurb();
} catch(Exception e) {
logger.Error(e);
}
}
public async Task ShowBlurbAsync(Blurb blurb) {
await ShowBlurbAsync(blurb.Message, null, blurb.Level);
}
/// <summary>
/// Execute the hide operation wihout an action from the growler
/// </summary>
/// <param name="sender">The object that was clicked</param>
/// <param name="e">The click event</param>
private void DoHideBlurb(object sender, MouseButtonEventArgs e) {
HideBlurb();
}
/// <summary>
/// Hide the blurb area
/// </summary>
private void HideBlurb() {
DoubleAnimation animation = new DoubleAnimation(0, TimeSpan.FromSeconds(.3));
ThicknessAnimation animateThick = new ThicknessAnimation(new Thickness(0, 0, 0, 0), TimeSpan.FromSeconds(.3));
animation.Completed += HideComplete;
BlurbArea.BeginAnimation(Grid.OpacityProperty, animation);
BlurbArea.BeginAnimation(Grid.MarginProperty, animateThick);
}
/// <summary>
/// Hide the blurb area after the animation fades out
/// </summary>
/// <param name="sender">The animation object</param>
/// <param name="e">The completion event</param>
private void HideComplete(object sender, EventArgs e) {
BlurbArea.Visibility = Visibility.Collapsed;
}
/// <summary>
/// Execute a predefined action or url when the pop up is clicked
/// </summary>
/// <param name="sender">The object that was clicked</param>
/// <param name="e">The click event</param>
private void BlurbAction(object sender, MouseButtonEventArgs e) {
HideBlurb();
}
private void ShowAuthenticate(ZitiIdentity identity) {
MFAAuthenticate(identity);
}
private void ShowRecovery(ZitiIdentity identity) {
ShowMFARecoveryCodes(identity);
}
private void DoLoading(bool isComplete) {
if (isComplete) HideLoad();
else ShowLoad("Loading", "Please Wait.");
}
private void AddIdentityContextMenu(object sender, MouseButtonEventArgs e) {
var stackPanel = sender as StackPanel;
if (stackPanel?.ContextMenu != null) {
stackPanel.ContextMenu.PlacementTarget = stackPanel;
stackPanel.ContextMenu.IsOpen = true;
}
}
private void WithJwt_Click(object sender, RoutedEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
// Handle "With JWT"
AddIdentity_Click(sender, e);
}
void WithUrl_Click(object sender, RoutedEventArgs e) {
ShowJoinByUrl();
}
void With3rdPartyCA_Click(object sender, RoutedEventArgs e) {
if (!UIUtils.IsLeftClick(e)) return;
if (!UIUtils.MouseUpForMouseDown(e)) return;
ShowJoinWith3rdPartyCA();
}
void OnNeedsSignerChoiceAction(AddIdentityViewModel vm, UserControl toClose) {
// Swap dialogs without touching the modal overlay so the main window stays dimmed.
AnimateDialogOut(toClose);
AddIdentitySignerPicker.Prepare(vm);
AnimateDialogIn(AddIdentitySignerPicker);
}
async void OnAddIdentityAction(EnrollIdentifierPayload payload, UserControl toClose) {
try {
CloseJoinByUrl(false, toClose);
await AddId(payload);
} catch (ServiceException se) {
if (se.Code == 500) {
if (se?.OriginalResponse?.Error == "ZITI_KEY_GENERATION_FAILED") {
HideLoad();
ShowKeychainOverrideDialog(payload, "Override Keychain?", "keychain support is enabled but key generation failed. Do you want to enroll without keychain support?");
return;
}
}
ShowError(se.Message, se.AdditionalInfo);
} catch (Exception ex) {
ShowError("Unexpected Error", "Code 2:" + ex.Message);
}
HideLoad();
}
private async void CompleteExternalAuthEvent(ZitiIdentity identity, string provider) {
try {
await identity.PerformExternalAuthEvent(serviceClient, provider);
} catch (Exception ex) {
logger.Error("external auth failed: [{}]", ex.Message);
ShowError("Failed to Authenticate", ex.Message);
}
}
private void MainUI_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
UIUtils.ClickedControl = e.Source as UIElement;
}
private async void IdentityMenu_ShowBlurb(Blurb blurb) {
await ShowBlurbAsync(blurb);
}
}
public class ActionCommand : ICommand {
private readonly Action _action;
public ActionCommand(Action action) {
_action = action;
}
public void Execute(object parameter) {
_action();
}
public bool CanExecute(object parameter) {
return true;
}
#pragma warning disable CS0067 //The event 'ActionCommand.CanExecuteChanged' is never used
public event EventHandler CanExecuteChanged;
#pragma warning restore CS0067 //The event 'ActionCommand.CanExecuteChanged' is never used
}
}