Hush

Subversion Repositories:
Compare Path: Rev
With Path: Rev
?path1? @ 2  →  ?path2? @ 3
/trunk/Hush/Hush.Designer.cs
@@ -83,7 +83,6 @@
this.chatTextBox.ReadOnly = true;
this.chatTextBox.Size = new System.Drawing.Size(627, 345);
this.chatTextBox.TabIndex = 1;
this.chatTextBox.TextChanged += new System.EventHandler(this.ChatTextBox_TextChanged);
this.chatTextBox.MouseDown += new System.Windows.Forms.MouseEventHandler(this.OnMouseDown);
this.chatTextBox.MouseEnter += new System.EventHandler(this.HushFocus);
this.chatTextBox.MouseLeave += new System.EventHandler(this.HushUnfocus);
/trunk/Hush/Hush.cs
@@ -8,19 +8,25 @@
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Gma.System.MouseKeyHook;
using Hush.Chat;
using Hush.Communication;
using Hush.Discovery;
using Hush.Properties;
using Hush.Utilities;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Server;
using WingMan.Communication;
using Message = System.Windows.Forms.Message;
 
namespace Hush
{
public partial class Hush : Form
{
private static TaskScheduler FormTaskScheduler { get; set; }
private static CancellationTokenSource FormCancellationTokenSource { get; set; }
private static MqttCommunication MqttCommunication { get; set; }
private static ChatMessageSynchronizer ChatMessageSynchronizer { get; set; }
private static Discovery.Discovery Discovery { get; set; }
 
public Hush()
{
InitializeComponent();
@@ -28,18 +34,11 @@
FormTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
FormCancellationTokenSource = new CancellationTokenSource();
 
// Initialize a message buffer.
Messages = new List<string>();
 
// Bind to settings changed event.
Settings.Default.SettingsLoaded += DefaultOnSettingsLoaded;
Settings.Default.SettingsSaving += DefaultOnSettingsSaving;
Settings.Default.PropertyChanged += DefaultOnPropertyChanged;
 
// Set up keyboard hook.
MouseKeyGlobalHook = Hook.GlobalEvents();
MouseKeyGlobalHook.KeyDown += MouseKeyGlobalHookOnKeyDown;
 
// Set up discovery.
Discovery = new Discovery.Discovery(Constants.AssemblyName, FormCancellationTokenSource.Token,
FormTaskScheduler);
@@ -47,216 +46,21 @@
 
// Bind to MQTT events.
MqttCommunication = new MqttCommunication(FormTaskScheduler, FormCancellationTokenSource.Token);
// TODO implement events.
//MqttCommunication.
MqttCommunication.OnClientConnectionFailed += MqttOnClientConnectionFailed;
MqttCommunication.OnClientAuthenticationFailed += MqttOnClientAuthenticationFailed;
MqttCommunication.OnClientConnected += MqttOnClientConnected;
MqttCommunication.OnClientDisconnected += MqttOnClientDisconnected;
MqttCommunication.OnServerClientConnected += MqttCommunicationOnOnServerClientConnected;
MqttCommunication.OnServerClientDisconnected += MqttOnServerClientDisconnected;
 
// Start message synchronizer.
ChatMessageSynchronizer = new ChatMessageSynchronizer(Constants.MqttTopic, MqttCommunication, FormTaskScheduler,
ChatMessageSynchronizer = new ChatMessageSynchronizer(Constants.MqttTopic, MqttCommunication,
FormTaskScheduler,
FormCancellationTokenSource.Token);
ChatMessageSynchronizer.OnMessageReceived += OnMessageReceived;
}
 
private static IKeyboardMouseEvents MouseKeyGlobalHook { get; set; }
private static TaskScheduler FormTaskScheduler { get; set; }
private static CancellationTokenSource FormCancellationTokenSource { get; set; }
private static MqttCommunication MqttCommunication { get; set; }
private static ChatMessageSynchronizer ChatMessageSynchronizer { get; set; }
private static Discovery.Discovery Discovery { get; set; }
private static List<string> Messages { get; set; }
 
private void MouseKeyGlobalHookOnKeyDown(object sender, KeyEventArgs keyEventArgs)
{
// Bind to CTRL+C
if (!keyEventArgs.Control || keyEventArgs.KeyCode != Keys.C)
return;
 
ActiveControl = messageTextBox;
messageTextBox.Focus();
}
 
private void OnDiscoveryPortMapFailed(object sender, DiscoveryFailedEventArgs args)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = Strings.Failed_to_create_automatic_NAT_port_mapping;
notifyIcon1.ShowBalloonTip(1000);
}
 
private void OnMessageReceived(object sender, ChatMessageReceivedEventArgs e)
{
var message = $"{e.Nick} : {e.Message}";
 
Messages.Add(message);
 
// Keep the content of the text box limited to the size of the displayed text.
var lineHeight = TextRenderer.MeasureText(message, chatTextBox.Font).Height;
var linesPerPage = (int) Math.Ceiling(1.0f * chatTextBox.ClientSize.Height / lineHeight);
 
if (Messages.Count > linesPerPage)
Messages.RemoveRange(0, Messages.Count - 1 - linesPerPage);
 
chatTextBox.Text = string.Join(Environment.NewLine, Messages);
}
 
private async void DefaultOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (string.Equals(e.PropertyName, "LaunchOnBoot"))
LaunchOnBoot.Set(Settings.Default.LaunchOnBoot);
 
if (string.Equals(e.PropertyName, "Start"))
await ToggleStart();
}
 
private async Task ToggleStart()
{
switch (Settings.Default.Mode)
{
case "Server":
await StopServer();
 
if (!Settings.Default.Start)
break;
 
try
{
await StartServer();
}
catch (ToolTippedException ex)
{
notifyIcon1.BalloonTipIcon = ex.Icon;
notifyIcon1.BalloonTipTitle = ex.Title;
notifyIcon1.BalloonTipText = ex.Body;
notifyIcon1.ShowBalloonTip(1000);
}
 
break;
case "Client":
await StopClient();
 
if (!Settings.Default.Start)
break;
 
try
{
await StartClient();
}
catch (ToolTippedException ex)
{
notifyIcon1.BalloonTipIcon = ex.Icon;
notifyIcon1.BalloonTipTitle = ex.Title;
notifyIcon1.BalloonTipText = ex.Body;
notifyIcon1.ShowBalloonTip(1000);
}
 
break;
}
 
startToolStripMenuItem.Checked = Settings.Default.Start;
}
 
private async Task StopClient()
{
// Stop the client if it is already started.
await MqttCommunication.Stop();
}
 
private async Task StopServer()
{
// Remove UPnP and Pmp mappings.
await Task.Delay(0, FormCancellationTokenSource.Token).ContinueWith(async _ =>
{
await Discovery.DeleteMapping(DiscoveryType.Upnp, MqttCommunication.Port);
await Discovery.DeleteMapping(DiscoveryType.Pmp, MqttCommunication.Port);
},
FormCancellationTokenSource.Token, TaskContinuationOptions.LongRunning, FormTaskScheduler);
 
// Stop the MQTT server if it is running.
await MqttCommunication.Stop();
}
 
private async Task StartClient()
{
if (!IPAddress.TryParse(Settings.Default.Address, out var address))
try
{
var getHostAddresses = await Dns.GetHostAddressesAsync(Settings.Default.Address);
if (!getHostAddresses.Any())
throw new Exception();
 
address = getHostAddresses.FirstOrDefault();
}
catch
{
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_address} {Settings.Default.Address}");
}
 
if (!uint.TryParse(Settings.Default.Port, out var port))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_port} {Settings.Default.Port}");
 
 
if (string.IsNullOrEmpty(Settings.Default.Nick))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_nickname_set);
 
if (string.IsNullOrEmpty(Settings.Default.Password))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_password_set);
 
if (!await MqttCommunication
.Start(MqttCommunicationType.Client, address, (int) port, Settings.Default.Nick,
Settings.Default.Password,
new[] {Constants.MqttTopic}))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.Unable_to_start_client);
}
 
private async Task StartServer()
{
if (!IPAddress.TryParse(Settings.Default.Address, out var address))
try
{
address = Dns.GetHostAddresses(Settings.Default.Address).FirstOrDefault();
}
catch
{
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_address} {Settings.Default.Address}");
}
 
if (!uint.TryParse(Settings.Default.Port, out var port))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_port} {Settings.Default.Port}");
 
 
if (string.IsNullOrEmpty(Settings.Default.Nick))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_nickname_set);
 
if (string.IsNullOrEmpty(Settings.Default.Password))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_password_set);
 
// Try to reserve port: try UPnP followed by PMP.
await Task.Delay(0, FormCancellationTokenSource.Token).ContinueWith(async _ =>
{
if (!await Discovery.CreateMapping(DiscoveryType.Upnp, (int) port))
await Discovery.CreateMapping(DiscoveryType.Pmp, (int) port);
},
FormCancellationTokenSource.Token, TaskContinuationOptions.LongRunning, FormTaskScheduler);
 
// Start the MQTT server.
if (!await MqttCommunication
.Start(MqttCommunicationType.Server, address, (int) port, Settings.Default.Nick,
Settings.Default.Password, new[] {Constants.MqttTopic}))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.Unable_to_start_server);
}
 
private void DefaultOnSettingsSaving(object sender, CancelEventArgs e)
{
}
 
private void DefaultOnSettingsLoaded(object sender, SettingsLoadedEventArgs e)
{
LaunchOnBoot.Set(Settings.Default.LaunchOnBoot);
}
 
/// <summary>
/// Clean up any resources being used.
/// </summary>
@@ -263,9 +67,6 @@
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
// Unbind global key hook.
MouseKeyGlobalHook.KeyDown -= MouseKeyGlobalHookOnKeyDown;
 
// Unbind message synchronizer.
ChatMessageSynchronizer.OnMessageReceived -= OnMessageReceived;
 
@@ -278,14 +79,7 @@
base.Dispose(disposing);
}
 
private void OnMouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
DllImports.ReleaseCapture();
DllImports.SendMessage(Handle, DllImports.WM_NCLBUTTONDOWN, DllImports.HT_CAPTION, 0);
}
}
#region Overrides
 
protected override void OnPaintBackground(PaintEventArgs e)
{
@@ -370,6 +164,104 @@
base.WndProc(ref m);
}
 
#endregion
 
#region Event Handlers
 
private void MqttOnServerClientDisconnected(object sender,
MqttClientDisconnectedEventArgs mqttClientDisconnectedEventArgs)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = $"{Strings.Client_disconnected}";
notifyIcon1.ShowBalloonTip(1000);
}
 
 
private void MqttCommunicationOnOnServerClientConnected(object sender,
MqttClientConnectedEventArgs mqttClientConnectedEventArgs)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = $"{Strings.Client_connected}";
notifyIcon1.ShowBalloonTip(1000);
}
 
private void MqttOnClientDisconnected(object sender,
MQTTnet.Client.MqttClientDisconnectedEventArgs mqttClientDisconnectedEventArgs)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = $"{Strings.Client_disconnected}";
notifyIcon1.ShowBalloonTip(1000);
}
 
private void MqttOnClientConnected(object sender,
MQTTnet.Client.MqttClientConnectedEventArgs mqttClientConnectedEventArgs)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = $"{Strings.Client_connected}";
notifyIcon1.ShowBalloonTip(1000);
}
 
private void MqttOnClientAuthenticationFailed(object sender,
MqttAuthenticationFailureEventArgs mqttAuthenticationFailureEventArgs)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = $"{Strings.Failed_to_authenticate_to_server}";
notifyIcon1.ShowBalloonTip(1000);
}
 
private void MqttOnClientConnectionFailed(object sender,
MqttManagedProcessFailedEventArgs mqttManagedProcessFailedEventArgs)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText =
$"{Strings.Failed_to_connect_to_server} {mqttManagedProcessFailedEventArgs.Exception.Message}";
notifyIcon1.ShowBalloonTip(1000);
}
 
private void OnDiscoveryPortMapFailed(object sender, DiscoveryFailedEventArgs args)
{
notifyIcon1.BalloonTipIcon = ToolTipIcon.Warning;
notifyIcon1.BalloonTipTitle = Strings.Network_warning;
notifyIcon1.BalloonTipText = Strings.Failed_to_create_automatic_NAT_port_mapping;
notifyIcon1.ShowBalloonTip(1000);
}
 
private void OnMessageReceived(object sender, ChatMessageReceivedEventArgs e)
{
var message = $"{e.Nick} : {e.Message}{Environment.NewLine}";
chatTextBox.AppendText(message);
}
 
private async void DefaultOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
LaunchOnBoot.Set(Settings.Default.LaunchOnBoot);
await ToggleStart();
}
 
private void DefaultOnSettingsSaving(object sender, CancelEventArgs e)
{
}
 
private void DefaultOnSettingsLoaded(object sender, SettingsLoadedEventArgs e)
{
LaunchOnBoot.Set(Settings.Default.LaunchOnBoot);
}
 
private void OnMouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
DllImports.ReleaseCapture();
DllImports.SendMessage(Handle, DllImports.WM_NCLBUTTONDOWN, DllImports.HT_CAPTION, 0);
}
}
 
private void HushFocus(object sender, EventArgs e)
{
Opacity = .75;
@@ -456,11 +348,147 @@
Settings.Default.Port = toolStripMenuItem.Text;
}
 
private void ChatTextBox_TextChanged(object sender, EventArgs e)
#endregion
 
private async Task ToggleStart()
{
var textBox = (TextBox) sender;
switch (Settings.Default.Mode)
{
case "Server":
await StopServer();
 
textBox.ScrollToCaret();
if (!Settings.Default.Start)
break;
 
try
{
await StartServer();
}
catch (ToolTippedException ex)
{
notifyIcon1.BalloonTipIcon = ex.Icon;
notifyIcon1.BalloonTipTitle = ex.Title;
notifyIcon1.BalloonTipText = ex.Body;
notifyIcon1.ShowBalloonTip(1000);
}
 
break;
case "Client":
await StopClient();
 
if (!Settings.Default.Start)
break;
 
try
{
await StartClient();
}
catch (ToolTippedException ex)
{
notifyIcon1.BalloonTipIcon = ex.Icon;
notifyIcon1.BalloonTipTitle = ex.Title;
notifyIcon1.BalloonTipText = ex.Body;
notifyIcon1.ShowBalloonTip(1000);
}
 
break;
}
 
startToolStripMenuItem.Checked = Settings.Default.Start;
}
 
private async Task StopClient()
{
// Stop the client if it is already started.
await MqttCommunication.Stop();
}
 
private async Task StopServer()
{
// Remove UPnP and Pmp mappings.
await Task.Delay(0, FormCancellationTokenSource.Token).ContinueWith(async _ =>
{
await Discovery.DeleteMapping(DiscoveryType.Upnp, MqttCommunication.Port);
await Discovery.DeleteMapping(DiscoveryType.Pmp, MqttCommunication.Port);
},
FormCancellationTokenSource.Token, TaskContinuationOptions.LongRunning, FormTaskScheduler);
 
// Stop the MQTT server if it is running.
await MqttCommunication.Stop();
}
 
private async Task StartClient()
{
if (!IPAddress.TryParse(Settings.Default.Address, out var address))
try
{
var getHostAddresses = await Dns.GetHostAddressesAsync(Settings.Default.Address);
if (!getHostAddresses.Any())
throw new Exception();
 
address = getHostAddresses.FirstOrDefault();
}
catch
{
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_address} {Settings.Default.Address}");
}
 
if (!uint.TryParse(Settings.Default.Port, out var port))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_port} {Settings.Default.Port}");
 
 
if (string.IsNullOrEmpty(Settings.Default.Nick))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_nickname_set);
 
if (string.IsNullOrEmpty(Settings.Default.Password))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_password_set);
 
if (!await MqttCommunication
.Start(MqttCommunicationType.Client, address, (int) port, Settings.Default.Nick,
Settings.Default.Password,
new[] {Constants.MqttTopic}))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.Unable_to_start_client);
}
 
private async Task StartServer()
{
if (!IPAddress.TryParse(Settings.Default.Address, out var address))
try
{
address = Dns.GetHostAddresses(Settings.Default.Address).FirstOrDefault();
}
catch
{
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_address} {Settings.Default.Address}");
}
 
if (!uint.TryParse(Settings.Default.Port, out var port))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error,
$"{Strings.Unable_to_determine_port} {Settings.Default.Port}");
 
 
if (string.IsNullOrEmpty(Settings.Default.Nick))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_nickname_set);
 
if (string.IsNullOrEmpty(Settings.Default.Password))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.No_password_set);
 
// Try to reserve port: try UPnP followed by PMP.
await Task.Delay(0, FormCancellationTokenSource.Token).ContinueWith(async _ =>
{
if (!await Discovery.CreateMapping(DiscoveryType.Upnp, (int) port))
await Discovery.CreateMapping(DiscoveryType.Pmp, (int) port);
},
FormCancellationTokenSource.Token, TaskContinuationOptions.LongRunning, FormTaskScheduler);
 
// Start the MQTT server.
if (!await MqttCommunication
.Start(MqttCommunicationType.Server, address, (int) port, Settings.Default.Nick,
Settings.Default.Password, new[] {Constants.MqttTopic}))
throw new ToolTippedException(ToolTipIcon.Error, Strings.Network_error, Strings.Unable_to_start_server);
}
}
}
/trunk/Hush/Hush.csproj
@@ -36,9 +36,6 @@
<ApplicationIcon>hush-icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="Gma.System.MouseKeyHook, Version=5.6.130.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MouseKeyHook.5.6.0\lib\net40\Gma.System.MouseKeyHook.dll</HintPath>
</Reference>
<Reference Include="LZ4, Version=1.0.15.93, Culture=neutral, PublicKeyToken=62e1b5ec1eec9bdd, processorArchitecture=MSIL">
<HintPath>..\packages\lz4net.1.0.15.93\lib\net4-client\LZ4.dll</HintPath>
</Reference>
/trunk/Hush/Properties/Strings.Designer.cs
@@ -61,6 +61,42 @@
}
/// <summary>
/// Looks up a localized string similar to Client connected.
/// </summary>
internal static string Client_connected {
get {
return ResourceManager.GetString("Client_connected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Client disconnected.
/// </summary>
internal static string Client_disconnected {
get {
return ResourceManager.GetString("Client_disconnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to authenticate to server.
/// </summary>
internal static string Failed_to_authenticate_to_server {
get {
return ResourceManager.GetString("Failed_to_authenticate_to_server", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to connect to server.
/// </summary>
internal static string Failed_to_connect_to_server {
get {
return ResourceManager.GetString("Failed_to_connect_to_server", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to create automatic NAT port mapping.
/// </summary>
internal static string Failed_to_create_automatic_NAT_port_mapping {
@@ -70,6 +106,15 @@
}
/// <summary>
/// Looks up a localized string similar to {0}.
/// </summary>
internal static string Hush_MqttOnClientConnected__0_ {
get {
return ResourceManager.GetString("Hush_MqttOnClientConnected__0_", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Network error.
/// </summary>
internal static string Network_error {
/trunk/Hush/Properties/Strings.resx
@@ -117,6 +117,18 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Client_connected" xml:space="preserve">
<value>Client connected</value>
</data>
<data name="Client_disconnected" xml:space="preserve">
<value>Client disconnected</value>
</data>
<data name="Failed_to_authenticate_to_server" xml:space="preserve">
<value>Failed to authenticate to server</value>
</data>
<data name="Failed_to_connect_to_server" xml:space="preserve">
<value>Failed to connect to server</value>
</data>
<data name="Failed_to_create_automatic_NAT_port_mapping" xml:space="preserve">
<value>Failed to create automatic NAT port mapping</value>
</data>
@@ -144,4 +156,7 @@
<data name="Unable_to_start_server" xml:space="preserve">
<value>Unable to start server</value>
</data>
<data name="Hush_MqttOnClientConnected__0_" xml:space="preserve">
<value>{0}</value>
</data>
</root>
/trunk/Hush/packages.config
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
 
<packages>
<package id="lz4net" version="1.0.15.93" targetFramework="net452" />
<package id="MouseKeyHook" version="5.6.0" targetFramework="net452" />
<package id="MQTTnet" version="2.8.5" targetFramework="net452" />
<package id="MQTTnet.Extensions.ManagedClient" version="2.8.5" targetFramework="net452" />
<package id="Open.NAT" version="2.1.0.0" targetFramework="net452" />