/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2012 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: http://getgreenshot.org/ * The Greenshot project is hosted on Sourceforge: http://sourceforge.net/projects/greenshot/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Drawing; using System.Net; using System.Security.Cryptography; using System.Text; using GreenshotPlugin.Controls; namespace GreenshotPlugin.Core { /// /// Provides a predefined set of algorithms that are supported officially by the protocol /// public enum OAuthSignatureTypes { HMACSHA1, PLAINTEXT, RSASHA1 } public enum HTTPMethod { GET, POST, PUT, DELETE }; public class OAuthSession { private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(OAuthSession)); protected const string OAUTH_VERSION = "1.0"; protected const string OAUTH_PARAMETER_PREFIX = "oauth_"; // // List of know and used oauth parameters' names // protected const string OAUTH_CONSUMER_KEY_KEY = "oauth_consumer_key"; protected const string OAUTH_CALLBACK_KEY = "oauth_callback"; protected const string OAUTH_VERSION_KEY = "oauth_version"; protected const string OAUTH_SIGNATURE_METHOD_KEY = "oauth_signature_method"; protected const string OAUTH_TIMESTAMP_KEY = "oauth_timestamp"; protected const string OAUTH_NONCE_KEY = "oauth_nonce"; protected const string OAUTH_TOKEN_KEY = "oauth_token"; protected const string OAUTH_VERIFIER_KEY = "oauth_verifier"; protected const string OAUTH_TOKEN_SECRET_KEY = "oauth_token_secret"; protected const string OAUTH_SIGNATURE_KEY = "oauth_signature"; protected const string HMACSHA1SignatureType = "HMAC-SHA1"; protected const string PlainTextSignatureType = "PLAINTEXT"; protected const string RSASHA1SignatureType = "RSA-SHA1"; protected static Random random = new Random(); protected const string UNRESERVED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; private string _userAgent = "Greenshot"; private string _callbackUrl = "http://getgreenshot.org"; private bool checkVerifier = true; private bool useHTTPHeadersForAuthorization = true; private IDictionary accessTokenResponseParameters = null; private IDictionary requestTokenResponseParameters = null; private IDictionary requestTokenParameters = new Dictionary(); public IDictionary RequestTokenParameters { get { return requestTokenParameters; } } /// /// Parameters of the last called getAccessToken /// public IDictionary AccessTokenResponseParameters { get { return accessTokenResponseParameters; } } /// /// Parameters of the last called getRequestToken /// public IDictionary RequestTokenResponseParameters { get { return requestTokenResponseParameters; } } private string consumerKey; private string consumerSecret; // default browser size private Size _browserSize = new Size(864, 587); private string loginTitle = "Authorize Greenshot access"; #region PublicProperties public string RequestTokenUrl { get; set; } public string AuthorizeUrl { get; set; } public string AccessTokenUrl { get; set; } public string Token { get; set; } public string TokenSecret { get; set; } public string Verifier { get; set; } public bool UseMultipartFormData { get; set; } public string UserAgent { get { return _userAgent; } set { _userAgent = value; } } public string CallbackUrl { get { return _callbackUrl; } set { _callbackUrl = value; } } public bool CheckVerifier { get { return checkVerifier; } set { checkVerifier = value; } } public Size BrowserSize { get { return _browserSize; } set { _browserSize = value; } } public string LoginTitle { get { return loginTitle; } set { loginTitle = value; } } public bool UseHTTPHeadersForAuthorization { get { return useHTTPHeadersForAuthorization; } set { useHTTPHeadersForAuthorization = value; } } #endregion public OAuthSession(string consumerKey, string consumerSecret) { this.consumerKey = consumerKey; this.consumerSecret = consumerSecret; this.UseMultipartFormData = true; } /// /// Helper function to compute a hash value /// /// The hashing algorithm used. If that algorithm needs some initialization, like HMAC and its derivatives, they should be initialized prior to passing it to this function /// The data to hash /// a Base64 string of the hash value private static string ComputeHash(HashAlgorithm hashAlgorithm, string data) { if (hashAlgorithm == null) { throw new ArgumentNullException("hashAlgorithm"); } if (string.IsNullOrEmpty(data)) { throw new ArgumentNullException("data"); } byte[] dataBuffer = Encoding.UTF8.GetBytes(data); byte[] hashBytes = hashAlgorithm.ComputeHash(dataBuffer); return Convert.ToBase64String(hashBytes); } /// /// Generate the normalized paramter string /// /// the list of query parameters /// a string with the normalized query parameters private static string GenerateNormalizedParametersString(IDictionary queryParameters) { if (queryParameters == null || queryParameters.Count == 0) { return string.Empty; } queryParameters = new SortedDictionary(queryParameters); StringBuilder sb = new StringBuilder(); foreach (string key in queryParameters.Keys) { if (queryParameters[key] is string) { sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", key, UrlEncode3986(string.Format("{0}",queryParameters[key]))); } } sb.Remove(sb.Length - 1, 1); return sb.ToString(); } /// /// 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 resulting string is for UTF-8 encoding! /// /// The value to Url encode /// Returns a Url encoded string (unicode) with UTF-8 encoded % values public static string UrlEncode3986(string value) { StringBuilder result = new StringBuilder(); foreach (char symbol in value) { if (UNRESERVED_CHARS.IndexOf(symbol) != -1) { result.Append(symbol); } else { byte[] utf8Bytes = Encoding.UTF8.GetBytes(symbol.ToString()); foreach(byte utf8Byte in utf8Bytes) { result.AppendFormat("%{0:X2}", utf8Byte); } } } return result.ToString(); } /// /// Generate the timestamp for the signature /// /// public static 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 static string GenerateNonce() { // Just a simple implementation of a random number between 123400 and 9999999 return random.Next(123400, 9999999).ToString(); } /// /// Get the request token using the consumer key and secret. Also initializes tokensecret /// /// The request token. private String getRequestToken() { string ret = null; IDictionary parameters = new Dictionary(); foreach(var value in requestTokenParameters) { parameters.Add(value); } Sign(HTTPMethod.GET, RequestTokenUrl, parameters); string response = MakeRequest(HTTPMethod.GET, RequestTokenUrl, parameters, null); if (response.Length > 0) { response = NetworkHelper.UrlDecode(response); LOG.DebugFormat("Request token response: {0}", response); requestTokenResponseParameters = NetworkHelper.ParseQueryString(response); if (requestTokenResponseParameters.ContainsKey(OAUTH_TOKEN_KEY)) { this.Token = requestTokenResponseParameters[OAUTH_TOKEN_KEY]; this.TokenSecret = requestTokenResponseParameters[OAUTH_TOKEN_SECRET_KEY]; ret = this.Token; } } return ret; } /// /// Authorize the token by showing the dialog /// /// The request token. private String getAuthorizeToken() { if (string.IsNullOrEmpty(Token)) { Exception e = new Exception("The request token is not set"); throw e; } LOG.DebugFormat("Opening AuthorizationLink: {0}", authorizationLink); OAuthLoginForm oAuthLoginForm = new OAuthLoginForm(LoginTitle, BrowserSize, authorizationLink, CallbackUrl); oAuthLoginForm.ShowDialog(); if (oAuthLoginForm.isOk) { if (oAuthLoginForm.CallbackParameters != null) { if (oAuthLoginForm.CallbackParameters.ContainsKey(OAUTH_TOKEN_KEY)) { Token = oAuthLoginForm.CallbackParameters[OAUTH_TOKEN_KEY]; } if (oAuthLoginForm.CallbackParameters.ContainsKey(OAUTH_VERIFIER_KEY)) { Verifier = oAuthLoginForm.CallbackParameters[OAUTH_VERIFIER_KEY]; } } } if (CheckVerifier) { if (!string.IsNullOrEmpty(Verifier)) { return Token; } else { return null; } } else { return Token; } } /// /// Get the access token /// /// The access token. private String getAccessToken() { if (string.IsNullOrEmpty(Token) || (CheckVerifier && string.IsNullOrEmpty(Verifier))) { Exception e = new Exception("The request token and verifier were not set"); throw e; } IDictionary parameters = new Dictionary(); Sign(HTTPMethod.GET, AccessTokenUrl, parameters); string response = MakeRequest(HTTPMethod.GET, AccessTokenUrl, parameters, null); if (response.Length > 0) { response = NetworkHelper.UrlDecode(response); LOG.DebugFormat("Access token response: {0}", response); accessTokenResponseParameters = NetworkHelper.ParseQueryString(response); if (accessTokenResponseParameters.ContainsKey(OAUTH_TOKEN_KEY) && accessTokenResponseParameters[OAUTH_TOKEN_KEY] != null) { this.Token = accessTokenResponseParameters[OAUTH_TOKEN_KEY]; } if (accessTokenResponseParameters.ContainsKey(OAUTH_TOKEN_SECRET_KEY) && accessTokenResponseParameters[OAUTH_TOKEN_SECRET_KEY] != null) { this.TokenSecret = accessTokenResponseParameters[OAUTH_TOKEN_SECRET_KEY]; } } return Token; } /// /// This method goes through the whole authorize process, including a Authorization window. /// /// true if the process is completed public bool Authorize() { this.Token = null; this.TokenSecret = null; this.Verifier = null; LOG.Debug("Creating Token"); try { getRequestToken(); } catch (Exception ex) { LOG.Error(ex); throw new NotSupportedException("Service is not available: " + ex.Message); } if (string.IsNullOrEmpty(getAuthorizeToken())) { LOG.Debug("User didn't authenticate!"); return false; } try { System.Threading.Thread.Sleep(1000); return getAccessToken() != null; } catch (Exception ex) { LOG.Error(ex); throw ex; } } /// /// Get the link to the authorization page for this application. /// /// The url with a valid request token, or a null string. private string authorizationLink { get { return AuthorizeUrl + "?" + OAUTH_TOKEN_KEY + "=" + this.Token + "&" + OAUTH_CALLBACK_KEY + "=" + UrlEncode3986(CallbackUrl); } } /// /// Submit a web request using oAuth. /// /// GET or POST /// The full url, including the querystring for the signing/request /// Parameters for the request, which need to be signed /// Parameters for the request, which do not need to be signed /// Data to post (MemoryStream) /// The web server response. public string MakeOAuthRequest(HTTPMethod method, string requestURL, IDictionary parametersToSign, IDictionary additionalParameters, IBinaryContainer postData) { return MakeOAuthRequest(method, requestURL, requestURL, parametersToSign, additionalParameters, postData); } /// /// Submit a web request using oAuth. /// /// GET or POST /// The full url, including the querystring for the signing /// The full url, including the querystring for the request /// Parameters for the request, which need to be signed /// Parameters for the request, which do not need to be signed /// Data to post (MemoryStream) /// The web server response. public string MakeOAuthRequest(HTTPMethod method, string signUrl, string requestURL, IDictionary parametersToSign, IDictionary additionalParameters, IBinaryContainer postData) { if (parametersToSign == null) { parametersToSign = new Dictionary(); } int retries = 2; Exception lastException = null; while (retries-- > 0) { // If we are not trying to get a Authorization or Accestoken, and we don't have a token, create one if (string.IsNullOrEmpty(Token)) { if (!Authorize()) { throw new Exception("Not authorized"); } } try { Sign(method, signUrl, parametersToSign); // Join all parameters IDictionary newParameters = new Dictionary(); foreach(var parameter in parametersToSign) { newParameters.Add(parameter); } if (additionalParameters != null) { foreach(var parameter in additionalParameters) { newParameters.Add(parameter); } } return MakeRequest(method, requestURL, newParameters, postData); } catch (WebException wEx) { lastException = wEx; if (wEx.Response != null) { HttpWebResponse response = wEx.Response as HttpWebResponse; if (response != null && response.StatusCode == HttpStatusCode.Unauthorized) { Token = null; TokenSecret = null; // Remove oauth keys, so they aren't added double List keysToDelete = new List(); foreach (string parameterKey in parametersToSign.Keys) { if (parameterKey.StartsWith(OAUTH_PARAMETER_PREFIX)) { keysToDelete.Add(parameterKey); } } foreach(string keyToDelete in keysToDelete) { parametersToSign.Remove(keyToDelete); } continue; } } throw wEx; } } if (lastException != null) { throw lastException; } throw new Exception("Not authorized"); } /// /// OAuth sign the parameters, meaning all oauth parameters are added to the supplied dictionary. /// And additionally a signature is added. /// /// Method (POST,PUT,GET) /// Url to call /// IDictionary private void Sign(HTTPMethod method, string requestURL, IDictionary parameters) { if (parameters == null) { throw new ArgumentNullException("parameters"); } // Build the signature base StringBuilder signatureBase = new StringBuilder(); // Add Method to signature base signatureBase.Append(method.ToString()).Append("&"); // Add normalized URL Uri url = new Uri(requestURL); string normalizedUrl = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{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; signatureBase.Append(UrlEncode3986(normalizedUrl)).Append("&"); // Add normalized parameters parameters.Add(OAUTH_VERSION_KEY, OAUTH_VERSION); parameters.Add(OAUTH_NONCE_KEY, GenerateNonce()); parameters.Add(OAUTH_TIMESTAMP_KEY, GenerateTimeStamp()); parameters.Add(OAUTH_SIGNATURE_METHOD_KEY, HMACSHA1SignatureType); parameters.Add(OAUTH_CONSUMER_KEY_KEY, consumerKey); if (CallbackUrl != null && RequestTokenUrl != null && requestURL.ToString().StartsWith(RequestTokenUrl)) { parameters.Add(OAUTH_CALLBACK_KEY, CallbackUrl); } if (!string.IsNullOrEmpty(Verifier)) { parameters.Add(OAUTH_VERIFIER_KEY, Verifier); } if (!string.IsNullOrEmpty(Token)) { parameters.Add(OAUTH_TOKEN_KEY, Token); } signatureBase.Append(UrlEncode3986(GenerateNormalizedParametersString(parameters))); LOG.DebugFormat("Signature base: {0}", signatureBase); // Generate Signature and add it to the parameters HMACSHA1 hmacsha1 = new HMACSHA1(); string key = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}&{1}", UrlEncode3986(consumerSecret), string.IsNullOrEmpty(TokenSecret) ? string.Empty : UrlEncode3986(TokenSecret)); hmacsha1.Key = Encoding.UTF8.GetBytes(key); string signature = ComputeHash(hmacsha1, signatureBase.ToString()); parameters.Add(OAUTH_SIGNATURE_KEY, signature); } /// /// Make the actual OAuth request, all oauth parameters are passed as header (default) and the others are placed in the url or post data. /// Any additional parameters added after the Sign call are not in the signature, this could be by design! /// /// /// /// /// IBinaryParameter /// Response from server private string MakeRequest(HTTPMethod method, string requestURL, IDictionary parameters, IBinaryContainer postData) { if (parameters == null) { throw new ArgumentNullException("parameters"); } IDictionary requestParameters = null; // Add oAuth values as HTTP headers, if this is allowed StringBuilder authHeader = null; if (UseHTTPHeadersForAuthorization) { authHeader = new StringBuilder(); requestParameters = new Dictionary(); foreach (string parameterKey in parameters.Keys) { if (parameterKey.StartsWith(OAUTH_PARAMETER_PREFIX)) { authHeader.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}=\"{1}\", ", parameterKey, UrlEncode3986(string.Format("{0}",parameters[parameterKey]))); } else if (!requestParameters.ContainsKey(parameterKey)) { requestParameters.Add(parameterKey, parameters[parameterKey]); } } // Remove trailing comma and space and add it to the headers if (authHeader.Length > 0) { authHeader.Remove(authHeader.Length - 2, 2); } } else { requestParameters = parameters; } if (HTTPMethod.GET == method || postData != null) { if (requestParameters.Count > 0) { // Add the parameters to the request requestURL += "?" + NetworkHelper.GenerateQueryParameters(requestParameters); } } // Create webrequest HttpWebRequest webRequest = (HttpWebRequest)NetworkHelper.CreateWebRequest(requestURL); webRequest.Method = method.ToString(); webRequest.ServicePoint.Expect100Continue = false; webRequest.UserAgent = _userAgent; webRequest.Timeout = 20000; if (UseHTTPHeadersForAuthorization && authHeader != null) { LOG.DebugFormat("Authorization: OAuth {0}", authHeader.ToString()); webRequest.Headers.Add("Authorization: OAuth " + authHeader.ToString()); } if ((HTTPMethod.POST == method || HTTPMethod.PUT == method) && postData == null && requestParameters != null && requestParameters.Count > 0) { if (UseMultipartFormData) { NetworkHelper.WriteMultipartFormData(webRequest, requestParameters); } else { StringBuilder form = new StringBuilder(); foreach (string parameterKey in requestParameters.Keys) { if (parameters[parameterKey] is IBinaryContainer) { IBinaryContainer binaryParameter = parameters[parameterKey] as IBinaryContainer; form.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", UrlEncode3986(parameterKey), UrlEncode3986(binaryParameter.ToBase64String(Base64FormattingOptions.None))); } else { form.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", UrlEncode3986(parameterKey), UrlEncode3986(string.Format("{0}",parameters[parameterKey]))); } } // Remove trailing & if (form.Length > 0) { form.Remove(form.Length - 1, 1); } webRequest.ContentType = "application/x-www-form-urlencoded"; byte[] data = Encoding.UTF8.GetBytes(form.ToString()); using (var requestStream = webRequest.GetRequestStream()) { requestStream.Write(data, 0, data.Length); } } } else if (postData != null) { postData.Upload(webRequest); } else { webRequest.ContentLength = 0; } string responseData = NetworkHelper.GetResponse(webRequest); LOG.DebugFormat("Response: {0}", responseData); webRequest = null; return responseData; } } }