Winify – Blame information for rev 59

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 using System;
2 using System.Drawing;
3 using System.IO;
50 office 4 using System.Net;
1 office 5 using System.Net.Http;
6 using System.Net.Http.Headers;
50 office 7 using System.Security.Authentication;
46 office 8 using System.Security.Cryptography.X509Certificates;
1 office 9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Newtonsoft.Json;
18 office 13 using Serilog;
24 office 14 using Servers;
44 office 15 using WebSocketSharp;
50 office 16 using WebSocketSharp.Net;
44 office 17 using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
50 office 18 using NetworkCredential = System.Net.NetworkCredential;
1 office 19  
20 namespace Winify.Gotify
21 {
22 public class GotifyConnection : IDisposable
23 {
24 #region Public Events & Delegates
25  
26 public event EventHandler<GotifyNotificationEventArgs> GotifyNotification;
27  
28 #endregion
29  
30 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
31  
25 office 32 private readonly Server _server;
33  
1 office 34 private CancellationToken _cancellationToken;
35  
36 private CancellationTokenSource _cancellationTokenSource;
37  
38 private Task _runTask;
39  
39 office 40 private HttpClient _httpClient;
41  
42 private readonly Uri _webSocketsUri;
43  
44 private readonly Uri _httpUri;
44 office 45 private WebSocket _webSocketSharp;
59 office 46 private readonly Configuration.Configuration _configuration;
47 private Task _initTask;
39 office 48  
25 office 49 #endregion
1 office 50  
25 office 51 #region Constructors, Destructors and Finalizers
24 office 52  
44 office 53 private GotifyConnection()
24 office 54 {
39 office 55 }
56  
59 office 57 public GotifyConnection(Server server, Configuration.Configuration configuration) : this()
39 office 58 {
24 office 59 _server = server;
44 office 60 _configuration = configuration;
39 office 61  
54 office 62 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
59 office 63 var httpClientHandler = new HttpClientHandler
50 office 64 {
54 office 65 // mono does not implement this
66 //SslProtocols = SslProtocols.Tls12
50 office 67 };
68  
44 office 69 _httpClient = new HttpClient(httpClientHandler);
70 if (_configuration.IgnoreSelfSignedCertificates)
71 httpClientHandler.ServerCertificateCustomValidationCallback =
46 office 72 (httpRequestMessage, cert, cetChain, policyErrors) => true;
39 office 73  
50 office 74 if (_configuration.Proxy.Enable)
75 httpClientHandler.Proxy = new WebProxy(_configuration.Proxy.Url, false, new string[] { },
76 new NetworkCredential(_configuration.Proxy.Username, _configuration.Proxy.Password));
77  
47 office 78 _httpClient = new HttpClient(httpClientHandler);
79 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
80 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
81 Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}")));
44 office 82  
47 office 83 if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri))
39 office 84 {
51 office 85 Log.Error($"No HTTP URL could be built out of the supplied server URI {_server.Url}");
47 office 86 return;
87 }
44 office 88  
47 office 89 // Build the web sockets URI.
90 var webSocketsUriBuilder = new UriBuilder(_httpUri);
91 switch (webSocketsUriBuilder.Scheme.ToUpperInvariant())
92 {
93 case "HTTP":
94 webSocketsUriBuilder.Scheme = "ws";
95 break;
96 case "HTTPS":
97 webSocketsUriBuilder.Scheme = "wss";
98 break;
99 }
100  
101 try
102 {
39 office 103 webSocketsUriBuilder.Path = Path.Combine(webSocketsUriBuilder.Path, "stream");
104 }
47 office 105 catch (ArgumentException exception)
106 {
59 office 107 Log.Error(
108 $"No WebSockets URL could be built from the provided URL {_server.Url} due to {exception.Message}");
47 office 109 }
110  
111 _webSocketsUri = webSocketsUriBuilder.Uri;
24 office 112 }
113  
1 office 114 public void Dispose()
115 {
116 if (_cancellationTokenSource != null)
117 {
118 _cancellationTokenSource.Dispose();
119 _cancellationTokenSource = null;
120 }
39 office 121  
48 office 122 if (_webSocketSharp != null)
123 {
124 _webSocketSharp.Close();
125 _webSocketSharp = null;
126 }
44 office 127  
48 office 128 if (_httpClient != null)
129 {
130 _httpClient.Dispose();
131 _httpClient = null;
132 }
1 office 133 }
134  
135 #endregion
136  
137 #region Public Methods
138  
25 office 139 public void Start()
1 office 140 {
47 office 141 if (_webSocketsUri == null || _httpUri == null)
142 {
51 office 143 Log.Error("Could not start connection to server due to unreadable URLs");
47 office 144 return;
145 }
146  
1 office 147 _cancellationTokenSource = new CancellationTokenSource();
148 _cancellationToken = _cancellationTokenSource.Token;
149  
44 office 150 Connect();
151  
59 office 152 if (_configuration.RetrievePastNotificationHours != 0)
153 {
154 _initTask = RetrievePastMessages(_cancellationToken);
155 }
156  
39 office 157 _runTask = Run(_cancellationToken);
1 office 158 }
159  
44 office 160 private void Connect()
1 office 161 {
44 office 162 _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri);
50 office 163 _webSocketSharp.SslConfiguration = new ClientSslConfiguration(_webSocketsUri.Host,
164 new X509CertificateCollection(new X509Certificate[] { }), SslProtocols.Tls12, false);
165 if (_configuration.Proxy.Enable)
59 office 166 _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username,
167 _configuration.Proxy.Password);
50 office 168  
47 office 169 if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password))
170 _webSocketSharp.SetCredentials(_server.Username, _server.Password, true);
171  
44 office 172 if (_configuration.IgnoreSelfSignedCertificates)
173 _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback +=
174 (sender, certificate, chain, errors) => true;
175  
51 office 176 _webSocketSharp.Log.Output = (logData, s) =>
177 {
178 Log.Information($"WebSockets low level logging reported: {logData.Message}");
179 };
180  
44 office 181 _webSocketSharp.OnMessage += WebSocketSharp_OnMessage;
182 _webSocketSharp.OnError += WebSocketSharp_OnError;
183 _webSocketSharp.OnOpen += WebSocketSharp_OnOpen;
184 _webSocketSharp.OnClose += WebSocketSharp_OnClose;
59 office 185  
44 office 186 _webSocketSharp.ConnectAsync();
1 office 187 }
188  
44 office 189 private void WebSocketSharp_OnClose(object sender, CloseEventArgs e)
190 {
59 office 191 Log.Information(
192 $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}");
44 office 193 }
1 office 194  
44 office 195 private void WebSocketSharp_OnOpen(object sender, EventArgs e)
196 {
51 office 197 Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open");
44 office 198 }
1 office 199  
44 office 200 private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e)
1 office 201 {
59 office 202 Log.Error(
203 $"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}",
204 e.Exception);
48 office 205  
44 office 206 if (_cancellationToken.IsCancellationRequested)
1 office 207 {
44 office 208 Stop();
209 return;
210 }
18 office 211  
44 office 212 await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken);
51 office 213 Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}");
18 office 214  
44 office 215 Connect();
216 }
1 office 217  
44 office 218 private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e)
219 {
220 if (e.RawData.Length == 0)
221 {
59 office 222 Log.Warning("Empty message received from server");
44 office 223 return;
224 }
1 office 225  
44 office 226 var message = Encoding.UTF8.GetString(e.RawData, 0, e.RawData.Length);
1 office 227  
59 office 228 GotifyMessage gotifyNotification;
1 office 229  
44 office 230 try
59 office 231 {
232 gotifyNotification = JsonConvert.DeserializeObject<GotifyMessage>(message);
44 office 233 }
234 catch (JsonSerializationException exception)
235 {
236 Log.Warning($"Could not deserialize notification: {exception.Message}");
237 return;
238 }
12 office 239  
44 office 240 if (gotifyNotification == null)
241 {
242 Log.Warning($"Could not deserialize gotify notification: {message}");
28 office 243  
44 office 244 return;
245 }
25 office 246  
44 office 247 gotifyNotification.Server = _server;
1 office 248  
59 office 249 var applicationUriBuilder = new UriBuilder(_httpUri);
250 try
44 office 251 {
59 office 252 applicationUriBuilder.Path = Path.Combine(applicationUriBuilder.Path, "application");
253 }
254 catch (ArgumentException exception)
255 {
256 Log.Warning("Could not build an URI to an application");
257  
44 office 258 return;
259 }
24 office 260  
44 office 261 using (var imageStream =
59 office 262 await RetrieveGotifyApplicationImage(gotifyNotification.AppId, applicationUriBuilder.Uri,
263 _cancellationToken))
44 office 264 {
265 if (imageStream == null)
266 {
51 office 267 Log.Warning("Could not find any application image for notification");
44 office 268 return;
269 }
3 office 270  
44 office 271 var image = Image.FromStream(imageStream);
12 office 272  
44 office 273 GotifyNotification?.Invoke(this,
274 new GotifyNotificationEventArgs(gotifyNotification, image));
275 }
3 office 276  
44 office 277 Log.Debug($"Notification message received: {gotifyNotification.Message}");
278 }
12 office 279  
44 office 280 public void Stop()
281 {
59 office 282 if (_cancellationTokenSource == null) return;
283  
284 _cancellationTokenSource.Cancel();
44 office 285 }
286  
287 #endregion
288  
289 #region Private Methods
290  
59 office 291 private async Task RetrievePastMessages(CancellationToken cancellationToken)
292 {
293 var messageUriBuilder = new UriBuilder(_httpUri);
294 foreach (var application in await RetrieveGotifyApplications(cancellationToken))
295 {
296 try
297 {
298 messageUriBuilder.Path = Path.Combine(messageUriBuilder.Path, "application", $"{application.Id}",
299 "message");
300 }
301 catch (ArgumentException exception)
302 {
303 Log.Error($"No application URL could be built for {_server.Url} due to {exception.Message}");
304  
305 continue;
306 }
307  
308 var messagesResponse = await _httpClient.GetAsync(messageUriBuilder.Uri, cancellationToken);
309  
310  
311 var messages = await messagesResponse.Content.ReadAsStringAsync();
312  
313 GotifyMessageQuery gotifyMessageQuery;
314 try
315 {
316 gotifyMessageQuery =
317 JsonConvert.DeserializeObject<GotifyMessageQuery>(messages);
318 }
319 catch (JsonSerializationException exception)
320 {
321 Log.Warning($"Could not deserialize the message response: {exception.Message}");
322  
323 continue;
324 }
325  
326 var applicationUriBuilder = new UriBuilder(_httpUri);
327 try
328 {
329 applicationUriBuilder.Path = Path.Combine(applicationUriBuilder.Path, "application");
330 }
331 catch (ArgumentException exception)
332 {
333 Log.Warning($"Could not build an URI to an application: {exception}");
334  
335 return;
336 }
337  
338 foreach (var message in gotifyMessageQuery.Messages)
339 {
340 if (message.Date < DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours))
341 continue;
342  
343 message.Server = _server;
344  
345 using (var imageStream =
346 await RetrieveGotifyApplicationImage(message.AppId, applicationUriBuilder.Uri,
347 _cancellationToken))
348 {
349 if (imageStream == null)
350 {
351 Log.Warning("Could not find any application image for notification");
352 return;
353 }
354  
355 var image = Image.FromStream(imageStream);
356  
357 GotifyNotification?.Invoke(this,
358 new GotifyNotificationEventArgs(message, image));
359 }
360 }
361 }
362 }
363  
44 office 364 private async Task Run(CancellationToken cancellationToken)
365 {
366 try
367 {
368 do
369 {
370 await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
1 office 371 } while (!cancellationToken.IsCancellationRequested);
372 }
59 office 373 catch (Exception exception) when (exception is OperationCanceledException ||
374 exception is ObjectDisposedException)
1 office 375 {
376 }
44 office 377 catch (Exception exception)
1 office 378 {
51 office 379 Log.Warning(exception, "Failure running connection loop");
1 office 380 }
381 }
382  
59 office 383 private async Task<GotifyApplication[]> RetrieveGotifyApplications(CancellationToken cancellationToken)
384 {
385 var applicationsUriBuilder = new UriBuilder(_httpUri);
386 try
387 {
388 applicationsUriBuilder.Path = Path.Combine(applicationsUriBuilder.Path, "application");
389 }
390 catch (ArgumentException exception)
391 {
392 Log.Error($"No application URL could be built for {_server.Url} due to {exception}");
393 }
394  
395 var applicationsResponse = await _httpClient.GetAsync(applicationsUriBuilder.Uri, cancellationToken);
396  
397 var applications = await applicationsResponse.Content.ReadAsStringAsync();
398  
399 GotifyApplication[] gotifyApplications;
400 try
401 {
402 gotifyApplications =
403 JsonConvert.DeserializeObject<GotifyApplication[]>(applications);
404 }
405 catch (JsonSerializationException exception)
406 {
407 Log.Warning($"Could not deserialize the list of applications from the server: {exception}");
408  
409 return null;
410 }
411  
412 return gotifyApplications;
413 }
414  
44 office 415 private async Task<Stream> RetrieveGotifyApplicationImage(int appId, Uri applicationUri,
28 office 416 CancellationToken cancellationToken)
25 office 417 {
39 office 418 var applicationResponse = await _httpClient.GetAsync(applicationUri, cancellationToken);
25 office 419  
39 office 420 var applications = await applicationResponse.Content.ReadAsStringAsync();
25 office 421  
44 office 422 GotifyApplication[] gotifyApplications;
423 try
424 {
425 gotifyApplications =
426 JsonConvert.DeserializeObject<GotifyApplication[]>(applications);
427 }
428 catch (JsonSerializationException exception)
429 {
430 Log.Warning($"Could not deserialize the list of applications from the server: {exception.Message}");
25 office 431  
39 office 432 return null;
433 }
25 office 434  
39 office 435 foreach (var application in gotifyApplications)
436 {
44 office 437 if (application.Id != appId) continue;
25 office 438  
39 office 439 if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute,
440 out var applicationImageUri))
25 office 441 {
51 office 442 Log.Warning("Could not build URL path to application icon");
39 office 443 continue;
444 }
25 office 445  
39 office 446 var imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken);
25 office 447  
44 office 448 var memoryStream = new MemoryStream();
25 office 449  
44 office 450 await imageResponse.Content.CopyToAsync(memoryStream);
451  
452 return memoryStream;
25 office 453 }
454  
455 return null;
456 }
457  
1 office 458 #endregion
459 }
460 }