From 350dfeabbaad25c59cfc24e53a86c12b58fa653d Mon Sep 17 00:00:00 2001 From: ta264 Date: Mon, 14 Oct 2019 21:21:00 +0100 Subject: [PATCH] New: Make Twitter NetStandard compatible --- src/NzbDrone.Common/OAuth/LICENSE | 13 + src/NzbDrone.Common/OAuth/OAuthRequest.cs | 508 ++++++++++++++++++ src/NzbDrone.Common/OAuth/OAuthRequestType.cs | 14 + .../OAuth/OAuthSignatureMethod.cs | 12 + .../OAuth/OAuthSignatureTreatment.cs | 13 + src/NzbDrone.Common/OAuth/OAuthTools.cs | 409 ++++++++++++++ src/NzbDrone.Common/OAuth/WebParameter.cs | 14 + .../OAuth/WebParameterCollection.cs | 202 +++++++ .../Notifications/Twitter/TwitterService.cs | 2 +- src/NzbDrone.Core/Radarr.Core.csproj | 1 - src/NzbDrone.Core/TinyTwitter.cs | 57 -- 11 files changed, 1186 insertions(+), 59 deletions(-) create mode 100644 src/NzbDrone.Common/OAuth/LICENSE create mode 100644 src/NzbDrone.Common/OAuth/OAuthRequest.cs create mode 100644 src/NzbDrone.Common/OAuth/OAuthRequestType.cs create mode 100644 src/NzbDrone.Common/OAuth/OAuthSignatureMethod.cs create mode 100644 src/NzbDrone.Common/OAuth/OAuthSignatureTreatment.cs create mode 100644 src/NzbDrone.Common/OAuth/OAuthTools.cs create mode 100644 src/NzbDrone.Common/OAuth/WebParameter.cs create mode 100644 src/NzbDrone.Common/OAuth/WebParameterCollection.cs diff --git a/src/NzbDrone.Common/OAuth/LICENSE b/src/NzbDrone.Common/OAuth/LICENSE new file mode 100644 index 000000000..2058ebc3b --- /dev/null +++ b/src/NzbDrone.Common/OAuth/LICENSE @@ -0,0 +1,13 @@ +OAuth (http://github.com/danielcrenna/oauth) +Written by Daniel Crenna +(http://danielcrenna.com) + +This work is public domain. +"The person who associated a work with this document has + dedicated the work to the Commons by waiving all of his + or her rights to the work worldwide under copyright law + and all related or neighboring legal rights he or she + had in the work, to the extent allowable by law." + +For more information, please visit: +http://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file diff --git a/src/NzbDrone.Common/OAuth/OAuthRequest.cs b/src/NzbDrone.Common/OAuth/OAuthRequest.cs new file mode 100644 index 000000000..9c3476101 --- /dev/null +++ b/src/NzbDrone.Common/OAuth/OAuthRequest.cs @@ -0,0 +1,508 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.OAuth +{ + /// + /// A request wrapper for the OAuth 1.0a specification. + /// + /// + public class OAuthRequest + { + public virtual OAuthSignatureMethod SignatureMethod { get; set; } + public virtual OAuthSignatureTreatment SignatureTreatment { get; set; } + public virtual OAuthRequestType Type { get; set; } + + public virtual string Method { get; set; } + public virtual string Realm { get; set; } + public virtual string ConsumerKey { get; set; } + public virtual string ConsumerSecret { get; set; } + public virtual string Token { get; set; } + public virtual string TokenSecret { get; set; } + public virtual string Verifier { get; set; } + public virtual string ClientUsername { get; set; } + public virtual string ClientPassword { get; set; } + public virtual string CallbackUrl { get; set; } + public virtual string Version { get; set; } + public virtual string SessionHandle { get; set; } + + /// + public virtual string RequestUrl { get; set; } + + #region Authorization Header + +#if !WINRT + public string GetAuthorizationHeader(NameValueCollection parameters) + { + var collection = new WebParameterCollection(parameters); + + return GetAuthorizationHeader(collection); + } +#endif + + public string GetAuthorizationHeader(IDictionary parameters) + { + var collection = new WebParameterCollection(parameters); + + return GetAuthorizationHeader(collection); + } + + public string GetAuthorizationHeader() + { + var collection = new WebParameterCollection(0); + + return GetAuthorizationHeader(collection); + } + + public string GetAuthorizationHeader(WebParameterCollection parameters) + { + switch (Type) + { + case OAuthRequestType.RequestToken: + ValidateRequestState(); + return GetSignatureAuthorizationHeader(parameters); + case OAuthRequestType.AccessToken: + ValidateAccessRequestState(); + return GetSignatureAuthorizationHeader(parameters); + case OAuthRequestType.ProtectedResource: + ValidateProtectedResourceState(); + return GetSignatureAuthorizationHeader(parameters); + case OAuthRequestType.ClientAuthentication: + ValidateClientAuthAccessRequestState(); + return GetClientSignatureAuthorizationHeader(parameters); + default: + throw new ArgumentOutOfRangeException(); + } + } + + private string GetSignatureAuthorizationHeader(WebParameterCollection parameters) + { + var signature = GetNewSignature(parameters); + + parameters.Add("oauth_signature", signature); + + return WriteAuthorizationHeader(parameters); + } + + private string GetClientSignatureAuthorizationHeader(WebParameterCollection parameters) + { + var signature = GetNewSignatureXAuth(parameters); + + parameters.Add("oauth_signature", signature); + + return WriteAuthorizationHeader(parameters); + } + + private string WriteAuthorizationHeader(WebParameterCollection parameters) + { + var sb = new StringBuilder("OAuth "); + + if (!IsNullOrBlank(Realm)) + { + sb.AppendFormat("realm=\"{0}\",", OAuthTools.UrlEncodeRelaxed(Realm)); + } + + parameters.Sort((l, r) => l.Name.CompareTo(r.Name)); + + foreach (var parameter in parameters.Where(parameter => + !IsNullOrBlank(parameter.Name) && + !IsNullOrBlank(parameter.Value) && + (parameter.Name.StartsWith("oauth_") || parameter.Name.StartsWith("x_auth_")))) + { + sb.AppendFormat("{0}=\"{1}\",", parameter.Name, parameter.Value); + } + + sb.Remove(sb.Length - 1, 1); + + var authorization = sb.ToString(); + return authorization; + } + + #endregion + + #region Authorization Query + +#if !WINRT + public string GetAuthorizationQuery(NameValueCollection parameters) + { + var collection = new WebParameterCollection(parameters); + + return GetAuthorizationQuery(collection); + } +#endif + + public string GetAuthorizationQuery(IDictionary parameters) + { + var collection = new WebParameterCollection(parameters); + + return GetAuthorizationQuery(collection); + } + + public string GetAuthorizationQuery() + { + var collection = new WebParameterCollection(0); + + return GetAuthorizationQuery(collection); + } + + private string GetAuthorizationQuery(WebParameterCollection parameters) + { + switch (Type) + { + case OAuthRequestType.RequestToken: + ValidateRequestState(); + return GetSignatureAuthorizationQuery(parameters); + case OAuthRequestType.AccessToken: + ValidateAccessRequestState(); + return GetSignatureAuthorizationQuery(parameters); + case OAuthRequestType.ProtectedResource: + ValidateProtectedResourceState(); + return GetSignatureAuthorizationQuery(parameters); + case OAuthRequestType.ClientAuthentication: + ValidateClientAuthAccessRequestState(); + return GetClientSignatureAuthorizationQuery(parameters); + default: + throw new ArgumentOutOfRangeException(); + } + } + + private string GetSignatureAuthorizationQuery(WebParameterCollection parameters) + { + var signature = GetNewSignature(parameters); + + parameters.Add("oauth_signature", signature); + + return WriteAuthorizationQuery(parameters); + } + + private string GetClientSignatureAuthorizationQuery(WebParameterCollection parameters) + { + var signature = GetNewSignatureXAuth(parameters); + + parameters.Add("oauth_signature", signature); + + return WriteAuthorizationQuery(parameters); + } + + private static string WriteAuthorizationQuery(WebParameterCollection parameters) + { + var sb = new StringBuilder(); + + parameters.Sort((l, r) => l.Name.CompareTo(r.Name)); + + var count = 0; + + foreach (var parameter in parameters.Where(parameter => + !IsNullOrBlank(parameter.Name) && + !IsNullOrBlank(parameter.Value) && + (parameter.Name.StartsWith("oauth_") || parameter.Name.StartsWith("x_auth_")))) + { + count++; + var format = count < parameters.Count ? "{0}={1}&" : "{0}={1}"; + sb.AppendFormat(format, parameter.Name, parameter.Value); + } + + var authorization = sb.ToString(); + return authorization; + } + + #endregion + + private string GetNewSignature(WebParameterCollection parameters) + { + var timestamp = OAuthTools.GetTimestamp(); + + var nonce = OAuthTools.GetNonce(); + + AddAuthParameters(parameters, timestamp, nonce); + + var signatureBase = OAuthTools.ConcatenateRequestElements(Method.ToUpperInvariant(), RequestUrl, parameters); + + var signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret); + + return signature; + } + + private string GetNewSignatureXAuth(WebParameterCollection parameters) + { + var timestamp = OAuthTools.GetTimestamp(); + + var nonce = OAuthTools.GetNonce(); + + AddXAuthParameters(parameters, timestamp, nonce); + + var signatureBase = OAuthTools.ConcatenateRequestElements(Method.ToUpperInvariant(), RequestUrl, parameters); + + var signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret); + + return signature; + } + + #region Static Helpers + + public static OAuthRequest ForRequestToken(string consumerKey, string consumerSecret) + { + var credentials = new OAuthRequest + { + Method = "GET", + Type = OAuthRequestType.RequestToken, + SignatureMethod = OAuthSignatureMethod.HmacSha1, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret + }; + return credentials; + } + + public static OAuthRequest ForRequestToken(string consumerKey, string consumerSecret, string callbackUrl) + { + var credentials = ForRequestToken(consumerKey, consumerSecret); + credentials.CallbackUrl = callbackUrl; + return credentials; + } + + public static OAuthRequest ForAccessToken(string consumerKey, string consumerSecret, string requestToken, string requestTokenSecret) + { + var credentials = new OAuthRequest + { + Method = "GET", + Type = OAuthRequestType.AccessToken, + SignatureMethod = OAuthSignatureMethod.HmacSha1, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + Token = requestToken, + TokenSecret = requestTokenSecret + }; + return credentials; + } + + public static OAuthRequest ForAccessToken(string consumerKey, string consumerSecret, string requestToken, string requestTokenSecret, string verifier) + { + var credentials = ForAccessToken(consumerKey, consumerSecret, requestToken, requestTokenSecret); + credentials.Verifier = verifier; + return credentials; + } + + public static OAuthRequest ForAccessTokenRefresh(string consumerKey, string consumerSecret, string accessToken, string accessTokenSecret, string sessionHandle) + { + var credentials = ForAccessToken(consumerKey, consumerSecret, accessToken, accessTokenSecret); + credentials.SessionHandle = sessionHandle; + return credentials; + } + + public static OAuthRequest ForAccessTokenRefresh(string consumerKey, string consumerSecret, string accessToken, string accessTokenSecret, string sessionHandle, string verifier) + { + var credentials = ForAccessToken(consumerKey, consumerSecret, accessToken, accessTokenSecret); + credentials.SessionHandle = sessionHandle; + credentials.Verifier = verifier; + return credentials; + } + + public static OAuthRequest ForClientAuthentication(string consumerKey, string consumerSecret, string username, string password) + { + var credentials = new OAuthRequest + { + Method = "GET", + Type = OAuthRequestType.ClientAuthentication, + SignatureMethod = OAuthSignatureMethod.HmacSha1, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + ClientUsername = username, + ClientPassword = password + }; + + return credentials; + } + + public static OAuthRequest ForProtectedResource(string method, string consumerKey, string consumerSecret, string accessToken, string accessTokenSecret) + { + var credentials = new OAuthRequest + { + Method = method ?? "GET", + Type = OAuthRequestType.ProtectedResource, + SignatureMethod = OAuthSignatureMethod.HmacSha1, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + Token = accessToken, + TokenSecret = accessTokenSecret + }; + return credentials; + } + + #endregion + + private void ValidateRequestState() + { + if (IsNullOrBlank(Method)) + { + throw new ArgumentException("You must specify an HTTP method"); + } + + if (IsNullOrBlank(RequestUrl)) + { + throw new ArgumentException("You must specify a request token URL"); + } + + if (IsNullOrBlank(ConsumerKey)) + { + throw new ArgumentException("You must specify a consumer key"); + } + + if (IsNullOrBlank(ConsumerSecret)) + { + throw new ArgumentException("You must specify a consumer secret"); + } + } + + private void ValidateAccessRequestState() + { + if (IsNullOrBlank(Method)) + { + throw new ArgumentException("You must specify an HTTP method"); + } + + if (IsNullOrBlank(RequestUrl)) + { + throw new ArgumentException("You must specify an access token URL"); + } + + if (IsNullOrBlank(ConsumerKey)) + { + throw new ArgumentException("You must specify a consumer key"); + } + + if (IsNullOrBlank(ConsumerSecret)) + { + throw new ArgumentException("You must specify a consumer secret"); + } + + if (IsNullOrBlank(Token)) + { + throw new ArgumentException("You must specify a token"); + } + } + + private void ValidateClientAuthAccessRequestState() + { + if (IsNullOrBlank(Method)) + { + throw new ArgumentException("You must specify an HTTP method"); + } + + if (IsNullOrBlank(RequestUrl)) + { + throw new ArgumentException("You must specify an access token URL"); + } + + if (IsNullOrBlank(ConsumerKey)) + { + throw new ArgumentException("You must specify a consumer key"); + } + + if (IsNullOrBlank(ConsumerSecret)) + { + throw new ArgumentException("You must specify a consumer secret"); + } + + if (IsNullOrBlank(ClientUsername) || IsNullOrBlank(ClientPassword)) + { + throw new ArgumentException("You must specify user credentials"); + } + } + + private void ValidateProtectedResourceState() + { + if (IsNullOrBlank(Method)) + { + throw new ArgumentException("You must specify an HTTP method"); + } + + if (IsNullOrBlank(ConsumerKey)) + { + throw new ArgumentException("You must specify a consumer key"); + } + + if (IsNullOrBlank(ConsumerSecret)) + { + throw new ArgumentException("You must specify a consumer secret"); + } + } + + private void AddAuthParameters(ICollection parameters, string timestamp, string nonce) + { + var authParameters = new WebParameterCollection + { + new WebParameter("oauth_consumer_key", ConsumerKey), + new WebParameter("oauth_nonce", nonce), + new WebParameter("oauth_signature_method", ToRequestValue(SignatureMethod)), + new WebParameter("oauth_timestamp", timestamp), + new WebParameter("oauth_version", Version ?? "1.0") + }; + + if (!IsNullOrBlank(Token)) + { + authParameters.Add(new WebParameter("oauth_token", Token)); + } + + if (!IsNullOrBlank(CallbackUrl)) + { + authParameters.Add(new WebParameter("oauth_callback", CallbackUrl)); + } + + if (!IsNullOrBlank(Verifier)) + { + authParameters.Add(new WebParameter("oauth_verifier", Verifier)); + } + + if (!IsNullOrBlank(SessionHandle)) + { + authParameters.Add(new WebParameter("oauth_session_handle", SessionHandle)); + } + + foreach (var authParameter in authParameters) + { + parameters.Add(authParameter); + } + } + + private void AddXAuthParameters(ICollection parameters, string timestamp, string nonce) + { + var authParameters = new WebParameterCollection + { + new WebParameter("x_auth_username", ClientUsername), + new WebParameter("x_auth_password", ClientPassword), + new WebParameter("x_auth_mode", "client_auth"), + new WebParameter("oauth_consumer_key", ConsumerKey), + new WebParameter("oauth_signature_method", ToRequestValue(SignatureMethod)), + new WebParameter("oauth_timestamp", timestamp), + new WebParameter("oauth_nonce", nonce), + new WebParameter("oauth_version", Version ?? "1.0") + }; + + foreach (var authParameter in authParameters) + { + parameters.Add(authParameter); + } + } + + public static string ToRequestValue(OAuthSignatureMethod signatureMethod) + { + var value = signatureMethod.ToString().ToUpper(); + var shaIndex = value.IndexOf("SHA1"); + return shaIndex > -1 ? value.Insert(shaIndex, "-") : value; + } + + private static bool IsNullOrBlank(string value) + { + return String.IsNullOrEmpty(value) || (!String.IsNullOrEmpty(value) && value.Trim() == String.Empty); + } + } +} + + diff --git a/src/NzbDrone.Common/OAuth/OAuthRequestType.cs b/src/NzbDrone.Common/OAuth/OAuthRequestType.cs new file mode 100644 index 000000000..e6f86dbd6 --- /dev/null +++ b/src/NzbDrone.Common/OAuth/OAuthRequestType.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Common.OAuth +{ + /// + /// The types of OAuth requests possible in a typical workflow. + /// Used for validation purposes and to build static helpers. + /// + public enum OAuthRequestType + { + RequestToken, + AccessToken, + ProtectedResource, + ClientAuthentication + } +} \ No newline at end of file diff --git a/src/NzbDrone.Common/OAuth/OAuthSignatureMethod.cs b/src/NzbDrone.Common/OAuth/OAuthSignatureMethod.cs new file mode 100644 index 000000000..173e03a3c --- /dev/null +++ b/src/NzbDrone.Common/OAuth/OAuthSignatureMethod.cs @@ -0,0 +1,12 @@ +namespace NzbDrone.Common.OAuth +{ + /// + /// The encryption method to use when hashing a request signature. + /// + public enum OAuthSignatureMethod + { + HmacSha1, + PlainText, + RsaSha1 + } +} \ No newline at end of file diff --git a/src/NzbDrone.Common/OAuth/OAuthSignatureTreatment.cs b/src/NzbDrone.Common/OAuth/OAuthSignatureTreatment.cs new file mode 100644 index 000000000..3b64d736e --- /dev/null +++ b/src/NzbDrone.Common/OAuth/OAuthSignatureTreatment.cs @@ -0,0 +1,13 @@ +namespace NzbDrone.Common.OAuth +{ + /// + /// Specifies whether the final signature value should be escaped during calculation. + /// This might be necessary for some OAuth implementations that do not obey the default + /// specification for signature escaping. + /// + public enum OAuthSignatureTreatment + { + Escaped, + Unescaped + } +} \ No newline at end of file diff --git a/src/NzbDrone.Common/OAuth/OAuthTools.cs b/src/NzbDrone.Common/OAuth/OAuthTools.cs new file mode 100644 index 000000000..3d0f8aabc --- /dev/null +++ b/src/NzbDrone.Common/OAuth/OAuthTools.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +#if !WINRT +using System.Security.Cryptography; +#else +using Windows.Security.Cryptography; +using Windows.Security.Cryptography.Core; +using Windows.Storage.Streams; +using System.Globalization; +#endif + +namespace NzbDrone.Common.OAuth +{ + /// + /// A general purpose toolset for creating components of an OAuth request. + /// + /// + public static class OAuthTools + { + private const string AlphaNumeric = Upper + Lower + Digit; + private const string Digit = "1234567890"; + private const string Lower = "abcdefghijklmnopqrstuvwxyz"; + private const string Unreserved = AlphaNumeric + "-._~"; + private const string Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + private static readonly Random _random; + private static readonly object _randomLock = new object(); + +#if !SILVERLIGHT && !WINRT + private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); +#endif + + static OAuthTools() + { +#if !SILVERLIGHT && !WINRT + var bytes = new byte[4]; + _rng.GetNonZeroBytes(bytes); + _random = new Random(BitConverter.ToInt32(bytes, 0)); +#else + _random = new Random(); +#endif + } + + /// + /// All text parameters are UTF-8 encoded (per section 5.1). + /// + /// +#if !WINRT + private static readonly Encoding _encoding = Encoding.UTF8; +#else + private static readonly BinaryStringEncoding _encoding = BinaryStringEncoding.Utf8; +#endif + + /// + /// Generates a random 16-byte lowercase alphanumeric string. + /// + /// + /// + public static string GetNonce() + { + const string chars = (Lower + Digit); + + var nonce = new char[16]; + lock (_randomLock) + { + for (var i = 0; i < nonce.Length; i++) + { + nonce[i] = chars[_random.Next(0, chars.Length)]; + } + } + return new string(nonce); + } + + /// + /// Generates a timestamp based on the current elapsed seconds since '01/01/1970 0000 GMT" + /// + /// + /// + public static string GetTimestamp() + { + return GetTimestamp(DateTime.UtcNow); + } + + /// + /// Generates a timestamp based on the elapsed seconds of a given time since '01/01/1970 0000 GMT" + /// + /// + /// A specified point in time. + /// + public static string GetTimestamp(DateTime dateTime) + { + var timestamp = ToUnixTime(dateTime); + return timestamp.ToString(); + } + + private static long ToUnixTime(DateTime dateTime) + { + var timeSpan = (dateTime - new DateTime(1970, 1, 1)); + var timestamp = (long)timeSpan.TotalSeconds; + + return timestamp; + } + + /// + /// URL encodes a string based on section 5.1 of the OAuth spec. + /// Namely, percent encoding with [RFC3986], avoiding unreserved characters, + /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs. + /// + /// + /// + public static string UrlEncodeRelaxed(string value) + { + var escaped = Uri.EscapeDataString(value); + + // LinkedIn users have problems because it requires escaping brackets + escaped = escaped.Replace("(", PercentEncode("(")) + .Replace(")", PercentEncode(")")); + + return escaped; + } + + private static string PercentEncode(string s) + { + var bytes = Encoding.UTF8.GetBytes(s); + var sb = new StringBuilder(); + foreach (var b in bytes) + { + // Supports proper encoding of special characters (\n\r\t\b) + if ((b > 7 && b < 11) || b == 13) + { + sb.Append(string.Format("%0{0:X}", b)); + } + else + { + sb.Append(string.Format("%{0:X}", b)); + } + } + return sb.ToString(); + } + + /// + /// URL encodes a string based on section 5.1 of the OAuth spec. + /// Namely, percent encoding with [RFC3986], avoiding unreserved characters, + /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs. + /// + /// + /// + public static string UrlEncodeStrict(string value) + { + // [JD]: We need to escape the apostrophe as well or the signature will fail + var original = value; + var ret = original.OfType().Where( + c => !Unreserved.OfType().Contains(c) && c != '%').Aggregate( + value, (current, c) => current.Replace( + c.ToString(), PercentEncode(c.ToString()) + )); + + return ret.Replace("%%", "%25%"); // Revisit to encode actual %'s + } + + /// + /// Sorts a collection of key-value pairs by name, and then value if equal, + /// concatenating them into a single string. This string should be encoded + /// prior to, or after normalization is run. + /// + /// + /// + /// + public static string NormalizeRequestParameters(WebParameterCollection parameters) + { + var copy = SortParametersExcludingSignature(parameters); + var concatenated = Concatenate(copy, "=", "&"); + return concatenated; + } + + private static string Concatenate(ICollection collection, string separator, string spacer) + { + var sb = new StringBuilder(); + + var total = collection.Count; + var count = 0; + + foreach (var item in collection) + { + sb.Append(item.Name); + sb.Append(separator); + sb.Append(item.Value); + + count++; + if (count < total) + { + sb.Append(spacer); + } + } + + return sb.ToString(); + } + + /// + /// Sorts a by name, and then value if equal. + /// + /// A collection of parameters to sort + /// A sorted parameter collection + public static WebParameterCollection SortParametersExcludingSignature(WebParameterCollection parameters) + { + var copy = new WebParameterCollection(parameters); + var exclusions = copy.Where(n => EqualsIgnoreCase(n.Name, "oauth_signature")); + + copy.RemoveAll(exclusions); + + foreach(var parameter in copy) + { + parameter.Value = UrlEncodeStrict(parameter.Value); + } + + copy.Sort((x, y) => x.Name.Equals(y.Name) ? x.Value.CompareTo(y.Value) : x.Name.CompareTo(y.Name)); + return copy; + } + + private static bool EqualsIgnoreCase(string left, string right) + { +#if WINRT + return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0; +#else + return String.Compare(left, right, StringComparison.InvariantCultureIgnoreCase) == 0; +#endif + } + + /// + /// Creates a request URL suitable for making OAuth requests. + /// Resulting URLs must exclude port 80 or port 443 when accompanied by HTTP and HTTPS, respectively. + /// Resulting URLs must be lower case. + /// + /// + /// The original request URL + /// + public static string ConstructRequestUrl(Uri url) + { + if (url == null) + { + throw new ArgumentNullException("url"); + } + + var sb = new StringBuilder(); + + var requestUrl = string.Format("{0}://{1}", url.Scheme, url.Host); + var qualified = string.Format(":{0}", url.Port); + var basic = url.Scheme == "http" && url.Port == 80; + var secure = url.Scheme == "https" && url.Port == 443; + + sb.Append(requestUrl); + sb.Append(!basic && !secure ? qualified : ""); + sb.Append(url.AbsolutePath); + + return sb.ToString(); //.ToLower(); + } + + /// + /// Creates a request elements concatentation value to send with a request. + /// This is also known as the signature base. + /// + /// + /// + /// The request's HTTP method type + /// The request URL + /// The request's parameters + /// A signature base string + public static string ConcatenateRequestElements(string method, string url, WebParameterCollection parameters) + { + var sb = new StringBuilder(); + + // Separating &'s are not URL encoded + var requestMethod = string.Concat(method.ToUpper(), "&"); + var requestUrl = string.Concat(UrlEncodeRelaxed(ConstructRequestUrl(new Uri(url))), "&"); + var requestParameters = UrlEncodeRelaxed(NormalizeRequestParameters(parameters)); + + sb.Append(requestMethod); + sb.Append(requestUrl); + sb.Append(requestParameters); + + return sb.ToString(); + } + + /// + /// Creates a signature value given a signature base and the consumer secret. + /// This method is used when the token secret is currently unknown. + /// + /// + /// The hashing method + /// The signature base + /// The consumer key + /// + public static string GetSignature(OAuthSignatureMethod signatureMethod, + string signatureBase, + string consumerSecret) + { + return GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret, null); + } + + /// + /// Creates a signature value given a signature base and the consumer secret. + /// This method is used when the token secret is currently unknown. + /// + /// + /// The hashing method + /// The treatment to use on a signature value + /// The signature base + /// The consumer key + /// + public static string GetSignature(OAuthSignatureMethod signatureMethod, + OAuthSignatureTreatment signatureTreatment, + string signatureBase, + string consumerSecret) + { + return GetSignature(signatureMethod, signatureTreatment, signatureBase, consumerSecret, null); + } + + /// + /// Creates a signature value given a signature base and the consumer secret and a known token secret. + /// + /// + /// The hashing method + /// The signature base + /// The consumer secret + /// The token secret + /// + public static string GetSignature(OAuthSignatureMethod signatureMethod, + string signatureBase, + string consumerSecret, + string tokenSecret) + { + return GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, consumerSecret, tokenSecret); + } + + /// + /// Creates a signature value given a signature base and the consumer secret and a known token secret. + /// + /// + /// The hashing method + /// The treatment to use on a signature value + /// The signature base + /// The consumer secret + /// The token secret + /// + public static string GetSignature(OAuthSignatureMethod signatureMethod, + OAuthSignatureTreatment signatureTreatment, + string signatureBase, + string consumerSecret, + string tokenSecret) + { + if (IsNullOrBlank(tokenSecret)) + { + tokenSecret = String.Empty; + } + + consumerSecret = UrlEncodeRelaxed(consumerSecret); + tokenSecret = UrlEncodeRelaxed(tokenSecret); + + string signature; + switch (signatureMethod) + { + case OAuthSignatureMethod.HmacSha1: + { + var key = string.Concat(consumerSecret, "&", tokenSecret); +#if WINRT + IBuffer keyMaterial = CryptographicBuffer.ConvertStringToBinary(key, _encoding); + MacAlgorithmProvider hmacSha1Provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha1); + CryptographicKey macKey = hmacSha1Provider.CreateKey(keyMaterial); + IBuffer dataToBeSigned = CryptographicBuffer.ConvertStringToBinary(signatureBase, _encoding); + IBuffer signatureBuffer = CryptographicEngine.Sign(macKey, dataToBeSigned); + signature = CryptographicBuffer.EncodeToBase64String(signatureBuffer); +#else + var crypto = new HMACSHA1(); + + crypto.Key = _encoding.GetBytes(key); + signature = HashWith(signatureBase, crypto); +#endif + + break; + } + default: + throw new NotImplementedException("Only HMAC-SHA1 is currently supported."); + } + + var result = signatureTreatment == OAuthSignatureTreatment.Escaped + ? UrlEncodeRelaxed(signature) + : signature; + + return result; + } + +#if !WINRT + private static string HashWith(string input, HashAlgorithm algorithm) + { + var data = Encoding.UTF8.GetBytes(input); + var hash = algorithm.ComputeHash(data); + return Convert.ToBase64String(hash); + } +#endif + + private static bool IsNullOrBlank(string value) + { + return String.IsNullOrEmpty(value) || (!String.IsNullOrEmpty(value) && value.Trim() == String.Empty); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Common/OAuth/WebParameter.cs b/src/NzbDrone.Common/OAuth/WebParameter.cs new file mode 100644 index 000000000..c5f7759d7 --- /dev/null +++ b/src/NzbDrone.Common/OAuth/WebParameter.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Common.OAuth +{ + public class WebParameter + { + public WebParameter(string name, string value) + { + Name = name; + Value = value; + } + + public string Value { get; set; } + public string Name { get; private set; } + } +} diff --git a/src/NzbDrone.Common/OAuth/WebParameterCollection.cs b/src/NzbDrone.Common/OAuth/WebParameterCollection.cs new file mode 100644 index 000000000..af3e92439 --- /dev/null +++ b/src/NzbDrone.Common/OAuth/WebParameterCollection.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Collections.Specialized; + +namespace NzbDrone.Common.OAuth +{ + public class WebParameterCollection : IList + { + private IList _parameters; + + public virtual WebParameter this[string name] + { + get + { + var parameters = this.Where(p => p.Name.Equals(name)); + + if(parameters.Count() == 0) + { + return null; + } + + if(parameters.Count() == 1) + { + return parameters.Single(); + } + + var value = string.Join(",", parameters.Select(p => p.Value).ToArray()); + return new WebParameter(name, value); + } + } + + public virtual IEnumerable Names + { + get { return _parameters.Select(p => p.Name); } + } + + public virtual IEnumerable Values + { + get { return _parameters.Select(p => p.Value); } + } + + public WebParameterCollection(IEnumerable parameters) + { + _parameters = new List(parameters); + } + +#if !WINRT + public WebParameterCollection(NameValueCollection collection) : this() + { + AddCollection(collection); + } + + public virtual void AddRange(NameValueCollection collection) + { + AddCollection(collection); + } + + private void AddCollection(NameValueCollection collection) + { + var parameters = collection.AllKeys.Select(key => new WebParameter(key, collection[key])); + foreach (var parameter in parameters) + { + _parameters.Add(parameter); + } + } +#endif + + public WebParameterCollection(IDictionary collection) : this() + { + AddCollection(collection); + } + + public void AddCollection(IDictionary collection) + { + foreach (var parameter in collection.Keys.Select(key => new WebParameter(key, collection[key]))) + { + _parameters.Add(parameter); + } + } + + public WebParameterCollection() + { + _parameters = new List(0); + } + + public WebParameterCollection(int capacity) + { + _parameters = new List(capacity); + } + + private void AddCollection(IEnumerable collection) + { + foreach (var pair in collection.Select(parameter => new WebParameter(parameter.Name, parameter.Value))) + { + _parameters.Add(pair); + } + } + + public virtual void AddRange(WebParameterCollection collection) + { + AddCollection(collection); + } + + public virtual void AddRange(IEnumerable collection) + { + AddCollection(collection); + } + + public virtual void Sort(Comparison comparison) + { + var sorted = new List(_parameters); + sorted.Sort(comparison); + _parameters = sorted; + } + + public virtual bool RemoveAll(IEnumerable parameters) + { + var array = parameters.ToArray(); + var success = array.Aggregate(true, (current, parameter) => current & _parameters.Remove(parameter)); + return success && array.Length > 0; + } + + public virtual void Add(string name, string value) + { + var pair = new WebParameter(name, value); + _parameters.Add(pair); + } + + #region IList Members + + public virtual IEnumerator GetEnumerator() + { + return _parameters.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public virtual void Add(WebParameter parameter) + { + + _parameters.Add(parameter); + } + + public virtual void Clear() + { + _parameters.Clear(); + } + + public virtual bool Contains(WebParameter parameter) + { + return _parameters.Contains(parameter); + } + + public virtual void CopyTo(WebParameter[] parameters, int arrayIndex) + { + _parameters.CopyTo(parameters, arrayIndex); + } + + public virtual bool Remove(WebParameter parameter) + { + return _parameters.Remove(parameter); + } + + public virtual int Count + { + get { return _parameters.Count; } + } + + public virtual bool IsReadOnly + { + get { return _parameters.IsReadOnly; } + } + + public virtual int IndexOf(WebParameter parameter) + { + return _parameters.IndexOf(parameter); + } + + public virtual void Insert(int index, WebParameter parameter) + { + _parameters.Insert(index, parameter); + } + + public virtual void RemoveAt(int index) + { + _parameters.RemoveAt(index); + } + + public virtual WebParameter this[int index] + { + get { return _parameters[index]; } + set { _parameters[index] = value; } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs index 60d6320f9..b8327b479 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs @@ -1,13 +1,13 @@ using FluentValidation.Results; using NLog; using System; -using OAuth; using System.Net; using System.Collections.Specialized; using System.IO; using System.Web; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.OAuth; namespace NzbDrone.Core.Notifications.Twitter { diff --git a/src/NzbDrone.Core/Radarr.Core.csproj b/src/NzbDrone.Core/Radarr.Core.csproj index 0c772f0fe..3a9a8e68b 100644 --- a/src/NzbDrone.Core/Radarr.Core.csproj +++ b/src/NzbDrone.Core/Radarr.Core.csproj @@ -10,7 +10,6 @@ - diff --git a/src/NzbDrone.Core/TinyTwitter.cs b/src/NzbDrone.Core/TinyTwitter.cs index 508783b6b..0c13a9157 100644 --- a/src/NzbDrone.Core/TinyTwitter.cs +++ b/src/NzbDrone.Core/TinyTwitter.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Web.Script.Serialization; namespace TinyTwitter { @@ -60,61 +58,6 @@ public void DirectMessage(string message, string screenName) .Execute(); } - public IEnumerable GetHomeTimeline(long? sinceId = null, long? maxId = null, int? count = 20) - { - return GetTimeline("https://api.twitter.com/1.1/statuses/home_timeline.json", sinceId, maxId, count, ""); - } - - public IEnumerable GetMentions(long? sinceId = null, long? maxId = null, int? count = 20) - { - return GetTimeline("https://api.twitter.com/1.1/statuses/mentions.json", sinceId, maxId, count, ""); - } - - public IEnumerable GetUserTimeline(long? sinceId = null, long? maxId = null, int? count = 20, string screenName = "") - { - return GetTimeline("https://api.twitter.com/1.1/statuses/user_timeline.json", sinceId, maxId, count, screenName); - } - - private IEnumerable GetTimeline(string url, long? sinceId, long? maxId, int? count, string screenName) - { - var builder = new RequestBuilder(oauth, "GET", url); - - if (sinceId.HasValue) - builder.AddParameter("since_id", sinceId.Value.ToString()); - - if (maxId.HasValue) - builder.AddParameter("max_id", maxId.Value.ToString()); - - if (count.HasValue) - builder.AddParameter("count", count.Value.ToString()); - - if (screenName != "") - builder.AddParameter("screen_name", screenName); - - var responseContent = builder.Execute(); - - var serializer = new JavaScriptSerializer(); - - var tweets = (object[])serializer.DeserializeObject(responseContent); - - return tweets.Cast>().Select(tweet => - { - var user = ((Dictionary)tweet["user"]); - var date = DateTime.ParseExact(tweet["created_at"].ToString(), - "ddd MMM dd HH:mm:ss zz00 yyyy", - CultureInfo.InvariantCulture).ToLocalTime(); - - return new Tweet - { - Id = (long)tweet["id"], - CreatedAt = date, - Text = (string)tweet["text"], - UserName = (string)user["name"], - ScreenName = (string)user["screen_name"] - }; - }).ToArray(); - } - #region RequestBuilder public class RequestBuilder