Winify – Blame information for rev 62

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