Mercurial > servermonitor
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" />