// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // Copyright (C) LibreHardwareMonitor and Contributors. // Partial Copyright (C) Michael Möller and Contributors. // All Rights Reserved. using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Drawing; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Windows.Forms.UI; namespace LibreHardwareMonitor.Windows.Forms.Utilities; public class HttpServer { private readonly HttpListener _listener; private readonly Node _root; private readonly IElement _rootElement; private readonly Version _version = typeof(HttpServer).Assembly.GetName().Version; private Task _listenerTask; private CancellationTokenSource _cts; public HttpServer(Node node, IElement rootElement, string ip, int port, bool authEnabled = false, string userName = "", string passwordSHA256 = "") { _root = node; _rootElement = rootElement; ListenerIp = ip; ListenerPort = port; AuthEnabled = authEnabled; UserName = userName; PasswordSHA256 = passwordSHA256; try { _listener = new HttpListener { IgnoreWriteExceptions = true }; } catch (PlatformNotSupportedException) { _listener = null; } } ~HttpServer() { if (PlatformNotSupported) return; StopHttpListener(); try { _listener?.Abort(); } catch { } } public bool AuthEnabled { get; set; } public string ListenerIp { get; set; } public int ListenerPort { get; set; } public void SetPassword(string plainPassword) { PasswordSHA256 = ComputeSHA256(plainPassword); } public bool PlatformNotSupported { get { return _listener == null; } } public string UserName { get; set; } public string PasswordSHA256 { get; set; } public bool StartHttpListener() { if (PlatformNotSupported) return false; try { if (_listener.IsListening) return true; // validate that the selected IP exists (it could have been previously selected before switching networks) IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName()); bool ipFound = false; foreach (IPAddress ip in host.AddressList) { if (ListenerIp == ip.ToString()) { ipFound = true; break; } } if (!ipFound) { // default to behavior of previous version if we don't know what interface to use. ListenerIp = "+"; } string prefix = "http://" + ListenerIp + ":" + ListenerPort + "/"; _listener.Prefixes.Clear(); _listener.Prefixes.Add(prefix); _listener.Realm = "Libre Hardware Monitor"; _listener.AuthenticationSchemes = AuthEnabled ? AuthenticationSchemes.Basic : AuthenticationSchemes.Anonymous; _listener.Start(); _cts = new CancellationTokenSource(); _listenerTask = Task.Run(() => ProcessRequestsAsync(_cts.Token)); } catch (Exception) { return false; } return true; } public bool StopHttpListener() { if (PlatformNotSupported) return false; try { _cts?.Cancel(); _listenerTask?.Wait(TimeSpan.FromSeconds(5)); // Graceful wait _listener?.Stop(); _cts?.Dispose(); } catch (HttpListenerException) { } catch (OperationCanceledException) { } catch (NullReferenceException) { } catch (Exception) { } return true; } private async Task ProcessRequestsAsync(CancellationToken cancellationToken) { while (_listener.IsListening && !cancellationToken.IsCancellationRequested) { try { var context = await _listener.GetContextAsync(); _ = Task.Run(() => HandleContextAsync(context), cancellationToken); } catch (HttpListenerException ex) when (ex.ErrorCode == 50) { // Handle Windows update bug (e.g., 2025-10 Cumulative Update): retry after delay System.Diagnostics.Debug.WriteLine($"HttpListener error (code {ex.ErrorCode}): {ex.Message}. Retrying in 5 seconds."); await Task.Delay(5000, cancellationToken); } catch (ObjectDisposedException) { break; // Listener stopped } catch (OperationCanceledException) { break; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Unexpected HttpListener error: {ex.Message}"); } } } public static IDictionary ToDictionary(NameValueCollection col) { IDictionary dict = new Dictionary(); foreach (string k in col.AllKeys) { dict.Add(k, col[k]); } return dict; } public SensorNode FindSensor(Node node, string id) { if (node is SensorNode sNode) { if (sNode.Sensor.Identifier.ToString() == id) return sNode; } foreach (Node child in node.Nodes) { SensorNode s = FindSensor(child, id); if (s != null) { return s; } } return null; } public void SetSensorControlValue(SensorNode sNode, string value) { if (sNode.Sensor.Control == null) { throw new ArgumentException("Specified sensor '" + sNode.Sensor.Identifier + "' can not be set"); } if (value == "null") { sNode.Sensor.Control.SetDefault(); } else { sNode.Sensor.Control.SetSoftware(float.Parse(value, CultureInfo.InvariantCulture)); } } //Handles "/Sensor" requests. //Parameters are taken from the query part of the URL. //Get: //http://localhost:8085/Sensor?action=Get&id=/some/node/path/0 //The output is either: //{"result":"fail","message":"Some error message"} //or: //{"result":"ok","value":42.0, "format":"{0:F2} RPM"} // //Set: //http://localhost:8085/Sensor?action=Set&id=/some/node/path/0&value=42.0 //http://localhost:8085/Sensor?action=Set&id=/some/node/path/0&value=null //The output is either: //{"result":"fail","message":"Some error message"} //or: //{"result":"ok"} private void HandleSensorRequest(HttpListenerRequest request, Dictionary result) { IDictionary dict = ToDictionary(HttpUtility.ParseQueryString(request.Url.Query)); if (dict.ContainsKey("action")) { if (dict.ContainsKey("id")) { SensorNode sNode = FindSensor(_root, dict["id"]); if (sNode == null) { throw new ArgumentException("Unknown id " + dict["id"] + " specified"); } if (dict["action"] == "ResetMinMax") { // Reset Min/Max, then return Sensor values... sNode.Sensor.ResetMin(); sNode.Sensor.ResetMax(); dict["action"] = "Get"; } switch (dict["action"]) { case "Set" when dict.ContainsKey("value"): SetSensorControlValue(sNode, dict["value"]); break; case "Set": throw new ArgumentNullException("No value provided"); case "Get": result["value"] = sNode.Sensor.Value; result["min"] = sNode.Sensor.Min; result["max"] = sNode.Sensor.Max; result["format"] = sNode.Format; break; default: throw new ArgumentException("Unknown action type " + dict["action"]); } } else { throw new ArgumentNullException("No id provided"); } } else { throw new ArgumentNullException("No action provided"); } } //Handles http POST requests in a REST like manner. //Currently the only supported base URL is http://localhost:8085/Sensor. private string HandlePostRequest(HttpListenerRequest request) { var result = new Dictionary { ["result"] = "ok" }; try { if (request.Url.Segments.Length == 2) { if (request.Url.Segments[1] == "Sensor") { HandleSensorRequest(request, result); } else { throw new ArgumentException("Invalid URL ('" + request.Url.Segments[1] + "'), possible values: ['Sensor']"); } } else throw new ArgumentException("Empty URL, possible values: ['Sensor']"); } catch (Exception e) { result["result"] = "fail"; result["message"] = e.ToString(); } return System.Text.Json.JsonSerializer.Serialize(result); } private async Task HandleContextAsync(HttpListenerContext context) { HttpListenerRequest request = context.Request; bool authenticated = true; if (AuthEnabled) { try { HttpListenerBasicIdentity identity = (HttpListenerBasicIdentity)context.User.Identity; authenticated = (identity.Name == UserName) && (ComputeSHA256(identity.Password) == PasswordSHA256); } catch { authenticated = false; } } if (authenticated) { switch (request.HttpMethod) { case "POST": { string postResult = HandlePostRequest(request); await SendResponseAsync(context.Response, postResult, "application/json"); break; } case "GET": { string requestedFile = request.RawUrl.Substring(1); if (requestedFile == "data.json") { await SendJsonAsync(context.Response, request); return; } if (requestedFile.Contains("images_icon")) { await ServeResourceImageAsync(context.Response, requestedFile.Replace("images_icon/", string.Empty)); return; } if (requestedFile.StartsWith("metrics?") || requestedFile == "metrics") { await SendPrometheusAsync(context.Response, request); return; } if (requestedFile.Contains("Sensor")) { var sensorResult = new Dictionary(); HandleSensorRequest(request, sensorResult); await SendJsonSensorAsync(context.Response, sensorResult); return; } if (requestedFile.Contains("ResetAllMinMax")) { _rootElement.Accept(new SensorVisitor(delegate (ISensor sensor) { sensor.ResetMin(); sensor.ResetMax(); })); await SendJsonAsync(context.Response, request); return; } // default file to be served if (string.IsNullOrEmpty(requestedFile)) requestedFile = "index.html"; string[] splits = requestedFile.Split('.'); string ext = splits[splits.Length - 1]; await ServeResourceFileAsync(context.Response, "Web." + requestedFile.Replace('/', '.'), ext); break; } default: { context.Response.StatusCode = 404; break; } } } else { context.Response.StatusCode = 401; } if (context.Response.StatusCode == 401) { const string responseString = @"401 Unauthorized

401 Unauthorized

Authorization required. "; await SendResponseAsync(context.Response, responseString, "text/html"); } try { context.Response.Close(); } catch { // client closed connection before the content was sent } } private async Task ServeResourceFileAsync(HttpListenerResponse response, string name, string ext) { // resource names do not support the hyphen name = Assembly.GetExecutingAssembly().GetName().Name + ".Resources." + name.Replace("custom-theme", "custom_theme"); string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames(); for (int i = 0; i < names.Length; i++) { if (names[i].Replace('\\', '.') == name) { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(names[i]); response.ContentType = GetContentType("." + ext); response.ContentLength64 = stream.Length; byte[] buffer = new byte[512 * 1024]; try { int len; while ((len = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await response.OutputStream.WriteAsync(buffer, 0, len); } await response.OutputStream.FlushAsync(); response.OutputStream.Close(); response.Close(); } catch (HttpListenerException) { } catch (InvalidOperationException) { } return; } } response.StatusCode = 404; response.Close(); } private async Task ServeResourceImageAsync(HttpListenerResponse response, string name) { name = Assembly.GetExecutingAssembly().GetName().Name + ".Resources." + name; string[] names = Assembly.GetExecutingAssembly().GetManifestResourceNames(); for (int i = 0; i < names.Length; i++) { if (names[i].Replace('\\', '.') == name) { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(names[i]); using Image image = Image.FromStream(stream); response.ContentType = "image/png"; try { using var ms = new MemoryStream(); image.Save(ms, ImageFormat.Png); byte[] buffer = ms.ToArray(); await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.OutputStream.Close(); } catch (HttpListenerException) { } response.Close(); return; } } response.StatusCode = 404; response.Close(); } private async Task SendJsonAsync(HttpListenerResponse response, HttpListenerRequest request = null) { Dictionary json = new(); int nodeIndex = 0; json["id"] = nodeIndex++; json["Version"] = $"{_version.Major}.{_version.Minor}.{_version.Build}"; json["Text"] = "Sensor"; json["Min"] = "Min"; json["Value"] = "Value"; json["Max"] = "Max"; json["ImageURL"] = string.Empty; json["Children"] = new List { GenerateJsonForNode(_root, ref nodeIndex) }; byte[] buffer = Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize( json, new System.Text.Json.JsonSerializerOptions { NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals, // Allow "NaN" } )); bool acceptGzip; try { acceptGzip = (request != null) && (request.Headers["Accept-Encoding"].IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0); } catch { acceptGzip = false; } response.AddHeader("Cache-Control", "no-cache"); response.AddHeader("Access-Control-Allow-Origin", "*"); response.ContentType = "application/json"; try { if (acceptGzip) { response.AddHeader("Content-Encoding", "gzip"); using var ms = new MemoryStream(); using (var zip = new GZipStream(ms, CompressionMode.Compress, true)) await zip.WriteAsync(buffer, 0, buffer.Length); buffer = ms.ToArray(); } response.ContentLength64 = buffer.Length; await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.OutputStream.Close(); } catch (HttpListenerException) { } response.Close(); } private string GeneratePrometheusResponse(Node node, Dictionary prometheusSettings) { string responseStr = ""; string lastTagName = ""; /// Dictionary to convert all data to base units for OpenMetrics /// SensorType, Item1 suffix, Item2 factor var units = new Dictionary { { SensorType.Clock, ("hertz", 1000000)}, //originally megahertz { SensorType.Conductivity, ("seconds_per_centimeter", 0.000001) }, //originally microseconds per centimeter { SensorType.Control, ("percent", 1) }, { SensorType.Current, ("amperes", 1) }, { SensorType.Data, ("bytes", 1000000000) }, //originally GB { SensorType.Energy, ("watthour", 0.001) }, { SensorType.Factor, ("", 1) }, { SensorType.Fan, ("rpm", 1) }, { SensorType.Flow, ("liters_per_hour", 1) }, { SensorType.Frequency, ("hertz", 1) }, { SensorType.Humidity, ("percent", 1) }, { SensorType.Level, ("percent", 1) }, { SensorType.Load, ("percent", 1) }, { SensorType.Noise, ("decibels", 1) }, { SensorType.Power, ("watts", 1) }, { SensorType.SmallData, ("bytes", 1024*1024) }, //originally MiB { SensorType.Temperature, ("celsius", 1) }, { SensorType.Throughput, ("bytes_per_second", 1) }, { SensorType.TimeSpan, ("seconds", 1) }, { SensorType.Timing, ("seconds", 0.000000001 ) }, //originally nanoseconds { SensorType.Voltage, ("volts", 1) }, }; for (int i = 0; i < node.Nodes.Count; i++) { if (node.Nodes[i].GetType().Name == "HardwareNode") { responseStr += GeneratePrometheusResponse(node.Nodes[i], prometheusSettings); } if (node.Nodes[i].GetType().Name == "TypeNode") { string tagHardware = ""; string valueHardwareName = ""; string valueHardwareId = ((HardwareNode)node).Hardware.Identifier.ToString(); if (((HardwareNode)node).Hardware.Parent != null) { tagHardware = ((HardwareNode)node).Hardware.Parent.HardwareType.ToString(); valueHardwareName = ((HardwareNode)node).Hardware.Parent.Name; } else { tagHardware = ((HardwareNode)node).Hardware.HardwareType.ToString(); valueHardwareName = node.Text; } string valueHardwareAlias = $"{valueHardwareName} ({valueHardwareId})"; foreach (SensorNode sensor in node.Nodes[i].Nodes) { string valueSensorName = sensor.Text.Replace("#", String.Empty); // Variables needed in dictionary lookup and error message string tagSensorType = sensor.Sensor.SensorType.ToString(); double factor = 1; string tagSensorUnits = ""; // Get factor and unit suffix from dictionary ... if (units.ContainsKey(sensor.Sensor.SensorType)) { factor = units[sensor.Sensor.SensorType].Item2; tagSensorUnits = (units[sensor.Sensor.SensorType].Item1.Length == 0 ? String.Empty : "_" + units[sensor.Sensor.SensorType].Item1); } // ... or print an error message else { responseStr += $"# HELP {tagHardware}_{tagSensorType}:{valueSensorName} This Sensor type is not defined in the prometheus adapter [{sensor.Sensor.SensorType}]\n"; } // Creating the tag name for prometheus string tagName = $"lhm_{tagHardware}_{tagSensorType}{tagSensorUnits}"; tagName = tagName.ToLower(); // Preparing the labels for all data and uniqueness string valueSensorId = sensor.Sensor.Identifier.ToString().Substring(valueHardwareId.Length); string valueSensorAlias = $"{valueSensorName} ({valueSensorId})"; string valueHost = _root.Text; // Creates the tag with labels string tagLine = $$"""{{tagName}} {"sensorName"="{{valueSensorName}}", "sensorAlias"="{{valueSensorAlias}}", "hardwareName"="{{valueHardwareName}}", "hardwareAlias"="{{valueHardwareAlias}}", "sensorId"="{{valueSensorId}}", "hardwareId"="{{valueHardwareId}}", "host"="{{valueHost}}"}"""; if (lastTagName != tagName) { responseStr += $"# TYPE {tagName} gauge\n"; lastTagName = tagName; } int counter = 0; foreach (SensorValue val in sensor.Sensor.Values.Reverse()) { if (counter++ > prometheusSettings["archivelength"]) break; if (float.IsNaN(val.Value)) { // Print a help line saying what tag had an invalid value responseStr += $"# HELP {tagLine} has an invalid value and was skipped.\n"; } else { if (counter == 1 && prometheusSettings["lastvalue"] == 0) continue; // skip the first value in the list if (prometheusSettings["timestamps"] == 1) { responseStr += $"{tagLine} {(val.Value * factor).ToString(CultureInfo.InvariantCulture)} {((DateTimeOffset)val.Time).ToUnixTimeMilliseconds()}\n"; } else { responseStr += $"{tagLine} {(val.Value * factor).ToString(CultureInfo.InvariantCulture)}\n"; } } } } } } return responseStr; } private async Task SendPrometheusAsync(HttpListenerResponse response, HttpListenerRequest request = null) { Dictionary prometheusSettings = new Dictionary(); //Default values: archivelength=0, timestamps=0, lastvalue=1 prometheusSettings["archivelength"] = 0; prometheusSettings["timestamps"] = 0; prometheusSettings["lastvalue"] = 1; if (request != null && request.QueryString != null && request.QueryString.Count > 0) { int archive = 0, timestamps = 0, lastvalue = 1; foreach (string key in request.QueryString.AllKeys) { switch (key) { case "timestamps": int.TryParse(request.QueryString[key], out timestamps); if (timestamps < 0 || timestamps > 1) timestamps = 0; // Enforce boolean range 0 to 1 if (archive > 0) timestamps = 1; // If archive is requested, timestamps must be enabled break; case "archivelength": int.TryParse(request.QueryString[key], out archive); archive = Math.Min(10, archive); // Enforce max 10 archive = Math.Max(0, archive); // Enforce min 0 if (archive == 0 && lastvalue == 0) archive = 1; // If lastvalue was not requested then return at least 1 archived value if (archive > 0) timestamps = 1; // If archive is requested, timestamps must be enabled break; case "lastvalue": int.TryParse(request.QueryString[key], out lastvalue); if (lastvalue < 0 || lastvalue > 1) lastvalue = 1; // Enforce boolean range 0 to 1 if (lastvalue == 0 && archive == 0) { archive = 1; timestamps = 1; } break; default: break; } } prometheusSettings["archivelength"] = archive; prometheusSettings["timestamps"] = timestamps; prometheusSettings["lastvalue"] = lastvalue; } string responseContent = GeneratePrometheusResponse(_root, prometheusSettings); response.AddHeader("Cache-Control", "no-cache"); response.AddHeader("Access-Control-Allow-Origin", "*"); // Add custom headers to inform the user what settings are in effect response.AddHeader("X-archivelength", prometheusSettings["archivelength"].ToString()); response.AddHeader("X-timestamps", prometheusSettings["timestamps"].ToString()); response.AddHeader("X-lastvalue", prometheusSettings["lastvalue"].ToString()); await SendResponseAsync(response, responseContent, "text/plain"); } private async Task SendJsonSensorAsync(HttpListenerResponse response, Dictionary sensorData) { // Convert the JObject to a JSON string string responseContent = System.Text.Json.JsonSerializer.Serialize(sensorData); response.AddHeader("Cache-Control", "no-cache"); response.AddHeader("Access-Control-Allow-Origin", "*"); await SendResponseAsync(response, responseContent, "application/json"); } private Dictionary GenerateJsonForNode(Node n, ref int nodeIndex) { Dictionary jsonNode = new() { ["id"] = nodeIndex++, ["Text"] = n.Text, ["Min"] = string.Empty, ["Value"] = string.Empty, ["Max"] = string.Empty }; switch (n) { case SensorNode sensorNode: jsonNode["SensorId"] = sensorNode.Sensor.Identifier.ToString(); jsonNode["Type"] = sensorNode.Sensor.SensorType.ToString(); // Formatted values, e.g. Throughput will be measured in KB/s or MB/s depending on the value jsonNode["Min"] = sensorNode.Min; jsonNode["Value"] = sensorNode.Value; jsonNode["Max"] = sensorNode.Max; // Unformatted values for external systems to have consistent readings, e.g. Throughput will always be measured in B/s jsonNode["RawMin"] = sensorNode.Sensor.Min; jsonNode["RawValue"] = sensorNode.Sensor.Value; jsonNode["RawMax"] = sensorNode.Sensor.Max; jsonNode["ImageURL"] = "images/transparent.png"; break; case HardwareNode hardwareNode: jsonNode["HardwareId"] = hardwareNode.Hardware.Identifier.ToString(); jsonNode["ImageURL"] = "images_icon/" + GetHardwareImageFile(hardwareNode); break; case TypeNode typeNode: jsonNode["ImageURL"] = "images_icon/" + GetTypeImageFile(typeNode); break; default: jsonNode["ImageURL"] = "images_icon/computer.png"; break; } List children = new(); foreach (Node child in n.Nodes) { children.Add(GenerateJsonForNode(child, ref nodeIndex)); } jsonNode["Children"] = children; return jsonNode; } private static string GetContentType(string extension) { switch (extension) { case ".avi": return "video/x-msvideo"; case ".css": return "text/css"; case ".doc": return "application/msword"; case ".gif": return "image/gif"; case ".htm": case ".html": return "text/html"; case ".jpg": case ".jpeg": return "image/jpeg"; case ".js": return "application/x-javascript"; case ".mp3": return "audio/mpeg"; case ".png": return "image/png"; case ".pdf": return "application/pdf"; case ".ppt": return "application/vnd.ms-powerpoint"; case ".zip": return "application/zip"; case ".txt": return "text/plain"; default: return "application/octet-stream"; } } private static string GetHardwareImageFile(HardwareNode hn) { switch (hn.Hardware.HardwareType) { case HardwareType.Cpu: return "cpu.png"; case HardwareType.GpuNvidia: return "nvidia.png"; case HardwareType.GpuAmd: return "ati.png"; case HardwareType.GpuIntel: return "intel.png"; case HardwareType.Storage: return "hdd.png"; case HardwareType.Motherboard: return "mainboard.png"; case HardwareType.SuperIO: return "chip.png"; case HardwareType.Memory: return "ram.png"; case HardwareType.Cooler: return "fan.png"; case HardwareType.Network: return "nic.png"; case HardwareType.Psu: return "power-supply.png"; case HardwareType.Battery: return "battery.png"; case HardwareType.PowerMonitor: return "powermonitor.png"; default: return "cpu.png"; } } private static string GetTypeImageFile(TypeNode tn) { switch (tn.SensorType) { case SensorType.Voltage: case SensorType.Current: return "voltage.png"; case SensorType.Clock: case SensorType.Timing: return "clock.png"; case SensorType.Load: return "load.png"; case SensorType.Temperature: return "temperature.png"; case SensorType.Fan: return "fan.png"; case SensorType.Flow: return "flow.png"; case SensorType.Control: return "control.png"; case SensorType.Level: return "level.png"; case SensorType.Power: return "power.png"; case SensorType.Noise: return "loudspeaker.png"; case SensorType.Conductivity: return "voltage.png"; case SensorType.Throughput: return "throughput.png"; case SensorType.Humidity: return "flow.png"; default: return "power.png"; } } private string ComputeSHA256(string text) { using SHA256 hash = SHA256.Create(); return string.Concat(hash .ComputeHash(Encoding.UTF8.GetBytes(text)) .Select(item => item.ToString("x2"))); } public void Quit() { if (PlatformNotSupported) return; StopHttpListener(); try { _listener?.Abort(); } catch { } } private static async Task SendResponseAsync(HttpListenerResponse response, string content, string contentType) { byte[] buffer = Encoding.UTF8.GetBytes(content); response.ContentType = contentType; response.ContentLength64 = buffer.Length; try { await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.OutputStream.Close(); } catch (HttpListenerException) { } } }