[tomboy] Implement proper OAuth callback and support for 1.0a (#591455, #594046)



commit 48124fecbe25ceeef98ffe837a28fbcfd072a1a7
Author: Sandy Armstrong <sanfordarmstrong gmail com>
Date:   Mon Sep 7 17:03:32 2009 -0700

    Implement proper OAuth callback and support for 1.0a (#591455, #594046)
    
    Use an HttpListener to set up a local callback URL, which automatically
    triggers token authorization (removing one extra user click).
    
    Add oauth_verifier support, which should make 1.0a servers work, but
    so far it is untested with any servers that support OAuth 1.0a.

 Tomboy/Addins/WebSyncService/Api/OAuth.cs          |    8 +-
 Tomboy/Addins/WebSyncService/OAuth/Base.cs         |   16 ++-
 .../WebSyncService/WebSyncPreferencesWidget.cs     |  109 +++++++++++++++-----
 3 files changed, 101 insertions(+), 32 deletions(-)
---
diff --git a/Tomboy/Addins/WebSyncService/Api/OAuth.cs b/Tomboy/Addins/WebSyncService/Api/OAuth.cs
index f158243..781886e 100644
--- a/Tomboy/Addins/WebSyncService/Api/OAuth.cs
+++ b/Tomboy/Addins/WebSyncService/Api/OAuth.cs
@@ -60,7 +60,7 @@ namespace Tomboy.WebSync.Api
 				if (!string.IsNullOrEmpty (qs ["oauth_token"])) {
 					Token = qs ["oauth_token"];
 					TokenSecret = qs ["oauth_token_secret"];
-					var link = string.Format ("{0}?oauth_token={1}&oauth_callback={2}", AuthorizeLocation, qs ["oauth_token"], "http://www.google.com";);
+					var link = string.Format ("{0}?oauth_token={1}&oauth_callback={2}", AuthorizeLocation, qs ["oauth_token"], HttpUtility.UrlEncode (CallbackUrl));
 					Logger.Debug ("Response from request for auth url: {0}", response);
 					return link;
 				}
@@ -147,6 +147,10 @@ namespace Tomboy.WebSync.Api
 		public string AccessTokenBaseUrl { get; set; }
 
 		public string Realm { get; set; }
+
+		public string CallbackUrl { get; set; }
+
+		public string Verifier { get; set; }
 		#endregion
 
 		#region Private Methods
@@ -188,7 +192,7 @@ namespace Tomboy.WebSync.Api
 			var outUrl = string.Empty;
 			List<IQueryParameter<string>> parameters = null;
 
-			var sig = GenerateSignature (uri, ConsumerKey, ConsumerSecret, Token, TokenSecret, method,
+			var sig = GenerateSignature (uri, ConsumerKey, ConsumerSecret, Token, TokenSecret, Verifier, method,
 				timeStamp, nonce, out outUrl, out parameters);
 
 			if (Debugging)
diff --git a/Tomboy/Addins/WebSyncService/OAuth/Base.cs b/Tomboy/Addins/WebSyncService/OAuth/Base.cs
index 90c0030..e24ec57 100644
--- a/Tomboy/Addins/WebSyncService/OAuth/Base.cs
+++ b/Tomboy/Addins/WebSyncService/OAuth/Base.cs
@@ -76,6 +76,7 @@ namespace OAuth
 		private const string OAuthNonceKey = "oauth_nonce";
 		private const string OAuthTokenKey = "oauth_token";
 		private const string OAuthTokenSecretKey = "oauth_token_secret";
+		private const string OAuthVerifierKey = "oauth_verifier";
 
 		private const string HMACSHA1SignatureType = "HMAC-SHA1";
 		private const string PlainTextSignatureType = "PLAINTEXT";
@@ -153,10 +154,11 @@ namespace OAuth
 		/// <param name="consumerKey">The consumer key.</param>
 		/// <param name="token">The token, if available. If not available pass null or an empty string.</param>
 		/// <param name="tokenSecret">The token secret, if available. If not available pass null or an empty string.</param>
+		/// <param name="verifier">The callback verifier, if available. If not available pass null or an empty string.</param>
 		/// <param name="httpMethod">The HTTP method used. Must be a valid HTTP method verb (POST,GET,PUT, etc)</param>
 		/// <param name="signatureType">The signature type. To use the default values use <see cref="SignatureType">SignatureType</see>.</param>
 		/// <returns>The signature base.</returns>
-		private string GenerateSignatureBase (Uri url, string consumerKey, string token, string tokenSecret,
+		private string GenerateSignatureBase (Uri url, string consumerKey, string token, string tokenSecret, string verifier,
 			RequestMethod method, TimeSpan timeStamp, string nonce, SignatureType signatureType, out string normalizedUrl,
 			out List<IQueryParameter<string>> parameters)
 		{
@@ -164,6 +166,7 @@ namespace OAuth
 
 			token = token ?? string.Empty;
 			tokenSecret = tokenSecret ?? string.Empty;
+			verifier = verifier ?? String.Empty;
 
 			if (consumerKey == null) throw new ArgumentNullException ("consumerKey");
 
@@ -192,6 +195,7 @@ namespace OAuth
 			}).ToList ();
 
 			if (!string.IsNullOrEmpty (token)) parameters.Add (new QueryParameter<string> (OAuthTokenKey, token, s => string.IsNullOrEmpty (s)));
+			if (!string.IsNullOrEmpty (verifier)) parameters.Add (new QueryParameter<string> (OAuthVerifierKey, verifier, s => string.IsNullOrEmpty (s)));
 
 			log.LogDebug ("Normalizing URL for signature.");
 
@@ -243,14 +247,15 @@ namespace OAuth
 		/// <param name="consumerSecret">The consumer seceret.</param>
 		/// <param name="token">The token, if available. If not available pass null or an empty string.</param>
 		/// <param name="tokenSecret">The token secret, if available. If not, pass null or an empty string.</param>
+		/// <param name="verifier">The callback verifier, if available. If not, pass null or an empty string.</param>
 		/// <param name="httpMethod">The HTTP method used. Must be valid HTTP method verb (POST, GET, PUT, etc).</param>
 		/// <returns>A Base64 string of the hash value.</returns>
 		protected string GenerateSignature (Uri url, string consumerKey, string consumerSecret, string token,
-			string tokenSecret, RequestMethod method, TimeSpan timeStamp, string nonce, out string normalizedUrl,
+			string tokenSecret, string verifier, RequestMethod method, TimeSpan timeStamp, string nonce, out string normalizedUrl,
 			out List<IQueryParameter<string>> parameters)
 		{
 			log.LogDebug ("Generating signature using HMAC-SHA1 algorithm.");
-			return GenerateSignature (url, consumerKey, consumerSecret, token, tokenSecret, method, timeStamp, nonce,
+			return GenerateSignature (url, consumerKey, consumerSecret, token, tokenSecret, verifier, method, timeStamp, nonce,
 				SignatureType.HMACSHA1, out normalizedUrl, out parameters);
 		}
 
@@ -262,11 +267,12 @@ namespace OAuth
 		/// <param name="consumerSecret">The consumer seceret.</param>
 		/// <param name="token">The token, if available. If not available pass null or an empty string.</param>
 		/// <param name="tokenSecret">The token secret, if available. If not, pass null or an empty string.</param>
+		/// <param name="verifier">The callback verifier, if available. If not, pass null or an empty string.</param>
 		/// <param name="httpMethod">The HTTP method used. Must be a valid HTTP method verb (POST,GET,PUT, etc).</param>
 		/// <param name="signatureType">The type of signature to use.</param>
 		/// <returns>A Base64 string of the hash value.</returns>
 		private string GenerateSignature (Uri url, string consumerKey, string consumerSecret, string token,
-			string tokenSecret, RequestMethod method, TimeSpan timeStamp, string nonce, SignatureType signatureType,
+			string tokenSecret, string verifier, RequestMethod method, TimeSpan timeStamp, string nonce, SignatureType signatureType,
 			out string normalizedUrl, out List<IQueryParameter<string>> parameters)
 		{
 			log.LogDebug ("Generating signature using signature type {0}", signatureType);
@@ -281,7 +287,7 @@ namespace OAuth
 					log.LogDebug ("Plaintext encoding signature {0} of consumer secret and token secret.", signature);
 					return signature;
 				case SignatureType.HMACSHA1:
-					string signatureBase = GenerateSignatureBase (url, consumerKey, token, tokenSecret, method,
+					string signatureBase = GenerateSignatureBase (url, consumerKey, token, tokenSecret, verifier, method,
 						timeStamp, nonce, SignatureType.HMACSHA1, out normalizedUrl, out parameters);
 
 					var hmacsha1 = new HMACSHA1 ();
diff --git a/Tomboy/Addins/WebSyncService/WebSyncPreferencesWidget.cs b/Tomboy/Addins/WebSyncService/WebSyncPreferencesWidget.cs
index 6cff772..a99c2cc 100644
--- a/Tomboy/Addins/WebSyncService/WebSyncPreferencesWidget.cs
+++ b/Tomboy/Addins/WebSyncService/WebSyncPreferencesWidget.cs
@@ -24,6 +24,8 @@
 // 
 
 using System;
+using System.Net;
+using System.Web;
 
 using Mono.Unix;
 using Tomboy.WebSync.Api;
@@ -34,8 +36,11 @@ namespace Tomboy.WebSync
 	{
 		private Gtk.Entry serverEntry;
 		private Gtk.Button authButton;
-		private bool authReqested;
 		private Api.OAuth oauth;
+		private HttpListener listener;
+		
+		private const string callbackHtmlTemplate =
+			@"<html><head><title>{0}</title></head><body><div><h1>{0}</h1>{1}</div></body></html>";
 		
 		public WebSyncPreferencesWidget (Api.OAuth oauth, string server) : base (false, 5)
 		{
@@ -92,6 +97,9 @@ namespace Tomboy.WebSync
 
 		private void OnAuthButtonClicked (object sender, EventArgs args)
 		{
+			if (listener != null && listener.IsListening)
+				listener.Close ();
+
 			// TODO: Move this
 			if (Auth == null) {
 				string rootUri = Server + "/api/1.0";
@@ -114,46 +122,97 @@ namespace Tomboy.WebSync
 				}
 			}
 
-			if (!Auth.IsAccessToken && !authReqested) {
+			if (!Auth.IsAccessToken) {
+				listener = new HttpListener ();
+				int portToTry = 8000;
+				string callbackUrl = string.Empty;
+				while (!listener.IsListening && portToTry < 9000) {
+					callbackUrl = String.Format ("http://localhost:{0}/tomboy-web-sync/";,
+					                            portToTry);
+					listener.Prefixes.Add (callbackUrl);
+					try {
+						listener.Start ();
+						Auth.CallbackUrl = callbackUrl;
+					} catch {
+						listener.Prefixes.Clear ();
+						portToTry++;
+					}
+				}
+
+				if (!listener.IsListening) {
+					Logger.Error ("Unable to start HttpListener on any port between 8000-8999");
+					authButton.Label = Catalog.GetString ("Server not responding. Try again later.");
+					oauth = null;
+					return;
+				}
+
 				string authUrl = string.Empty;
 				try {
 					authUrl = Auth.GetAuthorizationUrl ();
 				} catch (Exception e) {
+					listener.Close ();
 					Logger.Error ("Failed to get auth URL from " + Server + ". Exception was: " + e.ToString ());
+					// Translators: The web service supporting Tomboy WebSync is not responding as expected
 					authButton.Label = Catalog.GetString ("Server not responding. Try again later.");
 					oauth = null;
 					return;
 				}
+
+				IAsyncResult result = listener.BeginGetContext (delegate (IAsyncResult localResult) {
+					var context = listener.EndGetContext (localResult);
+					// Assuming if we got here user clicked Allow
+					Logger.Debug ("Context request uri query section: " + context.Request.Url.Query);
+					// oauth_verifier is required in OAuth 1.0a, not 1.0
+					var qs = HttpUtility.ParseQueryString (context.Request.Url.Query);
+					if (!String.IsNullOrEmpty (qs ["oauth_verifier"]))
+						Auth.Verifier = qs ["oauth_verifier"];
+					try {
+						if (!Auth.GetAccessAfterAuthorization ())
+							throw new ApplicationException ("Unknown error getting access token");
+						Logger.Debug ("Successfully authorized web sync");
+					} catch (Exception e) {
+						listener.Close ();
+						Logger.Error ("Failed to authorize web sync, with exception:");
+						Logger.Error (e.ToString ());
+						Gtk.Application.Invoke (delegate {
+							authButton.Label = Catalog.GetString ("Authorization Failed, Try Again");
+							authButton.Sensitive = true;
+						});
+						oauth = null;
+						return;
+					}
+					string htmlResponse =
+						String.Format (callbackHtmlTemplate,
+						               // Translators: Title of web page presented to user after they authorized Tomboy for sync
+						               Catalog.GetString ("Tomboy Web Authorization Successful"),
+						               // Translators: Body of web page presented to user after they authorized Tomboy for sync
+						               Catalog.GetString ("Please return to the Tomboy Preferences window and press Save to start synchronizing."));
+					using (var writer = new System.IO.StreamWriter (context.Response.OutputStream))
+						writer.Write (htmlResponse);
+					listener.Close ();
+
+					if (Auth.IsAccessToken) {
+						Gtk.Application.Invoke (delegate {
+							authButton.Sensitive = false;
+							authButton.Label = Catalog.GetString ("Connected. Press Save to start synchronizing");
+						});
+					}
+				}, null);
+
+				Logger.Debug ("Listening on {0} for OAuth callback", callbackUrl);
 				Logger.Debug ("Launching browser to authorize web sync: " + authUrl);
+				authButton.Label = Catalog.GetString ("Authorizing in browser (Press to reset connection)");
 				try {
 					Services.NativeApplication.OpenUrl (authUrl, Screen);
-					authReqested = true;
-					authButton.Label = Catalog.GetString ("Click Here After Authorizing");
 				} catch (Exception e) {
+					listener.Close ();
 					Logger.Error ("Exception opening URL: " + e.Message);
+					// Translators: Sometimes a user's default browser is not set, so we recommend setting it and trying again
 					authButton.Label = Catalog.GetString ("Set the default browser and try again");
+					return;
 				}
-			} else if (!Auth.IsAccessToken && authReqested) {
-				authButton.Sensitive = false;
-				authButton.Label = Catalog.GetString ("Processing...");
-				try {
-					if (!Auth.GetAccessAfterAuthorization ())
-						throw new ApplicationException ("Unknown error getting access token");
-					// TODO: Check Auth.IsAccessToken?
-					Logger.Debug ("Successfully authorized web sync");
-					authReqested = false;
-				} catch (Exception e) {
-					Logger.Error ("Failed to authorize web sync, with exception:");
-					Logger.Error (e.ToString ());
-					authReqested = true;
-					authButton.Label = Catalog.GetString ("Authorization Failed, Try Again");
-					authButton.Sensitive = true;
-				}
-			}
-
-			if (Auth.IsAccessToken) {
-				authButton.Sensitive = false;
-				authButton.Label = Catalog.GetString ("Connected. Click Save to start synchronizing");
+				// Translators: The user must take action in their web browser to continue the authorization process
+				authButton.Label = Catalog.GetString ("Authorizing in browser (Press to reset connection)");
 			}
 		}
 		



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]