From 861a69c04389f589568c678d9e92650f34219c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Comte?= Date: Thu, 26 Jul 2018 22:56:27 +0200 Subject: [PATCH 1/4] Added support for decentralized exchanges --- .../Authentication/AuthenticationProvider.cs | 8 ++++++++ CryptoExchange.Net/ExchangeOptions.cs | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 4dd1431..0dc3cd4 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Interfaces; +using System.Security; namespace CryptoExchange.Net.Authentication { @@ -6,11 +7,18 @@ namespace CryptoExchange.Net.Authentication { public ApiCredentials Credentials { get; } + public SecureString PrivateKey { get; } + protected AuthenticationProvider(ApiCredentials credentials) { Credentials = credentials; } + protected AuthenticationProvider(SecureString privateKey) + { + PrivateKey = privateKey; + } + public virtual string AddAuthenticationToUriString(string uri, bool signed) { return uri; diff --git a/CryptoExchange.Net/ExchangeOptions.cs b/CryptoExchange.Net/ExchangeOptions.cs index b2629da..0ce7720 100644 --- a/CryptoExchange.Net/ExchangeOptions.cs +++ b/CryptoExchange.Net/ExchangeOptions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Security; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; using CryptoExchange.Net.RateLimiter; @@ -17,6 +18,11 @@ namespace CryptoExchange.Net /// public ApiCredentials ApiCredentials { get; set; } + /// + /// The private key instead of api credentials + /// + public SecureString PrivateKey { get; set; } + /// /// Proxy to use /// From b0cf1a899ec8a88630434515f88bca89d0dad493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Comte?= Date: Thu, 26 Jul 2018 23:02:12 +0200 Subject: [PATCH 2/4] Added support for array parameters (e.g. field[]=a&field[]=b) --- CryptoExchange.Net/ExchangeClient.cs | 75 +++++++++++++++++++++------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/CryptoExchange.Net/ExchangeClient.cs b/CryptoExchange.Net/ExchangeClient.cs index 15a34dc..0606bff 100644 --- a/CryptoExchange.Net/ExchangeClient.cs +++ b/CryptoExchange.Net/ExchangeClient.cs @@ -15,7 +15,7 @@ using Newtonsoft.Json.Linq; namespace CryptoExchange.Net { - public abstract class ExchangeClient: IDisposable + public abstract class ExchangeClient : IDisposable { public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); @@ -29,7 +29,7 @@ namespace CryptoExchange.Net { DateTimeZoneHandling = DateTimeZoneHandling.Utc }); - + protected ExchangeClient(ExchangeOptions exchangeOptions, AuthenticationProvider authenticationProvider) { log = new Log(); @@ -47,14 +47,14 @@ namespace CryptoExchange.Net log.Level = exchangeOptions.LogVerbosity; apiProxy = exchangeOptions.Proxy; - if(apiProxy != null) + if (apiProxy != null) log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}"); rateLimiters = new List(); foreach (var rateLimiter in exchangeOptions.RateLimiters) rateLimiters.Add(rateLimiter); } - + /// /// Adds a rate limiter to the client. There are 2 choices, the and the . /// @@ -71,7 +71,7 @@ namespace CryptoExchange.Net { rateLimiters.Clear(); } - + /// /// Set the authentication provider /// @@ -104,12 +104,28 @@ namespace CryptoExchange.Net } string paramString = null; - if (parameters != null) { + if (parameters != null) + { paramString = "with parameters "; + int paramCount = 1; foreach (var param in parameters) - paramString += $"{param.Key}={param.Value}, "; + { + string paramValue = param.Value.ToString(); + if (param.Value.GetType().IsArray) + paramValue = string.Format("[{0}]", string.Join(", ", ((object[])param.Value).Select(p => p.ToString()))); + + paramString += $"{param.Key}={paramValue}"; + + if (paramCount < parameters.Count) + paramString += ", "; + else + paramString += "."; + + paramCount++; + } } - log.Write(LogVerbosity.Debug, $"Sending {(signed ? "signed": "")} request to {request.Uri} {(paramString ?? "")}"); + + log.Write(LogVerbosity.Debug, $"Sending {(signed ? "signed" : "")} request to {request.Uri} {(paramString ?? "")}"); var result = await ExecuteRequest(request).ConfigureAwait(false); return result.Error != null ? new CallResult(null, result.Error) : Deserialize(result.Data); } @@ -123,7 +139,26 @@ namespace CryptoExchange.Net if (!uriString.EndsWith("?")) uriString += "?"; - uriString += $"{string.Join("&", parameters.Select(s => $"{s.Key}={s.Value}"))}"; + var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); + if (arraysParameters != null && arraysParameters.Count() > 0) + { + bool isFirstEntry = true; + arraysParameters.ForEach((arrayEntry) => + { + if (!isFirstEntry) + uriString += "&"; + + List values = ((object[])arrayEntry.Value).ToList(); + uriString += $"{string.Join("&", values.Select(v => $"{arrayEntry.Key}[]={v}"))}"; + + isFirstEntry = false; + }); + + if (parameters.Count - arraysParameters.Count > 0) + uriString += "&"; + } + + uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={s.Value}"))}"; } if (authProvider != null) @@ -189,7 +224,7 @@ namespace CryptoExchange.Net return new ServerError(error); } - protected CallResult Deserialize(string data, bool checkObject = true) where T: class + protected CallResult Deserialize(string data, bool checkObject = true) where T : class { try { @@ -202,7 +237,7 @@ namespace CryptoExchange.Net CheckObject(typeof(T), o); else { - var ary = (JArray) obj; + var ary = (JArray)obj; if (ary.HasValues && ary[0] is JObject jObject) CheckObject(typeof(T).GetElementType(), jObject); } @@ -227,7 +262,7 @@ namespace CryptoExchange.Net log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } - catch(Exception ex) + catch (Exception ex) { var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {data}"; log.Write(LogVerbosity.Error, info); @@ -247,7 +282,7 @@ namespace CryptoExchange.Net if (ignore != null) continue; - properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute) attr).PropertyName); + properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName); } foreach (var token in obj) { @@ -257,7 +292,11 @@ namespace CryptoExchange.Net d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower()); if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) { - log.Write(LogVerbosity.Warning, $"Didn't find property `{token.Key}` in object of type `{type.Name}`"); + // Checking if missing properties is voluntary + // True : If entire class is handled by a JsonConveter + bool isManuallyDeserialized = type.GetCustomAttribute(true) != null; + if (!isManuallyDeserialized) + log.Write(LogVerbosity.Warning, $"Didn't find property `{token.Key}` in object of type `{type.Name}`"); isDif = true; continue; } @@ -269,9 +308,9 @@ namespace CryptoExchange.Net continue; if (!IsSimple(propType) && propType != typeof(DateTime)) { - if(propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject) + if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject) CheckObject(propType.GetElementType(), (JObject)token.Value[0]); - else if(token.Value is JObject) + else if (token.Value is JObject) CheckObject(propType, (JObject)token.Value); } } @@ -282,7 +321,7 @@ namespace CryptoExchange.Net log.Write(LogVerbosity.Warning, $"Didn't find key `{prop}` in returned data object of type `{type.Name}`"); } - if(isDif) + if (isDif) log.Write(LogVerbosity.Debug, "Returned data: " + obj); } @@ -298,7 +337,7 @@ namespace CryptoExchange.Net } else { - if (((JsonPropertyAttribute) attr).PropertyName == name) + if (((JsonPropertyAttribute)attr).PropertyName == name) return prop; } } From ed14082b9a42fa777a7181f2a9ff8dd76e87e39a Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 1 Aug 2018 14:41:53 +0200 Subject: [PATCH 3/4] Moved PrivateKey to ApiCredentials, ApiCredentials use SecureString, Refactored array param string building --- .../Authentication/ApiCredentials.cs | 73 ++++++++++++++++--- .../Authentication/AuthenticationProvider.cs | 8 -- CryptoExchange.Net/ExchangeClient.cs | 50 ++++--------- CryptoExchange.Net/ExchangeOptions.cs | 6 -- CryptoExchange.Net/ExtensionMethods.cs | 30 +++++++- 5 files changed, 107 insertions(+), 60 deletions(-) diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 141ff6a..f312ae3 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -1,27 +1,82 @@ using System; +using System.Security; namespace CryptoExchange.Net.Authentication { - public class ApiCredentials + public class ApiCredentials: IDisposable { /// - /// The api key + /// The api key to authenticate requests /// - public string Key { get; } + public SecureString Key { get; } + /// - /// The api secret + /// The api secret to authenticate requests /// - public string Secret { get; } + public SecureString Secret { get; } - public ApiCredentials() { } + /// + /// The private key to authenticate requests + /// + public SecureString PrivateKey { get; } - public ApiCredentials(string key, string secret) + /// + /// Create Api credentials providing a private key for authenication + /// + /// The private key used for signing + public ApiCredentials(SecureString privateKey) { - if(string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) - throw new ArgumentException("Apikey or apisecret not provided"); + PrivateKey = Key; + } + /// + /// Create Api credentials providing a private key for authenication + /// + /// The private key used for signing + public ApiCredentials(string privateKey) + { + var securePrivateKey = new SecureString(); + foreach (var c in privateKey) + securePrivateKey.AppendChar(c); + securePrivateKey.MakeReadOnly(); + PrivateKey = securePrivateKey; + } + + /// + /// Create Api credentials providing a api key and secret for authenciation + /// + /// The api key used for identification + /// The api secret used for signing + public ApiCredentials(SecureString key, SecureString secret) + { Key = key; Secret = secret; } + + /// + /// Create Api credentials providing a private key for authenication + /// + /// The private key used for signing + public ApiCredentials(string key, string secret) + { + var secureApiKey = new SecureString(); + foreach (var c in key) + secureApiKey.AppendChar(c); + secureApiKey.MakeReadOnly(); + Key = secureApiKey; + + var secureApiSecret = new SecureString(); + foreach (var c in secret) + secureApiSecret.AppendChar(c); + secureApiSecret.MakeReadOnly(); + Secret = secureApiSecret; + } + + public void Dispose() + { + Key?.Dispose(); + Secret?.Dispose(); + PrivateKey?.Dispose(); + } } } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 0dc3cd4..4dd1431 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,5 +1,4 @@ using CryptoExchange.Net.Interfaces; -using System.Security; namespace CryptoExchange.Net.Authentication { @@ -7,18 +6,11 @@ namespace CryptoExchange.Net.Authentication { public ApiCredentials Credentials { get; } - public SecureString PrivateKey { get; } - protected AuthenticationProvider(ApiCredentials credentials) { Credentials = credentials; } - protected AuthenticationProvider(SecureString privateKey) - { - PrivateKey = privateKey; - } - public virtual string AddAuthenticationToUriString(string uri, bool signed) { return uri; diff --git a/CryptoExchange.Net/ExchangeClient.cs b/CryptoExchange.Net/ExchangeClient.cs index 0606bff..6bf89cd 100644 --- a/CryptoExchange.Net/ExchangeClient.cs +++ b/CryptoExchange.Net/ExchangeClient.cs @@ -106,23 +106,12 @@ namespace CryptoExchange.Net string paramString = null; if (parameters != null) { - paramString = "with parameters "; - int paramCount = 1; + paramString = "with parameters"; + foreach (var param in parameters) - { - string paramValue = param.Value.ToString(); - if (param.Value.GetType().IsArray) - paramValue = string.Format("[{0}]", string.Join(", ", ((object[])param.Value).Select(p => p.ToString()))); + paramString += $" {param.Key}={(param.Value.GetType().IsArray ? $"[{string.Join(", ", ((object[])param.Value).Select(p => p.ToString()))}]": param.Value )},"; - paramString += $"{param.Key}={paramValue}"; - - if (paramCount < parameters.Count) - paramString += ", "; - else - paramString += "."; - - paramCount++; - } + paramString = paramString.Trim(','); } log.Write(LogVerbosity.Debug, $"Sending {(signed ? "signed" : "")} request to {request.Uri} {(paramString ?? "")}"); @@ -140,25 +129,13 @@ namespace CryptoExchange.Net uriString += "?"; var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); - if (arraysParameters != null && arraysParameters.Count() > 0) + foreach(var arrayEntry in arraysParameters) { - bool isFirstEntry = true; - arraysParameters.ForEach((arrayEntry) => - { - if (!isFirstEntry) - uriString += "&"; - - List values = ((object[])arrayEntry.Value).ToList(); - uriString += $"{string.Join("&", values.Select(v => $"{arrayEntry.Key}[]={v}"))}"; - - isFirstEntry = false; - }); - - if (parameters.Count - arraysParameters.Count > 0) - uriString += "&"; + uriString += $"{string.Join("&", ((object[])arrayEntry.Value).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; } uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={s.Value}"))}"; + uriString = uriString.TrimEnd('&'); } if (authProvider != null) @@ -272,6 +249,10 @@ namespace CryptoExchange.Net private void CheckObject(Type type, JObject obj) { + if (type.GetCustomAttribute(true) != null) + // If type has a custom JsonConverter we assume this will handle property mapping + return; + bool isDif = false; var properties = new List(); var props = type.GetProperties(); @@ -291,12 +272,8 @@ namespace CryptoExchange.Net { d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower()); if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) - { - // Checking if missing properties is voluntary - // True : If entire class is handled by a JsonConveter - bool isManuallyDeserialized = type.GetCustomAttribute(true) != null; - if (!isManuallyDeserialized) - log.Write(LogVerbosity.Warning, $"Didn't find property `{token.Key}` in object of type `{type.Name}`"); + { + log.Write(LogVerbosity.Warning, $"Didn't find property `{token.Key}` in object of type `{type.Name}`"); isDif = true; continue; } @@ -359,6 +336,7 @@ namespace CryptoExchange.Net public virtual void Dispose() { + authProvider?.Credentials?.Dispose(); log.Write(LogVerbosity.Debug, "Disposing exchange client"); } } diff --git a/CryptoExchange.Net/ExchangeOptions.cs b/CryptoExchange.Net/ExchangeOptions.cs index 0ce7720..b2629da 100644 --- a/CryptoExchange.Net/ExchangeOptions.cs +++ b/CryptoExchange.Net/ExchangeOptions.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Security; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; using CryptoExchange.Net.RateLimiter; @@ -18,11 +17,6 @@ namespace CryptoExchange.Net /// public ApiCredentials ApiCredentials { get; set; } - /// - /// The private key instead of api credentials - /// - public SecureString PrivateKey { get; set; } - /// /// Proxy to use /// diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 83dec83..f3fc934 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security; namespace CryptoExchange.Net { @@ -25,5 +28,30 @@ namespace CryptoExchange.Net if (value != null) parameters.Add(key, value); } + + public static string GetString(this SecureString source) + { + string result = null; + int length = source.Length; + IntPtr pointer = IntPtr.Zero; + char[] chars = new char[length]; + + try + { + pointer = Marshal.SecureStringToBSTR(source); + Marshal.Copy(pointer, chars, 0, length); + + result = string.Join("", chars); + } + finally + { + if (pointer != IntPtr.Zero) + { + Marshal.ZeroFreeBSTR(pointer); + } + } + + return result; + } } } From 2bed3eb47f27002fd5dee62b5b3906a9bc27b41f Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 1 Aug 2018 14:46:35 +0200 Subject: [PATCH 4/4] Re-added null checks --- CryptoExchange.Net/Authentication/ApiCredentials.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index f312ae3..721735f 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -35,6 +35,9 @@ namespace CryptoExchange.Net.Authentication /// The private key used for signing public ApiCredentials(string privateKey) { + if(string.IsNullOrEmpty(privateKey)) + throw new ArgumentException("Private key can't be null/empty"); + var securePrivateKey = new SecureString(); foreach (var c in privateKey) securePrivateKey.AppendChar(c); @@ -59,6 +62,9 @@ namespace CryptoExchange.Net.Authentication /// The private key used for signing public ApiCredentials(string key, string secret) { + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) + throw new ArgumentException("Key and secret can't be null/empty"); + var secureApiKey = new SecureString(); foreach (var c in key) secureApiKey.AppendChar(c);