339 lines
14 KiB
C#
339 lines
14 KiB
C#
/*
|
|
* 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;
|
|
|
|
namespace OpenSim.Grid.UserServer.Modules
|
|
{
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|