|
@@ -1,339 +1,339 @@
|
|
|
-/*
|
|
|
- * Copyright (c) Contributors, http://opensimulator.org/
|
|
|
- * See CONTRIBUTORS.TXT for a full list of copyright holders.
|
|
|
- *
|
|
|
- * Redistribution and use in source and binary forms, with or without
|
|
|
- * modification, are permitted provided that the following conditions are met:
|
|
|
- * * Redistributions of source code must retain the above copyright
|
|
|
- * notice, this list of conditions and the following disclaimer.
|
|
|
- * * Redistributions in binary form must reproduce the above copyright
|
|
|
- * notice, this list of conditions and the following disclaimer in the
|
|
|
- * documentation and/or other materials provided with the distribution.
|
|
|
- * * Neither the name of the OpenSim Project nor the
|
|
|
- * names of its contributors may be used to endorse or promote products
|
|
|
- * derived from this software without specific prior written permission.
|
|
|
- *
|
|
|
- * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
|
|
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
- * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
|
|
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
|
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
|
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
|
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
|
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
- */
|
|
|
-
|
|
|
-using System;
|
|
|
-using System.Collections.Generic;
|
|
|
-using System.Collections.Specialized;
|
|
|
-using System.IO;
|
|
|
-using System.Net;
|
|
|
-using System.Web;
|
|
|
-using System.Text;
|
|
|
-using DotNetOpenId;
|
|
|
-using DotNetOpenId.Provider;
|
|
|
-using log4net;
|
|
|
-using OpenSim.Framework;
|
|
|
-using OpenSim.Framework.Servers;
|
|
|
-
|
|
|
-namespace OpenSim.Grid.UserServer
|
|
|
-{
|
|
|
- /// <summary>
|
|
|
- /// Temporary, in-memory store for OpenID associations
|
|
|
- /// </summary>
|
|
|
- public class ProviderMemoryStore : IAssociationStore<AssociationRelyingPartyType>
|
|
|
- {
|
|
|
- private class AssociationItem
|
|
|
- {
|
|
|
- public AssociationRelyingPartyType DistinguishingFactor;
|
|
|
- public string Handle;
|
|
|
- public DateTime Expires;
|
|
|
- public byte[] PrivateData;
|
|
|
- }
|
|
|
-
|
|
|
- Dictionary<string, AssociationItem> m_store = new Dictionary<string, AssociationItem>();
|
|
|
- SortedList<DateTime, AssociationItem> m_sortedStore = new SortedList<DateTime, AssociationItem>();
|
|
|
- object m_syncRoot = new object();
|
|
|
-
|
|
|
- #region IAssociationStore<AssociationRelyingPartyType> Members
|
|
|
-
|
|
|
- public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc)
|
|
|
- {
|
|
|
- AssociationItem item = new AssociationItem();
|
|
|
- item.DistinguishingFactor = distinguishingFactor;
|
|
|
- item.Handle = assoc.Handle;
|
|
|
- item.Expires = assoc.Expires.ToLocalTime();
|
|
|
- item.PrivateData = assoc.SerializePrivateData();
|
|
|
-
|
|
|
- lock (m_syncRoot)
|
|
|
- {
|
|
|
- m_store[item.Handle] = item;
|
|
|
- m_sortedStore[item.Expires] = item;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor)
|
|
|
- {
|
|
|
- lock (m_syncRoot)
|
|
|
- {
|
|
|
- if (m_sortedStore.Count > 0)
|
|
|
- {
|
|
|
- AssociationItem item = m_sortedStore.Values[m_sortedStore.Count - 1];
|
|
|
- return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
|
|
|
- {
|
|
|
- AssociationItem item;
|
|
|
- bool success = false;
|
|
|
- lock (m_syncRoot)
|
|
|
- success = m_store.TryGetValue(handle, out item);
|
|
|
-
|
|
|
- if (success)
|
|
|
- return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
|
|
|
- else
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
|
|
|
- {
|
|
|
- lock (m_syncRoot)
|
|
|
- {
|
|
|
- for (int i = 0; i < m_sortedStore.Values.Count; i++)
|
|
|
- {
|
|
|
- AssociationItem item = m_sortedStore.Values[i];
|
|
|
- if (item.Handle == handle)
|
|
|
- {
|
|
|
- m_sortedStore.RemoveAt(i);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return m_store.Remove(handle);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public void ClearExpiredAssociations()
|
|
|
- {
|
|
|
- lock (m_syncRoot)
|
|
|
- {
|
|
|
- List<AssociationItem> itemsCopy = new List<AssociationItem>(m_sortedStore.Values);
|
|
|
- DateTime now = DateTime.Now;
|
|
|
-
|
|
|
- for (int i = 0; i < itemsCopy.Count; i++)
|
|
|
- {
|
|
|
- AssociationItem item = itemsCopy[i];
|
|
|
-
|
|
|
- if (item.Expires <= now)
|
|
|
- {
|
|
|
- m_sortedStore.RemoveAt(i);
|
|
|
- m_store.Remove(item.Handle);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- #endregion
|
|
|
- }
|
|
|
-
|
|
|
- public class OpenIdStreamHandler : IStreamHandler
|
|
|
- {
|
|
|
- #region HTML
|
|
|
-
|
|
|
- /// <summary>Login form used to authenticate OpenID requests</summary>
|
|
|
- const string LOGIN_PAGE =
|
|
|
-@"<html>
|
|
|
-<head><title>OpenSim OpenID Login</title></head>
|
|
|
-<body>
|
|
|
-<h3>OpenSim Login</h3>
|
|
|
-<form method=""post"">
|
|
|
-<label for=""first"">First Name:</label> <input readonly type=""text"" name=""first"" id=""first"" value=""{0}""/>
|
|
|
-<label for=""last"">Last Name:</label> <input readonly type=""text"" name=""last"" id=""last"" value=""{1}""/>
|
|
|
-<label for=""pass"">Password:</label> <input type=""password"" name=""pass"" id=""pass""/>
|
|
|
-<input type=""submit"" value=""Login"">
|
|
|
-</form>
|
|
|
-</body>
|
|
|
-</html>";
|
|
|
-
|
|
|
- /// <summary>Page shown for a valid OpenID identity</summary>
|
|
|
- const string OPENID_PAGE =
|
|
|
-@"<html>
|
|
|
-<head>
|
|
|
-<title>{2} {3}</title>
|
|
|
-<link rel=""openid2.provider openid.server"" href=""{0}://{1}/openid/server/""/>
|
|
|
-</head>
|
|
|
-<body>OpenID identifier for {2} {3}</body>
|
|
|
-</html>
|
|
|
-";
|
|
|
-
|
|
|
- /// <summary>Page shown for an invalid OpenID identity</summary>
|
|
|
- const string INVALID_OPENID_PAGE =
|
|
|
-@"<html><head><title>Identity not found</title></head>
|
|
|
-<body>Invalid OpenID identity</body></html>";
|
|
|
-
|
|
|
- /// <summary>Page shown if the OpenID endpoint is requested directly</summary>
|
|
|
- const string ENDPOINT_PAGE =
|
|
|
-@"<html><head><title>OpenID Endpoint</title></head><body>
|
|
|
-This is an OpenID server endpoint, not a human-readable resource.
|
|
|
-For more information, see <a href='http://openid.net/'>http://openid.net/</a>.
|
|
|
-</body></html>";
|
|
|
-
|
|
|
- #endregion HTML
|
|
|
-
|
|
|
- public string ContentType { get { return m_contentType; } }
|
|
|
- public string HttpMethod { get { return m_httpMethod; } }
|
|
|
- public string Path { get { return m_path; } }
|
|
|
-
|
|
|
- string m_contentType;
|
|
|
- string m_httpMethod;
|
|
|
- string m_path;
|
|
|
- UserLoginService m_loginService;
|
|
|
- ProviderMemoryStore m_openidStore = new ProviderMemoryStore();
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Constructor
|
|
|
- /// </summary>
|
|
|
- public OpenIdStreamHandler(string httpMethod, string path, UserLoginService loginService)
|
|
|
- {
|
|
|
- m_loginService = loginService;
|
|
|
- m_httpMethod = httpMethod;
|
|
|
- m_path = path;
|
|
|
-
|
|
|
- m_contentType = "text/html";
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Handles all GET and POST requests for OpenID identifier pages and endpoint
|
|
|
- /// server communication
|
|
|
- /// </summary>
|
|
|
- public void Handle(string path, Stream request, Stream response, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
|
|
|
- {
|
|
|
- Uri providerEndpoint = new Uri(String.Format("{0}://{1}{2}", httpRequest.Url.Scheme, httpRequest.Url.Authority, httpRequest.Url.AbsolutePath));
|
|
|
-
|
|
|
- // Defult to returning HTML content
|
|
|
- m_contentType = "text/html";
|
|
|
-
|
|
|
- try
|
|
|
- {
|
|
|
- NameValueCollection postQuery = HttpUtility.ParseQueryString(new StreamReader(httpRequest.InputStream).ReadToEnd());
|
|
|
- NameValueCollection getQuery = HttpUtility.ParseQueryString(httpRequest.Url.Query);
|
|
|
- NameValueCollection openIdQuery = (postQuery.GetValues("openid.mode") != null ? postQuery : getQuery);
|
|
|
-
|
|
|
- OpenIdProvider provider = new OpenIdProvider(m_openidStore, providerEndpoint, httpRequest.Url, openIdQuery);
|
|
|
-
|
|
|
- if (provider.Request != null)
|
|
|
- {
|
|
|
- if (!provider.Request.IsResponseReady && provider.Request is IAuthenticationRequest)
|
|
|
- {
|
|
|
- IAuthenticationRequest authRequest = (IAuthenticationRequest)provider.Request;
|
|
|
- string[] passwordValues = postQuery.GetValues("pass");
|
|
|
-
|
|
|
- UserProfileData profile;
|
|
|
- if (TryGetProfile(new Uri(authRequest.ClaimedIdentifier.ToString()), out profile))
|
|
|
- {
|
|
|
- // Check for form POST data
|
|
|
- if (passwordValues != null && passwordValues.Length == 1)
|
|
|
- {
|
|
|
- if (profile != null && m_loginService.AuthenticateUser(profile, passwordValues[0]))
|
|
|
- authRequest.IsAuthenticated = true;
|
|
|
- else
|
|
|
- authRequest.IsAuthenticated = false;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Authentication was requested, send the client a login form
|
|
|
- using (StreamWriter writer = new StreamWriter(response))
|
|
|
- writer.Write(String.Format(LOGIN_PAGE, profile.FirstName, profile.SurName));
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Cannot find an avatar matching the claimed identifier
|
|
|
- authRequest.IsAuthenticated = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Add OpenID headers to the response
|
|
|
- foreach (string key in provider.Request.Response.Headers.Keys)
|
|
|
- httpResponse.AddHeader(key, provider.Request.Response.Headers[key]);
|
|
|
-
|
|
|
- string[] contentTypeValues = provider.Request.Response.Headers.GetValues("Content-Type");
|
|
|
- if (contentTypeValues != null && contentTypeValues.Length == 1)
|
|
|
- m_contentType = contentTypeValues[0];
|
|
|
-
|
|
|
- // Set the response code and document body based on the OpenID result
|
|
|
- httpResponse.StatusCode = (int)provider.Request.Response.Code;
|
|
|
- response.Write(provider.Request.Response.Body, 0, provider.Request.Response.Body.Length);
|
|
|
- response.Close();
|
|
|
- }
|
|
|
- else if (httpRequest.Url.AbsolutePath.Contains("/openid/server"))
|
|
|
- {
|
|
|
- // Standard HTTP GET was made on the OpenID endpoint, send the client the default error page
|
|
|
- using (StreamWriter writer = new StreamWriter(response))
|
|
|
- writer.Write(ENDPOINT_PAGE);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Try and lookup this avatar
|
|
|
- UserProfileData profile;
|
|
|
- if (TryGetProfile(httpRequest.Url, out profile))
|
|
|
- {
|
|
|
- using (StreamWriter writer = new StreamWriter(response))
|
|
|
- {
|
|
|
- // TODO: Print out a full profile page for this avatar
|
|
|
- writer.Write(String.Format(OPENID_PAGE, httpRequest.Url.Scheme,
|
|
|
- httpRequest.Url.Authority, profile.FirstName, profile.SurName));
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Couldn't parse an avatar name, or couldn't find the avatar in the user server
|
|
|
- using (StreamWriter writer = new StreamWriter(response))
|
|
|
- writer.Write(INVALID_OPENID_PAGE);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
|
|
|
- using (StreamWriter writer = new StreamWriter(response))
|
|
|
- writer.Write(ex.Message);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Parse a URL with a relative path of the form /users/First_Last and try to
|
|
|
- /// retrieve the profile matching that avatar name
|
|
|
- /// </summary>
|
|
|
- /// <param name="requestUrl">URL to parse for an avatar name</param>
|
|
|
- /// <param name="profile">Profile data for the avatar</param>
|
|
|
- /// <returns>True if the parse and lookup were successful, otherwise false</returns>
|
|
|
- bool TryGetProfile(Uri requestUrl, out UserProfileData profile)
|
|
|
- {
|
|
|
- if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/")
|
|
|
- {
|
|
|
- // Parse the avatar name from the path
|
|
|
- string username = requestUrl.Segments[requestUrl.Segments.Length - 1];
|
|
|
- string[] name = username.Split('_');
|
|
|
-
|
|
|
- if (name.Length == 2)
|
|
|
- {
|
|
|
- profile = m_loginService.GetTheUser(name[0], name[1]);
|
|
|
- return (profile != null);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- profile = null;
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+/*
|
|
|
+ * Copyright (c) Contributors, http://opensimulator.org/
|
|
|
+ * See CONTRIBUTORS.TXT for a full list of copyright holders.
|
|
|
+ *
|
|
|
+ * Redistribution and use in source and binary forms, with or without
|
|
|
+ * modification, are permitted provided that the following conditions are met:
|
|
|
+ * * Redistributions of source code must retain the above copyright
|
|
|
+ * notice, this list of conditions and the following disclaimer.
|
|
|
+ * * Redistributions in binary form must reproduce the above copyright
|
|
|
+ * notice, this list of conditions and the following disclaimer in the
|
|
|
+ * documentation and/or other materials provided with the distribution.
|
|
|
+ * * Neither the name of the OpenSim Project nor the
|
|
|
+ * names of its contributors may be used to endorse or promote products
|
|
|
+ * derived from this software without specific prior written permission.
|
|
|
+ *
|
|
|
+ * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
|
|
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
+ * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
|
|
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
|
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
|
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
|
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
|
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
+ */
|
|
|
+
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Collections.Specialized;
|
|
|
+using System.IO;
|
|
|
+using System.Net;
|
|
|
+using System.Web;
|
|
|
+using System.Text;
|
|
|
+using DotNetOpenId;
|
|
|
+using DotNetOpenId.Provider;
|
|
|
+using log4net;
|
|
|
+using OpenSim.Framework;
|
|
|
+using OpenSim.Framework.Servers;
|
|
|
+
|
|
|
+namespace OpenSim.Grid.UserServer
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Temporary, in-memory store for OpenID associations
|
|
|
+ /// </summary>
|
|
|
+ public class ProviderMemoryStore : IAssociationStore<AssociationRelyingPartyType>
|
|
|
+ {
|
|
|
+ private class AssociationItem
|
|
|
+ {
|
|
|
+ public AssociationRelyingPartyType DistinguishingFactor;
|
|
|
+ public string Handle;
|
|
|
+ public DateTime Expires;
|
|
|
+ public byte[] PrivateData;
|
|
|
+ }
|
|
|
+
|
|
|
+ Dictionary<string, AssociationItem> m_store = new Dictionary<string, AssociationItem>();
|
|
|
+ SortedList<DateTime, AssociationItem> m_sortedStore = new SortedList<DateTime, AssociationItem>();
|
|
|
+ object m_syncRoot = new object();
|
|
|
+
|
|
|
+ #region IAssociationStore<AssociationRelyingPartyType> Members
|
|
|
+
|
|
|
+ public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc)
|
|
|
+ {
|
|
|
+ AssociationItem item = new AssociationItem();
|
|
|
+ item.DistinguishingFactor = distinguishingFactor;
|
|
|
+ item.Handle = assoc.Handle;
|
|
|
+ item.Expires = assoc.Expires.ToLocalTime();
|
|
|
+ item.PrivateData = assoc.SerializePrivateData();
|
|
|
+
|
|
|
+ lock (m_syncRoot)
|
|
|
+ {
|
|
|
+ m_store[item.Handle] = item;
|
|
|
+ m_sortedStore[item.Expires] = item;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor)
|
|
|
+ {
|
|
|
+ lock (m_syncRoot)
|
|
|
+ {
|
|
|
+ if (m_sortedStore.Count > 0)
|
|
|
+ {
|
|
|
+ AssociationItem item = m_sortedStore.Values[m_sortedStore.Count - 1];
|
|
|
+ return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
|
|
|
+ {
|
|
|
+ AssociationItem item;
|
|
|
+ bool success = false;
|
|
|
+ lock (m_syncRoot)
|
|
|
+ success = m_store.TryGetValue(handle, out item);
|
|
|
+
|
|
|
+ if (success)
|
|
|
+ return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
|
|
|
+ else
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
|
|
|
+ {
|
|
|
+ lock (m_syncRoot)
|
|
|
+ {
|
|
|
+ for (int i = 0; i < m_sortedStore.Values.Count; i++)
|
|
|
+ {
|
|
|
+ AssociationItem item = m_sortedStore.Values[i];
|
|
|
+ if (item.Handle == handle)
|
|
|
+ {
|
|
|
+ m_sortedStore.RemoveAt(i);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return m_store.Remove(handle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void ClearExpiredAssociations()
|
|
|
+ {
|
|
|
+ lock (m_syncRoot)
|
|
|
+ {
|
|
|
+ List<AssociationItem> itemsCopy = new List<AssociationItem>(m_sortedStore.Values);
|
|
|
+ DateTime now = DateTime.Now;
|
|
|
+
|
|
|
+ for (int i = 0; i < itemsCopy.Count; i++)
|
|
|
+ {
|
|
|
+ AssociationItem item = itemsCopy[i];
|
|
|
+
|
|
|
+ if (item.Expires <= now)
|
|
|
+ {
|
|
|
+ m_sortedStore.RemoveAt(i);
|
|
|
+ m_store.Remove(item.Handle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+ }
|
|
|
+
|
|
|
+ public class OpenIdStreamHandler : IStreamHandler
|
|
|
+ {
|
|
|
+ #region HTML
|
|
|
+
|
|
|
+ /// <summary>Login form used to authenticate OpenID requests</summary>
|
|
|
+ const string LOGIN_PAGE =
|
|
|
+@"<html>
|
|
|
+<head><title>OpenSim OpenID Login</title></head>
|
|
|
+<body>
|
|
|
+<h3>OpenSim Login</h3>
|
|
|
+<form method=""post"">
|
|
|
+<label for=""first"">First Name:</label> <input readonly type=""text"" name=""first"" id=""first"" value=""{0}""/>
|
|
|
+<label for=""last"">Last Name:</label> <input readonly type=""text"" name=""last"" id=""last"" value=""{1}""/>
|
|
|
+<label for=""pass"">Password:</label> <input type=""password"" name=""pass"" id=""pass""/>
|
|
|
+<input type=""submit"" value=""Login"">
|
|
|
+</form>
|
|
|
+</body>
|
|
|
+</html>";
|
|
|
+
|
|
|
+ /// <summary>Page shown for a valid OpenID identity</summary>
|
|
|
+ const string OPENID_PAGE =
|
|
|
+@"<html>
|
|
|
+<head>
|
|
|
+<title>{2} {3}</title>
|
|
|
+<link rel=""openid2.provider openid.server"" href=""{0}://{1}/openid/server/""/>
|
|
|
+</head>
|
|
|
+<body>OpenID identifier for {2} {3}</body>
|
|
|
+</html>
|
|
|
+";
|
|
|
+
|
|
|
+ /// <summary>Page shown for an invalid OpenID identity</summary>
|
|
|
+ const string INVALID_OPENID_PAGE =
|
|
|
+@"<html><head><title>Identity not found</title></head>
|
|
|
+<body>Invalid OpenID identity</body></html>";
|
|
|
+
|
|
|
+ /// <summary>Page shown if the OpenID endpoint is requested directly</summary>
|
|
|
+ const string ENDPOINT_PAGE =
|
|
|
+@"<html><head><title>OpenID Endpoint</title></head><body>
|
|
|
+This is an OpenID server endpoint, not a human-readable resource.
|
|
|
+For more information, see <a href='http://openid.net/'>http://openid.net/</a>.
|
|
|
+</body></html>";
|
|
|
+
|
|
|
+ #endregion HTML
|
|
|
+
|
|
|
+ public string ContentType { get { return m_contentType; } }
|
|
|
+ public string HttpMethod { get { return m_httpMethod; } }
|
|
|
+ public string Path { get { return m_path; } }
|
|
|
+
|
|
|
+ string m_contentType;
|
|
|
+ string m_httpMethod;
|
|
|
+ string m_path;
|
|
|
+ UserLoginService m_loginService;
|
|
|
+ ProviderMemoryStore m_openidStore = new ProviderMemoryStore();
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Constructor
|
|
|
+ /// </summary>
|
|
|
+ public OpenIdStreamHandler(string httpMethod, string path, UserLoginService loginService)
|
|
|
+ {
|
|
|
+ m_loginService = loginService;
|
|
|
+ m_httpMethod = httpMethod;
|
|
|
+ m_path = path;
|
|
|
+
|
|
|
+ m_contentType = "text/html";
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Handles all GET and POST requests for OpenID identifier pages and endpoint
|
|
|
+ /// server communication
|
|
|
+ /// </summary>
|
|
|
+ public void Handle(string path, Stream request, Stream response, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
|
|
|
+ {
|
|
|
+ Uri providerEndpoint = new Uri(String.Format("{0}://{1}{2}", httpRequest.Url.Scheme, httpRequest.Url.Authority, httpRequest.Url.AbsolutePath));
|
|
|
+
|
|
|
+ // Defult to returning HTML content
|
|
|
+ m_contentType = "text/html";
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ NameValueCollection postQuery = HttpUtility.ParseQueryString(new StreamReader(httpRequest.InputStream).ReadToEnd());
|
|
|
+ NameValueCollection getQuery = HttpUtility.ParseQueryString(httpRequest.Url.Query);
|
|
|
+ NameValueCollection openIdQuery = (postQuery.GetValues("openid.mode") != null ? postQuery : getQuery);
|
|
|
+
|
|
|
+ OpenIdProvider provider = new OpenIdProvider(m_openidStore, providerEndpoint, httpRequest.Url, openIdQuery);
|
|
|
+
|
|
|
+ if (provider.Request != null)
|
|
|
+ {
|
|
|
+ if (!provider.Request.IsResponseReady && provider.Request is IAuthenticationRequest)
|
|
|
+ {
|
|
|
+ IAuthenticationRequest authRequest = (IAuthenticationRequest)provider.Request;
|
|
|
+ string[] passwordValues = postQuery.GetValues("pass");
|
|
|
+
|
|
|
+ UserProfileData profile;
|
|
|
+ if (TryGetProfile(new Uri(authRequest.ClaimedIdentifier.ToString()), out profile))
|
|
|
+ {
|
|
|
+ // Check for form POST data
|
|
|
+ if (passwordValues != null && passwordValues.Length == 1)
|
|
|
+ {
|
|
|
+ if (profile != null && m_loginService.AuthenticateUser(profile, passwordValues[0]))
|
|
|
+ authRequest.IsAuthenticated = true;
|
|
|
+ else
|
|
|
+ authRequest.IsAuthenticated = false;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Authentication was requested, send the client a login form
|
|
|
+ using (StreamWriter writer = new StreamWriter(response))
|
|
|
+ writer.Write(String.Format(LOGIN_PAGE, profile.FirstName, profile.SurName));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Cannot find an avatar matching the claimed identifier
|
|
|
+ authRequest.IsAuthenticated = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add OpenID headers to the response
|
|
|
+ foreach (string key in provider.Request.Response.Headers.Keys)
|
|
|
+ httpResponse.AddHeader(key, provider.Request.Response.Headers[key]);
|
|
|
+
|
|
|
+ string[] contentTypeValues = provider.Request.Response.Headers.GetValues("Content-Type");
|
|
|
+ if (contentTypeValues != null && contentTypeValues.Length == 1)
|
|
|
+ m_contentType = contentTypeValues[0];
|
|
|
+
|
|
|
+ // Set the response code and document body based on the OpenID result
|
|
|
+ httpResponse.StatusCode = (int)provider.Request.Response.Code;
|
|
|
+ response.Write(provider.Request.Response.Body, 0, provider.Request.Response.Body.Length);
|
|
|
+ response.Close();
|
|
|
+ }
|
|
|
+ else if (httpRequest.Url.AbsolutePath.Contains("/openid/server"))
|
|
|
+ {
|
|
|
+ // Standard HTTP GET was made on the OpenID endpoint, send the client the default error page
|
|
|
+ using (StreamWriter writer = new StreamWriter(response))
|
|
|
+ writer.Write(ENDPOINT_PAGE);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Try and lookup this avatar
|
|
|
+ UserProfileData profile;
|
|
|
+ if (TryGetProfile(httpRequest.Url, out profile))
|
|
|
+ {
|
|
|
+ using (StreamWriter writer = new StreamWriter(response))
|
|
|
+ {
|
|
|
+ // TODO: Print out a full profile page for this avatar
|
|
|
+ writer.Write(String.Format(OPENID_PAGE, httpRequest.Url.Scheme,
|
|
|
+ httpRequest.Url.Authority, profile.FirstName, profile.SurName));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Couldn't parse an avatar name, or couldn't find the avatar in the user server
|
|
|
+ using (StreamWriter writer = new StreamWriter(response))
|
|
|
+ writer.Write(INVALID_OPENID_PAGE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
|
|
|
+ using (StreamWriter writer = new StreamWriter(response))
|
|
|
+ writer.Write(ex.Message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Parse a URL with a relative path of the form /users/First_Last and try to
|
|
|
+ /// retrieve the profile matching that avatar name
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="requestUrl">URL to parse for an avatar name</param>
|
|
|
+ /// <param name="profile">Profile data for the avatar</param>
|
|
|
+ /// <returns>True if the parse and lookup were successful, otherwise false</returns>
|
|
|
+ bool TryGetProfile(Uri requestUrl, out UserProfileData profile)
|
|
|
+ {
|
|
|
+ if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/")
|
|
|
+ {
|
|
|
+ // Parse the avatar name from the path
|
|
|
+ string username = requestUrl.Segments[requestUrl.Segments.Length - 1];
|
|
|
+ string[] name = username.Split('_');
|
|
|
+
|
|
|
+ if (name.Length == 2)
|
|
|
+ {
|
|
|
+ profile = m_loginService.GetTheUser(name[0], name[1]);
|
|
|
+ return (profile != null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ profile = null;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|