Winify – Blame information for rev 66

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