Winify – Blame information for rev 68

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  
68 office 315 if (_heartBeatTask != null)
316 {
317 await _heartBeatTask;
318 }
66 office 319  
68 office 320 if (_retrievePastMessagesTask != null)
321 {
322 await _retrievePastMessagesTask;
323 }
324  
66 office 325 if (_webSocketSharp != null)
326 {
327 _webSocketSharp.OnMessage -= WebSocketSharp_OnMessage;
328 _webSocketSharp.OnError -= WebSocketSharp_OnError;
329 _webSocketSharp.OnOpen -= WebSocketSharp_OnOpen;
330 _webSocketSharp.OnClose -= WebSocketSharp_OnClose;
331  
332 _webSocketSharp.Close();
333 _webSocketSharp = null;
334 }
335  
336 _cancellationTokenSource.Dispose();
337 _cancellationTokenSource = null;
44 office 338 }
339  
340 #endregion
341  
342 #region Private Methods
343  
59 office 344 private async Task RetrievePastMessages(CancellationToken cancellationToken)
345 {
66 office 346 var gotifyApplicationBufferBlock = new BufferBlock<GotifyConnectionApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken });
347 var gotifyApplicationActionBlock = new ActionBlock<GotifyConnectionApplication>(async gotifyConnectionApplication =>
59 office 348 {
66 office 349 if (!Uri.TryCreate(_httpUri, $"application/{gotifyConnectionApplication.Application.Id}/message", out var combinedUri))
59 office 350 {
66 office 351 Log.Error($"Could not get application message Uri {gotifyConnectionApplication.Application.Id}.");
59 office 352  
61 office 353 return;
59 office 354 }
355  
61 office 356 try
357 {
66 office 358 using var messageStream = await _httpClient.GetStreamAsync(combinedUri);
359 using var streamReader = new StreamReader(messageStream);
360 using var jsonTextReader = new JsonTextReader(streamReader);
361  
67 office 362 var gotifyMessageQuery = _jsonSerializer.Deserialize<GotifyMessageQuery>(jsonTextReader) ??
363 throw new ArgumentNullException();
364  
365 if (gotifyMessageQuery.Messages == null)
366 {
367 Log.Warning("Invalid application messages deserialized deserialized.");
368  
369 return;
370 }
371  
372 foreach (var message in gotifyMessageQuery.Messages)
373 {
374 if (message.Date <
375 DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours))
376 {
377 continue;
378 }
379  
380 using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken);
381 if (imageStream == null || imageStream.Length == 0)
382 {
383 Log.Warning("Could not find any application image for notification.");
384  
385 continue;
386 }
387  
388 var image = new Bitmap(imageStream);
389 message.Server = gotifyConnectionApplication.Server;
390  
391 GotifyMessage?.Invoke(this,
392 new GotifyMessageEventArgs(message, image));
393 }
61 office 394 }
66 office 395 catch (HttpRequestException exception)
61 office 396 {
66 office 397 Log.Warning(exception, $"Could not get application {gotifyConnectionApplication.Application.Id}.");
61 office 398 }
66 office 399 catch (JsonSerializationException exception)
400 {
67 office 401 Log.Warning(exception,
402 $"Could not deserialize the message response for application {gotifyConnectionApplication.Application.Id}.");
66 office 403 }
67 office 404 catch (Exception exception)
59 office 405 {
67 office 406 Log.Warning(exception, "Generic failure.");
59 office 407 }
61 office 408 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
409  
410 gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock,
411 new DataflowLinkOptions { PropagateCompletion = true });
412  
66 office 413 var tasks = new ConcurrentBag<Task>();
61 office 414 await foreach (var application in RetrieveGotifyApplications(cancellationToken))
415 {
66 office 416 var gotifyConnectionApplication = new GotifyConnectionApplication(_server, application);
417 tasks.Add(gotifyApplicationBufferBlock.SendAsync(gotifyConnectionApplication, cancellationToken));
59 office 418 }
61 office 419  
66 office 420 await Task.WhenAll(tasks);
61 office 421 gotifyApplicationBufferBlock.Complete();
422 await gotifyApplicationActionBlock.Completion;
59 office 423 }
424  
61 office 425 private async Task HeartBeat(CancellationToken cancellationToken)
44 office 426 {
427 try
428 {
429 do
430 {
61 office 431 await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
432  
433 _webSocketsClientPingStopWatch.Restart();
434 if (!_webSocketSharp.Ping())
435 {
67 office 436 Log.Warning($"Server {_server.Name} did not respond to PING message.");
61 office 437 continue;
438 }
439  
440 var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds;
441  
67 office 442 Log.Information($"PING response latency for {_server.Name} is {delta}ms");
61 office 443  
444 _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken);
1 office 445 } while (!cancellationToken.IsCancellationRequested);
446 }
59 office 447 catch (Exception exception) when (exception is OperationCanceledException ||
448 exception is ObjectDisposedException)
1 office 449 {
450 }
44 office 451 catch (Exception exception)
1 office 452 {
67 office 453 Log.Warning(exception, $"Heartbeat for server {_server.Name} has failed.");
1 office 454 }
455 }
456  
61 office 457 private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken)
59 office 458 {
66 office 459 if (!Uri.TryCreate(_httpUri, "application", out var combinedUri))
59 office 460 {
66 office 461 Log.Error($"No application URL could be built for {_server.Url}.");
61 office 462  
463 yield break;
59 office 464 }
465  
66 office 466 GotifyApplication[] gotifyApplications;
59 office 467 try
468 {
66 office 469 using var messageStream = await _httpClient.GetStreamAsync(combinedUri);
470 using var streamReader = new StreamReader(messageStream);
471 using var jsonTextReader = new JsonTextReader(streamReader);
59 office 472  
66 office 473 gotifyApplications = _jsonSerializer.Deserialize<GotifyApplication[]>(jsonTextReader);
59 office 474  
66 office 475 if (gotifyApplications == null)
476 {
477 throw new ArgumentNullException();
478 }
44 office 479 }
66 office 480 catch (Exception exception)
44 office 481 {
66 office 482 Log.Warning(exception,"Could not deserialize the list of applications from the server.");
25 office 483  
61 office 484 yield break;
39 office 485 }
25 office 486  
39 office 487 foreach (var application in gotifyApplications)
488 {
61 office 489 yield return application;
490 }
491 }
492  
493 private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken)
494 {
495 await foreach (var application in RetrieveGotifyApplications(cancellationToken))
496 {
66 office 497 if (application.Id != appId)
498 {
499 continue;
500 }
25 office 501  
61 office 502 if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, out var applicationImageUri))
25 office 503 {
51 office 504 Log.Warning("Could not build URL path to application icon");
66 office 505  
39 office 506 continue;
507 }
25 office 508  
61 office 509 try
510 {
67 office 511 var imageResponse = await _httpClient.GetStreamAsync(applicationImageUri);
66 office 512  
513 var memoryStream = new MemoryStream();
514  
67 office 515 await imageResponse.CopyToAsync(memoryStream);
66 office 516  
517 return memoryStream;
61 office 518 }
519 catch (Exception exception)
520 {
67 office 521 Log.Error(exception,"Could not retrieve application image.");
61 office 522 }
66 office 523 }
61 office 524  
66 office 525 return new MemoryStream();
526 }
25 office 527  
66 office 528 #endregion
44 office 529  
66 office 530 private class GotifyConnectionApplication
531 {
532 public GotifyApplication Application { get; }
533 public Server Server { get; }
61 office 534  
66 office 535 public GotifyConnectionApplication(Server server, GotifyApplication application)
536 {
537 Server = server;
538 Application = application;
25 office 539 }
66 office 540 }
25 office 541  
66 office 542 private class GotifyConnectionData
543 {
544 public byte[] Payload { get; }
545 public Server Server { get; }
546  
547 public GotifyConnectionData(byte[] payload, Server server)
548 {
549 Payload = payload;
550 Server = server;
551 }
25 office 552 }
1 office 553 }
554 }