Winify – Blame information for rev 80

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 using System;
66 office 2 using System.Collections.Concurrent;
61 office 3 using System.Collections.Generic;
4 using System.Diagnostics;
1 office 5 using System.Drawing;
6 using System.IO;
50 office 7 using System.Net;
1 office 8 using System.Net.Http;
9 using System.Net.Http.Headers;
61 office 10 using System.Runtime.CompilerServices;
50 office 11 using System.Security.Authentication;
46 office 12 using System.Security.Cryptography.X509Certificates;
1 office 13 using System.Text;
14 using System.Threading;
15 using System.Threading.Tasks;
61 office 16 using System.Threading.Tasks.Dataflow;
1 office 17 using Newtonsoft.Json;
18 office 18 using Serilog;
24 office 19 using Servers;
44 office 20 using WebSocketSharp;
50 office 21 using WebSocketSharp.Net;
61 office 22 using Winify.Utilities;
44 office 23 using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
50 office 24 using NetworkCredential = System.Net.NetworkCredential;
1 office 25  
26 namespace Winify.Gotify
27 {
28 public class GotifyConnection : IDisposable
29 {
30 #region Public Events & Delegates
31  
67 office 32 public event EventHandler<GotifyMessageEventArgs> GotifyMessage;
1 office 33  
34 #endregion
35  
36 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
37  
25 office 38 private readonly Server _server;
39  
1 office 40 private CancellationToken _cancellationToken;
41  
42 private CancellationTokenSource _cancellationTokenSource;
43  
66 office 44 private Task _heartBeatTask;
1 office 45  
39 office 46 private HttpClient _httpClient;
47  
48 private readonly Uri _webSocketsUri;
49  
50 private readonly Uri _httpUri;
44 office 51 private WebSocket _webSocketSharp;
59 office 52 private readonly Configuration.Configuration _configuration;
61 office 53 private IDisposable _tplRetrievePastMessagesLink;
54 private IDisposable _tplWebSocketsBufferBlockTransformLink;
66 office 55 private readonly BufferBlock<GotifyConnectionData> _webSocketMessageBufferBlock;
61 office 56 private readonly Stopwatch _webSocketsClientPingStopWatch;
57 private readonly ScheduledContinuation _webSocketsServerResponseScheduledContinuation;
66 office 58  
59 private Task _retrievePastMessagesTask;
60 private static JsonSerializer _jsonSerializer;
39 office 61  
25 office 62 #endregion
1 office 63  
25 office 64 #region Constructors, Destructors and Finalizers
24 office 65  
44 office 66 private GotifyConnection()
24 office 67 {
66 office 68 _jsonSerializer = new JsonSerializer();
61 office 69 _webSocketsServerResponseScheduledContinuation = new ScheduledContinuation();
70 _webSocketsClientPingStopWatch = new Stopwatch();
71  
66 office 72 _webSocketMessageBufferBlock = new BufferBlock<GotifyConnectionData>(
73 new DataflowBlockOptions
74 {
75 CancellationToken = _cancellationToken
76 });
77  
67 office 78 var webSocketActionBlock = new ActionBlock<GotifyConnectionData>(async gotifyConnectionData =>
61 office 79 {
67 office 80 try
61 office 81 {
67 office 82 using var memoryStream = new MemoryStream(gotifyConnectionData.Payload);
83 using var streamReader = new StreamReader(memoryStream);
84 using var jsonTextReader = new JsonTextReader(streamReader);
61 office 85  
67 office 86 var gotifyMessage = _jsonSerializer.Deserialize<GotifyMessage>(jsonTextReader) ?? throw new ArgumentNullException();
61 office 87  
67 office 88 gotifyMessage.Server = gotifyConnectionData.Server;
66 office 89  
80 office 90 using var imageStream = await RetrieveGotifyApplicationImage(gotifyMessage.AppId);
61 office 91  
67 office 92 if (imageStream == null || imageStream.Length == 0)
66 office 93 {
67 office 94 Log.Warning("Could not find any application image for notification.");
95  
96 return;
66 office 97 }
98  
67 office 99 var image = new Bitmap(imageStream);
100  
101 GotifyMessage?.Invoke(this,
102 new GotifyMessageEventArgs(gotifyMessage, image));
103  
104 Log.Debug($"Notification message received: {gotifyMessage.Message}");
61 office 105 }
67 office 106 catch (JsonSerializationException exception)
61 office 107 {
67 office 108 Log.Warning(exception, "Could not deserialize notification message.");
61 office 109 }
67 office 110 catch (Exception exception)
61 office 111 {
67 office 112 Log.Warning(exception, "Generic failure.");
61 office 113 }
114  
115 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
116  
67 office 117 _tplWebSocketsBufferBlockTransformLink = _webSocketMessageBufferBlock.LinkTo(webSocketActionBlock,
61 office 118 new DataflowLinkOptions { PropagateCompletion = true });
39 office 119 }
120  
59 office 121 public GotifyConnection(Server server, Configuration.Configuration configuration) : this()
39 office 122 {
24 office 123 _server = server;
44 office 124 _configuration = configuration;
39 office 125  
54 office 126 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
59 office 127 var httpClientHandler = new HttpClientHandler
50 office 128 {
54 office 129 // mono does not implement this
130 //SslProtocols = SslProtocols.Tls12
50 office 131 };
132  
44 office 133 _httpClient = new HttpClient(httpClientHandler);
134 if (_configuration.IgnoreSelfSignedCertificates)
135 httpClientHandler.ServerCertificateCustomValidationCallback =
46 office 136 (httpRequestMessage, cert, cetChain, policyErrors) => true;
39 office 137  
50 office 138 if (_configuration.Proxy.Enable)
139 httpClientHandler.Proxy = new WebProxy(_configuration.Proxy.Url, false, new string[] { },
140 new NetworkCredential(_configuration.Proxy.Username, _configuration.Proxy.Password));
141  
47 office 142 _httpClient = new HttpClient(httpClientHandler);
143 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
144 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
145 Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}")));
44 office 146  
47 office 147 if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri))
39 office 148 {
51 office 149 Log.Error($"No HTTP URL could be built out of the supplied server URI {_server.Url}");
47 office 150 return;
151 }
44 office 152  
47 office 153 // Build the web sockets URI.
154 var webSocketsUriBuilder = new UriBuilder(_httpUri);
155 switch (webSocketsUriBuilder.Scheme.ToUpperInvariant())
156 {
157 case "HTTP":
158 webSocketsUriBuilder.Scheme = "ws";
159 break;
160 case "HTTPS":
161 webSocketsUriBuilder.Scheme = "wss";
162 break;
163 }
164  
66 office 165 if (!Uri.TryCreate(webSocketsUriBuilder.Uri, "stream", out var combinedUri))
47 office 166 {
66 office 167 Log.Error($"No WebSockets URL could be built from the provided URL {_server.Url}.");
39 office 168 }
47 office 169  
66 office 170 _webSocketsUri = combinedUri;
24 office 171 }
172  
1 office 173 public void Dispose()
174 {
175 if (_cancellationTokenSource != null)
176 {
177 _cancellationTokenSource.Dispose();
178 _cancellationTokenSource = null;
179 }
39 office 180  
61 office 181 if (_tplWebSocketsBufferBlockTransformLink != null)
182 {
183 _tplWebSocketsBufferBlockTransformLink.Dispose();
184 _tplWebSocketsBufferBlockTransformLink = null;
185 }
186  
187 if (_tplRetrievePastMessagesLink != null)
188 {
189 _tplRetrievePastMessagesLink.Dispose();
190 _tplRetrievePastMessagesLink = null;
191 }
192  
48 office 193 if (_webSocketSharp != null)
194 {
195 _webSocketSharp.Close();
196 _webSocketSharp = null;
197 }
44 office 198  
48 office 199 if (_httpClient != null)
200 {
201 _httpClient.Dispose();
202 _httpClient = null;
203 }
1 office 204 }
205  
206 #endregion
207  
208 #region Public Methods
209  
25 office 210 public void Start()
1 office 211 {
47 office 212 if (_webSocketsUri == null || _httpUri == null)
213 {
51 office 214 Log.Error("Could not start connection to server due to unreadable URLs");
47 office 215 return;
216 }
217  
1 office 218 _cancellationTokenSource = new CancellationTokenSource();
219 _cancellationToken = _cancellationTokenSource.Token;
220  
66 office 221 if (_webSocketSharp != null)
59 office 222 {
66 office 223 return;
59 office 224 }
225  
44 office 226 _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri);
61 office 227 _webSocketSharp.EmitOnPing = true;
228 _webSocketSharp.WaitTime = TimeSpan.FromMinutes(1);
50 office 229 _webSocketSharp.SslConfiguration = new ClientSslConfiguration(_webSocketsUri.Host,
230 new X509CertificateCollection(new X509Certificate[] { }), SslProtocols.Tls12, false);
80 office 231  
50 office 232 if (_configuration.Proxy.Enable)
80 office 233 {
234 if (!string.IsNullOrEmpty(_configuration.Proxy.Url))
235 {
236 _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username, _configuration.Proxy.Password);
237 }
238 }
50 office 239  
47 office 240 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
80 office 241 {
47 office 242 _webSocketSharp.SetCredentials(_server.Username, _server.Password, true);
80 office 243 }
47 office 244  
44 office 245 if (_configuration.IgnoreSelfSignedCertificates)
80 office 246 {
44 office 247 _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback +=
248 (sender, certificate, chain, errors) => true;
80 office 249 }
44 office 250  
51 office 251 _webSocketSharp.Log.Output = (logData, s) =>
252 {
253 Log.Information($"WebSockets low level logging reported: {logData.Message}");
254 };
255  
44 office 256 _webSocketSharp.OnMessage += WebSocketSharp_OnMessage;
257 _webSocketSharp.OnError += WebSocketSharp_OnError;
258 _webSocketSharp.OnOpen += WebSocketSharp_OnOpen;
259 _webSocketSharp.OnClose += WebSocketSharp_OnClose;
59 office 260  
66 office 261 _webSocketSharp.Connect();
262 _heartBeatTask = HeartBeat(_cancellationToken);
263  
264 if (_configuration.RetrievePastNotificationHours != 0)
265 {
266 _retrievePastMessagesTask = RetrievePastMessages(_cancellationToken);
267 }
1 office 268 }
269  
80 office 270 private async void WebSocketSharp_OnClose(object sender, CloseEventArgs e)
44 office 271 {
59 office 272 Log.Information(
273 $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}");
80 office 274  
275 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken);
276  
277 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}");
278  
279 await Stop().ContinueWith(task => Start(), CancellationToken.None);
44 office 280 }
1 office 281  
44 office 282 private void WebSocketSharp_OnOpen(object sender, EventArgs e)
283 {
51 office 284 Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open");
61 office 285  
286 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
44 office 287 }
1 office 288  
80 office 289 private async void OnUnresponsiveServer()
61 office 290 {
67 office 291 Log.Warning($"Server {_server.Name} has not responded in a long while...");
80 office 292  
293 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken);
294  
295 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}");
296  
297 await Stop().ContinueWith(task => Start(), CancellationToken.None);
61 office 298 }
299  
44 office 300 private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e)
1 office 301 {
59 office 302 Log.Error(
303 $"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}",
304 e.Exception);
48 office 305  
44 office 306 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken);
61 office 307  
51 office 308 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}");
18 office 309  
66 office 310 await Stop().ContinueWith(task => Start(), CancellationToken.None);
44 office 311 }
1 office 312  
44 office 313 private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e)
314 {
61 office 315 if (e.IsPing)
44 office 316 {
67 office 317 Log.Information($"Server {_server.Name} sent PING message");
1 office 318  
61 office 319 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
44 office 320 return;
321 }
12 office 322  
66 office 323 await _webSocketMessageBufferBlock.SendAsync(new GotifyConnectionData(e.RawData, _server), _cancellationToken);
44 office 324 }
12 office 325  
66 office 326 public async Task Stop()
44 office 327 {
59 office 328  
66 office 329 if (_webSocketSharp == null || _cancellationTokenSource == null)
330 {
331 return;
332 }
333  
59 office 334 _cancellationTokenSource.Cancel();
66 office 335  
68 office 336 if (_heartBeatTask != null)
337 {
338 await _heartBeatTask;
339 }
66 office 340  
68 office 341 if (_retrievePastMessagesTask != null)
342 {
343 await _retrievePastMessagesTask;
344 }
345  
66 office 346 if (_webSocketSharp != null)
347 {
348 _webSocketSharp.OnMessage -= WebSocketSharp_OnMessage;
349 _webSocketSharp.OnError -= WebSocketSharp_OnError;
350 _webSocketSharp.OnOpen -= WebSocketSharp_OnOpen;
351 _webSocketSharp.OnClose -= WebSocketSharp_OnClose;
352  
353 _webSocketSharp.Close();
354 _webSocketSharp = null;
355 }
356  
357 _cancellationTokenSource.Dispose();
358 _cancellationTokenSource = null;
44 office 359 }
360  
361 #endregion
362  
363 #region Private Methods
364  
59 office 365 private async Task RetrievePastMessages(CancellationToken cancellationToken)
366 {
66 office 367 var gotifyApplicationBufferBlock = new BufferBlock<GotifyConnectionApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken });
368 var gotifyApplicationActionBlock = new ActionBlock<GotifyConnectionApplication>(async gotifyConnectionApplication =>
59 office 369 {
66 office 370 if (!Uri.TryCreate(_httpUri, $"application/{gotifyConnectionApplication.Application.Id}/message", out var combinedUri))
59 office 371 {
66 office 372 Log.Error($"Could not get application message Uri {gotifyConnectionApplication.Application.Id}.");
59 office 373  
61 office 374 return;
59 office 375 }
376  
61 office 377 try
378 {
66 office 379 using var messageStream = await _httpClient.GetStreamAsync(combinedUri);
380 using var streamReader = new StreamReader(messageStream);
381 using var jsonTextReader = new JsonTextReader(streamReader);
382  
67 office 383 var gotifyMessageQuery = _jsonSerializer.Deserialize<GotifyMessageQuery>(jsonTextReader) ??
384 throw new ArgumentNullException();
385  
386 if (gotifyMessageQuery.Messages == null)
387 {
80 office 388 Log.Warning("Invalid application messages deserialized.");
67 office 389  
390 return;
391 }
392  
393 foreach (var message in gotifyMessageQuery.Messages)
394 {
80 office 395 if (message.Date < DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours))
67 office 396 {
397 continue;
398 }
399  
80 office 400 using var imageStream = await RetrieveGotifyApplicationImage(message.AppId);
67 office 401 if (imageStream == null || imageStream.Length == 0)
402 {
403 Log.Warning("Could not find any application image for notification.");
404  
405 continue;
406 }
407  
408 var image = new Bitmap(imageStream);
409 message.Server = gotifyConnectionApplication.Server;
410  
80 office 411 GotifyMessage?.Invoke(this, new GotifyMessageEventArgs(message, image));
67 office 412 }
61 office 413 }
66 office 414 catch (HttpRequestException exception)
61 office 415 {
66 office 416 Log.Warning(exception, $"Could not get application {gotifyConnectionApplication.Application.Id}.");
61 office 417 }
66 office 418 catch (JsonSerializationException exception)
419 {
67 office 420 Log.Warning(exception,
421 $"Could not deserialize the message response for application {gotifyConnectionApplication.Application.Id}.");
66 office 422 }
67 office 423 catch (Exception exception)
59 office 424 {
67 office 425 Log.Warning(exception, "Generic failure.");
59 office 426 }
61 office 427 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
428  
80 office 429 using var _1 = gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock,
61 office 430 new DataflowLinkOptions { PropagateCompletion = true });
431  
66 office 432 var tasks = new ConcurrentBag<Task>();
80 office 433 foreach (var application in await RetrieveGotifyApplications())
61 office 434 {
66 office 435 var gotifyConnectionApplication = new GotifyConnectionApplication(_server, application);
80 office 436  
66 office 437 tasks.Add(gotifyApplicationBufferBlock.SendAsync(gotifyConnectionApplication, cancellationToken));
59 office 438 }
61 office 439  
66 office 440 await Task.WhenAll(tasks);
61 office 441 gotifyApplicationBufferBlock.Complete();
442 await gotifyApplicationActionBlock.Completion;
59 office 443 }
444  
61 office 445 private async Task HeartBeat(CancellationToken cancellationToken)
44 office 446 {
447 try
448 {
449 do
450 {
61 office 451 await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
452  
453 _webSocketsClientPingStopWatch.Restart();
454 if (!_webSocketSharp.Ping())
455 {
67 office 456 Log.Warning($"Server {_server.Name} did not respond to PING message.");
61 office 457 continue;
458 }
459  
460 var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds;
461  
67 office 462 Log.Information($"PING response latency for {_server.Name} is {delta}ms");
61 office 463  
464 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
80 office 465  
1 office 466 } while (!cancellationToken.IsCancellationRequested);
467 }
59 office 468 catch (Exception exception) when (exception is OperationCanceledException ||
469 exception is ObjectDisposedException)
1 office 470 {
471 }
44 office 472 catch (Exception exception)
1 office 473 {
67 office 474 Log.Warning(exception, $"Heartbeat for server {_server.Name} has failed.");
1 office 475 }
476 }
477  
80 office 478 private async Task<GotifyApplication[]> RetrieveGotifyApplications()
59 office 479 {
66 office 480 if (!Uri.TryCreate(_httpUri, "application", out var combinedUri))
59 office 481 {
66 office 482 Log.Error($"No application URL could be built for {_server.Url}.");
61 office 483  
80 office 484 return Array.Empty<GotifyApplication>();
59 office 485 }
80 office 486  
59 office 487 try
488 {
66 office 489 using var messageStream = await _httpClient.GetStreamAsync(combinedUri);
490 using var streamReader = new StreamReader(messageStream);
491 using var jsonTextReader = new JsonTextReader(streamReader);
59 office 492  
80 office 493 return _jsonSerializer.Deserialize<GotifyApplication[]>(jsonTextReader);
44 office 494 }
66 office 495 catch (Exception exception)
44 office 496 {
66 office 497 Log.Warning(exception,"Could not deserialize the list of applications from the server.");
25 office 498  
80 office 499 return Array.Empty<GotifyApplication>();
39 office 500 }
61 office 501 }
502  
80 office 503 private async Task<Stream> RetrieveGotifyApplicationImage(int appId)
61 office 504 {
80 office 505 var memoryStream = new MemoryStream();
506  
507 foreach (var application in await RetrieveGotifyApplications())
61 office 508 {
66 office 509 if (application.Id != appId)
510 {
511 continue;
512 }
25 office 513  
61 office 514 if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, out var applicationImageUri))
25 office 515 {
51 office 516 Log.Warning("Could not build URL path to application icon");
66 office 517  
39 office 518 continue;
519 }
25 office 520  
61 office 521 try
522 {
80 office 523 using var imageResponse = await _httpClient.GetStreamAsync(applicationImageUri);
66 office 524  
67 office 525 await imageResponse.CopyToAsync(memoryStream);
66 office 526  
527 return memoryStream;
61 office 528 }
529 catch (Exception exception)
530 {
67 office 531 Log.Error(exception,"Could not retrieve application image.");
61 office 532 }
66 office 533 }
61 office 534  
80 office 535 return memoryStream;
66 office 536 }
25 office 537  
66 office 538 #endregion
44 office 539  
66 office 540 private class GotifyConnectionApplication
541 {
542 public GotifyApplication Application { get; }
543 public Server Server { get; }
61 office 544  
66 office 545 public GotifyConnectionApplication(Server server, GotifyApplication application)
546 {
547 Server = server;
548 Application = application;
25 office 549 }
66 office 550 }
25 office 551  
66 office 552 private class GotifyConnectionData
553 {
554 public byte[] Payload { get; }
555 public Server Server { get; }
556  
557 public GotifyConnectionData(byte[] payload, Server server)
558 {
559 Payload = payload;
560 Server = server;
561 }
25 office 562 }
1 office 563 }
564 }