Winify – Blame information for rev 67

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  
67 office 90 using var imageStream = await RetrieveGotifyApplicationImage(gotifyMessage.AppId, _cancellationToken);
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);
231 if (_configuration.Proxy.Enable)
59 office 232 _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username,
233 _configuration.Proxy.Password);
50 office 234  
47 office 235 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
236 _webSocketSharp.SetCredentials(_server.Username, _server.Password, true);
237  
44 office 238 if (_configuration.IgnoreSelfSignedCertificates)
239 _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback +=
240 (sender, certificate, chain, errors) => true;
241  
51 office 242 _webSocketSharp.Log.Output = (logData, s) =>
243 {
244 Log.Information($"WebSockets low level logging reported: {logData.Message}");
245 };
246  
44 office 247 _webSocketSharp.OnMessage += WebSocketSharp_OnMessage;
248 _webSocketSharp.OnError += WebSocketSharp_OnError;
249 _webSocketSharp.OnOpen += WebSocketSharp_OnOpen;
250 _webSocketSharp.OnClose += WebSocketSharp_OnClose;
59 office 251  
66 office 252 _webSocketSharp.Connect();
253 _heartBeatTask = HeartBeat(_cancellationToken);
254  
255 if (_configuration.RetrievePastNotificationHours != 0)
256 {
257 _retrievePastMessagesTask = RetrievePastMessages(_cancellationToken);
258 }
1 office 259 }
260  
44 office 261 private void WebSocketSharp_OnClose(object sender, CloseEventArgs e)
262 {
59 office 263 Log.Information(
264 $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}");
44 office 265 }
1 office 266  
44 office 267 private void WebSocketSharp_OnOpen(object sender, EventArgs e)
268 {
51 office 269 Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open");
61 office 270  
271 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
44 office 272 }
1 office 273  
61 office 274 private void OnUnresponsiveServer()
275 {
67 office 276 Log.Warning($"Server {_server.Name} has not responded in a long while...");
61 office 277 }
278  
44 office 279 private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e)
1 office 280 {
59 office 281 Log.Error(
282 $"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}",
283 e.Exception);
48 office 284  
44 office 285 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken);
61 office 286  
51 office 287 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}");
18 office 288  
66 office 289 await Stop().ContinueWith(task => Start(), CancellationToken.None);
44 office 290 }
1 office 291  
44 office 292 private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e)
293 {
61 office 294 if (e.IsPing)
44 office 295 {
67 office 296 Log.Information($"Server {_server.Name} sent PING message");
1 office 297  
61 office 298 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
44 office 299 return;
300 }
12 office 301  
66 office 302 await _webSocketMessageBufferBlock.SendAsync(new GotifyConnectionData(e.RawData, _server), _cancellationToken);
44 office 303 }
12 office 304  
66 office 305 public async Task Stop()
44 office 306 {
59 office 307  
66 office 308 if (_webSocketSharp == null || _cancellationTokenSource == null)
309 {
310 return;
311 }
312  
59 office 313 _cancellationTokenSource.Cancel();
66 office 314  
315 await _heartBeatTask;
316 await _retrievePastMessagesTask;
317  
318 if (_webSocketSharp != null)
319 {
320 _webSocketSharp.OnMessage -= WebSocketSharp_OnMessage;
321 _webSocketSharp.OnError -= WebSocketSharp_OnError;
322 _webSocketSharp.OnOpen -= WebSocketSharp_OnOpen;
323 _webSocketSharp.OnClose -= WebSocketSharp_OnClose;
324  
325 _webSocketSharp.Close();
326 _webSocketSharp = null;
327 }
328  
329 _cancellationTokenSource.Dispose();
330 _cancellationTokenSource = null;
44 office 331 }
332  
333 #endregion
334  
335 #region Private Methods
336  
59 office 337 private async Task RetrievePastMessages(CancellationToken cancellationToken)
338 {
66 office 339 var gotifyApplicationBufferBlock = new BufferBlock<GotifyConnectionApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken });
340 var gotifyApplicationActionBlock = new ActionBlock<GotifyConnectionApplication>(async gotifyConnectionApplication =>
59 office 341 {
66 office 342 if (!Uri.TryCreate(_httpUri, $"application/{gotifyConnectionApplication.Application.Id}/message", out var combinedUri))
59 office 343 {
66 office 344 Log.Error($"Could not get application message Uri {gotifyConnectionApplication.Application.Id}.");
59 office 345  
61 office 346 return;
59 office 347 }
348  
61 office 349 try
350 {
66 office 351 using var messageStream = await _httpClient.GetStreamAsync(combinedUri);
352 using var streamReader = new StreamReader(messageStream);
353 using var jsonTextReader = new JsonTextReader(streamReader);
354  
67 office 355 var gotifyMessageQuery = _jsonSerializer.Deserialize<GotifyMessageQuery>(jsonTextReader) ??
356 throw new ArgumentNullException();
357  
358 if (gotifyMessageQuery.Messages == null)
359 {
360 Log.Warning("Invalid application messages deserialized deserialized.");
361  
362 return;
363 }
364  
365 foreach (var message in gotifyMessageQuery.Messages)
366 {
367 if (message.Date <
368 DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours))
369 {
370 continue;
371 }
372  
373 using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken);
374 if (imageStream == null || imageStream.Length == 0)
375 {
376 Log.Warning("Could not find any application image for notification.");
377  
378 continue;
379 }
380  
381 var image = new Bitmap(imageStream);
382 message.Server = gotifyConnectionApplication.Server;
383  
384 GotifyMessage?.Invoke(this,
385 new GotifyMessageEventArgs(message, image));
386 }
61 office 387 }
66 office 388 catch (HttpRequestException exception)
61 office 389 {
66 office 390 Log.Warning(exception, $"Could not get application {gotifyConnectionApplication.Application.Id}.");
61 office 391 }
66 office 392 catch (JsonSerializationException exception)
393 {
67 office 394 Log.Warning(exception,
395 $"Could not deserialize the message response for application {gotifyConnectionApplication.Application.Id}.");
66 office 396 }
67 office 397 catch (Exception exception)
59 office 398 {
67 office 399 Log.Warning(exception, "Generic failure.");
59 office 400 }
61 office 401 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
402  
403 gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock,
404 new DataflowLinkOptions { PropagateCompletion = true });
405  
66 office 406 var tasks = new ConcurrentBag<Task>();
61 office 407 await foreach (var application in RetrieveGotifyApplications(cancellationToken))
408 {
66 office 409 var gotifyConnectionApplication = new GotifyConnectionApplication(_server, application);
410 tasks.Add(gotifyApplicationBufferBlock.SendAsync(gotifyConnectionApplication, cancellationToken));
59 office 411 }
61 office 412  
66 office 413 await Task.WhenAll(tasks);
61 office 414 gotifyApplicationBufferBlock.Complete();
415 await gotifyApplicationActionBlock.Completion;
59 office 416 }
417  
61 office 418 private async Task HeartBeat(CancellationToken cancellationToken)
44 office 419 {
420 try
421 {
422 do
423 {
61 office 424 await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
425  
426 _webSocketsClientPingStopWatch.Restart();
427 if (!_webSocketSharp.Ping())
428 {
67 office 429 Log.Warning($"Server {_server.Name} did not respond to PING message.");
61 office 430 continue;
431 }
432  
433 var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds;
434  
67 office 435 Log.Information($"PING response latency for {_server.Name} is {delta}ms");
61 office 436  
437 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
1 office 438 } while (!cancellationToken.IsCancellationRequested);
439 }
59 office 440 catch (Exception exception) when (exception is OperationCanceledException ||
441 exception is ObjectDisposedException)
1 office 442 {
443 }
44 office 444 catch (Exception exception)
1 office 445 {
67 office 446 Log.Warning(exception, $"Heartbeat for server {_server.Name} has failed.");
1 office 447 }
448 }
449  
61 office 450 private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken)
59 office 451 {
66 office 452 if (!Uri.TryCreate(_httpUri, "application", out var combinedUri))
59 office 453 {
66 office 454 Log.Error($"No application URL could be built for {_server.Url}.");
61 office 455  
456 yield break;
59 office 457 }
458  
66 office 459 GotifyApplication[] gotifyApplications;
59 office 460 try
461 {
66 office 462 using var messageStream = await _httpClient.GetStreamAsync(combinedUri);
463 using var streamReader = new StreamReader(messageStream);
464 using var jsonTextReader = new JsonTextReader(streamReader);
59 office 465  
66 office 466 gotifyApplications = _jsonSerializer.Deserialize<GotifyApplication[]>(jsonTextReader);
59 office 467  
66 office 468 if (gotifyApplications == null)
469 {
470 throw new ArgumentNullException();
471 }
44 office 472 }
66 office 473 catch (Exception exception)
44 office 474 {
66 office 475 Log.Warning(exception,"Could not deserialize the list of applications from the server.");
25 office 476  
61 office 477 yield break;
39 office 478 }
25 office 479  
39 office 480 foreach (var application in gotifyApplications)
481 {
61 office 482 yield return application;
483 }
484 }
485  
486 private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken)
487 {
488 await foreach (var application in RetrieveGotifyApplications(cancellationToken))
489 {
66 office 490 if (application.Id != appId)
491 {
492 continue;
493 }
25 office 494  
61 office 495 if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, out var applicationImageUri))
25 office 496 {
51 office 497 Log.Warning("Could not build URL path to application icon");
66 office 498  
39 office 499 continue;
500 }
25 office 501  
61 office 502 try
503 {
67 office 504 var imageResponse = await _httpClient.GetStreamAsync(applicationImageUri);
66 office 505  
506 var memoryStream = new MemoryStream();
507  
67 office 508 await imageResponse.CopyToAsync(memoryStream);
66 office 509  
510 return memoryStream;
61 office 511 }
512 catch (Exception exception)
513 {
67 office 514 Log.Error(exception,"Could not retrieve application image.");
61 office 515 }
66 office 516 }
61 office 517  
66 office 518 return new MemoryStream();
519 }
25 office 520  
66 office 521 #endregion
44 office 522  
66 office 523 private class GotifyConnectionApplication
524 {
525 public GotifyApplication Application { get; }
526 public Server Server { get; }
61 office 527  
66 office 528 public GotifyConnectionApplication(Server server, GotifyApplication application)
529 {
530 Server = server;
531 Application = application;
25 office 532 }
66 office 533 }
25 office 534  
66 office 535 private class GotifyConnectionData
536 {
537 public byte[] Payload { get; }
538 public Server Server { get; }
539  
540 public GotifyConnectionData(byte[] payload, Server server)
541 {
542 Payload = payload;
543 Server = server;
544 }
25 office 545 }
1 office 546 }
547 }