using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; /// /// This class provides various low level services like network operations etc. /// namespace BrainFailProductions.PolyFew { public static class SystemServices { /// Contains different regex patterns used in the application. public static RegexPatterns regexPatterns; [System.Serializable] public struct RegexPatterns { public string netError; public string nullOrEmpty; public string generalError; public string apiMistmatch; public string parametersMismatch; public string nothing; } private static void SetPatterns() { regexPatterns.netError = ""; regexPatterns.nullOrEmpty = ""; regexPatterns.generalError = ""; regexPatterns.apiMistmatch = ""; regexPatterns.parametersMismatch = ""; regexPatterns.nothing = ""; } /// Make an asynchronous GET request to the recepient specified in the encodedUrl parameter.Optionally any headers can also be specified. public static IEnumerator UnityAsyncGETRequest(string encodedUrl, Action callback, int? timeout = null, Dictionary headers = null) { SetPatterns(); UnityWebRequest webRequest = new UnityWebRequest(encodedUrl); webRequest.timeout = timeout == null ? webRequest.timeout : (int)timeout; webRequest.method = "GET"; DownloadHandlerBuffer downloadHandler = new DownloadHandlerBuffer(); webRequest.downloadHandler = downloadHandler; if (headers != null) { foreach (var header in headers) { webRequest.SetRequestHeader(header.Key, header.Value); } } #if !UNITY_2017_1 yield return webRequest.SendWebRequest(); #else yield return webRequest.Send(); #endif long responseCode = webRequest.responseCode; #if UNITY_2020_1_OR_NEWER if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError) #else if (webRequest.isHttpError || webRequest.isNetworkError) #endif { callback("" + webRequest.error, responseCode); //callback(webRequest.responseCode+"-" + webRequest.error); } else { if (string.IsNullOrEmpty(webRequest.downloadHandler.text)) { callback("" + "Error! server returned an empty response.", responseCode); } else { callback(webRequest.downloadHandler.text, responseCode); } } } /// Make a blocking GET request to the recepient specified in the encodedUrl parameter.Optionally any headers can also be specified. public static void UnityBlockingGETRequest(string encodedUrl, Action callback, int? timeout = null, Dictionary headers = null) { SetPatterns(); UnityWebRequest webRequest = new UnityWebRequest(encodedUrl); webRequest.timeout = timeout == null ? webRequest.timeout : (int)timeout; webRequest.method = "GET"; DownloadHandlerBuffer downloadHandler = new DownloadHandlerBuffer(); webRequest.downloadHandler = downloadHandler; if (headers != null) { foreach (var header in headers) { webRequest.SetRequestHeader(header.Key, header.Value); } } #if !UNITY_2017_1 webRequest.SendWebRequest(); #else webRequest.Send(); #endif while (!webRequest.isDone) { } long responseCode = webRequest.responseCode; #if UNITY_2020_1_OR_NEWER if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError) #else if (webRequest.isHttpError || webRequest.isNetworkError) #endif { callback("" + webRequest.error, responseCode); } else { if (string.IsNullOrEmpty(webRequest.downloadHandler.text)) { callback("" + "Error! server returned an empty response.", responseCode); } else { callback(webRequest.downloadHandler.text, responseCode); } } } /// Make a blocking call to POST data to the recepient specified in the baseUrl parameter.Optionally any headers can also be specified. public static void UnityBlockingPOSTRequest(string baseUrl, Action callback, byte[] data, int? timeout = null, Dictionary headers = null) { SetPatterns(); UnityWebRequest webRequest = new UnityWebRequest(baseUrl); webRequest.timeout = timeout == null ? webRequest.timeout : (int)timeout; webRequest.method = "POST"; UploadHandlerRaw uploadHandler = new UploadHandlerRaw(data); DownloadHandlerBuffer downloadHandler = new DownloadHandlerBuffer(); webRequest.uploadHandler = uploadHandler; webRequest.downloadHandler = downloadHandler; webRequest.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if (headers != null) { foreach (var header in headers) { webRequest.SetRequestHeader(header.Key, header.Value); } } #if !UNITY_2017_1 webRequest.SendWebRequest(); #else webRequest.Send(); #endif while (!webRequest.isDone) { } long responseCode = webRequest.responseCode; #if UNITY_2020_1_OR_NEWER if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError) #else if (webRequest.isHttpError || webRequest.isNetworkError) #endif { callback("" + webRequest.error, responseCode); //callback(webRequest.responseCode + "-" + webRequest.error); } else { if (string.IsNullOrEmpty(webRequest.downloadHandler.text)) { callback("" + "Error! server returned an empty response.", responseCode); //callback(webRequest.responseCode + "-" + "Error! server returned an empty response."); } else { callback(webRequest.downloadHandler.text, responseCode); } } } /// POST form data asynchronously to the recipient specified in the baseUrl parameter.Optionally any headers can also be specified.Any response will be passed to the callback as a string. public static IEnumerator UnityAsyncPOSTRequest(string baseUrl, Action callback, byte[] data, int? timeout = null, Dictionary headers = null) { SetPatterns(); UnityWebRequest webRequest = new UnityWebRequest(baseUrl); webRequest.timeout = timeout == null ? webRequest.timeout : (int)timeout; webRequest.method = "POST"; UploadHandlerRaw uploadHandler = new UploadHandlerRaw(data); DownloadHandlerBuffer downloadHandler = new DownloadHandlerBuffer(); webRequest.uploadHandler = uploadHandler; webRequest.downloadHandler = downloadHandler; webRequest.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if (headers != null) { foreach (var header in headers) { webRequest.SetRequestHeader(header.Key, header.Value); } } // This sometimes get stuck and clogs the main thread #if !UNITY_2017_1 yield return webRequest.SendWebRequest(); #else yield return webRequest.Send(); #endif long responseCode = webRequest.responseCode; #if UNITY_2020_1_OR_NEWER if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError) #else if (webRequest.isHttpError || webRequest.isNetworkError) #endif { callback("" + webRequest.error, responseCode); } else { if (string.IsNullOrEmpty(webRequest.downloadHandler.text)) { callback("" + "Error! server returned an empty response.", responseCode); } else { callback(webRequest.downloadHandler.text, responseCode); } } } /// Make an asynchronous web request using the HTTP method provided to the recepient specified in the encodedUrl parameter.Optionally a timeout value in milliSeconds and any header containing fields such as ContentType can also be specified. Please note that in case of any error the response string will be appended with the string 'neterror'. See regex patterns for more info. public static async Task SendHTTPRequestAsync(string baseUrl, HTTPMethod requestMethod, Action callback, Dictionary requestParameters, byte[] postData, string contentType, int? timeout = null, Dictionary header = null) { SetPatterns(); await Task.Delay(0); //Debug.Log("Async GET request has just started"); // Make a get call and respond back with the resuts to the lambda expression HttpWebRequest request; try { request = (HttpWebRequest)WebRequest.Create(baseUrl); } catch (Exception e) { callback(regexPatterns.generalError + "+" + e.ToString(), null); return; } HttpWebResponse httpResponse = null; try { request.Timeout = timeout == null ? 100000 : (int)timeout; request.Method = requestMethod.methodName; request.Headers = new WebHeaderCollection(); request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; if (header != null) { // Append fields from the header parameter to the request header foreach (KeyValuePair pair in header) { request.Headers.Add(pair.Key, pair.Value); } } if (requestParameters != null) { string queryString = GetQueryStringFromKeyValues(requestParameters); if (requestMethod.methodName == "GET") { baseUrl += queryString; } else { byte[] paramsData = Encoding.UTF8.GetBytes(queryString); request.ContentLength = paramsData.Length; using (var dataStream = await request.GetRequestStreamAsync()) { dataStream.Write(paramsData, 0, paramsData.Length); } } } if (requestParameters == null && postData != null && requestMethod.methodName == "POST") { request.ContentLength = postData.Length; await Task.Run(() => { using (var dataStream = request.GetRequestStream()) { dataStream.Write(postData, 0, postData.Length); } }); /* using (var dataStream = await request.GetRequestStreamAsync()) { dataStream.Write(postData, 0, postData.Length); } */ } //Task webTask = request.GetResponseAsync(); //httpResponse = (HttpWebResponse)await webTask; await Task.Run(() => { httpResponse = (HttpWebResponse)request.GetResponse(); }); if (httpResponse.StatusCode != HttpStatusCode.OK) { //Debug.Log($"Failed to make HTTP request to resource"); callback(regexPatterns.netError + "+" + httpResponse.StatusDescription, httpResponse.StatusCode); } else { Stream stream = httpResponse.GetResponseStream(); StreamReader reader = new StreamReader(stream); string response = await reader.ReadToEndAsync(); //Debug.Log("Response from the web request is " + response); callback(response, httpResponse.StatusCode); } httpResponse.Dispose(); } catch (Exception e) { HttpStatusCode? statusCode = (httpResponse == null) ? null : (HttpStatusCode?)httpResponse.StatusCode; if (e.InnerException is WebException || e.InnerException is SocketException) { WebException ex = e as WebException; if (ex.Status == WebExceptionStatus.Timeout) { callback(regexPatterns.generalError + "+" + ex.ToString(), statusCode); } else { callback(regexPatterns.netError + "+" + ex.ToString(), statusCode); } } else { callback(regexPatterns.generalError + "+" + e.ToString(), statusCode); } } } /// Make a Blocking web request using the HTTP method provided to the recepient specified in the encodedUrl parameter.Optionally a timeout value in milliSeconds and any header containing fields such as ContentType can also be specified. Please note that in case of any error the response string will be appended with the string 'neterror'. See regex patterns for more info. public static void SendHTTPRequestBlocking(string baseUrl, HTTPMethod requestMethod, Action callback, Dictionary requestParameters, byte[] postData, string contentType, int? timeout = null, Dictionary header = null) { SetPatterns(); HttpWebResponse httpResponse = null; try { if (requestParameters != null && requestMethod.methodName == "GET") { string queryString = GetQueryStringFromKeyValues(requestParameters); baseUrl += "?" + queryString; } HttpWebRequest request = (HttpWebRequest)WebRequest.Create(baseUrl); ; request.Timeout = timeout == null ? 100000 : (int)timeout; request.Method = requestMethod.methodName; request.Headers = new WebHeaderCollection(); request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; request.ContentType = contentType; if (header != null) { // Append fields from the header parameter to the request header foreach (KeyValuePair pair in header) { request.Headers.Add(pair.Key, pair.Value); } } if (requestParameters != null && requestMethod.methodName == "POST") { string queryString = GetQueryStringFromKeyValues(requestParameters); byte[] paramsData = Encoding.ASCII.GetBytes(queryString); request.ContentLength = paramsData.Length; using (var dataStream = request.GetRequestStream()) { dataStream.Write(paramsData, 0, paramsData.Length); } } else if (requestParameters == null && requestMethod.methodName != "GET") { request.ContentLength = 0; } if (requestParameters == null && postData != null && requestMethod.methodName == "POST") { request.ContentLength = postData.Length; using (var dataStream = request.GetRequestStream()) { dataStream.Write(postData, 0, postData.Length); } } httpResponse = (HttpWebResponse)request.GetResponse(); if (httpResponse.StatusCode != HttpStatusCode.OK) { //Debug.Log($"Failed to make HTTP request to resource {encodedUrl}"); callback(regexPatterns.netError + "+" + httpResponse.StatusDescription, httpResponse.StatusCode); } else { Stream stream = httpResponse.GetResponseStream(); StreamReader reader = new StreamReader(stream); string response = reader.ReadToEnd(); //Debug.Log("Response from the web request is " + response); callback(response, httpResponse.StatusCode); } httpResponse.Dispose(); } catch (Exception e) { HttpStatusCode? statusCode = (httpResponse == null) ? null : (HttpStatusCode?)httpResponse.StatusCode; if (e.InnerException is WebException || e.InnerException is SocketException) { WebException ex = e as WebException; if (ex.Status == WebExceptionStatus.Timeout) { callback(regexPatterns.generalError + "+" + ex.ToString(), statusCode); } else { callback(regexPatterns.netError + "+" + ex.ToString(), statusCode); } } else { callback(regexPatterns.generalError + "+" + e.ToString(), statusCode); } } } /// Download the resource given by the absolute URL. The callback is passed the bytes of the resource, an error message if any and the HTTPStatus code. public static async Task AsyncResourceDownload(string resourceUrl, Action callback, int? timeout = null) { SetPatterns(); await Task.Delay(0); //Debug.Log("Async GET request has just started"); // Make a get call and respond back with the resuts to the lambda expression HttpWebRequest request; try { request = (HttpWebRequest)WebRequest.Create(resourceUrl); } catch (Exception e) { callback(null, e.ToString(), null); return; } HttpWebResponse httpResponse = null; try { request.Timeout = timeout == null ? 100000 : (int)timeout; await Task.Run(() => { httpResponse = (HttpWebResponse)request.GetResponse(); }); if (httpResponse.StatusCode != HttpStatusCode.OK) { callback(null, httpResponse.StatusDescription, httpResponse.StatusCode); } else { Stream stream = httpResponse.GetResponseStream(); Byte[] bytes = null; try { using (BinaryReader br = new BinaryReader(stream)) { bytes = br.ReadBytes((int)stream.Length); } } catch (Exception ex) { Debug.LogWarning(ex); callback(bytes, ex.ToString(), httpResponse.StatusCode); } callback(bytes, "", httpResponse.StatusCode); } httpResponse.Dispose(); } catch (Exception e) { HttpStatusCode? statusCode = (httpResponse == null) ? null : (HttpStatusCode?)httpResponse.StatusCode; if (e.InnerException is WebException || e.InnerException is SocketException) { WebException ex = e as WebException; if (ex.Status == WebExceptionStatus.Timeout) { callback(null, ex.ToString(), statusCode); } else { callback(null, ex.ToString(), statusCode); } } else { callback(null, e.ToString(), statusCode); } } } /// Checks asynchronously whether the url given is reachable. Passes true to the callback provided or false if the url is not reachable. public static async Task AsyncReachabilityCheck(string testUrl, Action callback) { var method = new HTTPMethod(HTTPMethod.HTTPMethods.GET); await SendHTTPRequestAsync(testUrl, method, (string response, HttpStatusCode? statusCode) => { /* if (Regex.IsMatch(response, regexPatterns.netError, RegexOptions.Compiled)) { Debug.Log("No internet connection or system services are down"); if (callback != null) { callback(false); } } else { Debug.Log("Connection to the internet exists"); if (callback != null) { callback(true); } } */ if (statusCode != null && statusCode == HttpStatusCode.OK) { callback(true); } else { callback(false); } }, null, null, "application/json"); } /// Checks in a blocking manner whether the url given is reachable. Passes true to the callback provided or false if the url is not reachable. public static void BlockingReachabilityCheck(string url, Action callback) { var method = new HTTPMethod(HTTPMethod.HTTPMethods.GET); SendHTTPRequestBlocking(url, method, (string response, HttpStatusCode? statusCode) => { /* if (Regex.IsMatch(response, regexPatterns.netError, RegexOptions.Compiled)) { Debug.Log("No internet connection or system services are down"); if (callback != null) { callback(false); } } else { Debug.Log("Connection to the internet exists"); if (callback != null) { callback(true); } } */ if (statusCode != null && statusCode == HttpStatusCode.OK) { callback(true); } else { callback(false); } }, null, null, "application/json"); } /// /// Parses a message appended with any of the "RegexPatterns" and then returns the appropriate type. /// /// The message appended with a "RegexPatterns" /// A struct consisting of the parsed message and the "RegexPatterns" that was originally appended with the message. public static MessagePatternPair ParseResponseMessage(string message) { string msg = null; string pattern = regexPatterns.nothing; if (Regex.IsMatch(message, regexPatterns.netError, RegexOptions.Compiled)) { msg = message.Replace(regexPatterns.netError + "+", ""); pattern = regexPatterns.netError; } else if (Regex.IsMatch(message, regexPatterns.apiMistmatch, RegexOptions.Compiled)) { msg = message.Replace(regexPatterns.apiMistmatch + "+", ""); pattern = regexPatterns.apiMistmatch; } else if (Regex.IsMatch(message, regexPatterns.generalError, RegexOptions.Compiled)) { msg = message.Replace(regexPatterns.generalError + "+", ""); pattern = regexPatterns.generalError; } else if (Regex.IsMatch(message, regexPatterns.parametersMismatch, RegexOptions.Compiled)) { msg = message.Replace(regexPatterns.parametersMismatch + "+", ""); pattern = regexPatterns.parametersMismatch; } else if (Regex.IsMatch(message, regexPatterns.nullOrEmpty, RegexOptions.Compiled)) { msg = message.Replace(regexPatterns.nullOrEmpty + "+", ""); pattern = regexPatterns.nullOrEmpty; } else { msg = null; pattern = regexPatterns.nothing; } return new MessagePatternPair(pattern, msg); } public struct MessagePatternPair { public string patternAppended { private set; get; } public string parsedMessage { private set; get; } public MessagePatternPair(string patternAppended, string parsedMessage) { this.patternAppended = patternAppended; this.parsedMessage = parsedMessage; } } public static bool IsSuccessStatusCode(long statusCode) { return ((int)statusCode >= 200) && ((int)statusCode <= 299); } public class HTTPMethod { public readonly string methodName; public HTTPMethod(HTTPMethods method) { this.methodName = Enum.GetName(typeof(HTTPMethods), method); } public enum HTTPMethods { POST, GET } } public static string GetQueryStringFromKeyValues(Dictionary parameters) { var list = new List(); foreach (var item in parameters) { list.Add(item.Key + "=" + Uri.EscapeDataString(item.Value)); } return string.Join("&", list); } public static async Task RunDelayedCommand(float secs, Action command) { await Task.Delay((int)(secs * 1000)); command(); } public static byte[] ReadAllBytes(Stream source) { long originalPosition = source.Position; source.Position = 0; try { byte[] readBuffer = new byte[4096]; int totalBytesRead = 0; int bytesRead; while ((bytesRead = source.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0) { totalBytesRead += bytesRead; if (totalBytesRead == readBuffer.Length) { int nextByte = source.ReadByte(); if (nextByte != -1) { byte[] temp = new byte[readBuffer.Length * 2]; Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length); Buffer.SetByte(temp, totalBytesRead, (byte)nextByte); readBuffer = temp; totalBytesRead++; } } } byte[] buffer = readBuffer; if (readBuffer.Length != totalBytesRead) { buffer = new byte[totalBytesRead]; Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead); } return buffer; } finally { source.Position = originalPosition; } } /// /// Writes the given texture asynchronously to the disk at the given path. If a file already exists at the given path it will be overwritten /// /// The texture to write /// The image format to save the texture in. Note that EXR format requires an uncompressed HDR texture /// The file name without the extension. /// The absolute path where the file will be saved /// The method to call when the operation ends. The method will be passed an error if any error occurred public static async Task WriteTextureAsync(Texture2D texture, ImageFormat format, string fileName, string path, Action callback) { try { byte[] data = null; switch (format) { case ImageFormat.PNG: data = ImageConversion.EncodeToPNG(texture); if (!fileName.ToLower().Contains(".png")) { fileName += ".png"; } break; case ImageFormat.JPG: data = ImageConversion.EncodeToJPG(texture); if (!fileName.ToLower().Contains(".jpg")) { fileName += ".jpg"; } break; case ImageFormat.EXR: data = ImageConversion.EncodeToEXR(texture); if (!fileName.ToLower().Contains(".exr")) { fileName += ".exr"; } break; } if (data == null) { Debug.Log("Failed encoding"); } if (path.EndsWith("/") || path.EndsWith("\\")) { path += fileName; } else { path += "/" + fileName; } using (FileStream fileStream = File.Create(path)) { await fileStream.WriteAsync(data, 0, data.Length); } callback(""); Debug.Log(data.Length / 1024 + "Kb was saved as: " + path); } catch (Exception ex) { callback(ex.ToString()); } } /// /// Writes the given texture asynchronously to the disk /// /// The given bytes to write /// The fully qualified path of the file. If a file already exists at this path it will be overwritten /// The method to call when the operation ends. The method will be passed an error if any error occurred public static async Task WriteBytesAsync(byte[] data, string fullPath, Action callback) { try { using (FileStream fileStream = File.Create(fullPath)) { await fileStream.WriteAsync(data, 0, data.Length); } callback(""); Debug.Log(data.Length / 1024 + "Kb was saved as: " + fullPath); } catch (Exception ex) { callback(ex.ToString()); } } public enum ImageFormat { PNG, JPG, EXR } } }