Adding a new account now forces a logout to clear any stale Pithos cookies
[pithos-ms-client] / trunk / Pithos.Client.WPF / PithosAccount.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="PithosAccount.cs" company="GRNet">
4  * 
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or
8  * without modification, are permitted provided that the following
9  * conditions are met:
10  *
11  *   1. Redistributions of source code must retain the above
12  *      copyright notice, this list of conditions and the following
13  *      disclaimer.
14  *
15  *   2. Redistributions in binary form must reproduce the above
16  *      copyright notice, this list of conditions and the following
17  *      disclaimer in the documentation and/or other materials
18  *      provided with the distribution.
19  *
20  *
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  *
34  * The views and conclusions contained in the software and
35  * documentation are those of the authors and should not be
36  * interpreted as representing official policies, either expressed
37  * or implied, of GRNET S.A.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42
43 // </copyright>
44 // -----------------------------------------------------------------------
45
46 using System.IO;
47 using System.Reflection;
48 using Pithos.Client.WPF.Preferences;
49 using Pithos.Client.WPF.Properties;
50 using Pithos.Network;
51 using log4net;
52
53 namespace Pithos.Client.WPF
54 {
55     using System;
56     using System.Collections.Generic;
57     using System.Linq;
58     using System.Text;
59     using System.Net;
60     using System.Threading.Tasks;
61     using System.Diagnostics.Contracts;
62     using System.Diagnostics;
63     using System.Net.Sockets;
64
65     /// <summary>
66     /// TODO: Update summary.
67     /// </summary>
68     /// <summary>
69     /// Retrieves an account name and token from PITHOS
70     /// </summary>
71     public static class PithosAccount
72     {
73         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
74
75         /// <summary>
76         /// Asynchronously retrieves PITHOS credentials
77         /// </summary>
78         /// <param name="loginUrl">URL to retrieve the account info from PITHOS. Must end with =</param>
79         /// <returns>The credentials wrapped in a Task</returns>
80         public static NetworkCredential RetrieveCredentials(string loginUrl,string accountName=null)
81         {
82             Contract.Requires(Uri.IsWellFormedUriString(loginUrl, UriKind.Absolute));
83
84             if (!Uri.IsWellFormedUriString(loginUrl, UriKind.Absolute))
85                 throw new ArgumentException("The pithosSite parameter must be a valid absolute URL", "loginUrl");
86             
87             //int port = GetFreePort();
88             
89             //TODO:Force logout here to take care of existing cookies
90
91
92             //var listenerUrl = String.Format("http://127.0.0.1:{0}", port);
93             var listenerUrl = "pithos://127.0.0.1";
94
95             
96
97             //var receiveCredentials = ListenForRedirectAsync(port);
98
99             //var logoutUrl = loginUrl.Replace("login", "im/logout");
100             //var logoutProcess=Process.Start(logoutUrl);            
101             //TaskEx.Delay(2000).Wait();
102             var uriBuilder = new UriBuilder(loginUrl)
103                                  {
104                                      Query = String.Format("next={0}&force=", listenerUrl)
105                                  };
106
107             var retrieveUri = uriBuilder.Uri;
108
109             var logoutUrl= (from server in Settings.Default.Servers
110                            where server.LoginUri == loginUrl
111                                select new Uri(server.LogoutUri)).FirstOrDefault();
112
113
114
115             var browser = new LoginView(retrieveUri,logoutUrl,accountName);            
116             if (true == browser.ShowDialog())
117             {
118                 return new NetworkCredential(browser.Account, browser.Token);
119             }
120             return null;
121
122 /*
123
124             Log.InfoFormat("[RETRIEVE] Open Browser at {0}", retrieveUri);
125             Process.Start(retrieveUri.ToString());
126
127             return receiveCredentials;
128 */
129         }
130
131 /*
132         private static async Task<NetworkCredential> ListenHttpForRedirectAsync(string listenerUrl)
133         {
134             using (var listener = new HttpListener())
135             {
136                 listener.Prefixes.Add(listenerUrl);
137
138                 Log.InfoFormat("[RETRIEVE] Listening at {0}", listenerUrl);
139
140
141                 try
142                 {
143                     listener.Start();
144
145                     var context = await listener.GetContextAsync()
146                                             .WithTimeout(TimeSpan.FromMinutes(5));
147
148                     var request = context.Request;
149                     Log.InfoFormat("[RETRIEVE] Got Connection {0}", request.RawUrl);
150
151                     var query = request.QueryString;
152                     var userName = query["user"];
153                     var token = query["token"];
154                     if (String.IsNullOrWhiteSpace(userName) ||
155                         String.IsNullOrWhiteSpace(token))
156                     {
157                         Respond(context, "Failure", "The server did not return a username or token");
158                         Log.ErrorFormat("[RETRIEVE] No credentials returned by server");
159                         throw new Exception("The server did not return a username or token");
160                     }
161                     else
162                     {
163                         Log.InfoFormat("[RETRIEVE] Credentials retrieved user:{0} token:{1}", userName, token);
164                         Respond(context, "Authenticated", "Got It");
165
166                         return new NetworkCredential(userName, token);
167                     }
168                 }
169                 catch (Exception exc)
170                 {
171                     Log.Error("[RETRIEVE][ERROR] Receive connection {0}", exc);
172                     throw;
173                 }
174             }
175         }
176 */
177
178         private static async Task<NetworkCredential> ListenForRedirectAsync(int port)
179         {
180             var listener = new TcpListener(IPAddress.Any, port);            
181             Log.InfoFormat("[RETRIEVE] Listening at {0}", port);
182
183             try
184             {
185                 listener.Start();
186
187                 using (var client = await listener.AcceptTcpClientAsync()
188                                         .WithTimeout(TimeSpan.FromMinutes(5)))
189                 {
190                     using (var stream = client.GetStream())
191                     using (var reader=new StreamReader(stream))
192                     {
193                         var request = await reader.ReadLineAsync();
194                         //BUG
195                         //TODO: Add proper warnings here if the content is empty, don't just throw an exception
196                         //This may be a common occurence 
197
198                         if (String.IsNullOrWhiteSpace(request))
199                             throw new PithosException("The server did send any information");
200                         Log.InfoFormat("[RETRIEVE] Got Connection {0}", request);
201
202
203                         var items = ParseResponse(request);
204
205                         var userName = items["user"];
206                         var token = items["token"];
207                         if (String.IsNullOrWhiteSpace(userName) ||
208                             String.IsNullOrWhiteSpace(token))
209                         {
210                             Respond(stream, "Failure", "The server did not return a username or token");
211                             Log.ErrorFormat("[RETRIEVE] No credentials returned by server");
212                             throw new PithosException("The server did not return a username or token");
213                         }
214                         else
215                         {
216                             Log.InfoFormat("[RETRIEVE] Credentials retrieved user:{0} token:{1}", userName, token);
217                             Respond(stream, "Authenticated", "Got It");                            
218                         }
219                         return new NetworkCredential(userName, token);
220                     }
221                 }
222             }
223             catch (Exception exc)
224             {
225                 Log.Error("[RETRIEVE][ERROR] Receive connection {0}", exc);
226                 throw new PithosException("An error occured while retrieving credentials",exc);
227             }
228         }
229
230         private static Dictionary<string, string> ParseResponse(string request)
231         {
232             var parts = request.Split(' ');
233             var query = parts[1].TrimStart('/', '?');
234
235             var items = query.Split('&')
236                 .Select(pair => pair.Split('='))
237                 .ToDictionary(arr => arr[0].ToLower(), arr => Uri.UnescapeDataString(arr[1]));
238             return items;
239         }
240
241
242         private static void Respond(HttpListenerContext context,string title,string message)
243         {
244             var response = context.Response;
245             var html = String.Format("<html><head><title>{0}</title></head><body><h1>{1}</h1></body></html>", title, message);
246             var outBuffer = Encoding.UTF8.GetBytes(html);
247             response.ContentLength64 = outBuffer.Length;
248             using (var stream = response.OutputStream)
249             {
250                 stream.Write(outBuffer, 0, outBuffer.Length);
251             }
252
253             Log.InfoFormat("[RETRIEVE] Responded");
254         }
255
256         private static void Respond(Stream stream,string title,string message)
257         {
258             
259             var html = String.Format("<html><head><title>{0}</title></head><body><h1>{1}</h1></body></html>", title, message);
260             
261             var response = new StringBuilder();
262             response.AppendLine("HTTP/1.1 200 OK");
263             response.AppendFormat("Content-Length: {0}\n", html.Length);
264             response.AppendLine("Server: Microsoft-HTTPAPI/2.0");
265             response.AppendFormat("Date: {0}\n\n", DateTime.UtcNow);
266             response.AppendLine(html);
267
268             var outBuffer=Encoding.UTF8.GetBytes(response.ToString());
269             
270             stream.Write(outBuffer, 0, outBuffer.Length);
271
272             Log.InfoFormat("[RETRIEVE] Responded");
273         }
274
275        
276         /// <summary>
277         /// Locates a free local port 
278         /// </summary>
279         /// <returns>A free port</returns>
280         /// <remarks>The method starts and stops a TcpListener on port 0 to locate a free port.</remarks>
281         public static int GetFreePort()
282         {
283             //The TcpListener will locate a free port             
284             var listener = new TcpListener(IPAddress.Any, 0);
285             listener.Start();
286             var port = ((IPEndPoint)listener.LocalEndpoint).Port;
287             listener.Stop();
288             return port;
289         }
290     }
291 }