Winify – Blame information for rev 59
?pathlinks?
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 | } |