Winify – Blame information for rev 48
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | using System; |
2 | using System.Drawing; |
||
3 | using System.IO; |
||
4 | using System.Net.Http; |
||
5 | using System.Net.Http.Headers; |
||
46 | office | 6 | using System.Net.Security; |
7 | using System.Security.Cryptography.X509Certificates; |
||
1 | office | 8 | using System.Text; |
9 | using System.Threading; |
||
10 | using System.Threading.Tasks; |
||
11 | using Newtonsoft.Json; |
||
18 | office | 12 | using Serilog; |
24 | office | 13 | using Servers; |
44 | office | 14 | using WebSocketSharp; |
15 | using Configuration = Configuration.Configuration; |
||
16 | using ErrorEventArgs = WebSocketSharp.ErrorEventArgs; |
||
1 | office | 17 | |
18 | namespace Winify.Gotify |
||
19 | { |
||
20 | public class GotifyConnection : IDisposable |
||
21 | { |
||
22 | #region Public Events & Delegates |
||
23 | |||
24 | public event EventHandler<GotifyNotificationEventArgs> GotifyNotification; |
||
25 | |||
26 | #endregion |
||
27 | |||
28 | #region Private Delegates, Events, Enums, Properties, Indexers and Fields |
||
29 | |||
25 | office | 30 | private readonly Server _server; |
31 | |||
1 | office | 32 | private CancellationToken _cancellationToken; |
33 | |||
34 | private CancellationTokenSource _cancellationTokenSource; |
||
35 | |||
36 | private Task _runTask; |
||
37 | |||
39 | office | 38 | private HttpClient _httpClient; |
39 | |||
40 | private readonly Uri _webSocketsUri; |
||
41 | |||
42 | private readonly Uri _httpUri; |
||
44 | office | 43 | private WebSocket _webSocketSharp; |
44 | private readonly global::Configuration.Configuration _configuration; |
||
39 | office | 45 | |
25 | office | 46 | #endregion |
1 | office | 47 | |
25 | office | 48 | #region Constructors, Destructors and Finalizers |
24 | office | 49 | |
44 | office | 50 | private GotifyConnection() |
24 | office | 51 | { |
44 | office | 52 | |
39 | office | 53 | } |
54 | |||
44 | office | 55 | public GotifyConnection(Server server, global::Configuration.Configuration configuration) : this() |
39 | office | 56 | { |
24 | office | 57 | _server = server; |
44 | office | 58 | _configuration = configuration; |
39 | office | 59 | |
44 | office | 60 | var httpClientHandler = new HttpClientHandler(); |
61 | _httpClient = new HttpClient(httpClientHandler); |
||
62 | if (_configuration.IgnoreSelfSignedCertificates) |
||
63 | { |
||
64 | httpClientHandler.ServerCertificateCustomValidationCallback = |
||
46 | office | 65 | (httpRequestMessage, cert, cetChain, policyErrors) => true; |
44 | office | 66 | } |
39 | office | 67 | |
47 | office | 68 | _httpClient = new HttpClient(httpClientHandler); |
69 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
||
44 | office | 70 | { |
47 | office | 71 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", |
72 | Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}"))); |
||
73 | } |
||
44 | office | 74 | |
47 | office | 75 | if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri)) |
39 | office | 76 | { |
47 | office | 77 | Log.Error($"No HTTP URL could be built out of the supplied server URI {_server.Url}."); |
78 | return; |
||
79 | } |
||
44 | office | 80 | |
47 | office | 81 | // Build the web sockets URI. |
82 | var webSocketsUriBuilder = new UriBuilder(_httpUri); |
||
83 | switch (webSocketsUriBuilder.Scheme.ToUpperInvariant()) |
||
84 | { |
||
85 | case "HTTP": |
||
86 | webSocketsUriBuilder.Scheme = "ws"; |
||
87 | break; |
||
88 | case "HTTPS": |
||
89 | webSocketsUriBuilder.Scheme = "wss"; |
||
90 | break; |
||
91 | } |
||
92 | |||
93 | try |
||
94 | { |
||
39 | office | 95 | webSocketsUriBuilder.Path = Path.Combine(webSocketsUriBuilder.Path, "stream"); |
96 | } |
||
47 | office | 97 | catch (ArgumentException exception) |
98 | { |
||
99 | Log.Error($"No WebSockets URL could be built from the provided URL {_server.Url} due to {exception.Message}."); |
||
100 | } |
||
101 | |||
102 | _webSocketsUri = webSocketsUriBuilder.Uri; |
||
24 | office | 103 | } |
104 | |||
1 | office | 105 | public void Dispose() |
106 | { |
||
107 | if (_cancellationTokenSource != null) |
||
108 | { |
||
109 | _cancellationTokenSource.Dispose(); |
||
110 | _cancellationTokenSource = null; |
||
111 | } |
||
39 | office | 112 | |
48 | office | 113 | if (_webSocketSharp != null) |
114 | { |
||
115 | _webSocketSharp.Close(); |
||
116 | _webSocketSharp = null; |
||
117 | } |
||
44 | office | 118 | |
48 | office | 119 | if (_httpClient != null) |
120 | { |
||
121 | _httpClient.Dispose(); |
||
122 | _httpClient = null; |
||
123 | } |
||
1 | office | 124 | } |
125 | |||
126 | #endregion |
||
127 | |||
128 | #region Public Methods |
||
129 | |||
25 | office | 130 | public void Start() |
1 | office | 131 | { |
47 | office | 132 | if (_webSocketsUri == null || _httpUri == null) |
133 | { |
||
134 | Log.Error("Could not start connection to server due to unreadable URLs."); |
||
135 | return; |
||
136 | } |
||
137 | |||
1 | office | 138 | _cancellationTokenSource = new CancellationTokenSource(); |
139 | _cancellationToken = _cancellationTokenSource.Token; |
||
140 | |||
44 | office | 141 | Connect(); |
142 | |||
39 | office | 143 | _runTask = Run(_cancellationToken); |
1 | office | 144 | } |
145 | |||
44 | office | 146 | private void Connect() |
1 | office | 147 | { |
44 | office | 148 | _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri); |
47 | office | 149 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
150 | { |
||
151 | _webSocketSharp.SetCredentials(_server.Username, _server.Password, true); |
||
152 | } |
||
153 | |||
44 | office | 154 | if (_configuration.IgnoreSelfSignedCertificates) |
39 | office | 155 | { |
44 | office | 156 | _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback += |
157 | (sender, certificate, chain, errors) => true; |
||
39 | office | 158 | } |
44 | office | 159 | |
160 | _webSocketSharp.OnMessage += WebSocketSharp_OnMessage; |
||
161 | _webSocketSharp.OnError += WebSocketSharp_OnError; |
||
162 | _webSocketSharp.OnOpen += WebSocketSharp_OnOpen; |
||
163 | _webSocketSharp.OnClose += WebSocketSharp_OnClose; |
||
164 | _webSocketSharp.ConnectAsync(); |
||
1 | office | 165 | } |
166 | |||
44 | office | 167 | private void WebSocketSharp_OnClose(object sender, CloseEventArgs e) |
168 | { |
||
48 | office | 169 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}."); |
44 | office | 170 | } |
1 | office | 171 | |
44 | office | 172 | private void WebSocketSharp_OnOpen(object sender, EventArgs e) |
173 | { |
||
48 | office | 174 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open."); |
44 | office | 175 | } |
1 | office | 176 | |
44 | office | 177 | private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e) |
1 | office | 178 | { |
48 | office | 179 | Log.Error($"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}.", e.Exception); |
180 | |||
44 | office | 181 | if (_cancellationToken.IsCancellationRequested) |
1 | office | 182 | { |
44 | office | 183 | Stop(); |
184 | return; |
||
185 | } |
||
18 | office | 186 | |
44 | office | 187 | await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken); |
48 | office | 188 | Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}."); |
18 | office | 189 | |
44 | office | 190 | Connect(); |
191 | } |
||
1 | office | 192 | |
44 | office | 193 | private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e) |
194 | { |
||
195 | if (e.RawData.Length == 0) |
||
196 | { |
||
197 | Log.Warning($"Empty message received from server."); |
||
198 | return; |
||
199 | } |
||
1 | office | 200 | |
44 | office | 201 | var message = Encoding.UTF8.GetString(e.RawData, 0, e.RawData.Length); |
1 | office | 202 | |
44 | office | 203 | GotifyNotification gotifyNotification; |
1 | office | 204 | |
44 | office | 205 | try |
206 | { |
||
207 | gotifyNotification = JsonConvert.DeserializeObject<GotifyNotification>(message); |
||
208 | } |
||
209 | catch (JsonSerializationException exception) |
||
210 | { |
||
211 | Log.Warning($"Could not deserialize notification: {exception.Message}"); |
||
212 | return; |
||
213 | } |
||
12 | office | 214 | |
44 | office | 215 | if (gotifyNotification == null) |
216 | { |
||
217 | Log.Warning($"Could not deserialize gotify notification: {message}"); |
||
28 | office | 218 | |
44 | office | 219 | return; |
220 | } |
||
25 | office | 221 | |
44 | office | 222 | gotifyNotification.Server = _server; |
1 | office | 223 | |
44 | office | 224 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", "application"), UriKind.Absolute, |
225 | out var applicationUri)) |
||
226 | { |
||
227 | Log.Warning($"Could not build an URI to an application."); |
||
228 | return; |
||
229 | } |
||
24 | office | 230 | |
44 | office | 231 | using (var imageStream = |
232 | await RetrieveGotifyApplicationImage(gotifyNotification.AppId, applicationUri, _cancellationToken)) |
||
233 | { |
||
234 | if (imageStream == null) |
||
235 | { |
||
236 | Log.Warning("Could not find any application image for notification."); |
||
237 | return; |
||
238 | } |
||
3 | office | 239 | |
44 | office | 240 | var image = Image.FromStream(imageStream); |
12 | office | 241 | |
44 | office | 242 | GotifyNotification?.Invoke(this, |
243 | new GotifyNotificationEventArgs(gotifyNotification, image)); |
||
244 | } |
||
3 | office | 245 | |
44 | office | 246 | Log.Debug($"Notification message received: {gotifyNotification.Message}"); |
247 | } |
||
12 | office | 248 | |
44 | office | 249 | public void Stop() |
250 | { |
||
251 | if (_cancellationTokenSource != null) _cancellationTokenSource.Cancel(); |
||
252 | } |
||
253 | |||
254 | #endregion |
||
255 | |||
256 | #region Private Methods |
||
257 | |||
258 | private async Task Run(CancellationToken cancellationToken) |
||
259 | { |
||
260 | try |
||
261 | { |
||
262 | do |
||
263 | { |
||
264 | await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); |
||
1 | office | 265 | } while (!cancellationToken.IsCancellationRequested); |
266 | } |
||
44 | office | 267 | catch (Exception exception) when (exception is OperationCanceledException || exception is ObjectDisposedException) |
1 | office | 268 | { |
269 | } |
||
44 | office | 270 | catch (Exception exception) |
1 | office | 271 | { |
44 | office | 272 | Log.Warning(exception, "Failure running connection loop."); |
1 | office | 273 | } |
274 | } |
||
275 | |||
44 | office | 276 | private async Task<Stream> RetrieveGotifyApplicationImage(int appId, Uri applicationUri, |
28 | office | 277 | CancellationToken cancellationToken) |
25 | office | 278 | { |
39 | office | 279 | var applicationResponse = await _httpClient.GetAsync(applicationUri, cancellationToken); |
25 | office | 280 | |
39 | office | 281 | var applications = await applicationResponse.Content.ReadAsStringAsync(); |
25 | office | 282 | |
44 | office | 283 | GotifyApplication[] gotifyApplications; |
284 | try |
||
285 | { |
||
286 | gotifyApplications = |
||
287 | JsonConvert.DeserializeObject<GotifyApplication[]>(applications); |
||
288 | } |
||
289 | catch (JsonSerializationException exception) |
||
290 | { |
||
291 | Log.Warning($"Could not deserialize the list of applications from the server: {exception.Message}"); |
||
25 | office | 292 | |
39 | office | 293 | return null; |
294 | } |
||
25 | office | 295 | |
39 | office | 296 | foreach (var application in gotifyApplications) |
297 | { |
||
44 | office | 298 | if (application.Id != appId) continue; |
25 | office | 299 | |
39 | office | 300 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, |
301 | out var applicationImageUri)) |
||
25 | office | 302 | { |
44 | office | 303 | Log.Warning("Could not build URL path to application icon."); |
39 | office | 304 | continue; |
305 | } |
||
25 | office | 306 | |
39 | office | 307 | var imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken); |
25 | office | 308 | |
44 | office | 309 | var memoryStream = new MemoryStream(); |
25 | office | 310 | |
44 | office | 311 | await imageResponse.Content.CopyToAsync(memoryStream); |
312 | |||
313 | return memoryStream; |
||
25 | office | 314 | } |
315 | |||
316 | return null; |
||
317 | } |
||
318 | |||
1 | office | 319 | #endregion |
320 | } |
||
321 | } |