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