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