Winify – Blame information for rev 84

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