Winify – Blame information for rev 64

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