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