Winify – Blame information for rev 61

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