diff --git a/GreenshotBoxPlugin/BoxConfiguration.cs b/GreenshotBoxPlugin/BoxConfiguration.cs index b7632f865..b75a3c2ae 100644 --- a/GreenshotBoxPlugin/BoxConfiguration.cs +++ b/GreenshotBoxPlugin/BoxConfiguration.cs @@ -50,12 +50,6 @@ namespace GreenshotBoxPlugin { set; } - [IniProperty("AddFilename", Description = "Is the filename passed on to Box", DefaultValue = "False")] - public bool AddFilename { - get; - set; - } - [IniProperty("RefreshToken", Description = "Box authorization refresh Token", Encrypted = true)] public string RefreshToken { get; diff --git a/GreenshotBoxPlugin/BoxUtils.cs b/GreenshotBoxPlugin/BoxUtils.cs index 2cb343c06..e0e1566d5 100644 --- a/GreenshotBoxPlugin/BoxUtils.cs +++ b/GreenshotBoxPlugin/BoxUtils.cs @@ -68,8 +68,8 @@ namespace GreenshotBoxPlugin { // Fill the OAuth2Settings OAuth2Settings settings = new OAuth2Settings(); - settings.AuthUrlPattern = "https://www.box.com/api/oauth2/authorize?client_id={ClientId}&response_type={response_type}&state{State}&redirect_uri={RedirectUrl}"; - settings.TokenUrlPattern = "https://www.box.com/api/oauth2/token"; + settings.AuthUrlPattern = "https://app.box.com/api/oauth2/authorize?client_id={ClientId}&response_type=code&state={State}&redirect_uri={RedirectUrl}"; + settings.TokenUrl = "https://api.box.com/oauth2/token"; settings.CloudServiceName = "Box"; settings.ClientId = BoxCredentials.ClientId; settings.ClientSecret = BoxCredentials.ClientSecret; @@ -83,11 +83,9 @@ namespace GreenshotBoxPlugin { settings.AccessTokenExpires = Config.AccessTokenExpires; try { - var webRequest = OAuth2Helper.CreateOAuth2WebRequest(HTTPMethod.POST, FilesUri, settings); + var webRequest = OAuth2Helper.CreateOAuth2WebRequest(HTTPMethod.POST, UploadFileUri, settings); IDictionary parameters = new Dictionary(); - if (Config.AddFilename) { - parameters.Add("filename", image); - } + parameters.Add("file", image); parameters.Add("parent_id", Config.FolderId); NetworkHelper.WriteMultipartFormData(webRequest, parameters); @@ -111,6 +109,7 @@ namespace GreenshotBoxPlugin { Config.AccessToken = settings.AccessToken; Config.AccessTokenExpires = settings.AccessTokenExpires; Config.IsDirty = true; + IniConfig.Save(); } } } diff --git a/GreenshotPicasaPlugin/PicasaUtils.cs b/GreenshotPicasaPlugin/PicasaUtils.cs index 8ea4d57c6..14c8307d1 100644 --- a/GreenshotPicasaPlugin/PicasaUtils.cs +++ b/GreenshotPicasaPlugin/PicasaUtils.cs @@ -33,7 +33,7 @@ namespace GreenshotPicasaPlugin { private const string PicasaScope = "https://picasaweb.google.com/data/"; private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(PicasaUtils)); private static readonly PicasaConfiguration Config = IniConfig.GetIniSection(); - private const string AuthUrl = "https://accounts.google.com/o/oauth2/auth?response_type={response_type}&client_id={ClientId}&redirect_uri={RedirectUrl}&state={State}&scope={scope}"; + private const string AuthUrl = "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={ClientId}&redirect_uri={RedirectUrl}&state={State}&scope=" + PicasaScope; private const string TokenUrl = "https://www.googleapis.com/oauth2/v3/token"; private const string UploadUrl = "https://picasaweb.google.com/data/feed/api/user/{0}/albumid/{1}"; @@ -49,10 +49,8 @@ namespace GreenshotPicasaPlugin { // Fill the OAuth2Settings OAuth2Settings settings = new OAuth2Settings(); settings.AuthUrlPattern = AuthUrl; - settings.TokenUrlPattern = TokenUrl; + settings.TokenUrl = TokenUrl; settings.CloudServiceName = "Picasa"; - settings.AdditionalAttributes.Add("response_type", "code"); - settings.AdditionalAttributes.Add("scope", PicasaScope); settings.ClientId = PicasaCredentials.ClientId; settings.ClientSecret = PicasaCredentials.ClientSecret; settings.AuthorizeMode = OAuth2AuthorizeMode.LocalServer; @@ -79,6 +77,7 @@ namespace GreenshotPicasaPlugin { Config.AccessToken = settings.AccessToken; Config.AccessTokenExpires = settings.AccessTokenExpires; Config.IsDirty = true; + IniConfig.Save(); } } diff --git a/GreenshotPlugin/Controls/OAuthLoginForm.cs b/GreenshotPlugin/Controls/OAuthLoginForm.cs index ce721def5..e562b1185 100644 --- a/GreenshotPlugin/Controls/OAuthLoginForm.cs +++ b/GreenshotPlugin/Controls/OAuthLoginForm.cs @@ -60,7 +60,9 @@ namespace GreenshotPlugin.Controls { // The script errors are suppressed by using the ExtendedWebBrowser _browser.ScriptErrorsSuppressed = false; - _browser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(Browser_DocumentCompleted); + _browser.DocumentCompleted += Browser_DocumentCompleted; + _browser.Navigated += Browser_Navigated; + _browser.Navigating += Browser_Navigating; _browser.Navigate(new Uri(authorizationLink)); } @@ -78,6 +80,16 @@ namespace GreenshotPlugin.Controls { CheckUrl(); } + private void Browser_Navigating(object sender, WebBrowserNavigatingEventArgs e) { + LOG.DebugFormat("Navigating to url: {0}", _browser.Url); + _addressTextBox.Text = e.Url.ToString(); + } + + private void Browser_Navigated(object sender, WebBrowserNavigatedEventArgs e) { + LOG.DebugFormat("Navigated to url: {0}", _browser.Url); + CheckUrl(); + } + private void CheckUrl() { if (_browser.Url.ToString().StartsWith(_callbackUrl)) { string queryParams = _browser.Url.Query; diff --git a/GreenshotPlugin/Core/NetworkHelper.cs b/GreenshotPlugin/Core/NetworkHelper.cs index 4ff398158..0acf63bf3 100644 --- a/GreenshotPlugin/Core/NetworkHelper.cs +++ b/GreenshotPlugin/Core/NetworkHelper.cs @@ -65,7 +65,6 @@ namespace GreenshotPlugin.Core { /// An Uri to specify the download location /// string with the file content public static string GetAsString(Uri uri) { - HttpWebRequest webRequest = CreateWebRequest(uri); return GetResponseAsString(CreateWebRequest(uri)); } @@ -391,15 +390,14 @@ namespace GreenshotPlugin.Core { /// Post the parameters "x-www-form-urlencoded" /// /// - /// - public static string UploadFormUrlEncoded(HttpWebRequest webRequest, IDictionary parameters) { + public static void UploadFormUrlEncoded(HttpWebRequest webRequest, IDictionary parameters) { webRequest.ContentType = "application/x-www-form-urlencoded"; string urlEncoded = NetworkHelper.GenerateQueryParameters(parameters); - using (var requestStream = webRequest.GetRequestStream()) - using (var streamWriter = new StreamWriter(requestStream, Encoding.UTF8)) { - streamWriter.Write(urlEncoded); + + byte[] data = Encoding.UTF8.GetBytes(urlEncoded); + using (var requestStream = webRequest.GetRequestStream()) { + requestStream.Write(data, 0, data.Length); } - return GetResponseAsString(webRequest); } /// @@ -423,6 +421,15 @@ namespace GreenshotPlugin.Core { /// The response data. /// TODO: This method should handle the StatusCode better! public static string GetResponseAsString(HttpWebRequest webRequest) { + return GetResponseAsString(webRequest, false); + } + + /// + /// + /// + /// + /// + public static string GetResponseAsString(HttpWebRequest webRequest, bool alsoReturnContentOnError) { string responseData = null; try { HttpWebResponse response = (HttpWebResponse) webRequest.GetResponse(); @@ -444,7 +451,13 @@ namespace GreenshotPlugin.Core { LOG.ErrorFormat("HTTP error {0}", response.StatusCode); using (Stream responseStream = response.GetResponseStream()) { if (responseStream != null) { - LOG.ErrorFormat("Content: {0}", new StreamReader(responseStream, true).ReadToEnd()); + using (StreamReader streamReader = new StreamReader(responseStream, true)) { + string errorContent = streamReader.ReadToEnd(); + if (alsoReturnContentOnError) { + return errorContent; + } + LOG.ErrorFormat("Content: {0}", errorContent); + } } } } diff --git a/GreenshotPlugin/Core/OAuthHelper.cs b/GreenshotPlugin/Core/OAuthHelper.cs index 2a8995131..94b73dc8c 100644 --- a/GreenshotPlugin/Core/OAuthHelper.cs +++ b/GreenshotPlugin/Core/OAuthHelper.cs @@ -134,20 +134,11 @@ namespace GreenshotPlugin.Core { /// /// The URL to get a Token /// - public string TokenUrlPattern { + public string TokenUrl { get; set; } - /// - /// Get formatted Token url (this will call a FormatWith(this) on the TokenUrlPattern - /// - public string FormattedTokenUrl { - get { - return TokenUrlPattern.FormatWith(this); - } - } - /// /// This is the redirect URL, in some implementations this is automatically set (LocalServerCodeReceiver) /// In some implementations this could be e.g. urn:ietf:wg:oauth:2.0:oob or urn:ietf:wg:oauth:2.0:oob:auto @@ -206,6 +197,15 @@ namespace GreenshotPlugin.Core { get; set; } + + /// + /// This contains the code returned from the authorization, but only shortly after it was received. + /// It will be cleared as soon as it was used. + /// + public string Code { + get; + set; + } } /// @@ -1046,7 +1046,7 @@ Greenshot received information from CloudServiceName. You can close this browser _returnValues.Add(name, nameValueCollection[name]); } } - } catch (Exception ex) { + } catch (Exception) { context.Response.OutputStream.Close(); throw; } @@ -1072,35 +1072,59 @@ Greenshot received information from CloudServiceName. You can close this browser /// Code to simplify OAuth 2 /// public static class OAuth2Helper { + private const string REFRESH_TOKEN = "refresh_token"; + private const string ACCESS_TOKEN = "access_token"; + private const string CODE = "code"; + private const string CLIENT_ID = "client_id"; + private const string CLIENT_SECRET = "client_secret"; + private const string GRANT_TYPE = "grant_type"; + private const string AUTHORIZATION_CODE = "authorization_code"; + private const string REDIRECT_URI = "redirect_uri"; + private const string EXPIRES_IN = "expires_in"; + /// /// Generate an OAuth 2 Token by using the supplied code /// /// Code to get the RefreshToken /// OAuth2Settings to update with the information that was retrieved - public static void GenerateRefreshToken(string code, OAuth2Settings settings) { - // Use the returned code to get a refresh code + public static void GenerateRefreshToken(OAuth2Settings settings) { IDictionary data = new Dictionary(); - data.Add("code", code); - data.Add("client_id", settings.ClientId); - data.Add("redirect_uri", settings.RedirectUrl); - data.Add("client_secret", settings.ClientSecret); - data.Add("grant_type", "authorization_code"); + // Use the returned code to get a refresh code + data.Add(CODE, settings.Code); + data.Add(CLIENT_ID, settings.ClientId); + data.Add(REDIRECT_URI, settings.RedirectUrl); + data.Add(CLIENT_SECRET, settings.ClientSecret); + data.Add(GRANT_TYPE, AUTHORIZATION_CODE); + foreach (string key in settings.AdditionalAttributes.Keys) { + data.Add(key, settings.AdditionalAttributes[key]); + } + + HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(settings.TokenUrl, HTTPMethod.POST); + NetworkHelper.UploadFormUrlEncoded(webRequest, data); + string accessTokenJsonResult = NetworkHelper.GetResponseAsString(webRequest, true); - HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(settings.FormattedTokenUrl, HTTPMethod.POST); - string accessTokenJsonResult = NetworkHelper.UploadFormUrlEncoded(webRequest, data); IDictionary refreshTokenResult = JSONHelper.JsonDecode(accessTokenJsonResult); + if (refreshTokenResult.ContainsKey("error")) { + if (refreshTokenResult.ContainsKey("error_description")) { + throw new Exception(string.Format("{0} - {1}", refreshTokenResult["error"], refreshTokenResult["error_description"])); + } else { + throw new Exception((string)refreshTokenResult["error"]); + } + } + // gives as described here: https://developers.google.com/identity/protocols/OAuth2InstalledApp // "access_token":"1/fFAGRNJru1FTz70BzhT3Zg", // "expires_in":3920, // "token_type":"Bearer", // "refresh_token":"1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI" - settings.AccessToken = (string)refreshTokenResult["access_token"] as string; - settings.RefreshToken = (string)refreshTokenResult["refresh_token"] as string; + settings.AccessToken = (string)refreshTokenResult[ACCESS_TOKEN] as string; + settings.RefreshToken = (string)refreshTokenResult[REFRESH_TOKEN] as string; - object seconds = refreshTokenResult["expires_in"]; + object seconds = refreshTokenResult[EXPIRES_IN]; if (seconds != null) { settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds((double)seconds); } + settings.Code = null; } /// @@ -1110,13 +1134,17 @@ Greenshot received information from CloudServiceName. You can close this browser /// public static void GenerateAccessToken(OAuth2Settings settings) { IDictionary data = new Dictionary(); - data.Add("refresh_token", settings.RefreshToken); - data.Add("client_id", settings.ClientId); - data.Add("client_secret", settings.ClientSecret); - data.Add("grant_type", "refresh_token"); + data.Add(REFRESH_TOKEN, settings.RefreshToken); + data.Add(CLIENT_ID, settings.ClientId); + data.Add(CLIENT_SECRET, settings.ClientSecret); + data.Add(GRANT_TYPE, REFRESH_TOKEN); + foreach (string key in settings.AdditionalAttributes.Keys) { + data.Add(key, settings.AdditionalAttributes[key]); + } - HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(settings.FormattedTokenUrl, HTTPMethod.POST); - string accessTokenJsonResult = NetworkHelper.UploadFormUrlEncoded(webRequest, data); + HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(settings.TokenUrl, HTTPMethod.POST); + NetworkHelper.UploadFormUrlEncoded(webRequest, data); + string accessTokenJsonResult = NetworkHelper.GetResponseAsString(webRequest, true); // gives as described here: https://developers.google.com/identity/protocols/OAuth2InstalledApp // "access_token":"1/fFAGRNJru1FTz70BzhT3Zg", @@ -1124,10 +1152,33 @@ Greenshot received information from CloudServiceName. You can close this browser // "token_type":"Bearer", IDictionary accessTokenResult = JSONHelper.JsonDecode(accessTokenJsonResult); - settings.AccessToken = (string)accessTokenResult["access_token"] as string; - object seconds = accessTokenResult["expires_in"]; + if (accessTokenResult.ContainsKey("error")) { + if ("invalid_grant" == (string)accessTokenResult["error"]) { + // Refresh token has also expired, we need a new one! + settings.RefreshToken = null; + settings.AccessToken = null; + settings.AccessTokenExpires = DateTimeOffset.MinValue; + settings.Code = null; + return; + } else { + if (accessTokenResult.ContainsKey("error_description")) { + throw new Exception(string.Format("{0} - {1}", accessTokenResult["error"], accessTokenResult["error_description"])); + } else { + throw new Exception((string)accessTokenResult["error"]); + } + } + } + + settings.AccessToken = (string)accessTokenResult[ACCESS_TOKEN] as string; + if (accessTokenResult.ContainsKey(REFRESH_TOKEN)) { + // Refresh the refresh token :) + settings.RefreshToken = (string)accessTokenResult[REFRESH_TOKEN] as string; + } + object seconds = accessTokenResult[EXPIRES_IN]; if (seconds != null) { settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds((double)seconds); + } else { + settings.AccessTokenExpires = DateTimeOffset.MaxValue; } } @@ -1168,8 +1219,9 @@ Greenshot received information from CloudServiceName. You can close this browser loginForm.ShowDialog(); if (loginForm.IsOk) { string code; - if (loginForm.CallbackParameters.TryGetValue("code", out code) && !string.IsNullOrEmpty(code)) { - GenerateRefreshToken(code, settings); + if (loginForm.CallbackParameters.TryGetValue(CODE, out code) && !string.IsNullOrEmpty(code)) { + settings.Code = code; + GenerateRefreshToken(settings); return true; } } @@ -1187,8 +1239,9 @@ Greenshot received information from CloudServiceName. You can close this browser IDictionary result = codeReceiver.ReceiveCode(settings); string code; - if (result.TryGetValue("code", out code) && !string.IsNullOrEmpty(code)) { - GenerateRefreshToken(code, settings); + if (result.TryGetValue(CODE, out code) && !string.IsNullOrEmpty(code)) { + settings.Code = code; + GenerateRefreshToken(settings); return true; } string error; @@ -1224,17 +1277,25 @@ Greenshot received information from CloudServiceName. You can close this browser public static void CheckAndAuthenticateOrRefresh(OAuth2Settings settings) { // Get Refresh / Access token if (string.IsNullOrEmpty(settings.RefreshToken)) { - if (!OAuth2Helper.Authenticate(settings)) { + if (!Authenticate(settings)) { throw new Exception("Authentication cancelled"); } } - if (settings.IsAccessTokenExpired) { - OAuth2Helper.GenerateAccessToken(settings); + GenerateAccessToken(settings); + // Get Refresh / Access token + if (string.IsNullOrEmpty(settings.RefreshToken)) { + if (!Authenticate(settings)) { + throw new Exception("Authentication cancelled"); + } + GenerateAccessToken(settings); + } + } + if (settings.IsAccessTokenExpired) { + throw new Exception("Authentication failed"); } } - /// /// CreateWebRequest ready for OAuth 2 access /// @@ -1245,8 +1306,8 @@ Greenshot received information from CloudServiceName. You can close this browser public static HttpWebRequest CreateOAuth2WebRequest(HTTPMethod method, string url, OAuth2Settings settings) { CheckAndAuthenticateOrRefresh(settings); - HttpWebRequest webRequest = (HttpWebRequest)NetworkHelper.CreateWebRequest(url, method); - OAuth2Helper.AddOAuth2Credentials(webRequest, settings); + HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(url, method); + AddOAuth2Credentials(webRequest, settings); return webRequest; } }