Mercurial > servermonitor
diff ServerMonitor/Objects/Checks/HttpCheck.cs @ 18:b713b9db4c82
HTTP checks.
author | Brad Greco <brad@bgreco.net> |
---|---|
date | Mon, 27 May 2019 15:40:44 -0400 |
parents | 68d7834dc28e |
children | 7645122aa7a9 |
line wrap: on
line diff
--- a/ServerMonitor/Objects/Checks/HttpCheck.cs Sat May 25 15:14:26 2019 -0400 +++ b/ServerMonitor/Objects/Checks/HttpCheck.cs Mon May 27 15:40:44 2019 -0400 @@ -1,68 +1,127 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; -using System.Text; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace ServerMonitorApp { + /// <summary>Executes an HTTP request and checks the result or status code.</summary> [DisplayName("HTTP check"), Description("Check the result of an HTTP request"), DisplayWeight(1)] public class HttpCheck : Check { + /// <summary>The URL to request.</summary> public string Url { get; set; } + /// <summary>The HTTP method for the request.</summary> + /// <remarks>Only HEAD and GET are supported.</remarks> + public string Method { get; set; } + + /// <summary>Whether the HTTP status code should be checked.</summary> public bool CheckResponseCode { get; set; } + /// <summary>The required response code if CheckResponseCode is true.</summary> public int ResponseCode { get; set; } + /// <summary>Whether the response lenth should be checked.</summary> public bool CheckResponseLength { get; set; } + /// <summary>The required minimum response length if CheckResponseLength is true.</summary> public string ResponseLengthMin { get; set; } + /// <summary>The required maximum response length if CheckResponseLength is true.</summary> public string ResponseLengthMax { get; set; } + /// <summary>Whether the response body should be checked.</summary> public bool CheckResponseBody { get; set; } + /// <summary>The method to use when checking the response content against the pattern.</summary> public MatchType ResponseBodyMatchType { get; set; } + /// <summary>The string or pattern that the response content must match if CheckResponseBody is true.</summary> public string ResponseBodyPattern { get; set; } + /// <summary>Whether the ResponseBodyPattern should be interpreted as a regular expression.</summary> public bool ResponseBodyUseRegex { get; set; } - protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token) + /// <summary>Executes the HTTP command on the server.</summary> + protected async override Task<CheckResult> ExecuteCheckAsync(CancellationToken token) { - throw new NotImplementedException(); - - - - - // Cancellable version that doesn't actulaly work - // might be useful as a TaskCompletionSource example though - // - //TaskCompletionSource<CheckResult> tcs = new TaskCompletionSource<CheckResult - // - //using (Ping ping = new Ping()) - //{ - // token.Register(ping.SendAsyncCancel); - // ping.PingCompleted += (sender, e) => - // { - // if (e.Error != null) - // tcs.SetResult(Fail("Ping failed: " + e.Error.GetBaseException().Message)); - // else if (e.Reply.Status != IPStatus.Success) - // tcs.SetResult(Fail("Ping failed: " + e.Reply.Status.ToString())); - // else - // tcs.SetResult(Pass("Ping completed in " + e.Reply.RoundtripTime + "ms")); - // }; - // ping.SendAsync(Server.Host, Timeout, null); - //} - - //return tcs.Task; - + try + { + // Disable auto redirect so we can get the true response code of the request. + using (HttpClientHandler handler = new HttpClientHandler() { AllowAutoRedirect = false }) + using (HttpClient client = new HttpClient(handler)) + using (HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(Method), new Uri(Url))) + { + token.Register(client.CancelPendingRequests); + HttpResponseMessage response = await client.SendAsync(request, token); + token.ThrowIfCancellationRequested(); + List<CheckResult> results = await ProcessResponse(response); + return MergeResults(results.ToArray()); + } + } + catch (Exception e) + { + return Fail(e); + } } + ///// <summary>Processes an HTTP response and checks that it matches the expected values.</summary> + ///// <param name="response">The HTTP response.</param> + ///// <returns>A list of check results according to user preferences.</returns> + protected async virtual Task<List<CheckResult>> ProcessResponse(HttpResponseMessage response) + { + List<CheckResult> results = new List<CheckResult>(); + + // Check the actual response code against the expected response code if response code checking is enabled. + if (CheckResponseCode) + results.Add(GetIntResult(ResponseCode, (int)response.StatusCode, "Response code")); + + // Check the actual response length against the expected response length if response length checking is enabled. + if (CheckResponseLength) + { + string length = null; + if (Method == "HEAD") + { + // Use the Content-Length header if a HEAD request. + if (response.Headers.TryGetValues("Content-Length", out IEnumerable<string> values)) + { + length = values.First(); + } + } + else + { + // For a GET request, read the actual length + Stream stream = await response.Content.ReadAsStreamAsync(); + length = stream.Length.ToString(); + } + if (length != null) + { + results.Add(GetStringResult(MatchType.GreaterThan, (int.Parse(ResponseLengthMin) * 1024).ToString(), false, length, "Response length")); + results.Add(GetStringResult(MatchType.LessThan, (int.Parse(ResponseLengthMax) * 1024).ToString(), false, length, "Response length")); + } + else + { + results.Add(Fail("Could not get content length")); + } + } + + // Check the actual response content against the expected response content if response content checking is enabled. + if (CheckResponseBody && Method != "HEAD") + { + string content = await response.Content.ReadAsStringAsync(); + results.Add(GetStringResult(ResponseBodyMatchType, ResponseBodyPattern, ResponseBodyUseRegex, content, "Response body")); + } + + return results; + } + + /// <summary>Validates HTTP check options.</summary> public override string Validate(bool saving = true) { string message = base.Validate(); @@ -84,78 +143,5 @@ return message; } - //protected override CheckResult GetIntResult(int expectedValue, int resultValue, string description) - //{ - // CheckResult result = base.GetIntResult(expectedValue, resultValue, description); - - //} - -/* -100 Continue[RFC7231, Section 6.2.1] -101 Switching Protocols[RFC7231, Section 6.2.2] -102 Processing[RFC2518] -103 Early Hints[RFC8297] -200 OK[RFC7231, Section 6.3.1] -201 Created[RFC7231, Section 6.3.2] -202 Accepted[RFC7231, Section 6.3.3] -203 Non-Authoritative Information[RFC7231, Section 6.3.4] -204 No Content[RFC7231, Section 6.3.5] -205 Reset Content[RFC7231, Section 6.3.6] -206 Partial Content[RFC7233, Section 4.1] -207 Multi-Status[RFC4918] -208 Already Reported[RFC5842] -226 IM Used[RFC3229] -300 Multiple Choices[RFC7231, Section 6.4.1] -301 Moved Permanently[RFC7231, Section 6.4.2] -302 Found[RFC7231, Section 6.4.3] -303 See Other[RFC7231, Section 6.4.4] -304 Not Modified[RFC7232, Section 4.1] -305 Use Proxy[RFC7231, Section 6.4.5] -306 (Unused)[RFC7231, Section 6.4.6] -307 Temporary Redirect[RFC7231, Section 6.4.7] -308 Permanent Redirect[RFC7538] -400 Bad Request[RFC7231, Section 6.5.1] -401 Unauthorized[RFC7235, Section 3.1] -402 Payment Required[RFC7231, Section 6.5.2] -403 Forbidden[RFC7231, Section 6.5.3] -404 Not Found[RFC7231, Section 6.5.4] -405 Method Not Allowed[RFC7231, Section 6.5.5] -406 Not Acceptable[RFC7231, Section 6.5.6] -407 Proxy Authentication Required[RFC7235, Section 3.2] -408 Request Timeout[RFC7231, Section 6.5.7] -409 Conflict[RFC7231, Section 6.5.8] -410 Gone[RFC7231, Section 6.5.9] -411 Length Required[RFC7231, Section 6.5.10] -412 Precondition Failed[RFC7232, Section 4.2][RFC8144, Section 3.2] -413 Payload Too Large[RFC7231, Section 6.5.11] -414 URI Too Long[RFC7231, Section 6.5.12] -415 Unsupported Media Type[RFC7231, Section 6.5.13][RFC7694, Section 3] -416 Range Not Satisfiable[RFC7233, Section 4.4] -417 Expectation Failed[RFC7231, Section 6.5.14] -421 Misdirected Request[RFC7540, Section 9.1.2] -422 Unprocessable Entity[RFC4918] -423 Locked[RFC4918] -424 Failed Dependency[RFC4918] -425 Too Early[RFC8470] -426 Upgrade Required[RFC7231, Section 6.5.15] -427 Unassigned -428 Precondition Required[RFC6585] -429 Too Many Requests[RFC6585] -430 Unassigned -431 Request Header Fields Too Large[RFC6585] -451 Unavailable For Legal Reasons[RFC7725] -500 Internal Server Error[RFC7231, Section 6.6.1] -501 Not Implemented[RFC7231, Section 6.6.2] -502 Bad Gateway[RFC7231, Section 6.6.3] -503 Service Unavailable[RFC7231, Section 6.6.4] -504 Gateway Timeout[RFC7231, Section 6.6.5] -505 HTTP Version Not Supported[RFC7231, Section 6.6.6] -506 Variant Also Negotiates[RFC2295] -507 Insufficient Storage[RFC4918] -508 Loop Detected[RFC5842] -509 Unassigned -510 Not Extended[RFC2774] -511 Network Authentication Required[RFC6585] -*/ } }