Winify – Blame information for rev 50
?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 | { |
47 | office | 92 | Log.Error($"No HTTP URL could be built out of the supplied server URI {_server.Url}."); |
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 | { |
||
114 | Log.Error($"No WebSockets URL could be built from the provided URL {_server.Url} due to {exception.Message}."); |
||
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 | { |
||
149 | Log.Error("Could not start connection to server due to unreadable URLs."); |
||
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 | |
182 | _webSocketSharp.OnMessage += WebSocketSharp_OnMessage; |
||
183 | _webSocketSharp.OnError += WebSocketSharp_OnError; |
||
184 | _webSocketSharp.OnOpen += WebSocketSharp_OnOpen; |
||
185 | _webSocketSharp.OnClose += WebSocketSharp_OnClose; |
||
50 | office | 186 | _webSocketSharp.Log.Output = (data, s) => |
187 | { |
||
188 | var a = s; |
||
189 | }; |
||
44 | office | 190 | _webSocketSharp.ConnectAsync(); |
1 | office | 191 | } |
192 | |||
44 | office | 193 | private void WebSocketSharp_OnClose(object sender, CloseEventArgs e) |
194 | { |
||
48 | office | 195 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}."); |
44 | office | 196 | } |
1 | office | 197 | |
44 | office | 198 | private void WebSocketSharp_OnOpen(object sender, EventArgs e) |
199 | { |
||
48 | office | 200 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open."); |
44 | office | 201 | } |
1 | office | 202 | |
44 | office | 203 | private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e) |
1 | office | 204 | { |
48 | office | 205 | Log.Error($"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}.", e.Exception); |
206 | |||
44 | office | 207 | if (_cancellationToken.IsCancellationRequested) |
1 | office | 208 | { |
44 | office | 209 | Stop(); |
210 | return; |
||
211 | } |
||
18 | office | 212 | |
44 | office | 213 | await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken); |
48 | office | 214 | Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}."); |
18 | office | 215 | |
44 | office | 216 | Connect(); |
217 | } |
||
1 | office | 218 | |
44 | office | 219 | private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e) |
220 | { |
||
221 | if (e.RawData.Length == 0) |
||
222 | { |
||
223 | Log.Warning($"Empty message received from server."); |
||
224 | return; |
||
225 | } |
||
1 | office | 226 | |
44 | office | 227 | var message = Encoding.UTF8.GetString(e.RawData, 0, e.RawData.Length); |
1 | office | 228 | |
44 | office | 229 | GotifyNotification gotifyNotification; |
1 | office | 230 | |
44 | office | 231 | try |
232 | { |
||
233 | gotifyNotification = JsonConvert.DeserializeObject<GotifyNotification>(message); |
||
234 | } |
||
235 | catch (JsonSerializationException exception) |
||
236 | { |
||
237 | Log.Warning($"Could not deserialize notification: {exception.Message}"); |
||
238 | return; |
||
239 | } |
||
12 | office | 240 | |
44 | office | 241 | if (gotifyNotification == null) |
242 | { |
||
243 | Log.Warning($"Could not deserialize gotify notification: {message}"); |
||
28 | office | 244 | |
44 | office | 245 | return; |
246 | } |
||
25 | office | 247 | |
44 | office | 248 | gotifyNotification.Server = _server; |
1 | office | 249 | |
44 | office | 250 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", "application"), UriKind.Absolute, |
251 | out var applicationUri)) |
||
252 | { |
||
253 | Log.Warning($"Could not build an URI to an application."); |
||
254 | return; |
||
255 | } |
||
24 | office | 256 | |
44 | office | 257 | using (var imageStream = |
258 | await RetrieveGotifyApplicationImage(gotifyNotification.AppId, applicationUri, _cancellationToken)) |
||
259 | { |
||
260 | if (imageStream == null) |
||
261 | { |
||
262 | Log.Warning("Could not find any application image for notification."); |
||
263 | return; |
||
264 | } |
||
3 | office | 265 | |
44 | office | 266 | var image = Image.FromStream(imageStream); |
12 | office | 267 | |
44 | office | 268 | GotifyNotification?.Invoke(this, |
269 | new GotifyNotificationEventArgs(gotifyNotification, image)); |
||
270 | } |
||
3 | office | 271 | |
44 | office | 272 | Log.Debug($"Notification message received: {gotifyNotification.Message}"); |
273 | } |
||
12 | office | 274 | |
44 | office | 275 | public void Stop() |
276 | { |
||
277 | if (_cancellationTokenSource != null) _cancellationTokenSource.Cancel(); |
||
278 | } |
||
279 | |||
280 | #endregion |
||
281 | |||
282 | #region Private Methods |
||
283 | |||
284 | private async Task Run(CancellationToken cancellationToken) |
||
285 | { |
||
286 | try |
||
287 | { |
||
288 | do |
||
289 | { |
||
290 | await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); |
||
1 | office | 291 | } while (!cancellationToken.IsCancellationRequested); |
292 | } |
||
44 | office | 293 | catch (Exception exception) when (exception is OperationCanceledException || exception is ObjectDisposedException) |
1 | office | 294 | { |
295 | } |
||
44 | office | 296 | catch (Exception exception) |
1 | office | 297 | { |
44 | office | 298 | Log.Warning(exception, "Failure running connection loop."); |
1 | office | 299 | } |
300 | } |
||
301 | |||
44 | office | 302 | private async Task<Stream> RetrieveGotifyApplicationImage(int appId, Uri applicationUri, |
28 | office | 303 | CancellationToken cancellationToken) |
25 | office | 304 | { |
39 | office | 305 | var applicationResponse = await _httpClient.GetAsync(applicationUri, cancellationToken); |
25 | office | 306 | |
39 | office | 307 | var applications = await applicationResponse.Content.ReadAsStringAsync(); |
25 | office | 308 | |
44 | office | 309 | GotifyApplication[] gotifyApplications; |
310 | try |
||
311 | { |
||
312 | gotifyApplications = |
||
313 | JsonConvert.DeserializeObject<GotifyApplication[]>(applications); |
||
314 | } |
||
315 | catch (JsonSerializationException exception) |
||
316 | { |
||
317 | Log.Warning($"Could not deserialize the list of applications from the server: {exception.Message}"); |
||
25 | office | 318 | |
39 | office | 319 | return null; |
320 | } |
||
25 | office | 321 | |
39 | office | 322 | foreach (var application in gotifyApplications) |
323 | { |
||
44 | office | 324 | if (application.Id != appId) continue; |
25 | office | 325 | |
39 | office | 326 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, |
327 | out var applicationImageUri)) |
||
25 | office | 328 | { |
44 | office | 329 | Log.Warning("Could not build URL path to application icon."); |
39 | office | 330 | continue; |
331 | } |
||
25 | office | 332 | |
39 | office | 333 | var imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken); |
25 | office | 334 | |
44 | office | 335 | var memoryStream = new MemoryStream(); |
25 | office | 336 | |
44 | office | 337 | await imageResponse.Content.CopyToAsync(memoryStream); |
338 | |||
339 | return memoryStream; |
||
25 | office | 340 | } |
341 | |||
342 | return null; |
||
343 | } |
||
344 | |||
1 | office | 345 | #endregion |
346 | } |
||
347 | } |