changeset 18:b713b9db4c82

HTTP checks.
author Brad Greco <brad@bgreco.net>
date Mon, 27 May 2019 15:40:44 -0400
parents 68d7834dc28e
children b3128fe10d57
files ServerMonitor/Controls/HttpCheckControl.Designer.cs ServerMonitor/Controls/HttpCheckControl.cs ServerMonitor/Objects/Checks/HttpCheck.cs ServerMonitor/ServerMonitor.csproj
diffstat 4 files changed, 145 insertions(+), 106 deletions(-) [+]
line wrap: on
line diff
--- a/ServerMonitor/Controls/HttpCheckControl.Designer.cs	Sat May 25 15:14:26 2019 -0400
+++ b/ServerMonitor/Controls/HttpCheckControl.Designer.cs	Mon May 27 15:40:44 2019 -0400
@@ -44,6 +44,8 @@
             this.ResponseCodeTextBox = new System.Windows.Forms.TextBox();
             this.HttpUrlLabel = new System.Windows.Forms.Label();
             this.UrlTextBox = new System.Windows.Forms.TextBox();
+            this.MethodLabel = new System.Windows.Forms.Label();
+            this.MethodComboBox = new System.Windows.Forms.ComboBox();
             this.CheckGroupBox.SuspendLayout();
             this.ResponseBodyPanel.SuspendLayout();
             this.ResponseLengthPanel.SuspendLayout();
@@ -52,6 +54,8 @@
             // 
             // CheckGroupBox
             // 
+            this.CheckGroupBox.Controls.Add(this.MethodComboBox);
+            this.CheckGroupBox.Controls.Add(this.MethodLabel);
             this.CheckGroupBox.Controls.Add(this.ResponseBodyPanel);
             this.CheckGroupBox.Controls.Add(this.ResponseLengthPanel);
             this.CheckGroupBox.Controls.Add(this.ResponseCodePanel);
@@ -210,7 +214,7 @@
             this.HttpUrlLabel.Location = new System.Drawing.Point(6, 25);
             this.HttpUrlLabel.Name = "HttpUrlLabel";
             this.HttpUrlLabel.Size = new System.Drawing.Size(32, 13);
-            this.HttpUrlLabel.TabIndex = 17;
+            this.HttpUrlLabel.TabIndex = 10;
             this.HttpUrlLabel.Text = "&URL:";
             // 
             // UrlTextBox
@@ -219,8 +223,30 @@
             | System.Windows.Forms.AnchorStyles.Right)));
             this.UrlTextBox.Location = new System.Drawing.Point(44, 22);
             this.UrlTextBox.Name = "UrlTextBox";
-            this.UrlTextBox.Size = new System.Drawing.Size(476, 20);
-            this.UrlTextBox.TabIndex = 18;
+            this.UrlTextBox.Size = new System.Drawing.Size(361, 20);
+            this.UrlTextBox.TabIndex = 11;
+            // 
+            // MethodLabel
+            // 
+            this.MethodLabel.AutoSize = true;
+            this.MethodLabel.Location = new System.Drawing.Point(411, 25);
+            this.MethodLabel.Name = "MethodLabel";
+            this.MethodLabel.Size = new System.Drawing.Size(46, 13);
+            this.MethodLabel.TabIndex = 12;
+            this.MethodLabel.Text = "&Method:";
+            // 
+            // MethodComboBox
+            // 
+            this.MethodComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+            this.MethodComboBox.FormattingEnabled = true;
+            this.MethodComboBox.Items.AddRange(new object[] {
+            "GET",
+            "HEAD"});
+            this.MethodComboBox.Location = new System.Drawing.Point(463, 22);
+            this.MethodComboBox.Name = "MethodComboBox";
+            this.MethodComboBox.Size = new System.Drawing.Size(57, 21);
+            this.MethodComboBox.TabIndex = 13;
+            this.MethodComboBox.SelectedIndexChanged += new System.EventHandler(this.MethodComboBox_SelectedIndexChanged);
             // 
             // HttpCheckControl
             // 
@@ -229,6 +255,7 @@
             this.BackColor = System.Drawing.SystemColors.Control;
             this.Name = "HttpCheckControl";
             this.Size = new System.Drawing.Size(526, 142);
+            this.Load += new System.EventHandler(this.HttpCheckControl_Load);
             this.CheckGroupBox.ResumeLayout(false);
             this.CheckGroupBox.PerformLayout();
             this.ResponseBodyPanel.ResumeLayout(false);
@@ -259,5 +286,7 @@
         private System.Windows.Forms.TextBox ResponseCodeTextBox;
         private System.Windows.Forms.Label HttpUrlLabel;
         private System.Windows.Forms.TextBox UrlTextBox;
+        private System.Windows.Forms.Label MethodLabel;
+        private System.Windows.Forms.ComboBox MethodComboBox;
     }
 }
--- a/ServerMonitor/Controls/HttpCheckControl.cs	Sat May 25 15:14:26 2019 -0400
+++ b/ServerMonitor/Controls/HttpCheckControl.cs	Mon May 27 15:40:44 2019 -0400
@@ -1,4 +1,6 @@
-namespace ServerMonitorApp
+using System;
+
+namespace ServerMonitorApp
 {
     /// <summary>Control for editing an HTTP check.</summary>
     [CheckType(typeof(HttpCheck))]
@@ -9,11 +11,18 @@
             InitializeComponent();
         }
 
+        private void HttpCheckControl_Load(object sender, System.EventArgs e)
+        {
+            // Initialize the combo boxes to non-empty values.
+            MethodComboBox.SelectedIndex = 0;
+        }
+
         /// <summary>Sets the values of the controls from a check's properties.</summary>
         public override void LoadCheck(Check check1)
         {
             HttpCheck check = (HttpCheck)check1;
             UrlTextBox.Text = check.Url;
+            MethodComboBox.SelectedItem = check.Method ?? "GET";
             ResponseCodeCheckBox.Checked = check.CheckResponseCode;
             ResponseCodeTextBox.Text = check.ResponseCode.ToString();
             ResponseLengthCheckbox.Checked = check.CheckResponseLength;
@@ -30,6 +39,7 @@
         {
             HttpCheck check = (HttpCheck)check1;
             check.Url = UrlTextBox.Text.Trim();
+            check.Method = MethodComboBox.SelectedItem.ToString();
             check.CheckResponseCode = ResponseCodeCheckBox.Checked;
             try
             {
@@ -47,5 +57,18 @@
             check.ResponseBodyPattern = ResponseBodyTextBox.Text;
             check.ResponseBodyUseRegex = ResponseBodyRegexCheckBox.Checked;
         }
+
+        private void MethodComboBox_SelectedIndexChanged(object sender, EventArgs e)
+        {
+            if (MethodComboBox.SelectedIndex == 1)
+            {
+                ResponseBodyCheckBox.Checked = false;
+                ResponseBodyCheckBox.Enabled = false;
+            }
+            else
+            {
+                ResponseBodyCheckBox.Enabled = true;
+            }
+        }
     }
 }
--- 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]
-*/
     }
 }
--- a/ServerMonitor/ServerMonitor.csproj	Sat May 25 15:14:26 2019 -0400
+++ b/ServerMonitor/ServerMonitor.csproj	Mon May 27 15:40:44 2019 -0400
@@ -48,6 +48,7 @@
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
+    <Reference Include="System.Net.Http" />
     <Reference Include="System.Security" />
     <Reference Include="System.Xml.Linq" />
     <Reference Include="System.Data.DataSetExtensions" />