Basic Websocket support working, without authentication (auth broken)

This commit is contained in:
Alexander Horner
2022-12-07 21:36:03 +00:00
parent fe4ae354b2
commit 1e1187e71f
5 changed files with 286 additions and 94 deletions

View File

@@ -34,7 +34,14 @@ namespace ntfysh_client
if (result != DialogResult.OK) return;
//Subscribe
if (dialog.UseWebsockets)
{
_notificationListener.SubscribeToTopicUsingWebsocket(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password);
}
else
{
_notificationListener.SubscribeToTopicUsingLongHttpJson(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password);
}
//Add to the user visible list
notificationTopics.Items.Add(dialog.Unique);
@@ -167,7 +174,24 @@ namespace ntfysh_client
//Load them in
foreach (SubscribedTopic topic in topics)
{
string[] parts = topic.ServerUrl.Split("://", 2);
switch (parts[0].ToLower())
{
case "ws":
case "wss":
_notificationListener.SubscribeToTopicUsingWebsocket($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password);
break;
case "http":
case "https":
_notificationListener.SubscribeToTopicUsingLongHttpJson($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password);
break;
default:
continue;
}
notificationTopics.Items.Add($"{topic.TopicId}@{topic.ServerUrl}");
}
}

View File

@@ -31,7 +31,7 @@ namespace ntfysh_client
ServicePointManager.DefaultConnectionLimit = 100;
}
private async Task ListenToTopicAsync(HttpRequestMessage message, CancellationToken cancellationToken)
private async Task ListenToTopicWithHttpLongJsonAsync(HttpRequestMessage message, CancellationToken cancellationToken)
{
if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));
@@ -84,6 +84,59 @@ namespace ntfysh_client
}
}
private async Task ListenToTopicWithWebsocketAsync(Uri uri, NetworkCredential credentials, CancellationToken cancellationToken)
{
if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));
while (!cancellationToken.IsCancellationRequested)
{
using ClientWebSocket socket = new();
socket.Options.Credentials = credentials;
try
{
StringBuilder mainBuffer = new();
await socket.ConnectAsync(uri, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
//Read as much as possible
byte[] buffer = new byte[8192];
WebSocketReceiveResult? result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
//Append it to our main buffer
mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
List<string> lines = mainBuffer.ToString().Split('\n').ToList();
//If we have not yet received a full line, meaning theres only 1 part, go back to reading
if (lines.Count <= 1) continue;
//We now have at least 1 line! Count how many full lines. There will always be a partial line at the end, even if that partial line is empty
//Separate the partial line from the full lines
int partialLineIndex = lines.Count - 1;
string partialLine = lines[partialLineIndex];
lines.RemoveAt(partialLineIndex);
//Process the full lines
foreach (string line in lines) ProcessMessage(line);
//Write back the partial line
mainBuffer.Clear();
mainBuffer.Append(partialLine);
}
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine(ex);
#endif
//Fall back to the outer loop to restart the listen, or cancel if requested
}
}
}
private void ProcessMessage(string message)
{
#if DEBUG
@@ -120,11 +173,26 @@ namespace ntfysh_client
}
CancellationTokenSource listenCanceller = new();
Task listenTask = ListenToTopicAsync(message, listenCanceller.Token);
Task listenTask = ListenToTopicWithHttpLongJsonAsync(message, listenCanceller.Token);
SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller));
}
public void SubscribeToTopicUsingWebsocket(string unique, string topicId, string serverUrl, string? username, string? password)
{
if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));
if (SubscribedTopicsByUnique.ContainsKey(unique)) throw new InvalidOperationException("A topic with this unique already exists");
if (string.IsNullOrWhiteSpace(username)) username = null;
if (string.IsNullOrWhiteSpace(password)) password = null;
Uri targetUri = new($"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/ws");
CancellationTokenSource listenCanceller = new();
Task listenTask = ListenToTopicWithWebsocketAsync(targetUri, new NetworkCredential(username, password), listenCanceller.Token);
SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller));
}
public async Task UnsubscribeFromTopicAsync(string topicUniqueString)
{
if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));

View File

@@ -40,6 +40,8 @@ namespace ntfysh_client
this.label3 = new System.Windows.Forms.Label();
this.password = new System.Windows.Forms.TextBox();
this.label4 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.connectionType = new System.Windows.Forms.ComboBox();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
@@ -49,18 +51,19 @@ namespace ntfysh_client
this.panel1.Controls.Add(this.button2);
this.panel1.Controls.Add(this.button1);
this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
this.panel1.Location = new System.Drawing.Point(0, 175);
this.panel1.Location = new System.Drawing.Point(0, 244);
this.panel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(297, 44);
this.panel1.Size = new System.Drawing.Size(346, 51);
this.panel1.TabIndex = 0;
//
// button2
//
this.button2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.button2.Location = new System.Drawing.Point(131, 11);
this.button2.Margin = new System.Windows.Forms.Padding(10, 10, 3, 10);
this.button2.Location = new System.Drawing.Point(153, 13);
this.button2.Margin = new System.Windows.Forms.Padding(12, 12, 4, 12);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(75, 23);
this.button2.Size = new System.Drawing.Size(88, 27);
this.button2.TabIndex = 1;
this.button2.Text = "Cancel";
this.button2.UseVisualStyleBackColor = true;
@@ -69,10 +72,10 @@ namespace ntfysh_client
// button1
//
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.button1.Location = new System.Drawing.Point(212, 11);
this.button1.Margin = new System.Windows.Forms.Padding(3, 10, 10, 10);
this.button1.Location = new System.Drawing.Point(247, 13);
this.button1.Margin = new System.Windows.Forms.Padding(4, 12, 12, 12);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.Size = new System.Drawing.Size(88, 27);
this.button1.TabIndex = 2;
this.button1.Text = "Subscribe";
this.button1.UseVisualStyleBackColor = true;
@@ -81,9 +84,10 @@ namespace ntfysh_client
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Location = new System.Drawing.Point(14, 10);
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(51, 13);
this.label1.Size = new System.Drawing.Size(52, 15);
this.label1.TabIndex = 1;
this.label1.Text = "Topic ID:";
//
@@ -91,9 +95,10 @@ namespace ntfysh_client
//
this.topicId.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.topicId.Location = new System.Drawing.Point(12, 25);
this.topicId.Location = new System.Drawing.Point(14, 29);
this.topicId.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.topicId.Name = "topicId";
this.topicId.Size = new System.Drawing.Size(273, 20);
this.topicId.Size = new System.Drawing.Size(318, 23);
this.topicId.TabIndex = 0;
this.topicId.KeyDown += new System.Windows.Forms.KeyEventHandler(this.topicId_KeyDown);
//
@@ -101,19 +106,21 @@ namespace ntfysh_client
//
this.serverUrl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.serverUrl.Location = new System.Drawing.Point(12, 64);
this.serverUrl.Location = new System.Drawing.Point(14, 74);
this.serverUrl.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.serverUrl.Name = "serverUrl";
this.serverUrl.Size = new System.Drawing.Size(273, 20);
this.serverUrl.Size = new System.Drawing.Size(318, 23);
this.serverUrl.TabIndex = 2;
this.serverUrl.Text = "https://ntfy.sh";
this.serverUrl.Text = "wss://ntfy.sh";
this.serverUrl.KeyDown += new System.Windows.Forms.KeyEventHandler(this.serverUrl_KeyDown);
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(10, 48);
this.label2.Location = new System.Drawing.Point(12, 55);
this.label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(66, 13);
this.label2.Size = new System.Drawing.Size(66, 15);
this.label2.TabIndex = 3;
this.label2.Text = "Server URL:";
//
@@ -121,18 +128,20 @@ namespace ntfysh_client
//
this.username.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.username.Location = new System.Drawing.Point(12, 103);
this.username.Location = new System.Drawing.Point(14, 119);
this.username.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.username.Name = "username";
this.username.Size = new System.Drawing.Size(273, 20);
this.username.Size = new System.Drawing.Size(318, 23);
this.username.TabIndex = 4;
this.username.KeyDown += new System.Windows.Forms.KeyEventHandler(this.username_KeyDown);
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(10, 87);
this.label3.Location = new System.Drawing.Point(12, 100);
this.label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(58, 13);
this.label3.Size = new System.Drawing.Size(63, 15);
this.label3.TabIndex = 5;
this.label3.Text = "Username:";
//
@@ -140,9 +149,10 @@ namespace ntfysh_client
//
this.password.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.password.Location = new System.Drawing.Point(12, 142);
this.password.Location = new System.Drawing.Point(14, 164);
this.password.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.password.Name = "password";
this.password.Size = new System.Drawing.Size(273, 20);
this.password.Size = new System.Drawing.Size(318, 23);
this.password.TabIndex = 6;
this.password.UseSystemPasswordChar = true;
this.password.KeyDown += new System.Windows.Forms.KeyEventHandler(this.password_KeyDown);
@@ -150,18 +160,44 @@ namespace ntfysh_client
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(10, 126);
this.label4.Location = new System.Drawing.Point(12, 145);
this.label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(56, 13);
this.label4.Size = new System.Drawing.Size(60, 15);
this.label4.TabIndex = 7;
this.label4.Text = "Password:";
//
// label5
//
this.label5.AutoSize = true;
this.label5.Location = new System.Drawing.Point(12, 190);
this.label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(99, 15);
this.label5.TabIndex = 9;
this.label5.Text = "Connection Type:";
//
// connectionType
//
this.connectionType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.connectionType.FormattingEnabled = true;
this.connectionType.Items.AddRange(new object[] {
"Websockets (Recommended)",
"Long HTTP JSON (Robust)"});
this.connectionType.Location = new System.Drawing.Point(14, 208);
this.connectionType.Name = "connectionType";
this.connectionType.Size = new System.Drawing.Size(318, 23);
this.connectionType.TabIndex = 10;
this.connectionType.TextChanged += new System.EventHandler(this.connectionType_TextChanged);
//
// SubscribeDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.ClientSize = new System.Drawing.Size(297, 219);
this.ClientSize = new System.Drawing.Size(346, 295);
this.Controls.Add(this.connectionType);
this.Controls.Add(this.label5);
this.Controls.Add(this.password);
this.Controls.Add(this.label4);
this.Controls.Add(this.username);
@@ -172,6 +208,7 @@ namespace ntfysh_client
this.Controls.Add(this.label1);
this.Controls.Add(this.panel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SubscribeDialog";
@@ -179,6 +216,7 @@ namespace ntfysh_client
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Subscribe to new topic";
this.Load += new System.EventHandler(this.SubscribeDialog_Load);
this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
@@ -199,5 +237,7 @@ namespace ntfysh_client
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox password;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.ComboBox connectionType;
}
}

View File

@@ -17,12 +17,107 @@ namespace ntfysh_client
public string Unique => $"{topicId.Text}@{serverUrl.Text}";
public bool UseWebsockets
{
get
{
switch (connectionType.Text)
{
case "Websockets (Recommended)":
return true;
case "Long HTTP JSON (Robust)":
return false;
default:
throw new InvalidOperationException();
}
}
}
public SubscribeDialog(ListBox notificationTopics)
{
_notificationTopics = notificationTopics;
InitializeComponent();
}
private void SubscribeDialog_Load(object sender, EventArgs e)
{
connectionType.SelectedIndex = 0;
}
private bool ReparseAddress()
{
//Separate schema and address
string[] parts = serverUrl.Text.Split("://", 2);
//Validate the basic formatting is correct
if (parts.Length != 2) return false;
//Take the schema aside for parsing
string schema = parts[0].ToLower();
//Ensure the schema is actually valid
switch (schema)
{
case "http":
case "https":
case "ws":
case "wss":
break;
default:
return false;
}
//Correct the schema based on connection type if required
if (UseWebsockets)
{
switch (schema)
{
case "http":
schema = "ws";
break;
case "https":
schema = "wss";
break;
case "ws":
case "wss":
break;
}
}
else
{
switch (schema)
{
case "ws":
schema = "http";
break;
case "wss":
schema = "https";
break;
case "http":
case "https":
break;
}
}
//Reconstruct the address
string finalAddress = schema + "://" + parts[1];
//Validate the address
if (!Uri.IsWellFormedUriString(finalAddress, UriKind.Absolute)) return false;
//Set the final address and OK it
serverUrl.Text = finalAddress;
return true;
}
private void button1_Click(object sender, EventArgs e)
{
if (topicId.Text.Length < 1)
@@ -35,7 +130,7 @@ namespace ntfysh_client
if (serverUrl.Text.Length < 1)
{
MessageBox.Show("You must specify a server URL. The default is https://ntfy.sh", "Server URL not specified", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("You must specify a server URL. The default is wss://ntfy.sh", "Server URL not specified", MessageBoxButtons.OK, MessageBoxIcon.Error);
DialogResult = DialogResult.None;
serverUrl.Focus();
return;
@@ -65,6 +160,26 @@ namespace ntfysh_client
return;
}
try
{
if (!ReparseAddress())
{
MessageBox.Show($"The specified server URL is invalid. Accepted schemas are: http:// https:// ws:// wss://", "Invalid Server URL", MessageBoxButtons.OK, MessageBoxIcon.Error);
DialogResult = DialogResult.None;
connectionType.Focus();
return;
}
}
catch (InvalidOperationException)
{
MessageBox.Show($"The selected Connection Type '{connectionType.Text}' is invalid.", "Invalid Connection Type", MessageBoxButtons.OK, MessageBoxIcon.Error);
DialogResult = DialogResult.None;
connectionType.Focus();
return;
}
DialogResult = DialogResult.OK;
}
@@ -108,5 +223,10 @@ namespace ntfysh_client
e.SuppressKeyPress = true;
}
}
private void connectionType_TextChanged(object sender, EventArgs e)
{
ReparseAddress();
}
}
}

View File

@@ -1,64 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">