clockwerk-opensim – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | vero | 1 | /* |
2 | * Copyright (c) Contributors, http://opensimulator.org/ |
||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. |
||
4 | * |
||
5 | * Redistribution and use in source and binary forms, with or without |
||
6 | * modification, are permitted provided that the following conditions are met: |
||
7 | * * Redistributions of source code must retain the above copyright |
||
8 | * notice, this list of conditions and the following disclaimer. |
||
9 | * * Redistributions in binary form must reproduce the above copyright |
||
10 | * notice, this list of conditions and the following disclaimer in the |
||
11 | * documentation and/or other materials provided with the distribution. |
||
12 | * * Neither the name of the OpenSimulator Project nor the |
||
13 | * names of its contributors may be used to endorse or promote products |
||
14 | * derived from this software without specific prior written permission. |
||
15 | * |
||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY |
||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY |
||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||
26 | */ |
||
27 | |||
28 | using System; |
||
29 | using System.Collections.Generic; |
||
30 | using System.Collections.Specialized; |
||
31 | using System.IO; |
||
32 | using System.Net; |
||
33 | using System.Web; |
||
34 | using DotNetOpenId; |
||
35 | using DotNetOpenId.Provider; |
||
36 | using OpenSim.Framework; |
||
37 | using OpenSim.Framework.Servers; |
||
38 | using OpenSim.Framework.Servers.HttpServer; |
||
39 | using OpenSim.Server.Handlers.Base; |
||
40 | using OpenSim.Services.Interfaces; |
||
41 | using Nini.Config; |
||
42 | using OpenMetaverse; |
||
43 | |||
44 | namespace OpenSim.Server.Handlers.Authentication |
||
45 | { |
||
46 | /// <summary> |
||
47 | /// Temporary, in-memory store for OpenID associations |
||
48 | /// </summary> |
||
49 | public class ProviderMemoryStore : IAssociationStore<AssociationRelyingPartyType> |
||
50 | { |
||
51 | private class AssociationItem |
||
52 | { |
||
53 | public AssociationRelyingPartyType DistinguishingFactor; |
||
54 | public string Handle; |
||
55 | public DateTime Expires; |
||
56 | public byte[] PrivateData; |
||
57 | } |
||
58 | |||
59 | Dictionary<string, AssociationItem> m_store = new Dictionary<string, AssociationItem>(); |
||
60 | SortedList<DateTime, AssociationItem> m_sortedStore = new SortedList<DateTime, AssociationItem>(); |
||
61 | object m_syncRoot = new object(); |
||
62 | |||
63 | #region IAssociationStore<AssociationRelyingPartyType> Members |
||
64 | |||
65 | public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc) |
||
66 | { |
||
67 | AssociationItem item = new AssociationItem(); |
||
68 | item.DistinguishingFactor = distinguishingFactor; |
||
69 | item.Handle = assoc.Handle; |
||
70 | item.Expires = assoc.Expires.ToLocalTime(); |
||
71 | item.PrivateData = assoc.SerializePrivateData(); |
||
72 | |||
73 | lock (m_syncRoot) |
||
74 | { |
||
75 | m_store[item.Handle] = item; |
||
76 | m_sortedStore[item.Expires] = item; |
||
77 | } |
||
78 | } |
||
79 | |||
80 | public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor) |
||
81 | { |
||
82 | lock (m_syncRoot) |
||
83 | { |
||
84 | if (m_sortedStore.Count > 0) |
||
85 | { |
||
86 | AssociationItem item = m_sortedStore.Values[m_sortedStore.Count - 1]; |
||
87 | return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData); |
||
88 | } |
||
89 | else |
||
90 | { |
||
91 | return null; |
||
92 | } |
||
93 | } |
||
94 | } |
||
95 | |||
96 | public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, string handle) |
||
97 | { |
||
98 | AssociationItem item; |
||
99 | bool success = false; |
||
100 | lock (m_syncRoot) |
||
101 | success = m_store.TryGetValue(handle, out item); |
||
102 | |||
103 | if (success) |
||
104 | return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData); |
||
105 | else |
||
106 | return null; |
||
107 | } |
||
108 | |||
109 | public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle) |
||
110 | { |
||
111 | lock (m_syncRoot) |
||
112 | { |
||
113 | for (int i = 0; i < m_sortedStore.Values.Count; i++) |
||
114 | { |
||
115 | AssociationItem item = m_sortedStore.Values[i]; |
||
116 | if (item.Handle == handle) |
||
117 | { |
||
118 | m_sortedStore.RemoveAt(i); |
||
119 | break; |
||
120 | } |
||
121 | } |
||
122 | |||
123 | return m_store.Remove(handle); |
||
124 | } |
||
125 | } |
||
126 | |||
127 | public void ClearExpiredAssociations() |
||
128 | { |
||
129 | lock (m_syncRoot) |
||
130 | { |
||
131 | List<AssociationItem> itemsCopy = new List<AssociationItem>(m_sortedStore.Values); |
||
132 | DateTime now = DateTime.Now; |
||
133 | |||
134 | for (int i = 0; i < itemsCopy.Count; i++) |
||
135 | { |
||
136 | AssociationItem item = itemsCopy[i]; |
||
137 | |||
138 | if (item.Expires <= now) |
||
139 | { |
||
140 | m_sortedStore.RemoveAt(i); |
||
141 | m_store.Remove(item.Handle); |
||
142 | } |
||
143 | } |
||
144 | } |
||
145 | } |
||
146 | |||
147 | #endregion |
||
148 | } |
||
149 | |||
150 | public class OpenIdStreamHandler : BaseOutputStreamHandler, IStreamHandler |
||
151 | { |
||
152 | #region HTML |
||
153 | |||
154 | /// <summary>Login form used to authenticate OpenID requests</summary> |
||
155 | const string LOGIN_PAGE = |
||
156 | @"<html> |
||
157 | <head><title>OpenSim OpenID Login</title></head> |
||
158 | <body> |
||
159 | <h3>OpenSim Login</h3> |
||
160 | <form method=""post""> |
||
161 | <label for=""first"">First Name:</label> <input readonly type=""text"" name=""first"" id=""first"" value=""{0}""/> |
||
162 | <label for=""last"">Last Name:</label> <input readonly type=""text"" name=""last"" id=""last"" value=""{1}""/> |
||
163 | <label for=""pass"">Password:</label> <input type=""password"" name=""pass"" id=""pass""/> |
||
164 | <input type=""submit"" value=""Login""> |
||
165 | </form> |
||
166 | </body> |
||
167 | </html>"; |
||
168 | |||
169 | /// <summary>Page shown for a valid OpenID identity</summary> |
||
170 | const string OPENID_PAGE = |
||
171 | @"<html> |
||
172 | <head> |
||
173 | <title>{2} {3}</title> |
||
174 | <link rel=""openid2.provider openid.server"" href=""{0}://{1}/openid/server/""/> |
||
175 | </head> |
||
176 | <body>OpenID identifier for {2} {3}</body> |
||
177 | </html> |
||
178 | "; |
||
179 | |||
180 | /// <summary>Page shown for an invalid OpenID identity</summary> |
||
181 | const string INVALID_OPENID_PAGE = |
||
182 | @"<html><head><title>Identity not found</title></head> |
||
183 | <body>Invalid OpenID identity</body></html>"; |
||
184 | |||
185 | /// <summary>Page shown if the OpenID endpoint is requested directly</summary> |
||
186 | const string ENDPOINT_PAGE = |
||
187 | @"<html><head><title>OpenID Endpoint</title></head><body> |
||
188 | This is an OpenID server endpoint, not a human-readable resource. |
||
189 | For more information, see <a href='http://openid.net/'>http://openid.net/</a>. |
||
190 | </body></html>"; |
||
191 | |||
192 | #endregion HTML |
||
193 | |||
194 | IAuthenticationService m_authenticationService; |
||
195 | IUserAccountService m_userAccountService; |
||
196 | ProviderMemoryStore m_openidStore = new ProviderMemoryStore(); |
||
197 | |||
198 | public override string ContentType { get { return "text/html"; } } |
||
199 | |||
200 | /// <summary> |
||
201 | /// Constructor |
||
202 | /// </summary> |
||
203 | public OpenIdStreamHandler( |
||
204 | string httpMethod, string path, IUserAccountService userService, IAuthenticationService authService) |
||
205 | : base(httpMethod, path, "OpenId", "OpenID stream handler") |
||
206 | { |
||
207 | m_authenticationService = authService; |
||
208 | m_userAccountService = userService; |
||
209 | } |
||
210 | |||
211 | /// <summary> |
||
212 | /// Handles all GET and POST requests for OpenID identifier pages and endpoint |
||
213 | /// server communication |
||
214 | /// </summary> |
||
215 | protected override void ProcessRequest( |
||
216 | string path, Stream request, Stream response, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) |
||
217 | { |
||
218 | Uri providerEndpoint = new Uri(String.Format("{0}://{1}{2}", httpRequest.Url.Scheme, httpRequest.Url.Authority, httpRequest.Url.AbsolutePath)); |
||
219 | |||
220 | // Defult to returning HTML content |
||
221 | httpResponse.ContentType = ContentType; |
||
222 | |||
223 | try |
||
224 | { |
||
225 | NameValueCollection postQuery = HttpUtility.ParseQueryString(new StreamReader(httpRequest.InputStream).ReadToEnd()); |
||
226 | NameValueCollection getQuery = HttpUtility.ParseQueryString(httpRequest.Url.Query); |
||
227 | NameValueCollection openIdQuery = (postQuery.GetValues("openid.mode") != null ? postQuery : getQuery); |
||
228 | |||
229 | OpenIdProvider provider = new OpenIdProvider(m_openidStore, providerEndpoint, httpRequest.Url, openIdQuery); |
||
230 | |||
231 | if (provider.Request != null) |
||
232 | { |
||
233 | if (!provider.Request.IsResponseReady && provider.Request is IAuthenticationRequest) |
||
234 | { |
||
235 | IAuthenticationRequest authRequest = (IAuthenticationRequest)provider.Request; |
||
236 | string[] passwordValues = postQuery.GetValues("pass"); |
||
237 | |||
238 | UserAccount account; |
||
239 | if (TryGetAccount(new Uri(authRequest.ClaimedIdentifier.ToString()), out account)) |
||
240 | { |
||
241 | // Check for form POST data |
||
242 | if (passwordValues != null && passwordValues.Length == 1) |
||
243 | { |
||
244 | if (account != null && |
||
245 | (m_authenticationService.Authenticate(account.PrincipalID,Util.Md5Hash(passwordValues[0]), 30) != string.Empty)) |
||
246 | authRequest.IsAuthenticated = true; |
||
247 | else |
||
248 | authRequest.IsAuthenticated = false; |
||
249 | } |
||
250 | else |
||
251 | { |
||
252 | // Authentication was requested, send the client a login form |
||
253 | using (StreamWriter writer = new StreamWriter(response)) |
||
254 | writer.Write(String.Format(LOGIN_PAGE, account.FirstName, account.LastName)); |
||
255 | return; |
||
256 | } |
||
257 | } |
||
258 | else |
||
259 | { |
||
260 | // Cannot find an avatar matching the claimed identifier |
||
261 | authRequest.IsAuthenticated = false; |
||
262 | } |
||
263 | } |
||
264 | |||
265 | // Add OpenID headers to the response |
||
266 | foreach (string key in provider.Request.Response.Headers.Keys) |
||
267 | httpResponse.AddHeader(key, provider.Request.Response.Headers[key]); |
||
268 | |||
269 | string[] contentTypeValues = provider.Request.Response.Headers.GetValues("Content-Type"); |
||
270 | if (contentTypeValues != null && contentTypeValues.Length == 1) |
||
271 | httpResponse.ContentType = contentTypeValues[0]; |
||
272 | |||
273 | // Set the response code and document body based on the OpenID result |
||
274 | httpResponse.StatusCode = (int)provider.Request.Response.Code; |
||
275 | response.Write(provider.Request.Response.Body, 0, provider.Request.Response.Body.Length); |
||
276 | response.Close(); |
||
277 | } |
||
278 | else if (httpRequest.Url.AbsolutePath.Contains("/openid/server")) |
||
279 | { |
||
280 | // Standard HTTP GET was made on the OpenID endpoint, send the client the default error page |
||
281 | using (StreamWriter writer = new StreamWriter(response)) |
||
282 | writer.Write(ENDPOINT_PAGE); |
||
283 | } |
||
284 | else |
||
285 | { |
||
286 | // Try and lookup this avatar |
||
287 | UserAccount account; |
||
288 | if (TryGetAccount(httpRequest.Url, out account)) |
||
289 | { |
||
290 | using (StreamWriter writer = new StreamWriter(response)) |
||
291 | { |
||
292 | // TODO: Print out a full profile page for this avatar |
||
293 | writer.Write(String.Format(OPENID_PAGE, httpRequest.Url.Scheme, |
||
294 | httpRequest.Url.Authority, account.FirstName, account.LastName)); |
||
295 | } |
||
296 | } |
||
297 | else |
||
298 | { |
||
299 | // Couldn't parse an avatar name, or couldn't find the avatar in the user server |
||
300 | using (StreamWriter writer = new StreamWriter(response)) |
||
301 | writer.Write(INVALID_OPENID_PAGE); |
||
302 | } |
||
303 | } |
||
304 | } |
||
305 | catch (Exception ex) |
||
306 | { |
||
307 | httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError; |
||
308 | using (StreamWriter writer = new StreamWriter(response)) |
||
309 | writer.Write(ex.Message); |
||
310 | } |
||
311 | } |
||
312 | |||
313 | /// <summary> |
||
314 | /// Parse a URL with a relative path of the form /users/First_Last and try to |
||
315 | /// retrieve the profile matching that avatar name |
||
316 | /// </summary> |
||
317 | /// <param name="requestUrl">URL to parse for an avatar name</param> |
||
318 | /// <param name="profile">Profile data for the avatar</param> |
||
319 | /// <returns>True if the parse and lookup were successful, otherwise false</returns> |
||
320 | bool TryGetAccount(Uri requestUrl, out UserAccount account) |
||
321 | { |
||
322 | if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/") |
||
323 | { |
||
324 | // Parse the avatar name from the path |
||
325 | string username = requestUrl.Segments[requestUrl.Segments.Length - 1]; |
||
326 | string[] name = username.Split('_'); |
||
327 | |||
328 | if (name.Length == 2) |
||
329 | { |
||
330 | account = m_userAccountService.GetUserAccount(UUID.Zero, name[0], name[1]); |
||
331 | return (account != null); |
||
332 | } |
||
333 | } |
||
334 | |||
335 | account = null; |
||
336 | return false; |
||
337 | } |
||
338 | } |
||
339 | } |