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]
-*/
     }
 }