diff --git a/helpers/JsonHelper.cs b/helpers/JsonHelper.cs index 3f97288..1556f76 100644 --- a/helpers/JsonHelper.cs +++ b/helpers/JsonHelper.cs @@ -12,7 +12,8 @@ namespace RESTClient { /// - /// Wrapper work-around for json array described on https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ + /// Wrapper work-around for json array + /// Issue reference: https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ /// #pragma warning disable 0649 // suppresses warning: array "is never assigned to, and will always have its default value 'null'" [Serializable] @@ -52,7 +53,7 @@ public static T[] FromJsonArray(string json) { if (JsonObject.TryParse(json, out jsonObject)) { JsonArray jsonArray = jsonObject.GetNamedArray(namedArray); T[] array = GetArray(jsonArray); - N nestedResults = new N(); //NestedResults nestedResults = new NestedResults(array); + N nestedResults = new N(); nestedResults.SetArray(array); string namedCount = nestedResults.GetCountField(); @@ -65,7 +66,7 @@ public static T[] FromJsonArray(string json) { return default(N); } #endif - N results = JsonUtility.FromJson(json); // NestedResults nestedResults = JsonUtility.FromJson(json); + N results = JsonUtility.FromJson(json); return results; } diff --git a/helpers/SignatureHelper.cs b/helpers/SignatureHelper.cs new file mode 100644 index 0000000..c65a216 --- /dev/null +++ b/helpers/SignatureHelper.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Security.Cryptography; + +#if NETFX_CORE +using Windows.Security.Cryptography.Core; +using Windows.Security.Cryptography; +using System.Runtime.InteropServices.WindowsRuntime; +#endif + +namespace RESTClient { + public static class SignatureHelper { + public static string Sign(byte[] key, string stringToSign) { +#if NETFX_CORE + MacAlgorithmProvider provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha256); + CryptographicHash hash = provider.CreateHash(key.AsBuffer()); + hash.Append(CryptographicBuffer.ConvertStringToBinary(stringToSign, BinaryStringEncoding.Utf8)); + return CryptographicBuffer.EncodeToBase64String( hash.GetValueAndReset() ); +#else + var hmac = new HMACSHA256(); + hmac.Key = key; + byte[] sig = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); + return Convert.ToBase64String(sig); +#endif + } + } +} diff --git a/helpers/UrlHelper.cs b/helpers/UrlHelper.cs new file mode 100644 index 0000000..eddf630 --- /dev/null +++ b/helpers/UrlHelper.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Collections.Generic; + +namespace RESTClient { + public static class UrlHelper { + + /// + /// The returned url format will be: baseUrl + path(s) + query string + /// + /// The URL. + /// Base URL. + /// Query parameters. + /// Paths. + public static string BuildQuery(string baseUrl, Dictionary queryParams = null, params string[] paths) { + StringBuilder q = new StringBuilder(); + if (queryParams == null) { + return BuildQuery(baseUrl, "", paths); + } + + foreach (KeyValuePair param in queryParams) { + if (q.Length == 0) { + q.Append("?"); + } else { + q.Append("&"); + } + q.Append(param.Key + "=" + param.Value); + } + + return BuildQuery(baseUrl, q.ToString(), paths); + } + + public static string BuildQuery(string baseUrl, string queryString, params string[] paths) { + StringBuilder sb = new StringBuilder(); + sb.Append(baseUrl); + if (!baseUrl.EndsWith("/")) { + sb.Append("/"); + } + + foreach (string path in paths) { + if (!path.EndsWith("/")) { + sb.Append(path); + } else { + sb.Append(path + "/"); + } + } + + sb.Append(queryString); + return sb.ToString(); + } + } +} + diff --git a/helpers/XmlHelper.cs b/helpers/XmlHelper.cs new file mode 100644 index 0000000..004705e --- /dev/null +++ b/helpers/XmlHelper.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using UnityEngine; +using System.Xml; +using System.Xml.Serialization; +using System.IO; +using System.Text; + +namespace RESTClient { + public static class XmlHelper { + public static XmlDocument LoadResourceDocument(string filename) { + string xml = LoadResourceText(filename); + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + return doc; + } + + public static string LoadResourceText(string filename) { + TextAsset contents = (TextAsset)Resources.Load(filename); + return contents.text; + } + + public static string LoadAsset(string filepath, string extension = ".xml") { + string path = Path.Combine(Application.dataPath, filepath + extension); + return File.ReadAllText(path); + } + + public static T FromXml(string xml) where T : class { + XmlSerializer serializer = new XmlSerializer(typeof(T)); + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml))) { + return (T)serializer.Deserialize(stream); + } + } + } +} diff --git a/http/Method.cs b/http/Method.cs index 8ebb5da..7d161b3 100644 --- a/http/Method.cs +++ b/http/Method.cs @@ -9,6 +9,7 @@ public enum Method { GET, POST, PATCH, - DELETE + DELETE, + PUT } } diff --git a/http/RestRequest.cs b/http/RestRequest.cs index 169bfb2..f5f4662 100644 --- a/http/RestRequest.cs +++ b/http/RestRequest.cs @@ -11,14 +11,14 @@ namespace RESTClient { public class RestRequest : IDisposable { - public RestRequest(UnityWebRequest request) { - this.Request = request; - } - public UnityWebRequest Request { get; private set; } private QueryParams queryParams; + public RestRequest(UnityWebRequest request) { + this.Request = request; + } + public RestRequest(string url, Method method) { Request = new UnityWebRequest(url, method.ToString()); Request.downloadHandler = new DownloadHandlerBuffer(); @@ -28,6 +28,17 @@ public void AddHeader(string key, string value) { Request.SetRequestHeader(key, value); } + public void AddHeaders(Dictionary headers) { + foreach (KeyValuePair header in headers) { + AddHeader(header.Key, header.Value); + } + } + + public void AddBody(string text, string contentType = "text/plain; charset=UTF-8") { + byte[] bytes = Encoding.UTF8.GetBytes(text); + this.AddBody(bytes, contentType); + } + public void AddBody(byte[] bytes, string contentType) { if (Request.uploadHandler != null) { Debug.LogWarning("Request body can only be set once"); @@ -64,7 +75,24 @@ public virtual void UpdateRequestUrl() { } } - #region Response and json object parsing + #region Response and object parsing + + private RestResult GetRestResult(bool expectedBodyContent = true) { + HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), Request.responseCode.ToString()); + RestResult result = new RestResult(statusCode); + + if (result.IsError) { + result.ErrorMessage = "Response failed with status: " + statusCode.ToString(); + return result; + } + + if (expectedBodyContent && string.IsNullOrEmpty(Request.downloadHandler.text)) { + result.IsError = true; + result.ErrorMessage = "Response has empty body"; + return result; + } + return result; + } private RestResult GetRestResult() { HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), Request.responseCode.ToString()); @@ -84,6 +112,8 @@ private RestResult GetRestResult() { return result; } + #region JSON object parsing response + /// /// Shared method to return response result whether an object or array of objects /// @@ -174,6 +204,57 @@ public IRestResponse ParseJsonArray(Action> callback this.Dispose(); } + #endregion + + #region XML object parsing response + + private RestResult TrySerializeXml() where T : class { + RestResult result = GetRestResult(); + // return early if there was a status / data error other than Forbidden + if (result.IsError && result.StatusCode == HttpStatusCode.Forbidden) { + Debug.LogWarning("Authentication Failed: " + Request.downloadHandler.text); + return result; + } else if (result.IsError) { + return result; + } + // otherwise try and serialize XML response text to an object + try { + result.AnObject = XmlHelper.FromXml(Request.downloadHandler.text); + } catch (Exception e) { + result.IsError = true; + result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString() + " Exception message: " + e.Message; + } + return result; + } + + public void ParseXML(Action> callback = null) where T : class { + RestResult result = TrySerializeXml(); + + if (result.IsError) { + Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " request url:" + Request.url); + callback(new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text)); + } else { + callback(new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text, result.AnObject)); + } + this.Dispose(); + } + + /// + /// To be used with a callback which passes the response with result including status success or error code, request url and any body text. + /// + /// Callback. + public void Result(Action callback = null) { + RestResult result = GetRestResult(false); + if (result.IsError) { + Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " request url:" + Request.url); + callback(new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text)); + } else { + callback(new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text)); + } + } + + #endregion + /// Just return as plain text public IRestResponse GetText(Action> callback = null) { RestResult result = GetRestResult(); diff --git a/http/RestResponse.cs b/http/RestResponse.cs index aeb6c00..9d26118 100644 --- a/http/RestResponse.cs +++ b/http/RestResponse.cs @@ -76,5 +76,10 @@ public RestResult(HttpStatusCode statusCode) : base(statusCode) { } } + internal sealed class RestResult : Response { + public RestResult(HttpStatusCode statusCode) : base(statusCode) { + } + } + }