diff --git a/DropNet2/Authentication/OAuthBase.cs b/DropNet2/Authentication/OAuthBase.cs new file mode 100644 index 0000000..a3ba5cf --- /dev/null +++ b/DropNet2/Authentication/OAuthBase.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DropNet2.Authentication +{ + public class OAuthBase + { + #region SignatureTypes enum + + /// + /// Provides a predefined set of algorithms that are supported officially by the protocol + /// + public enum SignatureTypes + { + HMACSHA1, + PLAINTEXT, + RSASHA1 + } + + #endregion + + protected const string OAuthVersion = "1.0"; + protected const string OAuthParameterPrefix = "oauth_"; + + // + // List of know and used oauth parameters' names + // + protected const string OAuthConsumerKeyKey = "oauth_consumer_key"; + protected const string OAuthCallbackKey = "oauth_callback"; + protected const string OAuthVersionKey = "oauth_version"; + protected const string OAuthSignatureMethodKey = "oauth_signature_method"; + protected const string OAuthSignatureKey = "oauth_signature"; + protected const string OAuthTimestampKey = "oauth_timestamp"; + protected const string OAuthNonceKey = "oauth_nonce"; + protected const string OAuthTokenKey = "oauth_token"; + protected const string OAuthTokenSecretKey = "oauth_token_secret"; + + protected const string Hmacsha1SignatureType = "HMAC-SHA1"; + protected const string PlainTextSignatureType = "PLAINTEXT"; + protected const string Rsasha1SignatureType = "RSA-SHA1"; + + protected Random Random = new Random(); + + protected string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; + + /// + /// Internal function to cut out all non oauth query string parameters (all parameters not beginning with "oauth_") + /// + /// The query string part of the Url + /// A list of QueryParameter each containing the parameter name and value + private List GetQueryParameters(string parameters) + { + if (parameters.StartsWith("?")) + { + parameters = parameters.Remove(0, 1); + } + + var result = new List(); + + if (!string.IsNullOrEmpty(parameters)) + { + string[] p = parameters.Split('&'); + foreach (string s in p) + { + if (!string.IsNullOrEmpty(s) && !s.StartsWith(OAuthParameterPrefix)) + { + if (s.IndexOf('=') > -1) + { + string[] temp = s.Split('='); + result.Add(new QueryParameter(temp[0], temp[1])); + } + else + { + result.Add(new QueryParameter(s, string.Empty)); + } + } + } + } + + return result; + } + + /// + /// This is a different Url Encode implementation since the default .NET one outputs the percent encoding in lower case. + /// While this is not a problem with the percent encoding spec, it is used in upper case throughout OAuth + /// + /// The value to Url encode + /// Returns a Url encoded string + protected string UrlEncode(string value) + { + var result = new StringBuilder(); + + foreach (char symbol in value) + { + if (UnreservedChars.IndexOf(symbol) != -1) + { + result.Append(symbol); + } + else + { + result.Append('%' + String.Format("{0:X2}", (int)symbol)); + } + } + + return result.ToString(); + } + + /// + /// Normalizes the request parameters according to the spec + /// + /// The list of parameters already sorted + /// a string representing the normalized parameters + protected string NormalizeRequestParameters(IList parameters) + { + var sb = new StringBuilder(); + for (int i = 0; i < parameters.Count; i++) + { + QueryParameter p = parameters[i]; + sb.AppendFormat("{0}={1}", p.Name, p.Value); + + if (i < parameters.Count - 1) + { + sb.Append("&"); + } + } + + return sb.ToString(); + } + + /// + /// Generate the signature base that is used to produce the signature + /// + /// The full url that needs to be signed including its non OAuth url parameters + /// The consumer key + /// The token, if available. If not available pass null or an empty string + /// The token secret, if available. If not available pass null or an empty string + /// The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc) + /// TimeStamp + /// The nounce + /// The signature type. To use the default values use OAuthBase.SignatureTypes. + /// Normalised Url + /// The normalized request parameters. + /// + /// The signature base + /// + /// consumerKey + public string GenerateSignatureBase(Uri url, string consumerKey, string token, string tokenSecret, + string httpMethod, string timeStamp, string nonce, string signatureType, + out string normalizedUrl, out string normalizedRequestParameters) + { + if (token == null) + { + token = string.Empty; + } + + if (tokenSecret == null) + { + tokenSecret = string.Empty; + } + + if (string.IsNullOrEmpty(consumerKey)) + { + throw new ArgumentNullException("consumerKey"); + } + + if (string.IsNullOrEmpty(httpMethod)) + { + throw new ArgumentNullException("httpMethod"); + } + + if (string.IsNullOrEmpty(signatureType)) + { + throw new ArgumentNullException("signatureType"); + } + + normalizedUrl = null; + normalizedRequestParameters = null; + + List parameters = GetQueryParameters(url.Query); + parameters.Add(new QueryParameter(OAuthVersionKey, OAuthVersion)); + parameters.Add(new QueryParameter(OAuthNonceKey, nonce)); + parameters.Add(new QueryParameter(OAuthTimestampKey, timeStamp)); + parameters.Add(new QueryParameter(OAuthSignatureMethodKey, signatureType)); + parameters.Add(new QueryParameter(OAuthConsumerKeyKey, consumerKey)); + + if (!string.IsNullOrEmpty(token)) + { + parameters.Add(new QueryParameter(OAuthTokenKey, token)); + } + + parameters.Sort(new QueryParameterComparer()); + + normalizedUrl = string.Format("{0}://{1}", url.Scheme, url.Host); + if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443))) + { + normalizedUrl += ":" + url.Port; + } + normalizedUrl += url.AbsolutePath; + normalizedRequestParameters = NormalizeRequestParameters(parameters); + + var signatureBase = new StringBuilder(); + signatureBase.AppendFormat("{0}&", httpMethod.ToUpper()); + signatureBase.AppendFormat("{0}&", UrlEncode(normalizedUrl)); + signatureBase.AppendFormat("{0}", UrlEncode(normalizedRequestParameters)); + + return signatureBase.ToString(); + } + + + /// + /// Generates a signature using the HMAC-SHA1 algorithm + /// + /// The full url that needs to be signed including its non OAuth url parameters + /// The consumer key + /// The consumer seceret + /// The token, if available. If not available pass null or an empty string + /// The token secret, if available. If not available pass null or an empty string + /// The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc) + /// The time stamp. + /// The nonce. + /// The normalized URL. + /// The normalized request parameters. + /// The auth header. + /// + /// A base64 string of the hash value + /// + public string GenerateSignature(Uri url, string consumerKey, string consumerSecret, string token, + string tokenSecret, string httpMethod, string timeStamp, string nonce, + out string normalizedUrl, out string normalizedRequestParameters, + out string authHeader) + { + return GenerateSignature(url, consumerKey, consumerSecret, token, tokenSecret, httpMethod, timeStamp, nonce, + SignatureTypes.PLAINTEXT, out normalizedUrl, out normalizedRequestParameters, + out authHeader); + } + + /// + /// Generates a signature using the specified signatureType + /// + /// The full url that needs to be signed including its non OAuth url parameters + /// The consumer key + /// The consumer seceret + /// The token, if available. If not available pass null or an empty string + /// The token secret, if available. If not available pass null or an empty string + /// The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc) + /// The time stamp. + /// The nonce. + /// The type of signature to use + /// The normalized URL. + /// The normalized request parameters. + /// The auth header. + /// + /// A base64 string of the hash value + /// + /// + /// Unknown signature type;signatureType + public string GenerateSignature(Uri url, string consumerKey, string consumerSecret, string token, + string tokenSecret, string httpMethod, string timeStamp, string nonce, + SignatureTypes signatureType, out string normalizedUrl, + out string normalizedRequestParameters, out string authHeader) + { + normalizedUrl = null; + normalizedRequestParameters = null; + authHeader = null; + + switch (signatureType) + { + case SignatureTypes.PLAINTEXT: + var auth = new StringBuilder(); + auth.AppendFormat("{0}=\"{1}\", ", OAuthConsumerKeyKey, UrlEncode(consumerKey)); + auth.AppendFormat("{0}=\"{1}\", ", OAuthNonceKey, UrlEncode(nonce)); + auth.AppendFormat("{0}=\"{1}\", ", OAuthSignatureKey, UrlEncode(string.Format("{0}&{1}", consumerSecret, tokenSecret))); + auth.AppendFormat("{0}=\"{1}\", ", OAuthSignatureMethodKey, "PLAINTEXT"); + auth.AppendFormat("{0}=\"{1}\", ", OAuthTimestampKey, timeStamp); + if (!string.IsNullOrEmpty(token)) + { + auth.AppendFormat("{0}=\"{1}\", ", OAuthTokenKey, UrlEncode(token)); + } + auth.AppendFormat("{0}=\"{1}\"", OAuthVersionKey, "1.0"); + authHeader = auth.ToString(); + return UrlEncode(string.Format("{0}&{1}", consumerSecret, tokenSecret)); + + case SignatureTypes.RSASHA1: + throw new NotImplementedException(); + default: + throw new ArgumentException("Unknown signature type", "signatureType"); + } + } + + /// + /// Generate the timestamp for the signature + /// + /// + public virtual string GenerateTimeStamp() + { + // Default implementation of UNIX time of the current UTC time + TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); + return Convert.ToInt64(ts.TotalSeconds).ToString(); + } + + /// + /// Generate a nonce + /// + /// + public virtual string GenerateNonce() + { + // Just a simple implementation of a random number between 123400 and 9999999 + return Random.Next(123400, 9999999).ToString(); + } + + #region Nested type: QueryParameter + + /// + /// Provides an internal structure to sort the query parameter + /// + protected class QueryParameter + { + private readonly string _name; + private readonly string _value; + + public QueryParameter(string name, string value) + { + _name = name; + _value = value; + } + + public string Name + { + get { return _name; } + } + + public string Value + { + get { return _value; } + } + } + + #endregion + + #region Nested type: QueryParameterComparer + + /// + /// Comparer class used to perform the sorting of the query parameters + /// + protected class QueryParameterComparer : IComparer + { + #region IComparer Members + + public int Compare(QueryParameter x, QueryParameter y) + { + if (x.Name == y.Name) + { + return string.Compare(x.Value, y.Value); + } + + return string.Compare(x.Name, y.Name); + } + + #endregion + } + + #endregion + } +} \ No newline at end of file diff --git a/DropNet2/Authentication/OAuthMessageHandler.cs b/DropNet2/Authentication/OAuthMessageHandler.cs index dd813df..9207bc5 100644 --- a/DropNet2/Authentication/OAuthMessageHandler.cs +++ b/DropNet2/Authentication/OAuthMessageHandler.cs @@ -1,4 +1,5 @@ -using DropNet2.HttpHelpers; +using System.Net.Http.Headers; +using DropNet2.HttpHelpers; using System; using System.Net.Http; using System.Text; @@ -61,6 +62,30 @@ public HttpRequest Authenticate(HttpRequest request) return request; } + protected override System.Threading.Tasks.Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + string normalizedUri; + string authHeader; + string normalizedParameters; + + var url = AuthBase.GenerateSignature( + request.RequestUri, + ApiKey, + ApiSecret, + UserToken, + UserSecret, + request.Method.Method, + AuthBase.GenerateTimeStamp(), + AuthBase.GenerateNonce(), + out normalizedUri, + out normalizedParameters, + out authHeader); + + request.Headers.Authorization = new AuthenticationHeaderValue("OAuth", authHeader); + + return base.SendAsync(request, cancellationToken); + } + private static object EncodeParameters(HttpRequest request) { var querystring = new StringBuilder(); @@ -91,5 +116,12 @@ private static string GenerateTimeStamp() TimeSpan span = DateTime.UtcNow - new DateTime(0x7b2, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(span.TotalSeconds).ToString(); } + + private OAuthBase AuthBase + { + get { return _authBase ?? (_authBase = new OAuthBase()); } + } + + private OAuthBase _authBase; } } diff --git a/DropNet2/Client.Files.cs b/DropNet2/Client.Files.cs index 09a533e..f803fc4 100644 --- a/DropNet2/Client.Files.cs +++ b/DropNet2/Client.Files.cs @@ -32,6 +32,12 @@ public async Task GetMetaData(string path) return response; } + public async Task FilePut() + { + _httpClient.BaseAddress = new Uri("https://api-content.dropbox.com/1/"); + var d =await _httpClient.PutAsync("dropbox/files_put/data.txt", new StreamContent(new MemoryStream())); + } + /// /// Gets a share link from a give path /// diff --git a/DropNet2/Client.User.cs b/DropNet2/Client.User.cs index 58251fb..85a9282 100644 --- a/DropNet2/Client.User.cs +++ b/DropNet2/Client.User.cs @@ -1,7 +1,12 @@ -using DropNet2.HttpHelpers; +using System.Diagnostics; +using System.Net; +using DropNet2.Exceptions; +using DropNet2.Helpers; +using DropNet2.HttpHelpers; using DropNet2.Models; using System.Net.Http; using System.Threading.Tasks; +using Newtonsoft.Json; namespace DropNet2 { @@ -12,20 +17,14 @@ public partial class DropNetClient /// Auth Step 1. Gets a Request Token which is required for the login request /// /// - public async Task GetRequestToken() + public async Task GetRequestTokenAsync() { - var requestUrl = MakeRequestString("1/oauth/request_token", ApiType.Base); - - var request = new HttpRequest(HttpMethod.Get, requestUrl); - - _oauthHandler.Authenticate(request); - - var response = await _httpClient.SendAsync(request); + _httpClient.BaseAddress = GetBaseAddress(ApiType.Base); + var response = await _httpClient.GetAsync("oauth/request_token"); string responseBody = await response.Content.ReadAsStringAsync(); UserLogin = GetUserLoginFromParams(responseBody); - SetUserToken(UserLogin); return UserLogin; @@ -35,7 +34,7 @@ public async Task GetRequestToken() /// Auth Step 3. Once a Request Token has been authorized convert it to an access token for API usage. /// /// - public async Task GetAccessToken() + public async Task GetAccessTokenAsync() { var requestUrl = MakeRequestString("1/oauth/access_token", ApiType.Base); @@ -59,16 +58,19 @@ public async Task GetAccessToken() /// Gets the account info of the current logged in user /// /// - public async Task AccountInfo() + public async Task AccountInfoAsync() { - var requestUrl = MakeRequestString("1/account/info", ApiType.Base); - - var request = new HttpRequest(HttpMethod.Get, requestUrl); - - var response = await SendAsync(request); - - return response; + _httpClient.BaseAddress = GetBaseAddress(ApiType.Base); + + using (var response = await _httpClient.GetAsync("account/info")) + { + if (response.StatusCode != HttpStatusCode.OK) + { + throw new DropboxException(response); + } + + return await response.GetResultAsync(); + } } - } } diff --git a/DropNet2/Client.cs b/DropNet2/Client.cs index 6af4b41..efae941 100644 --- a/DropNet2/Client.cs +++ b/DropNet2/Client.cs @@ -13,8 +13,8 @@ namespace DropNet2 { public partial class DropNetClient { - private const string ApiBaseUrl = "https://api.dropbox.com"; - private const string ApiContentBaseUrl = "https://api-content.dropbox.com"; + private const string ApiBaseUrl = "https://api.dropbox.com/1/"; + private const string ApiContentBaseUrl = "https://api-content.dropbox.com/1/"; /// /// Do not set this property directly, instead use SetUserToken @@ -142,5 +142,10 @@ private async Task SendAsync(HttpRequest request) where T : class return JsonConvert.DeserializeObject(responseBody); } + static Uri GetBaseAddress(ApiType apiType) + { + string uri = apiType == ApiType.Base ? ApiBaseUrl : ApiContentBaseUrl; + return new Uri(uri); + } } } diff --git a/DropNet2/DropNet2.csproj b/DropNet2/DropNet2.csproj index 538f4bc..7dadc1e 100644 --- a/DropNet2/DropNet2.csproj +++ b/DropNet2/DropNet2.csproj @@ -39,11 +39,13 @@ + + diff --git a/DropNet2/Exceptions/DropboxException.cs b/DropNet2/Exceptions/DropboxException.cs index 52f874c..443a5ad 100644 --- a/DropNet2/Exceptions/DropboxException.cs +++ b/DropNet2/Exceptions/DropboxException.cs @@ -27,5 +27,6 @@ public DropboxException(Exception ex) : base("Dropbox error occurred", ex) { } + } } diff --git a/DropNet2/Helpers/Extensions.cs b/DropNet2/Helpers/Extensions.cs new file mode 100644 index 0000000..6d3042d --- /dev/null +++ b/DropNet2/Helpers/Extensions.cs @@ -0,0 +1,15 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DropNet2.Helpers +{ + public static class Extensions + { + public static async Task GetResultAsync(this HttpResponseMessage response) + { + string contentString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(contentString); + } + } +} diff --git a/DropNet2Sample/ContentsPage.xaml.cs b/DropNet2Sample/ContentsPage.xaml.cs index a9ffa9f..236f9fc 100644 --- a/DropNet2Sample/ContentsPage.xaml.cs +++ b/DropNet2Sample/ContentsPage.xaml.cs @@ -58,7 +58,7 @@ private void lsbContents_SelectionChanged(object sender, System.Windows.Controls if (selected == null) return; - if (selected.Is_Dir) + if (selected.IsDirectory) { //navigate to the new dir _model.LoadPath(selected.Path); diff --git a/DropNet2Sample/MainPage.xaml.cs b/DropNet2Sample/MainPage.xaml.cs index 12fdc62..df66fe6 100644 --- a/DropNet2Sample/MainPage.xaml.cs +++ b/DropNet2Sample/MainPage.xaml.cs @@ -45,7 +45,7 @@ private async void btnLogin_Click(object sender, System.Windows.RoutedEventArgs _model.ShowBrowser = true; //Get the request token - var requestToken = await App.DropNetClient.GetRequestToken(); + var requestToken = await App.DropNetClient.GetRequestTokenAsync(); var tokenUrl = App.DropNetClient.BuildAuthorizeUrl(requestToken, _tokenCallbackUrl); //Open a browser with the URL @@ -59,7 +59,7 @@ private async void loginBrowser_LoadCompleted(object sender, NavigationEventArgs { //SUCCESS! _model.SetStatus("Getting Access Token...", true); - var accessToken = await App.DropNetClient.GetAccessToken(); + var accessToken = await App.DropNetClient.GetAccessTokenAsync(); //TODO - Save this token/Secret for remember me function diff --git a/DropNet2Tests/ClientFileTests.cs b/DropNet2Tests/ClientFileTests.cs index 6432282..04bc7b5 100644 --- a/DropNet2Tests/ClientFileTests.cs +++ b/DropNet2Tests/ClientFileTests.cs @@ -21,9 +21,9 @@ public void Setup() [Test] public async Task Given_A_Root_Path_Get_Metadata() { - var data = await _client.GetMetaData("/"); - Assert.NotNull(data); - Assert.IsTrue(data.IsDirectory); + await _client.FilePut(); + // Assert.NotNull(data); + //Assert.IsTrue(data.IsDirectory); } [Test] diff --git a/DropNet2Tests/ClientUserTests.cs b/DropNet2Tests/ClientUserTests.cs index d6c6c3b..5b6aa9d 100644 --- a/DropNet2Tests/ClientUserTests.cs +++ b/DropNet2Tests/ClientUserTests.cs @@ -6,13 +6,13 @@ namespace DropNet2Tests { [TestFixture] public class ClientUserTests - { + { [Test] public async Task When_Token_Requested_Then_User_Token_Is_Returned() - { + { var client = new DropNetClient(AppKey, AppSecret); - var userToken = await client.GetRequestToken(); + var userToken = await client.GetRequestTokenAsync(); Assert.NotNull(userToken); } @@ -20,8 +20,8 @@ public async Task When_Token_Requested_Then_User_Token_Is_Returned() public async Task Given_UserToken_When_Build_Auth_Url_Then_The_Authentication_Url_Is_Returned() { var client = new DropNetClient(AppKey, AppSecret); - - var userToken = await client.GetRequestToken(); + + var userToken = await client.GetRequestTokenAsync(); string url = client.BuildAuthorizeUrl(userToken, "http://cloudyboxapp.com"); Assert.IsNotEmpty(url); } @@ -30,8 +30,7 @@ public async Task Given_UserToken_When_Build_Auth_Url_Then_The_Authentication_Ur public async Task Given_A_Clent_Get_User_Account_Infromation() { var client = new DropNetClient(AppKey, AppSecret, UserToken, UserSecret); - var accountInfromation = await client.AccountInfo(); - + var accountInfromation = await client.AccountInfoAsync(); Assert.NotNull(accountInfromation); Assert.NotNull(accountInfromation.QuotaInfo); } @@ -42,11 +41,11 @@ public async Task Get_Access_Token_Test() { var client = new DropNetClient(AppKey, AppSecret); - var userToken = await client.GetRequestToken(); - + var userToken = await client.GetRequestTokenAsync(); + //Open the url in browser and login string url = client.BuildAuthorizeUrl(userToken, "http://cloudyboxapp.com"); - var user = await client.GetAccessToken(); + var user = await client.GetAccessTokenAsync(); Assert.NotNull(user); } diff --git a/DropNet2Tests/DropNet2Tests.csproj b/DropNet2Tests/DropNet2Tests.csproj index 4fe4042..b36f4d0 100644 --- a/DropNet2Tests/DropNet2Tests.csproj +++ b/DropNet2Tests/DropNet2Tests.csproj @@ -40,11 +40,13 @@ - - ..\packages\Microsoft.Net.Http.2.1.3-beta\lib\net45\System.Net.Http.Extensions.dll + + False + ..\packages\Microsoft.Net.Http.2.1.10\lib\net45\System.Net.Http.Extensions.dll - - ..\packages\Microsoft.Net.Http.2.1.3-beta\lib\net45\System.Net.Http.Primitives.dll + + False + ..\packages\Microsoft.Net.Http.2.1.10\lib\net45\System.Net.Http.Primitives.dll @@ -71,6 +73,7 @@ +