/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Reflection; using System.Text; using System.Web; using System.Xml; using System.Xml.Serialization; using System.Xml.Linq; using log4net; using Nwc.XmlRpc; using OpenMetaverse.StructuredData; using OpenSim.Framework.ServiceAuth; using System.Net.Http; using System.Security.Authentication; using System.Runtime.CompilerServices; using System.Threading; namespace OpenSim.Framework { /// /// Miscellaneous static methods and extension methods related to the web /// /// public static class WebUtil { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static SocketsHttpHandler SharedSocketsHttpHandlerNoRedir = null; public static SocketsHttpHandler SharedSocketsHttpHandler = null; public static ExpiringKey GlobalExpiringBadURLs = new(30000); /// /// Control the printing of certain debug messages. /// /// /// If DebugLevel >= 3 then short notices about outgoing HTTP requests are logged. /// public static int DebugLevel { get; set; } /// /// Request number for diagnostic purposes. /// public static int RequestNumber { get; set; } /// /// this is the header field used to communicate the local request id /// used for performance and debugging /// public const string OSHeaderRequestID = "opensim-request-id"; /// /// Number of milliseconds a call can take before it is considered /// a "long" call for warning & debugging purposes /// public const int LongCallTime = 3000; /// /// The maximum length of any data logged because of a long request time. /// /// /// This is to truncate any really large post data, such as an asset. In theory, the first section should /// give us useful information about the call (which agent it relates to if applicable, etc.). /// This is also used to truncate messages when using DebugLevel 5. /// public const int MaxRequestDiagLength = 200; public static bool ValidateServerCertificateNoChecks( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors; sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNameMismatch; return sslPolicyErrors == SslPolicyErrors.None; } #region JSONRequest public static void SetupHTTPClients(bool NoVerifyCertChain, bool NoVerifyCertHostname, IWebProxy proxy, int MaxConnectionsPerServer ) { SocketsHttpHandler shh = new() { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, ConnectTimeout = TimeSpan.FromSeconds(120), PreAuthenticate = false, UseCookies = false, MaxConnectionsPerServer = MaxConnectionsPerServer, PooledConnectionIdleTimeout = TimeSpan.FromSeconds(31), PooledConnectionLifetime = TimeSpan.FromMinutes(3) }; //shh.SslOptions.ClientCertificates = null, shh.SslOptions.EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; if (NoVerifyCertChain) { shh.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; if (NoVerifyCertHostname) { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { errors &= ~(SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch); return errors == SslPolicyErrors.None; }; } else { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { errors &= ~SslPolicyErrors.RemoteCertificateChainErrors; return errors == SslPolicyErrors.None; }; } } else { shh.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; if (NoVerifyCertHostname) { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { errors &= ~SslPolicyErrors.RemoteCertificateNameMismatch; return errors == SslPolicyErrors.None; }; } else { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { return errors == SslPolicyErrors.None; }; } } if (proxy is null) shh.UseProxy = false; else { shh.Proxy = proxy; shh.UseProxy = true; } SharedSocketsHttpHandlerNoRedir = shh; // **************** shh = new() { AllowAutoRedirect = true, MaxAutomaticRedirections = 10, AutomaticDecompression = DecompressionMethods.None, ConnectTimeout = TimeSpan.FromSeconds(120), PreAuthenticate = false, UseCookies = false, MaxConnectionsPerServer = MaxConnectionsPerServer, PooledConnectionIdleTimeout = TimeSpan.FromSeconds(31), PooledConnectionLifetime = TimeSpan.FromMinutes(3) }; //shh.SslOptions.ClientCertificates = null, shh.SslOptions.EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; if (NoVerifyCertChain) { shh.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; if (NoVerifyCertHostname) { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { errors &= ~(SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch); return errors == SslPolicyErrors.None; }; } else { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { errors &= ~SslPolicyErrors.RemoteCertificateChainErrors; return errors == SslPolicyErrors.None; }; } } else { shh.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.NoCheck; if (NoVerifyCertHostname) { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { errors &= ~SslPolicyErrors.RemoteCertificateNameMismatch; return errors == SslPolicyErrors.None; }; } else { shh.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { return errors == SslPolicyErrors.None; }; } } if (proxy is null) shh.UseProxy = false; else { shh.Proxy = proxy; shh.UseProxy = true; } SharedSocketsHttpHandler = shh; } public static HttpClient GetNewGlobalHttpClient(int timeout) { var client = new HttpClient(SharedSocketsHttpHandler, false) { Timeout = TimeSpan.FromMilliseconds(timeout > 0 ? timeout : 30000), MaxResponseContentBufferSize = 250 * 1024 * 1024, }; client.DefaultRequestHeaders.ExpectContinue = false; return client; } public static HttpClient GetGlobalNoRedirHttpClient(int timeout) { var client = new HttpClient(SharedSocketsHttpHandlerNoRedir, false) { Timeout = TimeSpan.FromMilliseconds(timeout > 0 ? timeout : 30000), MaxResponseContentBufferSize = 250 * 1024 * 1024, }; client.DefaultRequestHeaders.ExpectContinue = false; return client; } /// /// PUT JSON-encoded data to a web service that returns LLSD or /// JSON data /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OSDMap PutToServiceCompressed(string url, OSDMap data, int timeout) { return ServiceOSDRequest(url, data, "PUT", timeout, true, false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OSDMap PutToService(string url, OSDMap data, int timeout) { return ServiceOSDRequest(url, data, "PUT", timeout, false, false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OSDMap PostToService(string url, OSDMap data, int timeout, bool rpc) { return ServiceOSDRequest(url, data, "POST", timeout, false, rpc); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OSDMap PostToServiceCompressed(string url, OSDMap data, int timeout) { return ServiceOSDRequest(url, data, "POST", timeout, true, false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OSDMap GetFromService(string url, int timeout) { return ServiceOSDRequest(url, null, "GET", timeout, false, false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void LogOutgoingDetail(Stream outputStream) { LogOutgoingDetail("", outputStream); } public static void LogOutgoingDetail(string context, Stream outputStream) { using Stream stream = Util.Copy(outputStream); using StreamReader reader = new(stream, Encoding.UTF8); string output; if (DebugLevel == 5) { char[] chars = new char[MaxRequestDiagLength + 1]; // +1 so we know to add "..." only if needed int len = reader.Read(chars, 0, MaxRequestDiagLength + 1); output = new string(chars, 0, len); } else { output = reader.ReadToEnd(); } LogOutgoingDetail(context, output); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void LogOutgoingDetail(string type, int reqnum, string output) { LogOutgoingDetail($"{type} {reqnum}: ", output); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void LogOutgoingDetail(string context, string output) { if (DebugLevel == 5) { if (output.Length > MaxRequestDiagLength) output = output[..MaxRequestDiagLength] + "..."; } m_log.DebugFormat($"[LOGHTTP]: {context}{Util.BinaryToASCII(output)}"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void LogResponseDetail(int reqnum, Stream inputStream) { LogOutgoingDetail($"RESPONSE {reqnum}: ", inputStream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void LogResponseDetail(int reqnum, string input) { LogOutgoingDetail($"RESPONSE {reqnum}: ", input); } public static OSDMap ServiceOSDRequest(string url, OSDMap data, string method, int timeout, bool compressed, bool rpc, bool keepalive = false) { int reqnum = RequestNumber++; if (DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} JSON-RPC {method} to {url}"); string errorMessage = "unknown error"; int ticks = Util.EnvironmentTickCount(); int sendlen = 0; int rcvlen = 0; HttpResponseMessage responseMessage = null; HttpRequestMessage request = null; HttpClient client = null; try { client = GetNewGlobalHttpClient(timeout); request = new(new HttpMethod(method), url); if (data is not null) { byte[] buffer; if (DebugLevel >= 5) { string strBuffer = OSDParser.SerializeJsonString(data); LogOutgoingDetail(method, reqnum, strBuffer); buffer = Util.UTF8Getbytes(strBuffer); } else buffer = OSDParser.SerializeJsonToBytes(data); if (buffer.Length > 0) { if (compressed) { using MemoryStream ms = new(); using (GZipStream comp = new(ms, CompressionMode.Compress, true)) { comp.Write(buffer, 0, buffer.Length); } buffer = ms.ToArray(); request.Headers.TryAddWithoutValidation("X-Content-Encoding", "gzip"); // can't set "Content-Encoding" because old OpenSims fail if they get an unrecognized Content-Encoding } sendlen = buffer.Length; request.Content = new ByteArrayContent(buffer); request.Content.Headers.TryAddWithoutValidation("Content-Type", rpc ? "application/json-rpc" : "application/json"); request.Content.Headers.TryAddWithoutValidation("Content-Length", sendlen.ToString()); } } request.Headers.ExpectContinue = false; request.Headers.TransferEncodingChunked = false; if(keepalive) { request.Headers.TryAddWithoutValidation("Keep-Alive", "timeout=30, max=10"); request.Headers.TryAddWithoutValidation("Connection", "Keep-Alive"); request.Headers.ConnectionClose = false; } else request.Headers.TryAddWithoutValidation("Connection", "close"); request.Headers.TryAddWithoutValidation(OSHeaderRequestID, reqnum.ToString()); responseMessage = client.Send(request, HttpCompletionOption.ResponseHeadersRead); responseMessage.EnsureSuccessStatusCode(); Stream resStream = responseMessage.Content.ReadAsStream(); if (resStream is not null) { using StreamReader reader = new(resStream); string responseStr = reader.ReadToEnd(); if (WebUtil.DebugLevel >= 5) WebUtil.LogResponseDetail(reqnum, responseStr); rcvlen = responseStr.Length; return CanonicalizeResults(responseStr); } } catch (HttpRequestException e) { int Status = e.StatusCode is null ? 499 : (int)e.StatusCode; errorMessage = $"[{Status}] {e.Message}"; } catch (Exception ex) { errorMessage = ex.Message; m_log.Debug($"[WEB UTIL]: Exception making request: {errorMessage}"); } finally { request?.Dispose(); responseMessage?.Dispose(); client?.Dispose(); ticks = Util.EnvironmentTickCountSubtract(ticks); if (ticks > LongCallTime) { m_log.Info($"[WEB UTIL]: SvcOSD {reqnum} {method} {url} took {ticks}ms, {sendlen}/{rcvlen}bytes"); } else if (DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {ticks}ms"); } } m_log.Debug($"[LOGHTTP]: request {reqnum} {method} to {url} FAILED: {errorMessage}"); return ErrorResponseMap(errorMessage); } /// /// Since there are no consistencies in the way web requests are /// formed, we need to do a little guessing about the result format. /// Keys: /// Success|success == the success fail of the request /// _RawResult == the raw string that came back /// _Result == the OSD unpacked string /// private static OSDMap CanonicalizeResults(string response) { OSDMap result = new() { // Default values ["Success"] = OSD.FromBoolean(true), ["success"] = OSD.FromBoolean(true), ["_RawResult"] = OSD.FromString(response), ["_Result"] = new OSDMap() }; if (response.Equals("true", StringComparison.OrdinalIgnoreCase)) return result; if (response.Equals("false", StringComparison.OrdinalIgnoreCase)) { result["Success"] = OSD.FromBoolean(false); result["success"] = OSD.FromBoolean(false); return result; } try { OSD responseOSD = OSDParser.Deserialize(response); if (responseOSD.Type == OSDType.Map) { result["_Result"] = (OSDMap)responseOSD; return result; } } catch { // don't need to treat this as an error... we're just guessing anyway //m_log.DebugFormat("[WEB UTIL] couldn't decode <{0}>: {1}",response,e.Message); } return result; } #endregion JSONRequest #region FormRequest /// /// POST URL-encoded form data to a web service that returns LLSD or /// JSON data /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OSDMap PostToService(string url, NameValueCollection data) { return ServiceFormRequest(url,data, 30000); } public static OSDMap ServiceFormRequest(string url, NameValueCollection data, int timeout) { int reqnum = RequestNumber++; string method = (data is not null && data["RequestMethod"] is not null) ? data["RequestMethod"] : "unknown"; if (DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} ServiceForm '{method}' to {url}"); string errorMessage = "unknown error"; int ticks = Util.EnvironmentTickCount(); int sendlen = 0; int rcvlen = 0; HttpResponseMessage responseMessage = null; HttpRequestMessage request = null; HttpClient client = null; try { client = GetNewGlobalHttpClient(timeout); request = new(HttpMethod.Post, url); request.Headers.ExpectContinue = false; request.Headers.TransferEncodingChunked = false; //if (keepalive) //{ // request.Headers.TryAddWithoutValidation("Keep-Alive", "timeout=30, max=10"); // request.Headers.TryAddWithoutValidation("Connection", "Keep-Alive"); // request.Headers.ConnectionClose = false; //} //else request.Headers.TryAddWithoutValidation("Connection", "close"); request.Headers.TryAddWithoutValidation(OSHeaderRequestID, reqnum.ToString()); if (data is not null) { string queryString = BuildQueryString(data); if (DebugLevel >= 5) LogOutgoingDetail("SEND", reqnum, queryString); byte[] buffer = System.Text.Encoding.UTF8.GetBytes(queryString); queryString = null; sendlen = buffer.Length; request.Content = new ByteArrayContent(buffer); request.Content.Headers.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"); request.Content.Headers.TryAddWithoutValidation("Content-Length", sendlen.ToString()); buffer = null; } else { request.Content = new ByteArrayContent(Array.Empty()); request.Content.Headers.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"); request.Content.Headers.TryAddWithoutValidation("Content-Length", "0"); } responseMessage = client.Send(request, HttpCompletionOption.ResponseHeadersRead); responseMessage.EnsureSuccessStatusCode(); using StreamReader reader = new(responseMessage.Content.ReadAsStream()); string responseStr = reader.ReadToEnd(); rcvlen = responseStr.Length; if (DebugLevel >= 5) LogResponseDetail(reqnum, responseStr); OSD responseOSD = OSDParser.Deserialize(responseStr); if (responseOSD.Type == OSDType.Map) return (OSDMap)responseOSD; } catch (HttpRequestException we) { if (we.StatusCode is HttpStatusCode status) errorMessage = $"[{status}] {we.Message}"; else errorMessage = we.Message; } catch (Exception ex) { errorMessage = ex.Message; } finally { request?.Dispose(); responseMessage?.Dispose(); client?.Dispose(); ticks = Util.EnvironmentTickCountSubtract(ticks); if (ticks > LongCallTime) { m_log.Info( $"[LOGHTTP]: Slow ServiceForm request {reqnum} '{method}' to {url} took {ticks}ms, {sendlen}/{rcvlen}bytes"); } else if (DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {ticks}ms"); } } m_log.Warn($"[LOGHTTP]: ServiceForm request {reqnum} '{method}' to {url} failed: {errorMessage}"); return ErrorResponseMap(errorMessage); } /// /// Create a response map for an error, trying to keep /// the result formats consistent /// private static OSDMap ErrorResponseMap(string msg) { OSDMap result = new() { ["Success"] = "False", ["Message"] = OSD.FromString("Service request failed: " + msg) }; return result; } #endregion FormRequest #region Uri /// /// Combines a Uri that can contain both a base Uri and relative path /// with a second relative path fragment /// /// Starting (base) Uri /// Relative path fragment to append to the end /// of the Uri /// The combined Uri /// This is similar to the Uri constructor that takes a base /// Uri and the relative path, except this method can append a relative /// path fragment on to an existing relative path public static Uri Combine(this Uri uri, string fragment) { string fragment1 = uri.Fragment; string fragment2 = fragment; if (!fragment1.EndsWith('/')) fragment1 += '/'; if (fragment2.StartsWith('/')) fragment2 = fragment2[1..]; return new Uri(uri, fragment1 + fragment2); } /// /// Combines a Uri that can contain both a base Uri and relative path /// with a second relative path fragment. If the fragment is absolute, /// it will be returned without modification /// /// Starting (base) Uri /// Relative path fragment to append to the end /// of the Uri, or an absolute Uri to return unmodified /// The combined Uri public static Uri Combine(this Uri uri, Uri fragment) { if (fragment.IsAbsoluteUri) return fragment; string fragment1 = uri.Fragment; string fragment2 = fragment.ToString(); if (!fragment1.EndsWith('/')) fragment1 += '/'; if (fragment2.StartsWith('/')) fragment2 = fragment2[1..]; return new Uri(uri, fragment1 + fragment2); } /// /// Appends a query string to a Uri that may or may not have existing /// query parameters /// /// Uri to append the query to /// Query string to append. Can either start with ? /// or just containg key/value pairs /// String representation of the Uri with the query string /// appended public static string AppendQuery(this Uri uri, string query) { if (string.IsNullOrEmpty(query)) return uri.ToString(); if (query[0] == '?' || query[0] == '&') query = query[1..]; string uriStr = uri.ToString(); if (uriStr.Contains('?')) return $"{uriStr}&{query}"; else return $"{uriStr}?{query}"; } #endregion Uri #region NameValueCollection /// /// Convert a NameValueCollection into a query string. This is the /// not exactly the inverse of HttpUtility.ParseQueryString() /// /// Collection of key/value pairs to convert /// A query string with URL-escaped values public static string BuildQueryString(NameValueCollection parameters) { if (parameters.Count == 0) return string.Empty; StringBuilder sb = new(4096); foreach (string key in parameters.Keys) { string[] values = parameters.GetValues(key); if (values is not null) { foreach (string value in values) { sb.Append(key); sb.Append('='); if(!string.IsNullOrWhiteSpace(value)) sb.Append(HttpUtility.UrlEncode(value)); sb.Append('&'); } } } if(sb.Length > 1) sb.Length--; return sb.ToString(); } /// /// /// /// /// /// public static string GetOne(this NameValueCollection collection, string key) { string[] values = collection.GetValues(key); if (values is not null && values.Length > 0) return values[0]; return null; } #endregion NameValueCollection #region Stream /// /// Copies the contents of one stream to another, starting at the /// current position of each stream /// /// The stream to copy from, at the position /// where copying should begin /// The stream to copy to, at the position where /// bytes should be written /// The maximum bytes to copy /// The total number of bytes copied /// /// Copying begins at the streams' current positions. The positions are /// NOT reset after copying is complete. /// NOTE!! .NET 4.0 adds the method 'Stream.CopyTo(stream, bufferSize)'. /// This function could be replaced with that method once we move /// totally to .NET 4.0. For versions before, this routine exists. /// This routine used to be named 'CopyTo' but the int parameter has /// a different meaning so this method was renamed to avoid any confusion. /// public static int CopyStream(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy) { byte[] buffer = new byte[4096]; int readBytes; int totalCopiedBytes = 0; while ((readBytes = copyFrom.Read(buffer, 0, Math.Min(4096, maximumBytesToCopy))) > 0) { int writeBytes = Math.Min(maximumBytesToCopy, readBytes); copyTo.Write(buffer, 0, writeBytes); totalCopiedBytes += writeBytes; maximumBytesToCopy -= writeBytes; } return totalCopiedBytes; } #endregion Stream public class QBasedComparer : IComparer { public int Compare(Object x, Object y) { float qx = GetQ(x); float qy = GetQ(y); return qy.CompareTo(qx); // descending order } private static float GetQ(Object o) { // Example: image/png;q=0.9 float qvalue = 1f; if (o is String mime) { string[] parts = mime.Split(';'); if (parts.Length > 1) { string[] kvp = parts[1].Split('='); if (kvp.Length == 2 && kvp[0] == "q") float.TryParse(kvp[1], NumberStyles.Number, CultureInfo.InvariantCulture, out qvalue); } } return qvalue; } } /// /// Takes the value of an Accept header and returns the preferred types /// ordered by q value (if it exists). /// Example input: image/jpg;q=0.7, image/png;q=0.8, image/jp2 /// Exmaple output: ["jp2", "png", "jpg"] /// NOTE: This doesn't handle the semantics of *'s... /// /// /// public static string[] GetPreferredImageTypes(string accept) { if (string.IsNullOrEmpty(accept)) return Array.Empty(); string[] types = accept.Split(Util.SplitCommaArray); if (types.Length > 0) { ArrayList tlist = new(); foreach(string s in types.AsSpan()) { if(s.StartsWith("image", StringComparison.InvariantCultureIgnoreCase)) tlist.Add(s); } if(tlist.Count == 0) return Array.Empty(); tlist.Sort(new QBasedComparer()); string[] result = new string[tlist.Count]; for (int i = 0; i < tlist.Count; i++) { string mime = (string)tlist[i]; string[] parts = mime.Split(Util.SplitSemicolonArray); string[] pair = parts[0].Split(Util.SplitSlashArray); if (pair.Length == 2) result[i] = pair[1].ToLower(); else // oops, we don't know what this is... result[i] = pair[0]; } return result; } return Array.Empty(); } } public static class AsynchronousRestObjectRequester { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// /// Perform an asynchronous REST request. /// /// GET or POST /// /// /// /// /// /// Thrown if we encounter a /// network issue while posting the request. You'll want to make /// sure you deal with this as they're not uncommon // [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MakeRequest(string verb, string requestUrl, TRequest obj, Action action) { MakeRequest(verb, requestUrl, obj, action, 0, null); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MakeRequest(string verb, string requestUrl, TRequest obj, Action action, int maxConnections) { MakeRequest(verb, requestUrl, obj, action, maxConnections, null); } /// /// Perform a synchronous REST request. /// /// /// /// /// /// Request timeout in seconds. Timeout.Infinite indicates no timeout. If 0 is passed then the default timeout is used (100 seconds) /// /// /// /// The response. If there was an internal exception or the request timed out, /// then the default(TResponse) is returned. /// public static void MakeRequest(string verb, string requestUrl, TRequest obj, Action action, int maxConnections, IServiceAuth auth) { int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} AsynchronousRequestObject {verb} to {requestUrl}"); int tickstart = Util.EnvironmentTickCount(); int tickdata = 0; int tickdiff = 0; Type type = typeof(TRequest); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUrl); if (auth != null) auth.AddAuthorization(request.Headers); request.AllowWriteStreamBuffering = false; if (maxConnections > 0 && request.ServicePoint.ConnectionLimit < maxConnections) request.ServicePoint.ConnectionLimit = maxConnections; TResponse deserial = default; request.Method = verb; byte[] data = null; try { if (verb == "POST") { request.ContentType = "text/xml"; XmlWriterSettings settings = new() { Encoding = Encoding.UTF8 }; using (MemoryStream buffer = new()) using (XmlWriter writer = XmlWriter.Create(buffer, settings)) { XmlSerializer serializer = new(type); serializer.Serialize(writer, obj); writer.Flush(); data = buffer.ToArray(); } int length = data.Length; request.ContentLength = length; if (WebUtil.DebugLevel >= 5) WebUtil.LogOutgoingDetail("SEND", reqnum, System.Text.Encoding.UTF8.GetString(data)); request.BeginGetRequestStream(delegate(IAsyncResult res) { using (Stream requestStream = request.EndGetRequestStream(res)) requestStream.Write(data, 0, length); // capture how much time was spent writing tickdata = Util.EnvironmentTickCountSubtract(tickstart); request.BeginGetResponse(delegate(IAsyncResult ar) { using (WebResponse response = request.EndGetResponse(ar)) { try { using Stream respStream = response.GetResponseStream(); deserial = XMLResponseHelper.LogAndDeserialize( reqnum, respStream, response.ContentLength); } catch (System.InvalidOperationException) { } } action(deserial); }, null); }, null); } else { request.BeginGetResponse(delegate(IAsyncResult res2) { try { // If the server returns a 404, this appears to trigger a System.Net.WebException even though that isn't // documented in MSDN using WebResponse response = request.EndGetResponse(res2); try { using Stream respStream = response.GetResponseStream(); deserial = XMLResponseHelper.LogAndDeserialize( reqnum, respStream, response.ContentLength); } catch (System.InvalidOperationException) { try { using Stream respStream = response.GetResponseStream(); deserial = XMLResponseHelper.LogAndDeserialize( reqnum, respStream, response.ContentLength); } catch (System.InvalidOperationException) { } } } catch (WebException e) { if (e.Status == WebExceptionStatus.ProtocolError) { if (e.Response is HttpWebResponse httpResponse) { if (httpResponse.StatusCode != HttpStatusCode.NotFound) { // We don't appear to be handling any other status codes, so log these feailures to that // people don't spend unnecessary hours hunting phantom bugs. m_log.Debug( $"[ASYNC REQUEST]: Request {verb} {requestUrl} failed with unexpected status code {httpResponse.StatusCode}"); } httpResponse.Dispose(); } } else { m_log.Error( $"[ASYNC REQUEST]: Request {verb} {requestUrl} failed with status {e.Status} and message {e.Message}"); } } catch (Exception e) { m_log.Error($"[ASYNC REQUEST]: Request {verb} {requestUrl} failed with exception {e.Message}"); } //m_log.DebugFormat("[ASYNC REQUEST]: Received {0}", deserial.ToString()); try { action(deserial); } catch (Exception e) { m_log.ErrorFormat($"[ASYNC REQUEST]: Request {verb} {requestUrl} callback failed with exception {e.Message}"); } }, null); } tickdiff = Util.EnvironmentTickCountSubtract(tickstart); if (tickdiff > WebUtil.LongCallTime) { string originalRequest = null; if (data != null) { originalRequest = Encoding.UTF8.GetString(data); if (originalRequest.Length > WebUtil.MaxRequestDiagLength) originalRequest = originalRequest.Remove(WebUtil.MaxRequestDiagLength); } m_log.InfoFormat( "[LOGHTTP]: Slow AsynchronousRequestObject request {0} {1} to {2} took {3}ms, {4}ms writing, {5}", reqnum, verb, requestUrl, tickdiff, tickdata, originalRequest); } else if (WebUtil.DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {tickdiff}ms, {tickdata}ms writing"); } } catch { } } } public static class SynchronousRestFormsRequester { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// /// Perform a synchronous REST request. /// /// /// /// /// /// /// /// Thrown if we encounter a network issue while posting /// the request. You'll want to make sure you deal with this as they're not uncommon public static string MakeRequest(string method, string requestUrl, string obj, int timeoutsecs = -1, IServiceAuth auth = null, bool keepalive = true) { int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} SynchronousRestForms {method} to {requestUrl}"); int ticks = Util.EnvironmentTickCount(); HttpResponseMessage responseMessage = null; HttpRequestMessage request = null; HttpClient client = null; string respstring = String.Empty; int sendlen = 0; int rcvlen = 0; try { client = WebUtil.GetNewGlobalHttpClient(timeoutsecs * 1000); request = new(new HttpMethod(method), requestUrl); auth?.AddAuthorization(request.Headers); request.Headers.ExpectContinue = false; request.Headers.TransferEncodingChunked = false; if (timeoutsecs > 0) if (keepalive) { request.Headers.TryAddWithoutValidation("Keep-Alive", "timeout=30, max=10"); request.Headers.TryAddWithoutValidation("Connection", "Keep-Alive"); request.Headers.ConnectionClose = false; } else request.Headers.TryAddWithoutValidation("Connection", "close"); if (obj.Length > 0 && (method.Equals("POST", StringComparison.OrdinalIgnoreCase) || method.Equals("PUT", StringComparison.OrdinalIgnoreCase))) { byte[] data = Util.UTF8NBGetbytes(obj); sendlen = data.Length; if (WebUtil.DebugLevel >= 5) WebUtil.LogOutgoingDetail("SEND", reqnum, System.Text.Encoding.UTF8.GetString(data)); request.Content = new ByteArrayContent(data); request.Content.Headers.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"); request.Content.Headers.TryAddWithoutValidation("Content-Length", sendlen.ToString()); } responseMessage = client.Send(request, HttpCompletionOption.ResponseHeadersRead); responseMessage.EnsureSuccessStatusCode(); if ((responseMessage.Content.Headers.ContentLength is long contentLength) && contentLength != 0) { using StreamReader reader = new(responseMessage.Content.ReadAsStream()); respstring = reader.ReadToEnd(); rcvlen = respstring.Length; } } catch (Exception e) { m_log.Info($"[FORMS]: Error receiving response from {requestUrl}: {e.Message}"); throw; } finally { request?.Dispose(); responseMessage?.Dispose(); client?.Dispose(); } ticks = Util.EnvironmentTickCountSubtract(ticks); if (ticks > WebUtil.LongCallTime) { m_log.Info($"[FORMS]: request {reqnum} {method} {requestUrl} took {ticks}ms, {sendlen}/{rcvlen}bytes"); } else if (WebUtil.DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {ticks}ms"); if (WebUtil.DebugLevel >= 5) WebUtil.LogResponseDetail(reqnum, respstring); } return respstring; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string MakeRequest(string verb, string requestUrl, string obj, IServiceAuth auth) { return MakeRequest(verb, requestUrl, obj, -1, auth); } public static string MakePostRequest(string requestUrl, string obj, IServiceAuth auth = null, int timeoutsecs = -1, bool keepalive = true) { int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} SynchronousRestForms POST to {requestUrl}"); int ticks = Util.EnvironmentTickCount(); HttpResponseMessage responseMessage = null; HttpRequestMessage request = null; HttpClient client = null; string respstring = String.Empty; int sendlen = 0; int rcvlen = 0; try { client = WebUtil.GetNewGlobalHttpClient(timeoutsecs * 1000); request = new(HttpMethod.Post, requestUrl); auth?.AddAuthorization(request.Headers); request.Headers.ExpectContinue = false; request.Headers.TransferEncodingChunked = false; if (keepalive) { request.Headers.TryAddWithoutValidation("Keep-Alive", "timeout=30, max=10"); request.Headers.TryAddWithoutValidation("Connection", "Keep-Alive"); request.Headers.ConnectionClose = false; } else request.Headers.TryAddWithoutValidation("Connection", "close"); byte[] data = Util.UTF8NBGetbytes(obj); sendlen = data.Length; request.Content = new ByteArrayContent(data); request.Content.Headers.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"); request.Content.Headers.TryAddWithoutValidation("Content-Length", sendlen.ToString()); if (WebUtil.DebugLevel >= 5) WebUtil.LogOutgoingDetail("SEND", reqnum, System.Text.Encoding.UTF8.GetString(data)); responseMessage = client.Send(request, HttpCompletionOption.ResponseHeadersRead); if ((responseMessage.Content.Headers.ContentLength is long contentLength) && contentLength != 0) { using StreamReader reader = new(responseMessage.Content.ReadAsStream()); respstring = reader.ReadToEnd(); } } catch (Exception e) { m_log.Info($"[FORMS]: Error receiving response from {requestUrl}: {e.Message}"); throw; } finally { request?.Dispose(); responseMessage?.Dispose(); client?.Dispose(); } ticks = Util.EnvironmentTickCountSubtract(ticks); if (ticks > WebUtil.LongCallTime) { m_log.Info($"[FORMS]: request {reqnum} POST {requestUrl} took {ticks}ms {sendlen}/{rcvlen}bytes"); } else if (WebUtil.DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {ticks}ms"); if (WebUtil.DebugLevel >= 5) WebUtil.LogResponseDetail(reqnum, respstring); } return respstring; } } public class SynchronousRestObjectRequester { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// /// Perform a synchronous REST request. /// /// /// /// /// /// The response. If there was an internal exception, then the default(TResponse) is returned. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TResponse MakeRequest(string verb, string requestUrl, TRequest obj) { return MakeRequest(verb, requestUrl, obj, 0, null); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TResponse MakeRequest(string verb, string requestUrl, TRequest obj, IServiceAuth auth) { return MakeRequest(verb, requestUrl, obj, 0, auth); } /// /// Perform a synchronous REST request. /// /// /// /// /// /// Request timeout in milliseconds. Timeout.Infinite indicates no timeout. If 0 is passed then the default timeout is used (100 seconds) /// /// /// The response. If there was an internal exception or the request timed out, /// then the default(TResponse) is returned. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TResponse MakeRequest(string verb, string requestUrl, TRequest obj, int pTimeout) { return MakeRequest(verb, requestUrl, obj, pTimeout, null); } /// /// Perform a synchronous something request. /// /// /// /// /// /// Request timeout in milliseconds. Timeout.Infinite indicates no timeout. If 0 is passed then the default timeout is used (100 seconds) /// /// /// The response. If there was an internal exception or the request timed out, /// then the default(TResponse) is returned. /// public static TResponse MakeRequest(string method, string requestUrl, TRequest obj, int pTimeout, IServiceAuth auth) { int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} SRestObjReq {method} {requestUrl}"); int ticks = Util.EnvironmentTickCount(); TResponse deserial = default; HttpResponseMessage responseMessage = null; HttpRequestMessage request = null; HttpClient client = null; try { client = WebUtil.GetNewGlobalHttpClient(pTimeout); request = new(new HttpMethod(method), requestUrl); auth?.AddAuthorization(request.Headers); request.Headers.ExpectContinue = false; request.Headers.TransferEncodingChunked = false; //if (keepalive) { request.Headers.TryAddWithoutValidation("Keep-Alive", "timeout=30, max=10"); request.Headers.TryAddWithoutValidation("Connection", "Keep-Alive"); request.Headers.ConnectionClose = false; } //else // request.Headers.TryAddWithoutValidation("Connection", "close"); if (method.Equals("POST",StringComparison.OrdinalIgnoreCase) || method.Equals("PUT", StringComparison.OrdinalIgnoreCase)) { byte[] data; XmlWriterSettings settings = new() { Encoding = Util.UTF8 }; using (MemoryStream ms = new()) using (XmlWriter writer = XmlWriter.Create(ms, settings)) { XmlSerializer serializer = new(typeof(TRequest)); serializer.Serialize(writer, obj); writer.Flush(); data = ms.ToArray(); } int sendlen = data.Length; if (WebUtil.DebugLevel >= 5) WebUtil.LogOutgoingDetail("SEND", reqnum, System.Text.Encoding.UTF8.GetString(data)); request.Content = new ByteArrayContent(data); request.Content.Headers.TryAddWithoutValidation("Content-Type", "text/xml"); request.Content.Headers.TryAddWithoutValidation("Content-Length", sendlen.ToString()); } responseMessage = client.Send(request, HttpCompletionOption.ResponseHeadersRead); responseMessage.EnsureSuccessStatusCode(); int rcvlen = 0; if ((responseMessage.Content.Headers.ContentLength is long contentLength) && contentLength != 0) { rcvlen = (int)contentLength; using Stream respStream = responseMessage.Content.ReadAsStream(); deserial = XMLResponseHelper.LogAndDeserialize( reqnum, respStream, contentLength); } else { m_log.Debug($"[SRestObjReq]: Oops! no content found in response stream from {method} {requestUrl}"); } ticks = Util.EnvironmentTickCountSubtract(ticks); if (ticks > WebUtil.LongCallTime) { m_log.Info($"[LOGHTTP]: Slow SRestObjReq {reqnum} {method} {requestUrl} took {ticks}ms, {rcvlen}bytes"); } else if (WebUtil.DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {ticks}ms"); } } catch (HttpRequestException e) { if (e.StatusCode is HttpStatusCode status) { if (status == HttpStatusCode.Unauthorized) { m_log.Error($"[SRestObjReq]: GET {requestUrl} requires authentication"); } else if (status != HttpStatusCode.NotFound) { m_log.Warn($"[SRestObjReq]: GET {requestUrl} returned error: {status}"); } } else m_log.ErrorFormat( "[SRestObjReq]: WebException for {0} {1} {2} {3}", method, requestUrl, typeof(TResponse).ToString(), e.Message); } catch (System.InvalidOperationException) { // This is what happens when there is invalid XML m_log.Debug($"[SRestObjReq]: Invalid XML from {method} {requestUrl} {typeof(TResponse)}"); } catch (Exception e) { m_log.Debug($"[SRestObjReq]: Exception on response from {method} {requestUrl}: {e.Message}"); } finally { request?.Dispose(); responseMessage?.Dispose(); client?.Dispose(); } return deserial; } public static TResponse MakeGetRequest(string requestUrl, int pTimeout, IServiceAuth auth) { int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} SRestObjReq GET {requestUrl}"); int ticks = Util.EnvironmentTickCount(); TResponse deserial = default; HttpResponseMessage responseMessage = null; HttpRequestMessage request = null; HttpClient client = null; try { client = WebUtil.GetNewGlobalHttpClient(pTimeout); request = new(HttpMethod.Get, requestUrl); auth?.AddAuthorization(request.Headers); request.Headers.ExpectContinue = false; request.Headers.TransferEncodingChunked = false; //if (keepalive) { request.Headers.TryAddWithoutValidation("Keep-Alive", "timeout=30, max=10"); request.Headers.TryAddWithoutValidation("Connection", "Keep-Alive"); request.Headers.ConnectionClose = false; } //else // request.Headers.TryAddWithoutValidation("Connection", "close"); responseMessage = client.Send(request, HttpCompletionOption.ResponseHeadersRead); responseMessage.EnsureSuccessStatusCode(); int rcvlen = 0; if ((responseMessage.Content.Headers.ContentLength is long contentLength) && contentLength != 0) { rcvlen = (int)contentLength; using Stream respStream = responseMessage.Content.ReadAsStream(); deserial = XMLResponseHelper.LogAndDeserialize( reqnum, respStream, contentLength); } else { m_log.Debug($"[SRestObjReq]: Oops! no content found in response stream from GET {requestUrl}"); } ticks = Util.EnvironmentTickCountSubtract(ticks); if (ticks > WebUtil.LongCallTime) { m_log.Info($"[LOGHTTP]: Slow SRestObjReq GET {reqnum} {requestUrl} took {ticks}ms, {rcvlen}bytes"); } else if (WebUtil.DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {ticks}ms"); } } catch (HttpRequestException e) { if(e.StatusCode is HttpStatusCode status) { if (status == HttpStatusCode.Unauthorized) { m_log.Error($"[SRestObjReq]: GET {requestUrl} requires authentication"); } else if (status != HttpStatusCode.NotFound) { m_log.Warn($"[SRestObjReq]: GET {requestUrl} returned error: {status}"); } } else m_log.Error($"[SRestObjReq]: WebException for GET {requestUrl} {typeof(TResponse)} {e.Message}"); } catch (System.InvalidOperationException) { // This is what happens when there is invalid XML m_log.Debug($"[SRestObjReq]: Invalid XML from GET {requestUrl} {typeof(TResponse)}"); } catch (Exception e) { m_log.Debug($"[SRestObjReq]: Exception on response from GET {requestUrl}: {e.Message}"); } finally { request?.Dispose(); responseMessage?.Dispose(); client?.Dispose(); } return deserial; } } public static class XMLResponseHelper { public static TResponse LogAndDeserialize(int reqnum, Stream respStream, long contentLength) { XmlSerializer deserializer = new (typeof(TResponse)); if (WebUtil.DebugLevel >= 5) { const int blockLength = 4096; byte[] dataBuffer = new byte[blockLength]; int curcount; using MemoryStream ms = new(4 * blockLength); if (contentLength == -1) { while (true) { curcount = respStream.Read(dataBuffer, 0, blockLength); if (curcount <= 0) break; ms.Write(dataBuffer, 0, curcount); } } else { int remaining = (int)contentLength; while (remaining > 0) { curcount = respStream.Read(dataBuffer, 0, remaining); if (curcount <= 0) throw new EndOfStreamException($"End of stream reached with {remaining} bytes left to read"); ms.Write(dataBuffer, 0, curcount); remaining -= curcount; } } dataBuffer = ms.ToArray(); WebUtil.LogResponseDetail(reqnum, System.Text.Encoding.UTF8.GetString(dataBuffer)); ms.Position = 0; return (TResponse)deserializer.Deserialize(ms); } else { return (TResponse)deserializer.Deserialize(respStream); } } } public static class XMLRPCRequester { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static Hashtable SendRequest(Hashtable ReqParams, string method, string url) { int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} XML-RPC '{method}' to {url}"); int tickstart = Util.EnvironmentTickCount(); string responseStr = null; HttpClient client = null; try { ArrayList SendParams = new() { ReqParams }; XmlRpcRequest Req = new(method, SendParams); if (WebUtil.DebugLevel >= 5) { string str = Req.ToString(); str = XElement.Parse(str).ToString(SaveOptions.DisableFormatting); WebUtil.LogOutgoingDetail("SEND", reqnum, str); } client = WebUtil.GetNewGlobalHttpClient(-1); XmlRpcResponse Resp = Req.Send(url, client); try { if (WebUtil.DebugLevel >= 5) { responseStr = Resp.ToString(); responseStr = XElement.Parse(responseStr).ToString(SaveOptions.DisableFormatting); WebUtil.LogResponseDetail(reqnum, responseStr); } } catch (Exception e) { m_log.Error($"[LOGHTTP]: Error parsing XML-RPC response: {e.Message}"); } if (Resp.IsFault) { m_log.Debug( $"[LOGHTTP]: XML-RPC request {reqnum} '{method}' to {url} FAILED: FaultCode={Resp.FaultCode}, {Resp.FaultString}"); return null; } Hashtable RespData = (Hashtable)Resp.Value; return RespData; } finally { client?.Dispose(); int tickdiff = Util.EnvironmentTickCountSubtract(tickstart); if (tickdiff > WebUtil.LongCallTime) { m_log.InfoFormat( "[LOGHTTP]: Slow XML-RPC request {0} '{1}' to {2} took {3}ms, {4}", reqnum, method, url, tickdiff, responseStr != null ? (responseStr.Length > WebUtil.MaxRequestDiagLength ? responseStr.Remove(WebUtil.MaxRequestDiagLength) : responseStr) : ""); } else if (WebUtil.DebugLevel >= 4) { m_log.Debug($"[LOGHTTP]: HTTP OUT {reqnum} took {tickdiff}ms"); } } } } }