/* * 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 OpenSimulator 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 DotNetOpenId; using DotNetOpenId.Provider; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.Framework.Servers.HttpServer; using OpenSim.Server.Handlers.Base; using OpenSim.Services.Interfaces; using Nini.Config; using OpenMetaverse; namespace OpenSim.Server.Handlers.Authentication { /// /// Temporary, in-memory store for OpenID associations /// public class ProviderMemoryStore : IAssociationStore { private class AssociationItem { public AssociationRelyingPartyType DistinguishingFactor; public string Handle; public DateTime Expires; public byte[] PrivateData; } Dictionary m_store = new Dictionary(); SortedList m_sortedStore = new SortedList(); object m_syncRoot = new object(); #region IAssociationStore 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 itemsCopy = new List(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 /// Login form used to authenticate OpenID requests const string LOGIN_PAGE = @" OpenSim OpenID Login

OpenSim Login

"; /// Page shown for a valid OpenID identity const string OPENID_PAGE = @" {2} {3} OpenID identifier for {2} {3} "; /// Page shown for an invalid OpenID identity const string INVALID_OPENID_PAGE = @"Identity not found Invalid OpenID identity"; /// Page shown if the OpenID endpoint is requested directly const string ENDPOINT_PAGE = @"OpenID Endpoint This is an OpenID server endpoint, not a human-readable resource. For more information, see http://openid.net/. "; #endregion HTML public string Name { get { return "OpenId"; } } public string Description { get { return null; } } 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; IAuthenticationService m_authenticationService; IUserAccountService m_userAccountService; ProviderMemoryStore m_openidStore = new ProviderMemoryStore(); /// /// Constructor /// public OpenIdStreamHandler(string httpMethod, string path, IUserAccountService userService, IAuthenticationService authService) { m_authenticationService = authService; m_userAccountService = userService; m_httpMethod = httpMethod; m_path = path; m_contentType = "text/html"; } /// /// Handles all GET and POST requests for OpenID identifier pages and endpoint /// server communication /// public void Handle(string path, Stream request, Stream response, IOSHttpRequest httpRequest, IOSHttpResponse 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"); UserAccount account; if (TryGetAccount(new Uri(authRequest.ClaimedIdentifier.ToString()), out account)) { // Check for form POST data if (passwordValues != null && passwordValues.Length == 1) { if (account != null && (m_authenticationService.Authenticate(account.PrincipalID, passwordValues[0], 30) != string.Empty)) 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, account.FirstName, account.LastName)); 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 UserAccount account; if (TryGetAccount(httpRequest.Url, out account)) { 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, account.FirstName, account.LastName)); } } 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); } } /// /// Parse a URL with a relative path of the form /users/First_Last and try to /// retrieve the profile matching that avatar name /// /// URL to parse for an avatar name /// Profile data for the avatar /// True if the parse and lookup were successful, otherwise false bool TryGetAccount(Uri requestUrl, out UserAccount account) { 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) { account = m_userAccountService.GetUserAccount(UUID.Zero, name[0], name[1]); return (account != null); } } account = null; return false; } } }