diff options
author | Dr Scofield | 2008-07-02 09:02:30 +0000 |
---|---|---|
committer | Dr Scofield | 2008-07-02 09:02:30 +0000 |
commit | d40bea4a8e09be1f8e87cf41405aaa60fa8826cb (patch) | |
tree | 91ef9356d8b284ac6fa5f0d588fedebe723b69ad | |
parent | Mantis#1643. Thank you Melanie for a patch that: (diff) | |
download | opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.zip opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.gz opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.bz2 opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.xz |
From: Alan M Webb <awebb@vnet.ibm.com>
This adds REST services for inventory access. It also allows inventory
uploads.
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs | 47 | ||||
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs | 1201 | ||||
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs | 479 | ||||
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs | 257 | ||||
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs | 547 | ||||
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs | 1993 | ||||
-rw-r--r-- | prebuild.xml | 38 |
7 files changed, 4562 insertions, 0 deletions
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs new file mode 100644 index 0000000..6f52582 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs | |||
@@ -0,0 +1,47 @@ | |||
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 OpenSim 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 | |||
29 | using System; | ||
30 | |||
31 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
32 | { | ||
33 | |||
34 | /// <summary> | ||
35 | /// This interface represents the boundary between the general purpose | ||
36 | /// REST plugin handling, and the functionally specific handlers. The | ||
37 | /// handler knows only to initialzie and terminate all such handlers | ||
38 | /// that it finds. | ||
39 | /// </summary> | ||
40 | |||
41 | internal interface IRest | ||
42 | { | ||
43 | void Initialize(); | ||
44 | void Close(); | ||
45 | } | ||
46 | |||
47 | } | ||
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs new file mode 100644 index 0000000..3de9f36 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs | |||
@@ -0,0 +1,1201 @@ | |||
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 OpenSim 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 | |||
29 | using System; | ||
30 | using System.IO; | ||
31 | using System.Reflection; | ||
32 | using System.Text; | ||
33 | using System.Security.Cryptography; | ||
34 | using System.Text.RegularExpressions; | ||
35 | using System.Collections.Generic; | ||
36 | using System.Collections.Specialized; | ||
37 | using OpenSim.Framework.Servers; | ||
38 | using libsecondlife; | ||
39 | using System.Xml; | ||
40 | |||
41 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
42 | { | ||
43 | |||
44 | /// <summary> | ||
45 | /// This class represents the current REST request. It | ||
46 | /// encapsulates the request/response state and takes care | ||
47 | /// of response generation without exposing the REST handler | ||
48 | /// to the actual mechanisms involved. | ||
49 | /// | ||
50 | /// This structure is created on entry to the Handler | ||
51 | /// method and is disposed of upon return. It is part of | ||
52 | /// the plug-in infrastructure, rather than the functionally | ||
53 | /// specifici REST handler, and fundamental changes to | ||
54 | /// this should be reflected in the Rest HandlerVersion. The | ||
55 | /// object is instantiated, and may be extended by, any | ||
56 | /// given handler. See the inventory handler for an example | ||
57 | /// of this. | ||
58 | /// | ||
59 | /// If possible, the underlying request/response state is not | ||
60 | /// changed until the handler explicitly issues a Respond call. | ||
61 | /// This ensures that the request/response pair can be safely | ||
62 | /// processed by subsequent, unrelated, handlers even id the | ||
63 | /// agent handler had completed much of its processing. Think | ||
64 | /// of it as a transactional req/resp capability. | ||
65 | /// </summary> | ||
66 | |||
67 | internal class RequestData | ||
68 | { | ||
69 | |||
70 | // HTTP Server interface data | ||
71 | |||
72 | internal OSHttpRequest request = null; | ||
73 | internal OSHttpResponse response = null; | ||
74 | |||
75 | // Request lifetime values | ||
76 | |||
77 | internal NameValueCollection headers = null; | ||
78 | internal List<string> removed_headers = null; | ||
79 | internal byte[] buffer = null; | ||
80 | internal string body = null; | ||
81 | internal string html = null; | ||
82 | internal string entity = null; | ||
83 | internal string path = null; | ||
84 | internal string method = null; | ||
85 | internal string statusDescription = null; | ||
86 | internal string redirectLocation = null; | ||
87 | internal string[] pathNodes = null; | ||
88 | internal string[] parameters = null; | ||
89 | internal int statusCode = 0; | ||
90 | internal bool handled = false; | ||
91 | internal LLUUID uuid = LLUUID.Zero; | ||
92 | internal Encoding encoding = Rest.Encoding; | ||
93 | internal Uri uri = null; | ||
94 | internal string query = null; | ||
95 | internal bool fail = false; | ||
96 | internal string hostname = "localhost"; | ||
97 | internal int port = 80; | ||
98 | internal string prefix = Rest.UrlPathSeparator; | ||
99 | |||
100 | // Authentication related state | ||
101 | |||
102 | internal bool authenticated = false; | ||
103 | internal string scheme = Rest.AS_DIGEST; | ||
104 | internal string realm = Rest.Realm; | ||
105 | internal string domain = null; | ||
106 | internal string nonce = null; | ||
107 | internal string cnonce = null; | ||
108 | internal string qop = Rest.Qop_Auth; | ||
109 | internal string opaque = null; | ||
110 | internal string stale = null; | ||
111 | internal string algorithm = Rest.Digest_MD5; | ||
112 | internal string authParms = null; | ||
113 | internal string authPrefix = null; | ||
114 | internal string userName = String.Empty; | ||
115 | internal string userPass = String.Empty; | ||
116 | internal LLUUID client = LLUUID.Zero; | ||
117 | |||
118 | // XML related state | ||
119 | |||
120 | internal XmlWriter writer = null; | ||
121 | internal XmlReader reader = null; | ||
122 | |||
123 | // Internal working state | ||
124 | |||
125 | private StringBuilder sbuilder = new StringBuilder(1024); | ||
126 | private MemoryStream xmldata = null; | ||
127 | |||
128 | private static readonly string[] EmptyPath = { String.Empty }; | ||
129 | |||
130 | // Session related tables. These are only needed if QOP is set to "auth-sess" | ||
131 | // and for now at least, it is not. Session related authentication is of | ||
132 | // questionable merit in the context of REST anyway, but it is, arguably, more | ||
133 | // secure. | ||
134 | |||
135 | private static Dictionary<string,string> cntable = new Dictionary<string,string>(); | ||
136 | private static Dictionary<string,string> sktable = new Dictionary<string,string>(); | ||
137 | |||
138 | // This dictionary is used to keep track fo all of the parameters discovered | ||
139 | // when the authorisation header is anaylsed. | ||
140 | |||
141 | private Dictionary<string,string> authparms = new Dictionary<string,string>(); | ||
142 | |||
143 | // These regular expressions are used to decipher the various header entries. | ||
144 | |||
145 | private static Regex schema = new Regex("^\\s*(?<scheme>\\w+)\\s*.*", | ||
146 | RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
147 | |||
148 | private static Regex basicParms = new Regex("^\\s*(?:\\w+)\\s+(?<pval>\\S+)\\s*", | ||
149 | RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
150 | |||
151 | private static Regex digestParm1 = new Regex("\\s*(?<parm>\\w+)\\s*=\\s*\"(?<pval>\\S+)\"", | ||
152 | RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
153 | |||
154 | private static Regex digestParm2 = new Regex("\\s*(?<parm>\\w+)\\s*=\\s*(?<pval>[^\\p{P}\\s]+)", | ||
155 | RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
156 | |||
157 | private static Regex reuserPass = new Regex("\\s*(?<user>\\w+)\\s*:\\s*(?<pass>\\S*)", | ||
158 | RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
159 | |||
160 | // For efficiency, we create static instances of these objects | ||
161 | |||
162 | private static MD5 md5hash = MD5.Create(); | ||
163 | |||
164 | private static StringComparer sc = StringComparer.OrdinalIgnoreCase; | ||
165 | |||
166 | // Constructor | ||
167 | |||
168 | internal RequestData(OSHttpRequest p_request, OSHttpResponse p_response, string qprefix) | ||
169 | { | ||
170 | |||
171 | request = p_request; | ||
172 | response = p_response; | ||
173 | |||
174 | sbuilder.Length = 0; | ||
175 | |||
176 | encoding = request.ContentEncoding; | ||
177 | if (encoding == null) | ||
178 | { | ||
179 | encoding = Rest.Encoding; | ||
180 | } | ||
181 | |||
182 | method = request.HttpMethod.ToLower(); | ||
183 | initUrl(); | ||
184 | |||
185 | initParameters(qprefix.Length); | ||
186 | |||
187 | } | ||
188 | |||
189 | // Just for convenience... | ||
190 | |||
191 | internal string MsgId | ||
192 | { | ||
193 | get { return Rest.MsgId; } | ||
194 | } | ||
195 | |||
196 | // Defer authentication check until requested | ||
197 | |||
198 | internal bool IsAuthenticated | ||
199 | { | ||
200 | get | ||
201 | { | ||
202 | if (Rest.Authenticate) | ||
203 | { | ||
204 | if (!authenticated) | ||
205 | { | ||
206 | authenticate(); | ||
207 | } | ||
208 | |||
209 | return authenticated; | ||
210 | } | ||
211 | else return true; | ||
212 | } | ||
213 | } | ||
214 | |||
215 | /// <summary> | ||
216 | /// The REST handler has requested authentication. Authentication | ||
217 | /// is considered to be with respect to the current values for | ||
218 | /// Realm, domain, etc. | ||
219 | /// | ||
220 | /// This method checks to see if the current request is already | ||
221 | /// authenticated for this domain. If it is, then it returns | ||
222 | /// true. If it is not, then it issues a challenge to the client | ||
223 | /// and responds negatively to the request. | ||
224 | /// </summary> | ||
225 | |||
226 | private void authenticate() | ||
227 | { | ||
228 | |||
229 | string authdata = request.Headers.Get("Authorization"); | ||
230 | string reqscheme = String.Empty; | ||
231 | |||
232 | // If we don't have an authorization header, then this | ||
233 | // user is certainly not authorized. This is the typical | ||
234 | // pivot for the 1st request by a client. | ||
235 | |||
236 | if (authdata == null) | ||
237 | { | ||
238 | Rest.Log.DebugFormat("{0} Challenge reason: No authorization data", MsgId); | ||
239 | DoChallenge(); | ||
240 | } | ||
241 | |||
242 | // So, we have authentication data, now we have to check to | ||
243 | // see what we got and whether or not it is valid for the | ||
244 | // current domain. To do this we need to interpret the data | ||
245 | // provided in the Authorization header. First we need to | ||
246 | // identify the scheme being used and route accordingly. | ||
247 | |||
248 | MatchCollection matches = schema.Matches(authdata); | ||
249 | |||
250 | foreach (Match m in matches) | ||
251 | { | ||
252 | Rest.Log.DebugFormat("{0} Scheme matched : {1}", MsgId, m.Groups["scheme"].Value); | ||
253 | reqscheme = m.Groups["scheme"].Value.ToLower(); | ||
254 | } | ||
255 | |||
256 | // If we want a specific authentication mechanism, make sure | ||
257 | // we get it. | ||
258 | |||
259 | if (scheme != null && scheme.ToLower() != reqscheme) | ||
260 | { | ||
261 | Rest.Log.DebugFormat("{0} Challenge reason: Required scheme not accepted", MsgId); | ||
262 | DoChallenge(); | ||
263 | } | ||
264 | |||
265 | // In the future, these could be made into plug-ins... | ||
266 | // But for now at least we have no reason to use anything other | ||
267 | // then MD5. TLS/SSL are taken care of elsewhere. | ||
268 | |||
269 | switch (reqscheme) | ||
270 | { | ||
271 | case "digest" : | ||
272 | Rest.Log.DebugFormat("{0} Digest authentication offered", MsgId); | ||
273 | DoDigest(authdata); | ||
274 | break; | ||
275 | |||
276 | case "basic" : | ||
277 | Rest.Log.DebugFormat("{0} Basic authentication offered", MsgId); | ||
278 | DoBasic(authdata); | ||
279 | break; | ||
280 | } | ||
281 | |||
282 | // If the current header is invalid, then a challenge is still needed. | ||
283 | |||
284 | if (!authenticated) | ||
285 | { | ||
286 | Rest.Log.DebugFormat("{0} Challenge reason: Authentication failed", MsgId); | ||
287 | DoChallenge(); | ||
288 | } | ||
289 | |||
290 | } | ||
291 | |||
292 | /// <summary> | ||
293 | /// Construct the necessary WWW-Authenticate headers and fail the request | ||
294 | /// with a NOT AUTHORIZED response. The parameters are the union of values | ||
295 | /// required by the supported schemes. | ||
296 | /// </summary> | ||
297 | |||
298 | private void DoChallenge() | ||
299 | { | ||
300 | Flush(); | ||
301 | nonce = Rest.NonceGenerator(); // should be unique per 401 (and it is) | ||
302 | Challenge(scheme, realm, domain, nonce, opaque, stale, algorithm, qop, authParms); | ||
303 | Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); | ||
304 | } | ||
305 | |||
306 | /// <summary> | ||
307 | /// Interpret a BASIC authorization claim | ||
308 | /// This is here for completeness, it is not used. | ||
309 | /// </summary> | ||
310 | |||
311 | private void DoBasic(string authdata) | ||
312 | { | ||
313 | |||
314 | string response = null; | ||
315 | |||
316 | MatchCollection matches = basicParms.Matches(authdata); | ||
317 | |||
318 | // In the case of basic authentication there is | ||
319 | // only expected to be a single argument. | ||
320 | |||
321 | foreach (Match m in matches) | ||
322 | { | ||
323 | authparms.Add("response",m.Groups["pval"].Value); | ||
324 | Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", | ||
325 | MsgId, "response", m.Groups["pval"].Value); | ||
326 | } | ||
327 | |||
328 | // Did we get a valid response? | ||
329 | |||
330 | if (authparms.TryGetValue("response", out response)) | ||
331 | { | ||
332 | // Decode | ||
333 | response = Rest.Base64ToString(response); | ||
334 | Rest.Log.DebugFormat("{0} Auth response is: <{1}>", MsgId, response); | ||
335 | |||
336 | // Extract user & password | ||
337 | Match m = reuserPass.Match(response); | ||
338 | userName = m.Groups["user"].Value; | ||
339 | userPass = m.Groups["pass"].Value; | ||
340 | |||
341 | // Validate against user database | ||
342 | authenticated = Validate(userName,userPass); | ||
343 | } | ||
344 | |||
345 | } | ||
346 | |||
347 | /// <summary> | ||
348 | /// This is an RFC2617 compliant HTTP MD5 Digest authentication | ||
349 | /// implementation. It has been tested with Firefox, Java HTTP client, | ||
350 | /// and Miscrosoft's Internet Explorer V7. | ||
351 | /// </summary> | ||
352 | |||
353 | private void DoDigest(string authdata) | ||
354 | { | ||
355 | |||
356 | string response = null; | ||
357 | |||
358 | MatchCollection matches = digestParm1.Matches(authdata); | ||
359 | |||
360 | // Collect all of the supplied parameters and store them | ||
361 | // in a dictionary (for ease of access) | ||
362 | |||
363 | foreach (Match m in matches) | ||
364 | { | ||
365 | authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); | ||
366 | Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", | ||
367 | MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); | ||
368 | } | ||
369 | |||
370 | // And pick up any tokens too | ||
371 | |||
372 | matches = digestParm2.Matches(authdata); | ||
373 | |||
374 | foreach (Match m in matches) | ||
375 | { | ||
376 | authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); | ||
377 | Rest.Log.DebugFormat("{0} Tokenized Parameter matched : {1} = {2}", | ||
378 | MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); | ||
379 | } | ||
380 | |||
381 | // A response string MUST be returned, otherwise we are | ||
382 | // NOT authenticated. | ||
383 | |||
384 | Rest.Log.DebugFormat("{0} Validating authorization parameters", MsgId); | ||
385 | |||
386 | if (authparms.TryGetValue("response", out response)) | ||
387 | { | ||
388 | |||
389 | string temp = null; | ||
390 | |||
391 | do | ||
392 | { | ||
393 | |||
394 | string nck = null; | ||
395 | string ncl = null; | ||
396 | |||
397 | // The userid is sent in clear text. Needed for the | ||
398 | // verification. | ||
399 | |||
400 | authparms.TryGetValue("username", out userName); | ||
401 | |||
402 | // All URI's of which this is a prefix are | ||
403 | // optimistically considered to be authenticated by the | ||
404 | // client. This is also needed to verify the response. | ||
405 | |||
406 | authparms.TryGetValue("uri", out authPrefix); | ||
407 | |||
408 | // There MUST be a nonce string present. We're not preserving any server | ||
409 | // side state and we can;t validate the MD5 unless the lcient returns it | ||
410 | // to us, as it should. | ||
411 | |||
412 | if (!authparms.TryGetValue("nonce", out nonce)) | ||
413 | { | ||
414 | Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); | ||
415 | break; | ||
416 | } | ||
417 | |||
418 | // If there is an opaque string present, it had better | ||
419 | // match what we sent. | ||
420 | |||
421 | if (authparms.TryGetValue("opaque", out temp)) | ||
422 | { | ||
423 | if (temp != opaque) | ||
424 | { | ||
425 | Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); | ||
426 | break; | ||
427 | } | ||
428 | } | ||
429 | |||
430 | // If an algorithm string is present, it had better | ||
431 | // match what we sent. | ||
432 | |||
433 | if (authparms.TryGetValue("algorithm", out temp)) | ||
434 | { | ||
435 | if (temp != algorithm) | ||
436 | { | ||
437 | Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); | ||
438 | break; | ||
439 | } | ||
440 | } | ||
441 | |||
442 | // Quality of protection considerations... | ||
443 | |||
444 | if (authparms.TryGetValue("qop", out temp)) | ||
445 | { | ||
446 | |||
447 | qop = temp.ToLower(); // replace with actual value used | ||
448 | |||
449 | // if QOP was specified then | ||
450 | // these MUST be present. | ||
451 | |||
452 | if (!authparms.ContainsKey("cnonce")) | ||
453 | { | ||
454 | Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); | ||
455 | break; | ||
456 | } | ||
457 | |||
458 | cnonce = authparms["cnonce"]; | ||
459 | |||
460 | if (!authparms.ContainsKey("nc")) | ||
461 | { | ||
462 | Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); | ||
463 | break; | ||
464 | } | ||
465 | |||
466 | nck = authparms["nc"]; | ||
467 | |||
468 | if (cntable.TryGetValue(cnonce, out ncl)) | ||
469 | { | ||
470 | if (Rest.Hex2Int(ncl) <= Rest.Hex2Int(nck)) | ||
471 | { | ||
472 | Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); | ||
473 | break; | ||
474 | } | ||
475 | cntable[cnonce] = nck; | ||
476 | } | ||
477 | else | ||
478 | { | ||
479 | lock(cntable) cntable.Add(cnonce, nck); | ||
480 | } | ||
481 | |||
482 | } | ||
483 | else | ||
484 | { | ||
485 | |||
486 | qop = String.Empty; | ||
487 | |||
488 | // if QOP was not specified then | ||
489 | // these MUST NOT be present. | ||
490 | if (authparms.ContainsKey("cnonce")) | ||
491 | { | ||
492 | Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce", MsgId); | ||
493 | break; | ||
494 | } | ||
495 | if (authparms.ContainsKey("nc")) | ||
496 | { | ||
497 | Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce counter[2]", MsgId); | ||
498 | break; | ||
499 | } | ||
500 | } | ||
501 | |||
502 | // Validate the supplied userid/password info | ||
503 | |||
504 | authenticated = ValidateDigest(userName, nonce, cnonce, nck, authPrefix, response); | ||
505 | |||
506 | } | ||
507 | while (false); | ||
508 | |||
509 | } | ||
510 | |||
511 | } | ||
512 | |||
513 | // Indicate that authentication is required | ||
514 | |||
515 | internal void Challenge(string scheme, string realm, string domain, string nonce, | ||
516 | string opaque, string stale, string alg, | ||
517 | string qop, string auth) | ||
518 | { | ||
519 | |||
520 | sbuilder.Length = 0; | ||
521 | |||
522 | if (scheme == null || scheme == Rest.AS_DIGEST) | ||
523 | { | ||
524 | |||
525 | sbuilder.Append(Rest.AS_DIGEST); | ||
526 | sbuilder.Append(" "); | ||
527 | |||
528 | if (realm != null) | ||
529 | { | ||
530 | sbuilder.Append("realm="); | ||
531 | sbuilder.Append(Rest.CS_DQUOTE); | ||
532 | sbuilder.Append(realm); | ||
533 | sbuilder.Append(Rest.CS_DQUOTE); | ||
534 | sbuilder.Append(Rest.CS_COMMA); | ||
535 | } | ||
536 | |||
537 | if (nonce != null) | ||
538 | { | ||
539 | sbuilder.Append("nonce="); | ||
540 | sbuilder.Append(Rest.CS_DQUOTE); | ||
541 | sbuilder.Append(nonce); | ||
542 | sbuilder.Append(Rest.CS_DQUOTE); | ||
543 | sbuilder.Append(Rest.CS_COMMA); | ||
544 | } | ||
545 | |||
546 | if (opaque != null) | ||
547 | { | ||
548 | sbuilder.Append("opaque="); | ||
549 | sbuilder.Append(Rest.CS_DQUOTE); | ||
550 | sbuilder.Append(opaque); | ||
551 | sbuilder.Append(Rest.CS_DQUOTE); | ||
552 | sbuilder.Append(Rest.CS_COMMA); | ||
553 | } | ||
554 | |||
555 | if (stale != null) | ||
556 | { | ||
557 | sbuilder.Append("stale="); | ||
558 | sbuilder.Append(Rest.CS_DQUOTE); | ||
559 | sbuilder.Append(stale); | ||
560 | sbuilder.Append(Rest.CS_DQUOTE); | ||
561 | sbuilder.Append(Rest.CS_COMMA); | ||
562 | } | ||
563 | |||
564 | if (alg != null) | ||
565 | { | ||
566 | sbuilder.Append("algorithm="); | ||
567 | sbuilder.Append(alg); | ||
568 | sbuilder.Append(Rest.CS_COMMA); | ||
569 | } | ||
570 | |||
571 | if (qop != String.Empty) | ||
572 | { | ||
573 | sbuilder.Append("qop="); | ||
574 | sbuilder.Append(Rest.CS_DQUOTE); | ||
575 | sbuilder.Append(qop); | ||
576 | sbuilder.Append(Rest.CS_DQUOTE); | ||
577 | sbuilder.Append(Rest.CS_COMMA); | ||
578 | } | ||
579 | |||
580 | if (auth != null) | ||
581 | { | ||
582 | sbuilder.Append(auth); | ||
583 | sbuilder.Append(Rest.CS_COMMA); | ||
584 | } | ||
585 | |||
586 | if (Rest.Domains.Count != 0) | ||
587 | { | ||
588 | sbuilder.Append("domain="); | ||
589 | sbuilder.Append(Rest.CS_DQUOTE); | ||
590 | foreach (string dom in Rest.Domains.Values) | ||
591 | { | ||
592 | sbuilder.Append(dom); | ||
593 | sbuilder.Append(Rest.CS_SPACE); | ||
594 | } | ||
595 | if (sbuilder[sbuilder.Length-1] == Rest.C_SPACE) | ||
596 | { | ||
597 | sbuilder.Length = sbuilder.Length-1; | ||
598 | } | ||
599 | sbuilder.Append(Rest.CS_DQUOTE); | ||
600 | sbuilder.Append(Rest.CS_COMMA); | ||
601 | } | ||
602 | |||
603 | if (sbuilder[sbuilder.Length-1] == Rest.C_COMMA) | ||
604 | { | ||
605 | sbuilder.Length = sbuilder.Length-1; | ||
606 | } | ||
607 | |||
608 | AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); | ||
609 | |||
610 | } | ||
611 | |||
612 | if (scheme == null || scheme == Rest.AS_BASIC) | ||
613 | { | ||
614 | |||
615 | sbuilder.Append(Rest.AS_BASIC); | ||
616 | |||
617 | if (realm != null) | ||
618 | { | ||
619 | sbuilder.Append(" realm=\""); | ||
620 | sbuilder.Append(realm); | ||
621 | sbuilder.Append("\""); | ||
622 | } | ||
623 | AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); | ||
624 | } | ||
625 | |||
626 | } | ||
627 | |||
628 | private bool Validate(string user, string pass) | ||
629 | { | ||
630 | Rest.Log.DebugFormat("{0} Validating {1}:{2}", MsgId, user, pass); | ||
631 | return user == "awebb" && pass == getPassword(user); | ||
632 | } | ||
633 | |||
634 | private string getPassword(string user) | ||
635 | { | ||
636 | return Rest.GodKey; | ||
637 | } | ||
638 | |||
639 | // Validate the request-digest | ||
640 | private bool ValidateDigest(string user, string nonce, string cnonce, string nck, string uri, string response) | ||
641 | { | ||
642 | |||
643 | string patt = null; | ||
644 | string payl = String.Empty; | ||
645 | string KDS = null; | ||
646 | string HA1 = null; | ||
647 | string HA2 = null; | ||
648 | string pass = getPassword(user); | ||
649 | |||
650 | // Generate H(A1) | ||
651 | |||
652 | if (algorithm == Rest.Digest_MD5Sess) | ||
653 | { | ||
654 | if (!sktable.ContainsKey(cnonce)) | ||
655 | { | ||
656 | patt = String.Format("{0}:{1}:{2}:{3}:{4}", user, realm, pass, nonce, cnonce); | ||
657 | HA1 = HashToString(patt); | ||
658 | sktable.Add(cnonce, HA1); | ||
659 | } | ||
660 | else | ||
661 | { | ||
662 | HA1 = sktable[cnonce]; | ||
663 | } | ||
664 | } | ||
665 | else | ||
666 | { | ||
667 | patt = String.Format("{0}:{1}:{2}", user, realm, pass); | ||
668 | HA1 = HashToString(patt); | ||
669 | } | ||
670 | |||
671 | // Generate H(A2) | ||
672 | |||
673 | if (qop == "auth-int") | ||
674 | { | ||
675 | patt = String.Format("{0}:{1}:{2}", request.HttpMethod, uri, HashToString(payl)); | ||
676 | } | ||
677 | else | ||
678 | { | ||
679 | patt = String.Format("{0}:{1}", request.HttpMethod, uri); | ||
680 | } | ||
681 | |||
682 | HA2 = HashToString(patt); | ||
683 | |||
684 | // Generate Digest | ||
685 | |||
686 | if (qop != String.Empty) | ||
687 | { | ||
688 | patt = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, nonce, nck, cnonce, qop, HA2); | ||
689 | } | ||
690 | else | ||
691 | { | ||
692 | patt = String.Format("{0}:{1}:{2}", HA1, nonce, HA2); | ||
693 | } | ||
694 | |||
695 | KDS = HashToString(patt); | ||
696 | |||
697 | // Compare the generated sequence with the original | ||
698 | |||
699 | return (0 == sc.Compare(KDS, response)); | ||
700 | |||
701 | } | ||
702 | |||
703 | private string HashToString(string pattern) | ||
704 | { | ||
705 | |||
706 | Rest.Log.DebugFormat("{0} Generate <{1}>", MsgId, pattern); | ||
707 | |||
708 | byte[] hash = md5hash.ComputeHash(encoding.GetBytes(pattern)); | ||
709 | |||
710 | sbuilder.Length = 0; | ||
711 | |||
712 | for (int i = 0; i < hash.Length; i++) | ||
713 | { | ||
714 | sbuilder.Append(hash[i].ToString("x2")); | ||
715 | } | ||
716 | |||
717 | Rest.Log.DebugFormat("{0} Hash = <{1}>", MsgId, sbuilder.ToString()); | ||
718 | |||
719 | return sbuilder.ToString(); | ||
720 | |||
721 | } | ||
722 | |||
723 | internal void Complete() | ||
724 | { | ||
725 | statusCode = Rest.HttpStatusCodeOK; | ||
726 | statusDescription = Rest.HttpStatusDescOK; | ||
727 | } | ||
728 | |||
729 | internal void Redirect(string Url, bool temp) | ||
730 | { | ||
731 | |||
732 | redirectLocation = Url; | ||
733 | |||
734 | if (temp) | ||
735 | { | ||
736 | statusCode = Rest.HttpStatusCodeTemporaryRedirect; | ||
737 | statusDescription = Rest.HttpStatusDescTemporaryRedirect; | ||
738 | } | ||
739 | else | ||
740 | { | ||
741 | statusCode = Rest.HttpStatusCodePermanentRedirect; | ||
742 | statusDescription = Rest.HttpStatusDescPermanentRedirect; | ||
743 | } | ||
744 | |||
745 | Fail(statusCode, statusDescription, true); | ||
746 | |||
747 | } | ||
748 | |||
749 | // Fail for an arbitrary reason. Just a failure with | ||
750 | // headers. | ||
751 | |||
752 | internal void Fail(int code, string message) | ||
753 | { | ||
754 | Fail(code, message, true); | ||
755 | } | ||
756 | |||
757 | // More adventurous. This failure also includes a | ||
758 | // specified entity. | ||
759 | |||
760 | internal void Fail(int code, string message, string data) | ||
761 | { | ||
762 | buffer = null; | ||
763 | body = data; | ||
764 | Fail(code, message, false); | ||
765 | } | ||
766 | |||
767 | internal void Fail(int code, string message, bool reset) | ||
768 | { | ||
769 | |||
770 | statusCode = code; | ||
771 | statusDescription = message; | ||
772 | |||
773 | if (reset) | ||
774 | { | ||
775 | buffer = null; | ||
776 | body = null; | ||
777 | } | ||
778 | |||
779 | if (Rest.DEBUG) | ||
780 | { | ||
781 | Rest.Log.DebugFormat("{0} Scheme = {1}", MsgId, scheme); | ||
782 | Rest.Log.DebugFormat("{0} Realm = {1}", MsgId, realm); | ||
783 | Rest.Log.DebugFormat("{0} Domain = {1}", MsgId, domain); | ||
784 | Rest.Log.DebugFormat("{0} Nonce = {1}", MsgId, nonce); | ||
785 | Rest.Log.DebugFormat("{0} CNonce = {1}", MsgId, cnonce); | ||
786 | Rest.Log.DebugFormat("{0} Opaque = {1}", MsgId, opaque); | ||
787 | Rest.Log.DebugFormat("{0} Stale = {1}", MsgId, stale); | ||
788 | Rest.Log.DebugFormat("{0} Algorithm = {1}", MsgId, algorithm); | ||
789 | Rest.Log.DebugFormat("{0} QOP = {1}", MsgId, qop); | ||
790 | Rest.Log.DebugFormat("{0} AuthPrefix = {1}", MsgId, authPrefix); | ||
791 | Rest.Log.DebugFormat("{0} UserName = {1}", MsgId, userName); | ||
792 | Rest.Log.DebugFormat("{0} UserPass = {1}", MsgId, userPass); | ||
793 | } | ||
794 | |||
795 | fail = true; | ||
796 | |||
797 | Respond("Failure response"); | ||
798 | |||
799 | RestException re = new RestException(message+" <"+code+">"); | ||
800 | |||
801 | re.statusCode = code; | ||
802 | re.statusDesc = message; | ||
803 | re.httpmethod = method; | ||
804 | re.httppath = path; | ||
805 | |||
806 | throw re; | ||
807 | |||
808 | } | ||
809 | |||
810 | // Reject this request | ||
811 | |||
812 | internal void Reject() | ||
813 | { | ||
814 | Fail(Rest.HttpStatusCodeNotImplemented, Rest.HttpStatusDescNotImplemented); | ||
815 | } | ||
816 | |||
817 | // This MUST be called by an agent handler before it returns | ||
818 | // control to Handle, otherwise the request will be ignored. | ||
819 | // This is called implciitly for the REST stream handlers and | ||
820 | // is harmless if it is called twice. | ||
821 | |||
822 | internal virtual bool Respond(string reason) | ||
823 | { | ||
824 | |||
825 | Rest.Log.DebugFormat("{0} Respond ENTRY, handled = {1}, reason = {2}", MsgId, handled, reason); | ||
826 | |||
827 | if (!handled) | ||
828 | { | ||
829 | |||
830 | Rest.Log.DebugFormat("{0} Generating Response", MsgId); | ||
831 | |||
832 | // Process any arbitrary headers collected | ||
833 | |||
834 | BuildHeaders(); | ||
835 | |||
836 | // A Head request can NOT have a body! | ||
837 | if (method != Rest.HEAD) | ||
838 | { | ||
839 | |||
840 | Rest.Log.DebugFormat("{0} Response is not abbreviated", MsgId); | ||
841 | |||
842 | if (writer != null) | ||
843 | { | ||
844 | Rest.Log.DebugFormat("{0} XML Response handler extension ENTRY", MsgId); | ||
845 | Rest.Log.DebugFormat("{0} XML Response exists", MsgId); | ||
846 | writer.Flush(); | ||
847 | writer.Close(); | ||
848 | if (!fail) | ||
849 | { | ||
850 | buffer = xmldata.ToArray(); | ||
851 | AddHeader("Content-Type","application/xml"); | ||
852 | } | ||
853 | xmldata.Close(); | ||
854 | Rest.Log.DebugFormat("{0} XML Response encoded", MsgId); | ||
855 | Rest.Log.DebugFormat("{0} XML Response handler extension EXIT", MsgId); | ||
856 | } | ||
857 | |||
858 | // If buffer != null, then we assume that | ||
859 | // this has already been done some other | ||
860 | // way. For example, transfer encoding might | ||
861 | // have been done. | ||
862 | |||
863 | if (buffer == null) | ||
864 | { | ||
865 | if (body != null && body.Length > 0) | ||
866 | { | ||
867 | Rest.Log.DebugFormat("{0} String-based entity", MsgId); | ||
868 | buffer = encoding.GetBytes(body); | ||
869 | } | ||
870 | } | ||
871 | |||
872 | if (buffer != null) | ||
873 | { | ||
874 | Rest.Log.DebugFormat("{0} Buffer-based entity", MsgId); | ||
875 | if (response.Headers.Get("Content-Encoding") == null) | ||
876 | response.ContentEncoding = encoding; | ||
877 | response.ContentLength64 = buffer.Length; | ||
878 | response.SendChunked = false; | ||
879 | response.KeepAlive = false; | ||
880 | } | ||
881 | |||
882 | } | ||
883 | |||
884 | // Set the status code & description. If nothing | ||
885 | // has been stored, we consider that a success | ||
886 | |||
887 | if (statusCode == 0) | ||
888 | { | ||
889 | Complete(); | ||
890 | } | ||
891 | |||
892 | response.StatusCode = statusCode; | ||
893 | |||
894 | if (response.StatusCode == (int)OSHttpStatusCode.RedirectMovedTemporarily || | ||
895 | response.StatusCode == (int)OSHttpStatusCode.RedirectMovedPermanently) | ||
896 | { | ||
897 | response.RedirectLocation = redirectLocation; | ||
898 | } | ||
899 | |||
900 | if (statusDescription != null) | ||
901 | { | ||
902 | response.StatusDescription = statusDescription; | ||
903 | } | ||
904 | |||
905 | // Finally we send back our response, consuming | ||
906 | // any exceptions that doing so might produce. | ||
907 | |||
908 | // We've left the setting of handled' until the | ||
909 | // last minute because the header settings included | ||
910 | // above are pretty harmless. But everything from | ||
911 | // here on down probably leaves the response | ||
912 | // element unusable by anyone else. | ||
913 | |||
914 | handled = true; | ||
915 | |||
916 | if (buffer != null && buffer.Length != 0) | ||
917 | { | ||
918 | Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", | ||
919 | MsgId, buffer.Length, encoding.GetString(buffer)); | ||
920 | response.OutputStream.Write(buffer, 0, buffer.Length); | ||
921 | } | ||
922 | |||
923 | response.OutputStream.Close(); | ||
924 | |||
925 | if (request.InputStream != null) | ||
926 | { | ||
927 | request.InputStream.Close(); | ||
928 | } | ||
929 | |||
930 | } | ||
931 | |||
932 | Rest.Log.DebugFormat("{0} Respond EXIT, handled = {1}, reason = {2}", MsgId, handled, reason); | ||
933 | |||
934 | return handled; | ||
935 | |||
936 | } | ||
937 | |||
938 | // Add a header to the table. If the header | ||
939 | // already exists, it is replaced. | ||
940 | |||
941 | internal void AddHeader(string hdr, string data) | ||
942 | { | ||
943 | |||
944 | if (headers == null) | ||
945 | { | ||
946 | headers = new NameValueCollection(); | ||
947 | } | ||
948 | |||
949 | headers[hdr] = data; | ||
950 | |||
951 | } | ||
952 | |||
953 | // Keep explicit track of any headers which | ||
954 | // are to be removed. | ||
955 | |||
956 | internal void RemoveHeader(string hdr) | ||
957 | { | ||
958 | |||
959 | if (removed_headers == null) | ||
960 | { | ||
961 | removed_headers = new List<string>(); | ||
962 | } | ||
963 | |||
964 | removed_headers.Add(hdr); | ||
965 | |||
966 | if (headers != null) | ||
967 | { | ||
968 | headers.Remove(hdr); | ||
969 | } | ||
970 | |||
971 | } | ||
972 | |||
973 | // Should it prove necessary, we could always | ||
974 | // restore the header collection from a cloned | ||
975 | // copy, but for now we'll assume that that is | ||
976 | // not necessary. | ||
977 | |||
978 | private void BuildHeaders() | ||
979 | { | ||
980 | if (removed_headers != null) | ||
981 | { | ||
982 | foreach (string h in removed_headers) | ||
983 | { | ||
984 | Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, h); | ||
985 | response.Headers.Remove(h); | ||
986 | } | ||
987 | } | ||
988 | if (headers!= null) | ||
989 | { | ||
990 | for (int i = 0; i < headers.Count; i++) | ||
991 | { | ||
992 | Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", | ||
993 | MsgId, headers.GetKey(i), headers.Get(i)); | ||
994 | response.Headers.Add(headers.GetKey(i), headers.Get(i)); | ||
995 | } | ||
996 | } | ||
997 | } | ||
998 | |||
999 | /// <summary> | ||
1000 | /// Helper methods for deconstructing and reconstructing | ||
1001 | /// URI path data. | ||
1002 | /// </summary> | ||
1003 | |||
1004 | private void initUrl() | ||
1005 | { | ||
1006 | |||
1007 | uri = request.Url; | ||
1008 | |||
1009 | if (query == null) | ||
1010 | { | ||
1011 | query = uri.Query; | ||
1012 | } | ||
1013 | |||
1014 | // If the path has not been previously initialized, | ||
1015 | // do so now. | ||
1016 | |||
1017 | if (path == null) | ||
1018 | { | ||
1019 | path = uri.AbsolutePath; | ||
1020 | if (path.EndsWith(Rest.UrlPathSeparator)) | ||
1021 | path = path.Substring(0,path.Length-1); | ||
1022 | path = Uri.UnescapeDataString(path); | ||
1023 | } | ||
1024 | |||
1025 | // If we succeeded in getting a path, perform any | ||
1026 | // additional pre-processing required. | ||
1027 | |||
1028 | if (path != null) | ||
1029 | { | ||
1030 | if (Rest.ExtendedEscape) | ||
1031 | { | ||
1032 | // Handle "+". Not a standard substitution, but | ||
1033 | // common enough... | ||
1034 | path = path.Replace(Rest.C_PLUS,Rest.C_SPACE); | ||
1035 | } | ||
1036 | pathNodes = path.Split(Rest.CA_PATHSEP); | ||
1037 | } | ||
1038 | else | ||
1039 | { | ||
1040 | pathNodes = EmptyPath; | ||
1041 | } | ||
1042 | |||
1043 | // Request server context info | ||
1044 | |||
1045 | hostname = uri.Host; | ||
1046 | port = uri.Port; | ||
1047 | |||
1048 | } | ||
1049 | |||
1050 | internal int initParameters(int prfxlen) | ||
1051 | { | ||
1052 | |||
1053 | if (prfxlen < path.Length-1) | ||
1054 | { | ||
1055 | parameters = path.Substring(prfxlen+1).Split(Rest.CA_PATHSEP); | ||
1056 | } | ||
1057 | else | ||
1058 | { | ||
1059 | parameters = new string[0]; | ||
1060 | } | ||
1061 | |||
1062 | // Generate a debug list of the decoded parameters | ||
1063 | |||
1064 | if (Rest.DEBUG && prfxlen < path.Length-1) | ||
1065 | { | ||
1066 | Rest.Log.DebugFormat("{0} URI: Parameters: {1}", MsgId, path.Substring(prfxlen)); | ||
1067 | for (int i = 0; i < parameters.Length; i++) | ||
1068 | { | ||
1069 | Rest.Log.DebugFormat("{0} Parameter[{1}]: {2}", MsgId, i, parameters[i]); | ||
1070 | } | ||
1071 | } | ||
1072 | |||
1073 | return parameters.Length; | ||
1074 | |||
1075 | } | ||
1076 | |||
1077 | internal string[] PathNodes | ||
1078 | { | ||
1079 | get | ||
1080 | { | ||
1081 | if (pathNodes == null) | ||
1082 | { | ||
1083 | initUrl(); | ||
1084 | } | ||
1085 | return pathNodes; | ||
1086 | } | ||
1087 | } | ||
1088 | |||
1089 | internal string BuildUrl(int first, int last) | ||
1090 | { | ||
1091 | |||
1092 | if (pathNodes == null) | ||
1093 | { | ||
1094 | initUrl(); | ||
1095 | } | ||
1096 | |||
1097 | if (first < 0) | ||
1098 | { | ||
1099 | first = first + pathNodes.Length; | ||
1100 | } | ||
1101 | |||
1102 | if (last < 0) | ||
1103 | { | ||
1104 | last = last + pathNodes.Length; | ||
1105 | if (last < 0) | ||
1106 | { | ||
1107 | return Rest.UrlPathSeparator; | ||
1108 | } | ||
1109 | } | ||
1110 | |||
1111 | sbuilder.Length = 0; | ||
1112 | sbuilder.Append(Rest.UrlPathSeparator); | ||
1113 | |||
1114 | if (first <= last) | ||
1115 | { | ||
1116 | for (int i = first; i <= last; i++) | ||
1117 | { | ||
1118 | sbuilder.Append(pathNodes[i]); | ||
1119 | sbuilder.Append(Rest.UrlPathSeparator); | ||
1120 | } | ||
1121 | } | ||
1122 | else | ||
1123 | { | ||
1124 | for (int i = last; i >= first; i--) | ||
1125 | { | ||
1126 | sbuilder.Append(pathNodes[i]); | ||
1127 | sbuilder.Append(Rest.UrlPathSeparator); | ||
1128 | } | ||
1129 | } | ||
1130 | |||
1131 | return sbuilder.ToString(); | ||
1132 | |||
1133 | } | ||
1134 | |||
1135 | // Setup the XML writer for output | ||
1136 | |||
1137 | internal void initXmlWriter() | ||
1138 | { | ||
1139 | XmlWriterSettings settings = new XmlWriterSettings(); | ||
1140 | xmldata = new MemoryStream(); | ||
1141 | settings.Indent = true; | ||
1142 | settings.IndentChars = " "; | ||
1143 | settings.Encoding = encoding; | ||
1144 | settings.CloseOutput = false; | ||
1145 | settings.OmitXmlDeclaration = true; | ||
1146 | settings.ConformanceLevel = ConformanceLevel.Fragment; | ||
1147 | writer = XmlWriter.Create(xmldata, settings); | ||
1148 | } | ||
1149 | |||
1150 | internal void initXmlReader() | ||
1151 | { | ||
1152 | XmlReaderSettings settings = new XmlReaderSettings(); | ||
1153 | settings.ConformanceLevel = ConformanceLevel.Fragment; | ||
1154 | settings.IgnoreComments = true; | ||
1155 | settings.IgnoreWhitespace = true; | ||
1156 | settings.IgnoreProcessingInstructions = true; | ||
1157 | settings.ValidationType = ValidationType.None; | ||
1158 | // reader = XmlReader.Create(new StringReader(entity),settings); | ||
1159 | reader = XmlReader.Create(request.InputStream,settings); | ||
1160 | } | ||
1161 | |||
1162 | private void Flush() | ||
1163 | { | ||
1164 | byte[] dbuffer = new byte[8192]; | ||
1165 | while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0); | ||
1166 | return; | ||
1167 | } | ||
1168 | |||
1169 | // This allows us to make errors a bit more apparent in REST | ||
1170 | |||
1171 | internal void SendHtml(string text) | ||
1172 | { | ||
1173 | SendHtml("OpenSim REST Interface 1.0", text); | ||
1174 | } | ||
1175 | |||
1176 | internal void SendHtml(string title, string text) | ||
1177 | { | ||
1178 | |||
1179 | AddHeader(Rest.HttpHeaderContentType, "text/html"); | ||
1180 | sbuilder.Length = 0; | ||
1181 | |||
1182 | sbuilder.Append("<html>"); | ||
1183 | sbuilder.Append("<head>"); | ||
1184 | sbuilder.Append("<title>"); | ||
1185 | sbuilder.Append(title); | ||
1186 | sbuilder.Append("</title>"); | ||
1187 | sbuilder.Append("</head>"); | ||
1188 | |||
1189 | sbuilder.Append("<body>"); | ||
1190 | sbuilder.Append("<br />"); | ||
1191 | sbuilder.Append("<p>"); | ||
1192 | sbuilder.Append(text); | ||
1193 | sbuilder.Append("</p>"); | ||
1194 | sbuilder.Append("</body>"); | ||
1195 | sbuilder.Append("</html>"); | ||
1196 | |||
1197 | html = sbuilder.ToString(); | ||
1198 | |||
1199 | } | ||
1200 | } | ||
1201 | } | ||
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs new file mode 100644 index 0000000..e88c54d --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs | |||
@@ -0,0 +1,479 @@ | |||
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 OpenSim 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 | |||
29 | using System; | ||
30 | using System.Collections.Generic; | ||
31 | using System.Reflection; | ||
32 | using System.Text; | ||
33 | using OpenSim.Framework; | ||
34 | using OpenSim.Framework.Servers; | ||
35 | using OpenSim.Framework.Communications; | ||
36 | using OpenSim.Framework.Communications.Cache; | ||
37 | using Nini.Config; | ||
38 | |||
39 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
40 | { | ||
41 | |||
42 | public class Rest | ||
43 | { | ||
44 | |||
45 | internal static readonly log4net.ILog Log = | ||
46 | log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | ||
47 | |||
48 | internal static bool DEBUG = Log.IsDebugEnabled; | ||
49 | |||
50 | /// <summary> | ||
51 | /// These values have a single value for the whole | ||
52 | /// domain and lifetime of the plugin handler. We | ||
53 | /// make them static for ease of reference within | ||
54 | /// the assembly. These are initialized by the | ||
55 | /// RestHandler class during start-up. | ||
56 | /// </summary> | ||
57 | |||
58 | internal static RestHandler Plugin = null; | ||
59 | internal static OpenSimBase main = null; | ||
60 | internal static CommunicationsManager Comms = null; | ||
61 | internal static IInventoryServices InventoryServices = null; | ||
62 | internal static IUserService UserServices = null; | ||
63 | internal static AssetCache AssetServices = null; | ||
64 | internal static string Prefix = null; | ||
65 | internal static IConfig Config = null; | ||
66 | internal static string GodKey = null; | ||
67 | internal static bool Authenticate = true; | ||
68 | internal static bool Secure = true; | ||
69 | internal static bool ExtendedEscape = true; | ||
70 | internal static bool DumpAsset = false; | ||
71 | internal static string Realm = "REST"; | ||
72 | internal static Dictionary<string,string> Domains = new Dictionary<string,string>(); | ||
73 | internal static int CreationDate = (int) (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; | ||
74 | internal static int DumpLineSize = 32; // Should be a multiple of 16 or (possibly) 4 | ||
75 | |||
76 | internal static string MsgId | ||
77 | { | ||
78 | get { return Plugin.MsgId; } | ||
79 | } | ||
80 | |||
81 | internal static string RequestId | ||
82 | { | ||
83 | get { return Plugin.RequestId; } | ||
84 | } | ||
85 | |||
86 | internal static Encoding Encoding = Encoding.UTF8; | ||
87 | |||
88 | /// <summary> | ||
89 | /// Version control for REST implementation. This | ||
90 | /// refers to the overall infrastructure represented | ||
91 | /// by the following classes | ||
92 | /// RequestData | ||
93 | /// RequestInventoryPlugin | ||
94 | /// Rest | ||
95 | /// It does no describe implementation classes such as | ||
96 | /// RestInventoryServices, which may morph much more | ||
97 | /// often. Such classes ARE dependent upon this however | ||
98 | /// and should check it in their Initialize method. | ||
99 | /// </summary> | ||
100 | |||
101 | public static readonly float Version = 1.0F; | ||
102 | public const string Name = "REST 1.0"; | ||
103 | |||
104 | /// <summary> | ||
105 | /// Currently defined HTTP methods. | ||
106 | /// Only GET and HEAD are required to be | ||
107 | /// supported by all servers. See Respond | ||
108 | /// to see how these are handled. | ||
109 | /// </summary> | ||
110 | |||
111 | // REST AGENT 1.0 interpretations | ||
112 | public const string GET = "get"; // information retrieval - server state unchanged | ||
113 | public const string HEAD = "head"; // same as get except only the headers are returned. | ||
114 | public const string POST = "post"; // Replace the URI designated resource with the entity. | ||
115 | public const string PUT = "put"; // Add the entity to the context represented by the URI | ||
116 | public const string DELETE = "delete"; // Remove the URI designated resource from the server. | ||
117 | |||
118 | public const string OPTIONS = "options"; // | ||
119 | public const string TRACE = "trace"; // | ||
120 | public const string CONNECT = "connect"; // | ||
121 | |||
122 | // Define this in one place... | ||
123 | |||
124 | public const string UrlPathSeparator = "/"; | ||
125 | public const string UrlMethodSeparator = ":"; | ||
126 | |||
127 | // Redirection qualifications | ||
128 | |||
129 | public const bool PERMANENT = false; | ||
130 | public const bool TEMPORARY = true; | ||
131 | |||
132 | // Constant arrays used by String.Split | ||
133 | |||
134 | public static readonly char C_SPACE = ' '; | ||
135 | public static readonly char C_SLASH = '/'; | ||
136 | public static readonly char C_PATHSEP = '/'; | ||
137 | public static readonly char C_COLON = ':'; | ||
138 | public static readonly char C_PLUS = '+'; | ||
139 | public static readonly char C_PERIOD = '.'; | ||
140 | public static readonly char C_COMMA = ','; | ||
141 | public static readonly char C_DQUOTE = '"'; | ||
142 | |||
143 | public static readonly string CS_SPACE = " "; | ||
144 | public static readonly string CS_SLASH = "/"; | ||
145 | public static readonly string CS_PATHSEP = "/"; | ||
146 | public static readonly string CS_COLON = ":"; | ||
147 | public static readonly string CS_PLUS = "+"; | ||
148 | public static readonly string CS_PERIOD = "."; | ||
149 | public static readonly string CS_COMMA = ","; | ||
150 | public static readonly string CS_DQUOTE = "\""; | ||
151 | |||
152 | public static readonly char[] CA_SPACE = { C_SPACE }; | ||
153 | public static readonly char[] CA_SLASH = { C_SLASH }; | ||
154 | public static readonly char[] CA_PATHSEP = { C_PATHSEP }; | ||
155 | public static readonly char[] CA_COLON = { C_COLON }; | ||
156 | public static readonly char[] CA_PERIOD = { C_PERIOD }; | ||
157 | public static readonly char[] CA_PLUS = { C_PLUS }; | ||
158 | public static readonly char[] CA_COMMA = { C_COMMA }; | ||
159 | public static readonly char[] CA_DQUOTE = { C_DQUOTE }; | ||
160 | |||
161 | // HTTP Code Values (in value order) | ||
162 | |||
163 | public const int HttpStatusCodeContinue = 100; | ||
164 | public const int HttpStatusCodeSwitchingProtocols = 101; | ||
165 | |||
166 | public const int HttpStatusCodeOK = 200; | ||
167 | public const int HttpStatusCodeCreated = 201; | ||
168 | public const int HttpStatusCodeAccepted = 202; | ||
169 | public const int HttpStatusCodeNonAuthoritative = 203; | ||
170 | public const int HttpStatusCodeNoContent = 204; | ||
171 | public const int HttpStatusCodeResetContent = 205; | ||
172 | public const int HttpStatusCodePartialContent = 206; | ||
173 | |||
174 | public const int HttpStatusCodeMultipleChoices = 300; | ||
175 | public const int HttpStatusCodePermanentRedirect = 301; | ||
176 | public const int HttpStatusCodeFound = 302; | ||
177 | public const int HttpStatusCodeSeeOther = 303; | ||
178 | public const int HttpStatusCodeNotModified = 304; | ||
179 | public const int HttpStatusCodeUseProxy = 305; | ||
180 | public const int HttpStatusCodeReserved306 = 306; | ||
181 | public const int HttpStatusCodeTemporaryRedirect = 307; | ||
182 | |||
183 | public const int HttpStatusCodeBadRequest = 400; | ||
184 | public const int HttpStatusCodeNotAuthorized = 401; | ||
185 | public const int HttpStatusCodePaymentRequired = 402; | ||
186 | public const int HttpStatusCodeForbidden = 403; | ||
187 | public const int HttpStatusCodeNotFound = 404; | ||
188 | public const int HttpStatusCodeMethodNotAllowed = 405; | ||
189 | public const int HttpStatusCodeNotAcceptable = 406; | ||
190 | public const int HttpStatusCodeProxyAuthenticate = 407; | ||
191 | public const int HttpStatusCodeTimeOut = 408; | ||
192 | public const int HttpStatusCodeConflict = 409; | ||
193 | public const int HttpStatusCodeGone = 410; | ||
194 | public const int HttpStatusCodeLengthRequired = 411; | ||
195 | public const int HttpStatusCodePreconditionFailed = 412; | ||
196 | public const int HttpStatusCodeEntityTooLarge = 413; | ||
197 | public const int HttpStatusCodeUriTooLarge = 414; | ||
198 | public const int HttpStatusCodeUnsupportedMedia = 415; | ||
199 | public const int HttpStatusCodeRangeNotSatsified = 416; | ||
200 | public const int HttpStatusCodeExpectationFailed = 417; | ||
201 | |||
202 | public const int HttpStatusCodeServerError = 500; | ||
203 | public const int HttpStatusCodeNotImplemented = 501; | ||
204 | public const int HttpStatusCodeBadGateway = 502; | ||
205 | public const int HttpStatusCodeServiceUnavailable = 503; | ||
206 | public const int HttpStatusCodeGatewayTimeout = 504; | ||
207 | public const int HttpStatusCodeHttpVersionError = 505; | ||
208 | |||
209 | // HTTP Status Descriptions (in status code order) | ||
210 | |||
211 | public const string HttpStatusDescContinue = "Continue Request"; // 100 | ||
212 | public const string HttpStatusDescSwitchingProtocols = "Switching Protocols"; // 101 | ||
213 | |||
214 | public const string HttpStatusDescOK = "OK"; | ||
215 | public const string HttpStatusDescCreated = "CREATED"; | ||
216 | public const string HttpStatusDescAccepted = "ACCEPTED"; | ||
217 | public const string HttpStatusDescNonAuthoritative = "NON-AUTHORITATIVE INFORMATION"; | ||
218 | public const string HttpStatusDescNoContent = "NO CONTENT"; | ||
219 | public const string HttpStatusDescResetContent = "RESET CONTENT"; | ||
220 | public const string HttpStatusDescPartialContent = "PARTIAL CONTENT"; | ||
221 | |||
222 | public const string HttpStatusDescMultipleChoices = "MULTIPLE CHOICES"; | ||
223 | public const string HttpStatusDescPermanentRedirect = "PERMANENT REDIRECT"; | ||
224 | public const string HttpStatusDescFound = "FOUND"; | ||
225 | public const string HttpStatusDescSeeOther = "SEE OTHER"; | ||
226 | public const string HttpStatusDescNotModified = "NOT MODIFIED"; | ||
227 | public const string HttpStatusDescUseProxy = "USE PROXY"; | ||
228 | public const string HttpStatusDescReserved306 = "RESERVED CODE 306"; | ||
229 | public const string HttpStatusDescTemporaryRedirect = "TEMPORARY REDIRECT"; | ||
230 | |||
231 | public const string HttpStatusDescBadRequest = "BAD REQUEST"; | ||
232 | public const string HttpStatusDescNotAuthorized = "NOT AUTHORIZED"; | ||
233 | public const string HttpStatusDescPaymentRequired = "PAYMENT REQUIRED"; | ||
234 | public const string HttpStatusDescForbidden = "FORBIDDEN"; | ||
235 | public const string HttpStatusDescNotFound = "NOT FOUND"; | ||
236 | public const string HttpStatusDescMethodNotAllowed = "METHOD NOT ALLOWED"; | ||
237 | public const string HttpStatusDescNotAcceptable = "NOT ACCEPTABLE"; | ||
238 | public const string HttpStatusDescProxyAuthenticate = "PROXY AUTHENTICATION REQUIRED"; | ||
239 | public const string HttpStatusDescTimeOut = "TIMEOUT"; | ||
240 | public const string HttpStatusDescConflict = "CONFLICT"; | ||
241 | public const string HttpStatusDescGone = "GONE"; | ||
242 | public const string HttpStatusDescLengthRequired = "LENGTH REQUIRED"; | ||
243 | public const string HttpStatusDescPreconditionFailed = "PRECONDITION FAILED"; | ||
244 | public const string HttpStatusDescEntityTooLarge = "ENTITY TOO LARGE"; | ||
245 | public const string HttpStatusDescUriTooLarge = "URI TOO LARGE"; | ||
246 | public const string HttpStatusDescUnsupportedMedia = "UNSUPPORTED MEDIA"; | ||
247 | public const string HttpStatusDescRangeNotSatisfied = "RANGE NOT SATISFIED"; | ||
248 | public const string HttpStatusDescExpectationFailed = "EXPECTATION FAILED"; | ||
249 | |||
250 | public const string HttpStatusDescServerError = "SERVER ERROR"; | ||
251 | public const string HttpStatusDescNotImplemented = "NOT IMPLEMENTED"; | ||
252 | public const string HttpStatusDescBadGateway = "BAD GATEWAY"; | ||
253 | public const string HttpStatusDescServiceUnavailable = "SERVICE UNAVAILABLE"; | ||
254 | public const string HttpStatusDescGatewayTimeout = "GATEWAY TIMEOUT"; | ||
255 | public const string HttpStatusDescHttpVersionError = "HTTP VERSION NOT SUPPORTED"; | ||
256 | |||
257 | // HTTP Headers | ||
258 | |||
259 | public const string HttpHeaderAccept = "Accept"; | ||
260 | public const string HttpHeaderAcceptCharset = "Accept-Charset"; | ||
261 | public const string HttpHeaderAcceptEncoding = "Accept-Encoding"; | ||
262 | public const string HttpHeaderAcceptLanguage = "Accept-Language"; | ||
263 | public const string HttpHeaderAcceptRanges = "Accept-Ranges"; | ||
264 | public const string HttpHeaderAge = "Age"; | ||
265 | public const string HttpHeaderAllow = "Allow"; | ||
266 | public const string HttpHeaderAuthorization = "Authorization"; | ||
267 | public const string HttpHeaderCacheControl = "Cache-Control"; | ||
268 | public const string HttpHeaderConnection = "Connection"; | ||
269 | public const string HttpHeaderContentEncoding = "Content-Encoding"; | ||
270 | public const string HttpHeaderContentLanguage = "Content-Language"; | ||
271 | public const string HttpHeaderContentLength = "Content-Length"; | ||
272 | public const string HttpHeaderContentLocation = "Content-Location"; | ||
273 | public const string HttpHeaderContentMD5 = "Content-MD5"; | ||
274 | public const string HttpHeaderContentRange = "Content-Range"; | ||
275 | public const string HttpHeaderContentType = "Content-Type"; | ||
276 | public const string HttpHeaderDate = "Date"; | ||
277 | public const string HttpHeaderETag = "ETag"; | ||
278 | public const string HttpHeaderExpect = "Expect"; | ||
279 | public const string HttpHeaderExpires = "Expires"; | ||
280 | public const string HttpHeaderFrom = "From"; | ||
281 | public const string HttpHeaderHost = "Host"; | ||
282 | public const string HttpHeaderIfMatch = "If-Match"; | ||
283 | public const string HttpHeaderIfModifiedSince = "If-Modified-Since"; | ||
284 | public const string HttpHeaderIfNoneMatch = "If-None-Match"; | ||
285 | public const string HttpHeaderIfRange = "If-Range"; | ||
286 | public const string HttpHeaderIfUnmodifiedSince = "If-Unmodified-Since"; | ||
287 | public const string HttpHeaderLastModified = "Last-Modified"; | ||
288 | public const string HttpHeaderLocation = "Location"; | ||
289 | public const string HttpHeaderMaxForwards = "Max-Forwards"; | ||
290 | public const string HttpHeaderPragma = "Pragma"; | ||
291 | public const string HttpHeaderProxyAuthenticate = "Proxy-Authenticate"; | ||
292 | public const string HttpHeaderProxyAuthorization = "Proxy-Authorization"; | ||
293 | public const string HttpHeaderRange = "Range"; | ||
294 | public const string HttpHeaderReferer = "Referer"; | ||
295 | public const string HttpHeaderRetryAfter = "Retry-After"; | ||
296 | public const string HttpHeaderServer = "Server"; | ||
297 | public const string HttpHeaderTE = "TE"; | ||
298 | public const string HttpHeaderTrailer = "Trailer"; | ||
299 | public const string HttpHeaderTransferEncoding = "Transfer-Encoding"; | ||
300 | public const string HttpHeaderUpgrade = "Upgrade"; | ||
301 | public const string HttpHeaderUserAgent = "User-Agent"; | ||
302 | public const string HttpHeaderVary = "Vary"; | ||
303 | public const string HttpHeaderVia = "Via"; | ||
304 | public const string HttpHeaderWarning = "Warning"; | ||
305 | public const string HttpHeaderWWWAuthenticate = "WWW-Authenticate"; | ||
306 | |||
307 | /// <summary> | ||
308 | /// Supported authentication schemes | ||
309 | /// </summary> | ||
310 | |||
311 | public const string AS_BASIC = "Basic"; | ||
312 | public const string AS_DIGEST = "Digest"; | ||
313 | |||
314 | /// Supported Digest algorithms | ||
315 | |||
316 | public const string Digest_MD5 = "MD5"; // assumedd efault if omitted | ||
317 | public const string Digest_MD5Sess = "MD5-sess"; | ||
318 | |||
319 | public const string Qop_Auth = "auth"; | ||
320 | public const string Qop_Int = "auth-int"; | ||
321 | |||
322 | /// Utility routines | ||
323 | |||
324 | public static string StringToBase64(string str) | ||
325 | { | ||
326 | try | ||
327 | { | ||
328 | byte[] encData_byte = new byte[str.Length]; | ||
329 | encData_byte = Encoding.UTF8.GetBytes(str); | ||
330 | return Convert.ToBase64String(encData_byte); | ||
331 | } | ||
332 | catch | ||
333 | { | ||
334 | return String.Empty; | ||
335 | } | ||
336 | } | ||
337 | |||
338 | public static string Base64ToString(string str) | ||
339 | { | ||
340 | UTF8Encoding encoder = new UTF8Encoding(); | ||
341 | Decoder utf8Decode = encoder.GetDecoder(); | ||
342 | try | ||
343 | { | ||
344 | byte[] todecode_byte = Convert.FromBase64String(str); | ||
345 | int charCount = utf8Decode.GetCharCount(todecode_byte, 0, todecode_byte.Length); | ||
346 | char[] decoded_char = new char[charCount]; | ||
347 | utf8Decode.GetChars(todecode_byte, 0, todecode_byte.Length, decoded_char, 0); | ||
348 | return new String(decoded_char); | ||
349 | } | ||
350 | catch | ||
351 | { | ||
352 | return String.Empty; | ||
353 | } | ||
354 | } | ||
355 | |||
356 | private const string hvals = "0123456789abcdef"; | ||
357 | |||
358 | public static int Hex2Int(string hex) | ||
359 | { | ||
360 | int val = 0; | ||
361 | int sum = 0; | ||
362 | string tmp = null; | ||
363 | |||
364 | if (hex != null) | ||
365 | { | ||
366 | tmp = hex.ToLower(); | ||
367 | for (int i = 0; i < tmp.Length; i++) | ||
368 | { | ||
369 | val = hvals.IndexOf(tmp[i]); | ||
370 | if (val == -1) | ||
371 | break; | ||
372 | sum *= 16; | ||
373 | sum += val; | ||
374 | } | ||
375 | } | ||
376 | |||
377 | return sum; | ||
378 | |||
379 | } | ||
380 | |||
381 | public static string Int2Hex8(int val) | ||
382 | { | ||
383 | string res = String.Empty; | ||
384 | for (int i = 0; i < 8; i++) | ||
385 | { | ||
386 | res = (val % 16) + res; | ||
387 | val = val / 16; | ||
388 | } | ||
389 | return res; | ||
390 | } | ||
391 | |||
392 | public static string ToHex32(int val) | ||
393 | { | ||
394 | return String.Empty; | ||
395 | } | ||
396 | |||
397 | public static string ToHex32(string val) | ||
398 | { | ||
399 | return String.Empty; | ||
400 | } | ||
401 | |||
402 | // Nonce management | ||
403 | |||
404 | public static string NonceGenerator() | ||
405 | { | ||
406 | return StringToBase64(Guid.NewGuid().ToString()); | ||
407 | } | ||
408 | |||
409 | // Dump he specified data stream; | ||
410 | |||
411 | public static void Dump(byte[] data) | ||
412 | { | ||
413 | |||
414 | char[] buffer = new char[Rest.DumpLineSize]; | ||
415 | int cc = 0; | ||
416 | |||
417 | for (int i = 0; i < data.Length; i++) | ||
418 | { | ||
419 | |||
420 | if (i % Rest.DumpLineSize == 0) Console.Write("\n{0}: ",i.ToString("d8")); | ||
421 | |||
422 | if (i % 4 == 0) Console.Write(" "); | ||
423 | // if (i%16 == 0) Console.Write(" "); | ||
424 | |||
425 | Console.Write("{0}",data[i].ToString("x2")); | ||
426 | |||
427 | if (data[i] < 127 && data[i] > 31) | ||
428 | buffer[i % Rest.DumpLineSize] = (char) data[i]; | ||
429 | else | ||
430 | buffer[i % Rest.DumpLineSize] = '.'; | ||
431 | |||
432 | cc++; | ||
433 | |||
434 | if (i != 0 && (i + 1) % Rest.DumpLineSize == 0) | ||
435 | { | ||
436 | Console.Write(" |"+(new String(buffer))+"|"); | ||
437 | cc = 0; | ||
438 | } | ||
439 | |||
440 | } | ||
441 | |||
442 | // Finish off any incomplete line | ||
443 | |||
444 | if (cc != 0) | ||
445 | { | ||
446 | for (int i = cc ; i < Rest.DumpLineSize; i++) | ||
447 | { | ||
448 | if (i % 4 == 0) Console.Write(" "); | ||
449 | // if (i%16 == 0) Console.Write(" "); | ||
450 | Console.Write(" "); | ||
451 | buffer[i % Rest.DumpLineSize] = ' '; | ||
452 | } | ||
453 | Console.WriteLine(" |"+(new String(buffer))+"|"); | ||
454 | } | ||
455 | else | ||
456 | { | ||
457 | Console.Write("\n"); | ||
458 | } | ||
459 | |||
460 | } | ||
461 | |||
462 | } | ||
463 | |||
464 | // Local exception type | ||
465 | |||
466 | public class RestException : Exception | ||
467 | { | ||
468 | |||
469 | internal int statusCode; | ||
470 | internal string statusDesc; | ||
471 | internal string httpmethod; | ||
472 | internal string httppath; | ||
473 | |||
474 | public RestException(string msg) : base(msg) | ||
475 | { | ||
476 | } | ||
477 | } | ||
478 | |||
479 | } | ||
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs new file mode 100644 index 0000000..839e0f2 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs | |||
@@ -0,0 +1,257 @@ | |||
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 OpenSim 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 | |||
29 | using libsecondlife; | ||
30 | using Nini.Config; | ||
31 | using System; | ||
32 | using System.Collections.Generic; | ||
33 | using System.IO; | ||
34 | using System.Threading; | ||
35 | using System.Xml; | ||
36 | using OpenSim.Framework; | ||
37 | using OpenSim.Framework.Servers; | ||
38 | using OpenSim.Framework.Communications; | ||
39 | using OpenSim.Framework.Communications.Cache; | ||
40 | |||
41 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
42 | { | ||
43 | |||
44 | public class RestAssetServices : IRest | ||
45 | { | ||
46 | |||
47 | private string key = "assets"; | ||
48 | private bool enabled = false; | ||
49 | private string qPrefix = "assets"; | ||
50 | |||
51 | // A simple constructor is used to handle any once-only | ||
52 | // initialization of working classes. | ||
53 | |||
54 | public RestAssetServices(RestHandler p_rest) | ||
55 | { | ||
56 | |||
57 | Rest.Log.InfoFormat("{0} Asset services initializing", MsgId); | ||
58 | Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); | ||
59 | |||
60 | // Integrate domain | ||
61 | |||
62 | if (!qPrefix.StartsWith(Rest.UrlPathSeparator)) | ||
63 | { | ||
64 | qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix; | ||
65 | } | ||
66 | |||
67 | // Authentication domain | ||
68 | |||
69 | Rest.Domains.Add(key,Rest.Config.GetString("asset-domain",qPrefix)); | ||
70 | |||
71 | // Register interface | ||
72 | |||
73 | Rest.Plugin.AddPathHandler(DoAsset, qPrefix, Allocate); | ||
74 | |||
75 | // Activate | ||
76 | |||
77 | enabled = true; | ||
78 | |||
79 | Rest.Log.InfoFormat("{0} Asset services initialization complete", MsgId); | ||
80 | |||
81 | } | ||
82 | |||
83 | // Post-construction, pre-enabled initialization opportunity | ||
84 | // Not currently exploited. | ||
85 | |||
86 | public void Initialize() | ||
87 | { | ||
88 | } | ||
89 | |||
90 | // Called by the plug-in to halt REST processing. Local processing is | ||
91 | // disabled, and control blocks until all current processing has | ||
92 | // completed. No new processing will be started | ||
93 | |||
94 | public void Close() | ||
95 | { | ||
96 | enabled = false; | ||
97 | Rest.Log.InfoFormat("{0} Asset services closing down", MsgId); | ||
98 | } | ||
99 | |||
100 | // Properties | ||
101 | |||
102 | internal string MsgId | ||
103 | { | ||
104 | get { return Rest.MsgId; } | ||
105 | } | ||
106 | |||
107 | #region Interface | ||
108 | |||
109 | private RequestData Allocate(OSHttpRequest request, OSHttpResponse response) | ||
110 | { | ||
111 | return (RequestData) new AssetRequestData(request, response, qPrefix); | ||
112 | } | ||
113 | |||
114 | // Asset Handler | ||
115 | |||
116 | private void DoAsset(RequestData rparm) | ||
117 | { | ||
118 | |||
119 | if (!enabled) return; | ||
120 | |||
121 | AssetRequestData rdata = (AssetRequestData) rparm; | ||
122 | |||
123 | Rest.Log.DebugFormat("{0} REST Asset handler ENTRY", MsgId); | ||
124 | |||
125 | // Now that we know this is a serious attempt to | ||
126 | // access inventory data, we should find out who | ||
127 | // is asking, and make sure they are authorized | ||
128 | // to do so. We need to validate the caller's | ||
129 | // identity before revealing anything about the | ||
130 | // status quo. Authenticate throws an exception | ||
131 | // via Fail if no identity information is present. | ||
132 | // | ||
133 | // With the present HTTP server we can't use the | ||
134 | // builtin authentication mechanisms because they | ||
135 | // would be enforced for all in-bound requests. | ||
136 | // Instead we look at the headers ourselves and | ||
137 | // handle authentication directly. | ||
138 | |||
139 | try | ||
140 | { | ||
141 | if (!rdata.IsAuthenticated) | ||
142 | { | ||
143 | rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); | ||
144 | } | ||
145 | } | ||
146 | catch (RestException e) | ||
147 | { | ||
148 | if (e.statusCode == Rest.HttpStatusCodeNotAuthorized) | ||
149 | { | ||
150 | Rest.Log.WarnFormat("{0} User not authenticated", MsgId); | ||
151 | Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, | ||
152 | rdata.request.Headers.Get("Authorization")); | ||
153 | } | ||
154 | else | ||
155 | { | ||
156 | Rest.Log.ErrorFormat("{0} User authentication failed", MsgId); | ||
157 | Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, | ||
158 | rdata.request.Headers.Get("Authorization")); | ||
159 | } | ||
160 | throw (e); | ||
161 | } | ||
162 | |||
163 | // Remove the prefix and what's left are the parameters. If we don't have | ||
164 | // the parameters we need, fail the request. Parameters do NOT include | ||
165 | // any supplied query values. | ||
166 | |||
167 | if (rdata.parameters.Length > 0) | ||
168 | { | ||
169 | switch (rdata.method) | ||
170 | { | ||
171 | case "get" : | ||
172 | DoGet(rdata); | ||
173 | break; | ||
174 | case "put" : | ||
175 | case "post" : | ||
176 | case "delete" : | ||
177 | default : | ||
178 | Rest.Log.WarnFormat("{0} Asset: Method not supported: {1}", | ||
179 | MsgId, rdata.method); | ||
180 | rdata.Fail(Rest.HttpStatusCodeBadRequest, | ||
181 | Rest.HttpStatusDescBadRequest); | ||
182 | break; | ||
183 | } | ||
184 | } | ||
185 | else | ||
186 | { | ||
187 | Rest.Log.WarnFormat("{0} Asset: No agent information provided", MsgId); | ||
188 | rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); | ||
189 | } | ||
190 | |||
191 | Rest.Log.DebugFormat("{0} REST Asset handler EXIT", MsgId); | ||
192 | |||
193 | } | ||
194 | |||
195 | #endregion Interface | ||
196 | |||
197 | private void DoGet(AssetRequestData rdata) | ||
198 | { | ||
199 | |||
200 | bool istexture = false; | ||
201 | |||
202 | Rest.Log.DebugFormat("{0} REST Asset handler, Method = <{1}> ENTRY", MsgId, rdata.method); | ||
203 | |||
204 | // The only parameter we accept is an LLUUID for | ||
205 | // the asset | ||
206 | |||
207 | if (rdata.parameters.Length == 1) | ||
208 | { | ||
209 | |||
210 | LLUUID uuid = new LLUUID(rdata.parameters[0]); | ||
211 | AssetBase asset = Rest.AssetServices.GetAsset(uuid, istexture); | ||
212 | |||
213 | if (asset != null) | ||
214 | { | ||
215 | |||
216 | Rest.Log.DebugFormat("{0} Asset located <{1}>", MsgId, rdata.parameters[0]); | ||
217 | |||
218 | rdata.initXmlWriter(); | ||
219 | |||
220 | rdata.writer.WriteStartElement(String.Empty,"Asset",String.Empty); | ||
221 | |||
222 | rdata.writer.WriteAttributeString("id", asset.ID.ToString()); | ||
223 | rdata.writer.WriteAttributeString("name", asset.Name); | ||
224 | rdata.writer.WriteAttributeString("desc", asset.Description); | ||
225 | rdata.writer.WriteAttributeString("type", asset.Type.ToString()); | ||
226 | rdata.writer.WriteAttributeString("invtype", asset.InvType.ToString()); | ||
227 | rdata.writer.WriteAttributeString("local", asset.Local.ToString()); | ||
228 | rdata.writer.WriteAttributeString("temporary", asset.Temporary.ToString()); | ||
229 | |||
230 | rdata.writer.WriteBase64(asset.Data,0,asset.Data.Length); | ||
231 | |||
232 | rdata.writer.WriteFullEndElement(); | ||
233 | |||
234 | } | ||
235 | else | ||
236 | { | ||
237 | Rest.Log.DebugFormat("{0} Invalid parameters: <{1}>", MsgId, rdata.path); | ||
238 | rdata.Fail(Rest.HttpStatusCodeNotFound, | ||
239 | Rest.HttpStatusDescNotFound); | ||
240 | } | ||
241 | } | ||
242 | |||
243 | rdata.Complete(); | ||
244 | rdata.Respond("Asset " + rdata.method + ": Normal completion"); | ||
245 | |||
246 | } | ||
247 | |||
248 | internal class AssetRequestData : RequestData | ||
249 | { | ||
250 | internal AssetRequestData(OSHttpRequest request, OSHttpResponse response, string prefix) | ||
251 | : base(request, response, prefix) | ||
252 | { | ||
253 | } | ||
254 | } | ||
255 | |||
256 | } | ||
257 | } | ||
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs new file mode 100644 index 0000000..0a0bf3f --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs | |||
@@ -0,0 +1,547 @@ | |||
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 OpenSim 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 | |||
29 | using System; | ||
30 | using System.Collections.Generic; | ||
31 | using System.Reflection; | ||
32 | using OpenSim.Framework; | ||
33 | using OpenSim.Framework.Servers; | ||
34 | using OpenSim.ApplicationPlugins.Rest; | ||
35 | using Mono.Addins; | ||
36 | |||
37 | [assembly : Addin] | ||
38 | [assembly : AddinDependency("OpenSim", "0.5")] | ||
39 | |||
40 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
41 | { | ||
42 | |||
43 | [Extension("/OpenSim/Startup")] | ||
44 | |||
45 | public class RestHandler : RestPlugin, IHttpAgentHandler | ||
46 | { | ||
47 | |||
48 | #region local static state | ||
49 | |||
50 | /// <summary> | ||
51 | /// This static initializer scans the assembly for classes that | ||
52 | /// export the IRest interface and builds a list of them. These | ||
53 | /// are later activated by the handler. To add a new handler it | ||
54 | /// is only necessary to create a new services class that implements | ||
55 | /// the IRest interface, and recompile the handler. This gives | ||
56 | /// all of the build-time flexibility of a modular approach | ||
57 | /// while not introducing yet-another module loader. Note that | ||
58 | /// multiple assembles can still be built, each with its own set | ||
59 | /// of handlers. | ||
60 | /// </summary> | ||
61 | |||
62 | private static bool handlersLoaded = false; | ||
63 | private static List<Type> classes = new List<Type>(); | ||
64 | private static List<IRest> handlers = new List<IRest>(); | ||
65 | private static Type[] parms = new Type[1]; | ||
66 | private static Object[] args = new Object[1]; | ||
67 | |||
68 | static RestHandler() | ||
69 | { | ||
70 | Module[] mods = Assembly.GetExecutingAssembly().GetModules(); | ||
71 | foreach (Module m in mods) | ||
72 | { | ||
73 | Type[] types = m.GetTypes(); | ||
74 | foreach (Type t in types) | ||
75 | { | ||
76 | if (t.GetInterface("IRest") != null) | ||
77 | { | ||
78 | classes.Add(t); | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | #endregion local static state | ||
85 | |||
86 | #region local instance state | ||
87 | |||
88 | /// <remarks> | ||
89 | /// The handler delegate is not noteworthy. The allocator allows | ||
90 | /// a given handler to optionally subclass the base RequestData | ||
91 | /// structure to carry any locally required per-request state | ||
92 | /// needed. | ||
93 | /// </remarks> | ||
94 | internal delegate void RestMethodHandler(RequestData rdata); | ||
95 | internal delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response); | ||
96 | |||
97 | // Handler tables: both stream and REST are supported | ||
98 | |||
99 | internal Dictionary<string,RestMethodHandler> pathHandlers = new Dictionary<string,RestMethodHandler>(); | ||
100 | internal Dictionary<string,RestMethodAllocator> pathAllocators = new Dictionary<string,RestMethodAllocator>(); | ||
101 | internal Dictionary<string,RestStreamHandler> streamHandlers = new Dictionary<string,RestStreamHandler>(); | ||
102 | |||
103 | /// <summary> | ||
104 | /// This routine loads all of the handlers discovered during | ||
105 | /// instance initialization. Each handler is responsible for | ||
106 | /// registering itself with this handler. | ||
107 | /// I was not able to make this code work in a constructor. | ||
108 | /// </summary> | ||
109 | private void LoadHandlers() | ||
110 | { | ||
111 | lock(handlers) | ||
112 | { | ||
113 | if (!handlersLoaded) | ||
114 | { | ||
115 | parms[0] = this.GetType(); | ||
116 | args[0] = this; | ||
117 | |||
118 | ConstructorInfo ci; | ||
119 | Object ht; | ||
120 | |||
121 | foreach (Type t in classes) | ||
122 | { | ||
123 | ci = t.GetConstructor(parms); | ||
124 | ht = ci.Invoke(args); | ||
125 | handlers.Add((IRest)ht); | ||
126 | } | ||
127 | handlersLoaded = true; | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | |||
132 | #endregion local instance state | ||
133 | |||
134 | #region overriding properties | ||
135 | |||
136 | // Used to differentiate the message header. | ||
137 | |||
138 | public override string Name | ||
139 | { | ||
140 | get { return "HANDLER"; } | ||
141 | } | ||
142 | |||
143 | // Used to partition the configuration space. | ||
144 | |||
145 | public override string ConfigName | ||
146 | { | ||
147 | get { return "RestHandler"; } | ||
148 | } | ||
149 | |||
150 | // We have to rename these because we want | ||
151 | // to be able to share the values with other | ||
152 | // classes in our assembly and the base | ||
153 | // names are protected. | ||
154 | |||
155 | internal string MsgId | ||
156 | { | ||
157 | get { return base.MsgID; } | ||
158 | } | ||
159 | |||
160 | internal string RequestId | ||
161 | { | ||
162 | get { return base.RequestID; } | ||
163 | } | ||
164 | |||
165 | #endregion overriding properties | ||
166 | |||
167 | #region overriding methods | ||
168 | |||
169 | /// <summary> | ||
170 | /// This method is called by OpenSimMain immediately after loading the | ||
171 | /// plugin and after basic server setup, but before running any server commands. | ||
172 | /// </summary> | ||
173 | /// <remarks> | ||
174 | /// Note that entries MUST be added to the active configuration files before | ||
175 | /// the plugin can be enabled. | ||
176 | /// </remarks> | ||
177 | public override void Initialise(OpenSimBase openSim) | ||
178 | { | ||
179 | try | ||
180 | { | ||
181 | |||
182 | /// <remarks> | ||
183 | /// This plugin will only be enabled if the broader | ||
184 | /// REST plugin mechanism is enabled. | ||
185 | /// </remarks> | ||
186 | |||
187 | Rest.Log.InfoFormat("{0} Plugin is initializing", MsgID); | ||
188 | |||
189 | base.Initialise(openSim); | ||
190 | |||
191 | if (!IsEnabled) | ||
192 | { | ||
193 | Rest.Log.WarnFormat("{0} Plugins are disabled", MsgID); | ||
194 | return; | ||
195 | } | ||
196 | |||
197 | Rest.Log.InfoFormat("{0} Plugin will be enabled", MsgID); | ||
198 | |||
199 | /// <remarks> | ||
200 | /// These are stored in static variables to make | ||
201 | /// them easy to reach from anywhere in the assembly. | ||
202 | /// </remarks> | ||
203 | |||
204 | Rest.main = openSim; | ||
205 | Rest.Plugin = this; | ||
206 | Rest.Comms = App.CommunicationsManager; | ||
207 | Rest.UserServices = Rest.Comms.UserService; | ||
208 | Rest.InventoryServices = Rest.Comms.InventoryService; | ||
209 | Rest.AssetServices = Rest.Comms.AssetCache; | ||
210 | Rest.Config = Config; | ||
211 | Rest.Prefix = Prefix; | ||
212 | Rest.GodKey = GodKey; | ||
213 | |||
214 | Rest.Authenticate = Rest.Config.GetBoolean("authenticate",true); | ||
215 | Rest.Secure = Rest.Config.GetBoolean("secured",true); | ||
216 | Rest.ExtendedEscape = Rest.Config.GetBoolean("extended-escape",true); | ||
217 | Rest.Realm = Rest.Config.GetString("realm","OpenSim REST"); | ||
218 | Rest.DumpAsset = Rest.Config.GetBoolean("dump-asset",false); | ||
219 | Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size",32); | ||
220 | |||
221 | Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId, | ||
222 | (Rest.Authenticate ? "" : "not ")); | ||
223 | |||
224 | Rest.Log.InfoFormat("{0} Security is {1}enabled", MsgId, | ||
225 | (Rest.Authenticate ? "" : "not ")); | ||
226 | |||
227 | Rest.Log.InfoFormat("{0} Extended URI escape processing is {1}enabled", MsgId, | ||
228 | (Rest.ExtendedEscape ? "" : "not ")); | ||
229 | |||
230 | Rest.Log.InfoFormat("{0} Dumping of asset data is {1}enabled", MsgId, | ||
231 | (Rest.DumpAsset ? "" : "not ")); | ||
232 | |||
233 | if (Rest.DumpAsset) | ||
234 | { | ||
235 | Rest.Log.InfoFormat("{0} Dump {1} bytes per line", MsgId, | ||
236 | Rest.DumpLineSize); | ||
237 | } | ||
238 | |||
239 | // Load all of the handlers present in the | ||
240 | // assembly | ||
241 | |||
242 | // In principle, as we're an application plug-in, | ||
243 | // most of what needs to be done could be done using | ||
244 | // static resources, however the Open Sim plug-in | ||
245 | // model makes this an instance, so that's what we | ||
246 | // need to be. | ||
247 | // There is only one Communications manager per | ||
248 | // server, and by inference, only one each of the | ||
249 | // user, asset, and inventory servers. So we can cache | ||
250 | // those using a static initializer. | ||
251 | // We move all of this processing off to another | ||
252 | // services class to minimize overlap between function | ||
253 | // and infrastructure. | ||
254 | |||
255 | LoadHandlers(); | ||
256 | |||
257 | /// <remarks> | ||
258 | /// The intention of a post construction initializer | ||
259 | /// is to allow for setup that is dependent upon other | ||
260 | /// activities outside of the agency. We don't currently | ||
261 | /// have any, but the design allows for it. | ||
262 | /// </remarks> | ||
263 | |||
264 | foreach (IRest handler in handlers) | ||
265 | { | ||
266 | handler.Initialize(); | ||
267 | } | ||
268 | |||
269 | /// <remarks> | ||
270 | /// Now that everything is setup we can proceed and | ||
271 | /// add this agent to the HTTP server's handler list | ||
272 | /// </remarks> | ||
273 | |||
274 | if (!AddAgentHandler(Rest.Name,this)) | ||
275 | { | ||
276 | Rest.Log.ErrorFormat("{0} Unable to activate handler interface", MsgId); | ||
277 | foreach (IRest handler in handlers) | ||
278 | { | ||
279 | handler.Close(); | ||
280 | } | ||
281 | } | ||
282 | |||
283 | } | ||
284 | catch (Exception e) | ||
285 | { | ||
286 | Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgID, e.Message); | ||
287 | } | ||
288 | |||
289 | } | ||
290 | |||
291 | /// <summary> | ||
292 | /// In the interests of efficiency, and because we cannot determine whether | ||
293 | /// or not this instance will actually be harvested, we clobber the only | ||
294 | /// anchoring reference to the working state for this plug-in. What the | ||
295 | /// call to close does is irrelevant to this class beyond knowing that it | ||
296 | /// can nullify the reference when it returns. | ||
297 | /// To make sure everything is copacetic we make sure the primary interface | ||
298 | /// is disabled by deleting the handler from the HTTP server tables. | ||
299 | /// </summary> | ||
300 | public override void Close() | ||
301 | { | ||
302 | |||
303 | Rest.Log.InfoFormat("{0} Plugin is terminating", MsgID); | ||
304 | |||
305 | try | ||
306 | { | ||
307 | RemoveAgentHandler(Rest.Name, this); | ||
308 | } | ||
309 | catch (KeyNotFoundException){} | ||
310 | |||
311 | foreach (IRest handler in handlers) | ||
312 | { | ||
313 | handler.Close(); | ||
314 | } | ||
315 | |||
316 | } | ||
317 | |||
318 | #endregion overriding methods | ||
319 | |||
320 | #region interface methods | ||
321 | |||
322 | /// <summary> | ||
323 | /// This method is called by the server to match the client, it could | ||
324 | /// just return true if we only want one such handler. For now we | ||
325 | /// match any explicitly specified client. | ||
326 | /// </summary> | ||
327 | public bool Match(OSHttpRequest request, OSHttpResponse response) | ||
328 | { | ||
329 | string path = request.RawUrl; | ||
330 | foreach (string key in pathHandlers.Keys) | ||
331 | { | ||
332 | if (path.StartsWith(key)) | ||
333 | { | ||
334 | return ( path.Length == key.Length || | ||
335 | path.Substring(key.Length,1) == Rest.UrlPathSeparator); | ||
336 | } | ||
337 | } | ||
338 | |||
339 | path = String.Format("{0}{1}{2}", request.HttpMethod, Rest.UrlMethodSeparator, path); | ||
340 | foreach (string key in streamHandlers.Keys) | ||
341 | { | ||
342 | if (path.StartsWith(key)) | ||
343 | { | ||
344 | return true; | ||
345 | } | ||
346 | } | ||
347 | |||
348 | return false; | ||
349 | } | ||
350 | |||
351 | /// <summary> | ||
352 | /// Preconditions: | ||
353 | /// [1] request != null and is a valid request object | ||
354 | /// [2] response != null and is a valid response object | ||
355 | /// Behavior is undefined if preconditions are not satisfied. | ||
356 | /// </summary> | ||
357 | public bool Handle(OSHttpRequest request, OSHttpResponse response) | ||
358 | { | ||
359 | bool handled; | ||
360 | base.MsgID = base.RequestID; | ||
361 | |||
362 | if (Rest.DEBUG) | ||
363 | { | ||
364 | Rest.Log.DebugFormat("{0} ENTRY", MsgId); | ||
365 | Rest.Log.DebugFormat("{0} Agent: {1}", MsgId, request.UserAgent); | ||
366 | Rest.Log.DebugFormat("{0} Method: {1}", MsgId, request.HttpMethod); | ||
367 | |||
368 | for (int i = 0; i < request.Headers.Count; i++) | ||
369 | { | ||
370 | Rest.Log.DebugFormat("{0} Header [{1}] : <{2}> = <{3}>", | ||
371 | MsgId, i, request.Headers.GetKey(i), request.Headers.Get(i)); | ||
372 | } | ||
373 | Rest.Log.DebugFormat("{0} URI: {1}", MsgId, request.RawUrl); | ||
374 | } | ||
375 | |||
376 | // If a path handler worked we're done, otherwise try any | ||
377 | // available stream handlers too. | ||
378 | |||
379 | try | ||
380 | { | ||
381 | handled = FindPathHandler(request, response) || | ||
382 | FindStreamHandler(request, response); | ||
383 | } | ||
384 | catch (Exception e) | ||
385 | { | ||
386 | // A raw exception indicates that something we weren't expecting has | ||
387 | // happened. This should always reflect a shortcoming in the plugin, | ||
388 | // or a failure to satisfy the preconditions. | ||
389 | Rest.Log.ErrorFormat("{0} Plugin error: {1}", MsgId, e.Message); | ||
390 | handled = true; | ||
391 | } | ||
392 | |||
393 | Rest.Log.DebugFormat("{0} EXIT", MsgId); | ||
394 | |||
395 | return handled; | ||
396 | |||
397 | } | ||
398 | |||
399 | #endregion interface methods | ||
400 | |||
401 | /// <summary> | ||
402 | /// If there is a stream handler registered that can handle the | ||
403 | /// request, then fine. If the request is not matched, do | ||
404 | /// nothing. | ||
405 | /// </summary> | ||
406 | |||
407 | private bool FindStreamHandler(OSHttpRequest request, OSHttpResponse response) | ||
408 | { | ||
409 | RequestData rdata = new RequestData(request, response, String.Empty); | ||
410 | |||
411 | string bestMatch = null; | ||
412 | string path = String.Format("{0}:{1}", rdata.method, rdata.path); | ||
413 | |||
414 | Rest.Log.DebugFormat("{0} Checking for stream handler for <{1}>", MsgId, path); | ||
415 | |||
416 | foreach (string pattern in streamHandlers.Keys) | ||
417 | { | ||
418 | if (path.StartsWith(pattern)) | ||
419 | { | ||
420 | if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length) | ||
421 | { | ||
422 | bestMatch = pattern; | ||
423 | } | ||
424 | } | ||
425 | } | ||
426 | |||
427 | // Handle using the best match available | ||
428 | |||
429 | if (!String.IsNullOrEmpty(bestMatch)) | ||
430 | { | ||
431 | Rest.Log.DebugFormat("{0} Stream-based handler matched with <{1}>", MsgId, bestMatch); | ||
432 | RestStreamHandler handler = streamHandlers[bestMatch]; | ||
433 | rdata.buffer = handler.Handle(rdata.path, rdata.request.InputStream, rdata.request, rdata.response); | ||
434 | rdata.AddHeader(rdata.response.ContentType,handler.ContentType); | ||
435 | rdata.Respond("FindStreamHandler Completion"); | ||
436 | } | ||
437 | |||
438 | return rdata.handled; | ||
439 | |||
440 | } | ||
441 | |||
442 | // Preserves the original handler's semantics | ||
443 | |||
444 | public new void AddStreamHandler(string httpMethod, string path, RestMethod method) | ||
445 | { | ||
446 | |||
447 | if (!IsEnabled) | ||
448 | { | ||
449 | return; | ||
450 | } | ||
451 | |||
452 | if (!path.StartsWith(Rest.Prefix)) | ||
453 | { | ||
454 | path = String.Format("{0}{1}", Rest.Prefix, path); | ||
455 | } | ||
456 | |||
457 | path = String.Format("{0}{1}{2}", httpMethod, Rest.UrlMethodSeparator, path); | ||
458 | |||
459 | // Conditionally add to the list | ||
460 | |||
461 | if (!streamHandlers.ContainsKey(path)) | ||
462 | { | ||
463 | streamHandlers.Add(path, new RestStreamHandler(httpMethod, path, method)); | ||
464 | Rest.Log.DebugFormat("{0} Added handler for {1}", MsgID, path); | ||
465 | } | ||
466 | else | ||
467 | { | ||
468 | Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgID, path); | ||
469 | } | ||
470 | |||
471 | } | ||
472 | |||
473 | |||
474 | internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response) | ||
475 | { | ||
476 | |||
477 | RequestData rdata = null; | ||
478 | string bestMatch = null; | ||
479 | |||
480 | if (!IsEnabled) | ||
481 | { | ||
482 | return false; | ||
483 | } | ||
484 | |||
485 | // Conditionally add to the list | ||
486 | |||
487 | Rest.Log.DebugFormat("{0} Checking for path handler for <{1}>", MsgId, request.RawUrl); | ||
488 | |||
489 | foreach (string pattern in pathHandlers.Keys) | ||
490 | { | ||
491 | if (request.RawUrl.StartsWith(pattern)) | ||
492 | { | ||
493 | if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length) | ||
494 | { | ||
495 | bestMatch = pattern; | ||
496 | } | ||
497 | } | ||
498 | } | ||
499 | |||
500 | if (!String.IsNullOrEmpty(bestMatch)) | ||
501 | { | ||
502 | |||
503 | rdata = pathAllocators[bestMatch](request, response); | ||
504 | |||
505 | Rest.Log.DebugFormat("{0} Path based REST handler matched with <{1}>", MsgId, bestMatch); | ||
506 | |||
507 | try | ||
508 | { | ||
509 | pathHandlers[bestMatch](rdata); | ||
510 | } | ||
511 | |||
512 | // A plugin generated error indicates a request-related error | ||
513 | // that has been handled by the plugin. | ||
514 | |||
515 | catch (RestException r) | ||
516 | { | ||
517 | Rest.Log.WarnFormat("{0} Request failed: {1}", MsgId, r.Message); | ||
518 | } | ||
519 | |||
520 | } | ||
521 | |||
522 | return (rdata == null) ? false : rdata.handled; | ||
523 | |||
524 | } | ||
525 | |||
526 | internal void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra) | ||
527 | { | ||
528 | if (pathHandlers.ContainsKey(path)) | ||
529 | { | ||
530 | Rest.Log.DebugFormat("{0} Replacing handler for <${1}>", MsgId, path); | ||
531 | pathHandlers.Remove(path); | ||
532 | } | ||
533 | |||
534 | if (pathAllocators.ContainsKey(path)) | ||
535 | { | ||
536 | Rest.Log.DebugFormat("{0} Replacing allocator for <${1}>", MsgId, path); | ||
537 | pathAllocators.Remove(path); | ||
538 | } | ||
539 | |||
540 | Rest.Log.DebugFormat("{0} Adding path handler for {1}", MsgId, path); | ||
541 | |||
542 | pathHandlers.Add(path, mh); | ||
543 | pathAllocators.Add(path, ra); | ||
544 | |||
545 | } | ||
546 | } | ||
547 | } | ||
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs new file mode 100644 index 0000000..0fc10f9 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs | |||
@@ -0,0 +1,1993 @@ | |||
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 OpenSim 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 | |||
29 | using System; | ||
30 | using System.Collections.Generic; | ||
31 | using System.IO; | ||
32 | using System.Threading; | ||
33 | using System.Xml; | ||
34 | using OpenSim.Framework; | ||
35 | using OpenSim.Framework.Servers; | ||
36 | using OpenSim.Framework.Communications; | ||
37 | using OpenSim.Framework.Communications.Cache; | ||
38 | using libsecondlife; | ||
39 | using Nini.Config; | ||
40 | |||
41 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
42 | { | ||
43 | |||
44 | public class RestInventoryServices : IRest | ||
45 | { | ||
46 | |||
47 | private string key = "inventory"; | ||
48 | private bool enabled = false; | ||
49 | private string qPrefix = "inventory"; | ||
50 | |||
51 | // A simple constructor is used to handle any once-only | ||
52 | // initialization of working classes. | ||
53 | |||
54 | public RestInventoryServices(RestHandler p_rest) | ||
55 | { | ||
56 | |||
57 | Rest.Log.InfoFormat("{0} Inventory services initializing", MsgId); | ||
58 | Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); | ||
59 | |||
60 | // Update to reflect the full prefix if not absolute | ||
61 | |||
62 | if (!qPrefix.StartsWith(Rest.UrlPathSeparator)) | ||
63 | { | ||
64 | qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix; | ||
65 | } | ||
66 | |||
67 | // Authentication domain | ||
68 | |||
69 | Rest.Domains.Add(key, Rest.Config.GetString("inventory-domain",qPrefix)); | ||
70 | |||
71 | // Register interface | ||
72 | |||
73 | Rest.Plugin.AddPathHandler(DoInventory,qPrefix,Allocate); | ||
74 | |||
75 | // Activate | ||
76 | |||
77 | enabled = true; | ||
78 | |||
79 | Rest.Log.InfoFormat("{0} Inventory services initialization complete", MsgId); | ||
80 | |||
81 | } | ||
82 | |||
83 | // Post-construction, pre-enabled initialization opportunity | ||
84 | // Not currently exploited. | ||
85 | |||
86 | public void Initialize() | ||
87 | { | ||
88 | } | ||
89 | |||
90 | // Called by the plug-in to halt REST processing. Local processing is | ||
91 | // disabled, and control blocks until all current processing has | ||
92 | // completed. No new processing will be started | ||
93 | |||
94 | public void Close() | ||
95 | { | ||
96 | enabled = false; | ||
97 | Rest.Log.InfoFormat("{0} Inventory services closing down", MsgId); | ||
98 | } | ||
99 | |||
100 | // Convenient properties | ||
101 | |||
102 | internal string MsgId | ||
103 | { | ||
104 | get { return Rest.MsgId; } | ||
105 | } | ||
106 | |||
107 | #region Interface | ||
108 | |||
109 | private RequestData Allocate(OSHttpRequest request, OSHttpResponse response) | ||
110 | { | ||
111 | return (RequestData) new InventoryRequestData(request, response, qPrefix); | ||
112 | } | ||
113 | |||
114 | /// <summary> | ||
115 | /// This method is registered with the handler when this class is | ||
116 | /// initialized. It is called whenever the URI includes this handler's | ||
117 | /// prefix string. | ||
118 | /// It handles all aspects of inventory REST processing. | ||
119 | /// </summary> | ||
120 | |||
121 | private void DoInventory(RequestData hdata) | ||
122 | { | ||
123 | |||
124 | InventoryRequestData rdata = (InventoryRequestData) hdata; | ||
125 | |||
126 | Rest.Log.DebugFormat("{0} DoInventory ENTRY", MsgId); | ||
127 | |||
128 | // We're disabled | ||
129 | if (!enabled) | ||
130 | { | ||
131 | return; | ||
132 | } | ||
133 | |||
134 | // Now that we know this is a serious attempt to | ||
135 | // access inventory data, we should find out who | ||
136 | // is asking, and make sure they are authorized | ||
137 | // to do so. We need to validate the caller's | ||
138 | // identity before revealing anything about the | ||
139 | // status quo. Authenticate throws an exception | ||
140 | // via Fail if no identity information is present. | ||
141 | // | ||
142 | // With the present HTTP server we can't use the | ||
143 | // builtin authentication mechanisms because they | ||
144 | // would be enforced for all in-bound requests. | ||
145 | // Instead we look at the headers ourselves and | ||
146 | // handle authentication directly. | ||
147 | |||
148 | try | ||
149 | { | ||
150 | if (!rdata.IsAuthenticated) | ||
151 | { | ||
152 | rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); | ||
153 | } | ||
154 | } | ||
155 | catch (RestException e) | ||
156 | { | ||
157 | if (e.statusCode == Rest.HttpStatusCodeNotAuthorized) | ||
158 | { | ||
159 | Rest.Log.WarnFormat("{0} User not authenticated", MsgId); | ||
160 | Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); | ||
161 | } | ||
162 | else | ||
163 | { | ||
164 | Rest.Log.ErrorFormat("{0} User authentication failed", MsgId); | ||
165 | Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); | ||
166 | } | ||
167 | throw (e); | ||
168 | } | ||
169 | |||
170 | Rest.Log.DebugFormat("{0} Authenticated {1}", MsgId, rdata.userName); | ||
171 | |||
172 | // We can only get here if we're authorized | ||
173 | // | ||
174 | // The requestor may have specified an LLUUID or | ||
175 | // a conjoined FirstNameLastName string. We'll | ||
176 | // try both. If we fail with the first, UUID, | ||
177 | // attempt, then we need two nodes to construct | ||
178 | // a valid avatar name. | ||
179 | |||
180 | // Do we have at least a user agent name? | ||
181 | |||
182 | if (rdata.parameters.Length < 1) | ||
183 | { | ||
184 | Rest.Log.WarnFormat("{0} Inventory: No user agent identifier specified", MsgId); | ||
185 | rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); | ||
186 | } | ||
187 | |||
188 | // The next parameter MUST be the agent identification, either an LLUUID | ||
189 | // or a space-separated First-name Last-Name specification. | ||
190 | |||
191 | try | ||
192 | { | ||
193 | rdata.uuid = new LLUUID(rdata.parameters[0]); | ||
194 | Rest.Log.DebugFormat("{0} LLUUID supplied", MsgId); | ||
195 | rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid); | ||
196 | } | ||
197 | catch | ||
198 | { | ||
199 | string[] names = rdata.parameters[0].Split(Rest.CA_SPACE); | ||
200 | if (names.Length == 2) | ||
201 | { | ||
202 | Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId); | ||
203 | rdata.userProfile = Rest.UserServices.GetUserProfile(names[0],names[1]); | ||
204 | } | ||
205 | else | ||
206 | { | ||
207 | Rest.Log.DebugFormat("{0} A Valid UUID or both first and last names must be specified", MsgId); | ||
208 | rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); | ||
209 | } | ||
210 | } | ||
211 | |||
212 | if (rdata.userProfile != null) | ||
213 | { | ||
214 | Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}", | ||
215 | MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); | ||
216 | } | ||
217 | else | ||
218 | { | ||
219 | Rest.Log.DebugFormat("{0} No profile for {1}", MsgId, rdata.path); | ||
220 | rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); | ||
221 | } | ||
222 | |||
223 | // If we get to here, then we have successfully obtained an inventory | ||
224 | // for the specified user. | ||
225 | |||
226 | rdata.uuid = rdata.userProfile.ID; | ||
227 | |||
228 | if (Rest.InventoryServices.HasInventoryForUser(rdata.uuid)) | ||
229 | { | ||
230 | |||
231 | rdata.root = Rest.InventoryServices.RequestRootFolder(rdata.uuid); | ||
232 | |||
233 | Rest.Log.DebugFormat("{0} Inventory Root retrieved for {1} {2}", | ||
234 | MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); | ||
235 | |||
236 | Rest.InventoryServices.RequestInventoryForUser(rdata.uuid, rdata.GetUserInventory); | ||
237 | |||
238 | Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}", | ||
239 | MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); | ||
240 | |||
241 | lock(rdata) | ||
242 | { | ||
243 | if (!rdata.HaveInventory) | ||
244 | { | ||
245 | Monitor.Wait(rdata); | ||
246 | } | ||
247 | } | ||
248 | |||
249 | if (rdata.root == null) | ||
250 | { | ||
251 | Rest.Log.DebugFormat("{0} Inventory is not available [1] for agent {1} {2}", | ||
252 | MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); | ||
253 | rdata.Fail(Rest.HttpStatusCodeServerError,Rest.HttpStatusDescServerError); | ||
254 | } | ||
255 | |||
256 | } | ||
257 | else | ||
258 | { | ||
259 | Rest.Log.DebugFormat("{0} Inventory is not available for agent [3] {1} {2}", | ||
260 | MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); | ||
261 | rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); | ||
262 | } | ||
263 | |||
264 | // If we get here, then we have successfully retrieved the user's information | ||
265 | // and inventory information is now available locally. | ||
266 | |||
267 | switch (rdata.method) | ||
268 | { | ||
269 | |||
270 | case Rest.HEAD : // Do the processing, set the status code, suppress entity | ||
271 | DoGet(rdata); | ||
272 | rdata.buffer = null; | ||
273 | break; | ||
274 | |||
275 | case Rest.GET : // Do the processing, set the status code, return entity | ||
276 | DoGet(rdata); | ||
277 | break; | ||
278 | |||
279 | case Rest.PUT : // Add new information | ||
280 | DoPut(rdata); | ||
281 | break; | ||
282 | |||
283 | case Rest.POST : // Update (replace) | ||
284 | DoPost(rdata); | ||
285 | break; | ||
286 | |||
287 | case Rest.DELETE : // Delete information | ||
288 | DoDelete(rdata); | ||
289 | break; | ||
290 | |||
291 | default : | ||
292 | Rest.Log.DebugFormat("{0} Method {1} not supported for {2}", | ||
293 | MsgId, rdata.method, rdata.path); | ||
294 | rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed, | ||
295 | Rest.HttpStatusDescMethodNotAllowed); | ||
296 | break; | ||
297 | } | ||
298 | |||
299 | } | ||
300 | |||
301 | #endregion Interface | ||
302 | |||
303 | #region method-specific processing | ||
304 | |||
305 | /// <summary> | ||
306 | /// This method implements GET processing for inventory. | ||
307 | /// Any remaining parameters are used to locate the | ||
308 | /// corresponding subtree based upon node name. | ||
309 | /// </summary> | ||
310 | |||
311 | private void DoGet(InventoryRequestData rdata) | ||
312 | { | ||
313 | |||
314 | rdata.initXmlWriter(); | ||
315 | |||
316 | rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty); | ||
317 | |||
318 | if (rdata.parameters.Length == 1) | ||
319 | { | ||
320 | formatInventory(rdata, rdata.root, String.Empty); | ||
321 | } | ||
322 | else | ||
323 | { | ||
324 | traverseInventory(rdata, rdata.root, 1); | ||
325 | } | ||
326 | |||
327 | rdata.writer.WriteFullEndElement(); | ||
328 | |||
329 | rdata.Complete(); | ||
330 | rdata.Respond("Inventory " + rdata.method + ": Normal completion"); | ||
331 | |||
332 | } | ||
333 | |||
334 | /// <summary> | ||
335 | /// In the case of the inventory, and probably much else | ||
336 | /// the distinction between PUT and POST is not always | ||
337 | /// easy to discern. Adding a directory can be viewed as | ||
338 | /// an addition, or as a modification to the inventory as | ||
339 | /// a whole. | ||
340 | /// | ||
341 | /// The best distinction may be the relationship between | ||
342 | /// the entity and the URI. If we view POST as an update, | ||
343 | /// then the enity represents a replacement for the | ||
344 | /// element named by the URI. If the operation is PUT, | ||
345 | /// then the URI describes the context into which the | ||
346 | /// entity will be added. | ||
347 | /// | ||
348 | /// As an example, suppose the URI contains: | ||
349 | /// /admin/inventory/Clothing | ||
350 | /// Suppose the entity represents a Folder, called | ||
351 | /// "Clothes". | ||
352 | /// | ||
353 | /// A POST request will result in the replacement of | ||
354 | /// "Clothing" by "Clothes". Whereas a PUT request | ||
355 | /// would add Clothes as a sub-directory of Clothing. | ||
356 | /// | ||
357 | /// This is the model followed by this implementation. | ||
358 | /// </summary> | ||
359 | |||
360 | /// <summary> | ||
361 | /// PUT adds new information to the inventory at the | ||
362 | /// context identified by the URI. | ||
363 | /// </summary> | ||
364 | |||
365 | private void DoPut(InventoryRequestData rdata) | ||
366 | { | ||
367 | |||
368 | // Resolve the context node specified in the URI. Entity | ||
369 | // data will be ADDED beneath this node. | ||
370 | |||
371 | Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); | ||
372 | |||
373 | // Processing depends upon the type of inventory node | ||
374 | // identified in the URI. This is the CONTEXT for the | ||
375 | // change. We either got a context or we threw an | ||
376 | // exception. | ||
377 | |||
378 | // It follows that we can only add information if the URI | ||
379 | // has identified a folder. So only folder is supported | ||
380 | // in this case. | ||
381 | |||
382 | if (typeof(InventoryFolderBase) == InventoryNode.GetType() || | ||
383 | typeof(InventoryFolderImpl) == InventoryNode.GetType()) | ||
384 | { | ||
385 | |||
386 | // Cast the context node appropriately. | ||
387 | |||
388 | InventoryFolderBase context = (InventoryFolderBase) InventoryNode; | ||
389 | |||
390 | Rest.Log.DebugFormat("{0} {1}: Resource(s) will be added to folder {2}", | ||
391 | MsgId, rdata.method, rdata.path); | ||
392 | |||
393 | // Reconstitute inventory sub-tree from the XML supplied in the entity. | ||
394 | // This is a stand-alone inventory subtree, not yet integrated into the | ||
395 | // existing tree. | ||
396 | |||
397 | XmlInventoryCollection entity = ReconstituteEntity(rdata); | ||
398 | |||
399 | // Inlined assest included in entity. If anything fails, | ||
400 | // return failure to requestor. | ||
401 | |||
402 | if (entity.Assets.Count > 0) | ||
403 | { | ||
404 | |||
405 | Rest.Log.DebugFormat("{0} Adding {1} assets to server", | ||
406 | MsgId, entity.Assets.Count); | ||
407 | |||
408 | foreach (AssetBase asset in entity.Assets) | ||
409 | { | ||
410 | Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}", | ||
411 | MsgId, asset.ID, asset.Type, asset.Name); | ||
412 | Rest.AssetServices.AddAsset(asset); | ||
413 | if (Rest.DumpAsset) | ||
414 | { | ||
415 | Rest.Dump(asset.Data); | ||
416 | } | ||
417 | } | ||
418 | |||
419 | } | ||
420 | |||
421 | // Modify the context using the collection of folders and items | ||
422 | // returned in the XmlInventoryCollection. | ||
423 | |||
424 | foreach (InventoryFolderBase folder in entity.Folders) | ||
425 | { | ||
426 | |||
427 | InventoryFolderBase found = null; | ||
428 | |||
429 | // If the parentID is zero, then this is going | ||
430 | // into the root identified by the URI. The requestor | ||
431 | // may have already set the parent ID correctly, in which | ||
432 | // case we don't have to do it here. | ||
433 | |||
434 | if (folder.ParentID == LLUUID.Zero) | ||
435 | { | ||
436 | folder.ParentID = context.ID; | ||
437 | } | ||
438 | |||
439 | // Search the existing inventory for an existing entry. If | ||
440 | // we have once, we need to decide if it has really changed. | ||
441 | // It could just be present as (unnecessary) context, and we | ||
442 | // don't want to waste time updating the database in that | ||
443 | // case, OR, it could be being moved from another location | ||
444 | // in which case an update is most certainly necessary. | ||
445 | |||
446 | found = null; | ||
447 | |||
448 | foreach (InventoryFolderBase xf in rdata.folders) | ||
449 | { | ||
450 | // Compare identifying attribute | ||
451 | if (xf.ID == folder.ID) | ||
452 | { | ||
453 | found = xf; | ||
454 | } | ||
455 | } | ||
456 | |||
457 | if (found != null && FolderHasChanged(folder,found)) | ||
458 | { | ||
459 | Rest.Log.DebugFormat("{0} Updating existing folder", MsgId); | ||
460 | Rest.InventoryServices.MoveFolder(folder); | ||
461 | } | ||
462 | else | ||
463 | { | ||
464 | Rest.Log.DebugFormat("{0} Adding new folder", MsgId); | ||
465 | Rest.InventoryServices.AddFolder(folder); | ||
466 | } | ||
467 | |||
468 | } | ||
469 | |||
470 | // Now we repeat a similar process for the items included | ||
471 | // in the entity. | ||
472 | |||
473 | foreach (InventoryItemBase item in entity.Items) | ||
474 | { | ||
475 | |||
476 | InventoryItemBase found = null; | ||
477 | |||
478 | // If the parentID is zero, then this is going | ||
479 | // directly into the root identified by the URI. | ||
480 | |||
481 | if (item.Folder == LLUUID.Zero) | ||
482 | { | ||
483 | item.Folder = context.ID; | ||
484 | } | ||
485 | |||
486 | // Determine whether this is a new item or a | ||
487 | // replacement definition. | ||
488 | |||
489 | foreach (InventoryItemBase xi in rdata.items) | ||
490 | { | ||
491 | // Compare identifying attribute | ||
492 | if (xi.ID == item.ID) | ||
493 | { | ||
494 | found = xi; | ||
495 | } | ||
496 | } | ||
497 | |||
498 | if (found != null && ItemHasChanged(item, found)) | ||
499 | { | ||
500 | Rest.Log.DebugFormat("{0} Updating item {1} {2} {3} {4} {5}", | ||
501 | MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name); | ||
502 | Rest.InventoryServices.UpdateItem(item); | ||
503 | } | ||
504 | else | ||
505 | { | ||
506 | Rest.Log.DebugFormat("{0} Adding item {1} {2} {3} {4} {5}", | ||
507 | MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name); | ||
508 | Rest.InventoryServices.AddItem(item); | ||
509 | } | ||
510 | |||
511 | } | ||
512 | |||
513 | } | ||
514 | else | ||
515 | { | ||
516 | Rest.Log.DebugFormat("{0} {1}: Resource {2} is not a valid context: {3}", | ||
517 | MsgId, rdata.method, rdata.path, InventoryNode.GetType()); | ||
518 | rdata.Fail(Rest.HttpStatusCodeBadRequest, | ||
519 | Rest.HttpStatusDescBadRequest); | ||
520 | } | ||
521 | |||
522 | rdata.Complete(); | ||
523 | rdata.Respond("Inventory " + rdata.method + ": Normal completion"); | ||
524 | |||
525 | } | ||
526 | |||
527 | /// <summary> | ||
528 | /// POST updates the URI-identified element in the inventory. This | ||
529 | /// is actually far more flexible than it might at first sound. For | ||
530 | /// POST the URI serves two purposes: | ||
531 | /// [1] It identifies the user whose inventory is to be | ||
532 | /// processed. | ||
533 | /// [2] It optionally specifies a subtree of the inventory | ||
534 | /// that is to be used to resolve an relative subtree | ||
535 | /// specifications in the entity. If nothing is specified | ||
536 | /// then the whole inventory is implied. | ||
537 | /// Please note that the subtree specified by the URI is only relevant | ||
538 | /// to an entity containing a URI relative specification, i.e. one or | ||
539 | /// more elements do not specify parent folder information. These | ||
540 | /// elements will be implicitly referenced within the context identified | ||
541 | /// by the URI. | ||
542 | /// If an element in the entity specifies an explicit parent folder, then | ||
543 | /// that parent is effective, regardless of nay value specified in the | ||
544 | /// URI. If the parent does not exist, then the element, and any dependent | ||
545 | /// elements, are ignored. This case is actually detected and handled | ||
546 | /// during the reconstitution process. | ||
547 | /// </summary> | ||
548 | |||
549 | private void DoPost(InventoryRequestData rdata) | ||
550 | { | ||
551 | |||
552 | int count = 0; | ||
553 | |||
554 | // Resolve the inventory node that is to be modified. | ||
555 | |||
556 | Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); | ||
557 | |||
558 | // As long as we have a context, then we have something | ||
559 | // meaningful to do, unlike PUT. So reconstitute the | ||
560 | // subtree before doing anything else. Note that we | ||
561 | // etiher got a context or we threw an exception. | ||
562 | |||
563 | XmlInventoryCollection entity = ReconstituteEntity(rdata); | ||
564 | |||
565 | // Incorporate any inlined assets first | ||
566 | |||
567 | if (entity.Assets.Count != 0) | ||
568 | { | ||
569 | foreach (AssetBase asset in entity.Assets) | ||
570 | { | ||
571 | // Asset was validated during the collection | ||
572 | // process | ||
573 | Rest.AssetServices.AddAsset(asset); | ||
574 | } | ||
575 | } | ||
576 | |||
577 | /// <summary> | ||
578 | /// URI specifies a folder to be updated. | ||
579 | /// </summary> | ||
580 | /// <remarks> | ||
581 | /// The root node in the entity must have the same | ||
582 | /// UUID as the node identified by the URI. The | ||
583 | /// parentID if different indicates that the updated | ||
584 | /// folder is actually being moved too. | ||
585 | /// </remarks> | ||
586 | |||
587 | if (typeof(InventoryFolderBase) == InventoryNode.GetType() || | ||
588 | typeof(InventoryFolderImpl) == InventoryNode.GetType()) | ||
589 | { | ||
590 | |||
591 | InventoryFolderBase uri = (InventoryFolderBase) InventoryNode; | ||
592 | InventoryFolderBase xml = null; | ||
593 | |||
594 | // Scan the set of folders in the entity collection for an | ||
595 | // entry that macthes the context folder. It is assumed that | ||
596 | // the only reliable indicator of this is a zero UUID ( using | ||
597 | // implicit context), or the parent's UUID matches that of the | ||
598 | // URI designated node (explicit context). We don't allow | ||
599 | // ambiguity in this case because this is POST and we are | ||
600 | // supposed to be modifying a specific node. | ||
601 | // We assign any element IDs required as an economy; we don't | ||
602 | // want to iterate over the fodler set again if it can be | ||
603 | // helped. | ||
604 | |||
605 | foreach (InventoryFolderBase folder in entity.Folders) | ||
606 | { | ||
607 | if (folder.ParentID == uri.ParentID || | ||
608 | folder.ParentID == LLUUID.Zero) | ||
609 | { | ||
610 | folder.ParentID = uri.ParentID; | ||
611 | xml = folder; | ||
612 | count++; | ||
613 | } | ||
614 | if (xml.ID == LLUUID.Zero) | ||
615 | { | ||
616 | xml.ID = LLUUID.Random(); | ||
617 | } | ||
618 | } | ||
619 | |||
620 | // More than one entry is ambiguous | ||
621 | |||
622 | if (count > 1) | ||
623 | { | ||
624 | Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous", | ||
625 | MsgId, rdata.method, rdata.path); | ||
626 | rdata.Fail(Rest.HttpStatusCodeBadRequest, | ||
627 | Rest.HttpStatusDescBadRequest); | ||
628 | } | ||
629 | |||
630 | // Exactly one entry means we ARE replacing the node | ||
631 | // identified by the URI. So we delete the old folder | ||
632 | // by moving it to the trash and then purging it. | ||
633 | // We then add all of the folders and items we | ||
634 | // included in the entity. The subtree has been | ||
635 | // modified. | ||
636 | |||
637 | if (count == 1) | ||
638 | { | ||
639 | |||
640 | InventoryFolderBase TrashCan = GetTrashCan(rdata); | ||
641 | |||
642 | uri.ParentID = TrashCan.ID; | ||
643 | Rest.InventoryServices.MoveFolder(uri); | ||
644 | Rest.InventoryServices.PurgeFolder(TrashCan); | ||
645 | |||
646 | } | ||
647 | |||
648 | // Now, regardelss of what they represent, we | ||
649 | // integrate all of the elements in the entity. | ||
650 | |||
651 | foreach (InventoryFolderBase f in entity.Folders) | ||
652 | { | ||
653 | Rest.InventoryServices.MoveFolder(f); | ||
654 | } | ||
655 | |||
656 | foreach (InventoryItemBase it in entity.Items) | ||
657 | { | ||
658 | Rest.InventoryServices.AddItem(it); | ||
659 | } | ||
660 | |||
661 | } | ||
662 | |||
663 | /// <summary> | ||
664 | /// URI specifies an item to be updated | ||
665 | /// </summary> | ||
666 | /// <remarks> | ||
667 | /// The entity must contain a single item node to be | ||
668 | /// updated. ID and Folder ID must be correct. | ||
669 | /// </remarks> | ||
670 | |||
671 | else | ||
672 | { | ||
673 | |||
674 | InventoryItemBase uri = (InventoryItemBase) InventoryNode; | ||
675 | InventoryItemBase xml = null; | ||
676 | |||
677 | if (entity.Folders.Count != 0) | ||
678 | { | ||
679 | Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>", | ||
680 | MsgId, rdata.method, rdata.path); | ||
681 | rdata.Fail(Rest.HttpStatusCodeBadRequest, | ||
682 | Rest.HttpStatusDescBadRequest); | ||
683 | } | ||
684 | |||
685 | if (entity.Items.Count > 1) | ||
686 | { | ||
687 | Rest.Log.DebugFormat("{0} {1}: Entity contains too many items <{2}>", | ||
688 | MsgId, rdata.method, rdata.path); | ||
689 | rdata.Fail(Rest.HttpStatusCodeBadRequest, | ||
690 | Rest.HttpStatusDescBadRequest); | ||
691 | } | ||
692 | |||
693 | xml = entity.Items[0]; | ||
694 | |||
695 | if (xml.ID == LLUUID.Zero) | ||
696 | { | ||
697 | xml.ID = LLUUID.Random(); | ||
698 | } | ||
699 | |||
700 | // If the folder reference has changed, then this item is | ||
701 | // being moved. Otherwise we'll just delete the old, and | ||
702 | // add in the new. | ||
703 | |||
704 | // Delete the old item | ||
705 | |||
706 | Rest.InventoryServices.DeleteItem(uri); | ||
707 | |||
708 | // Add the new item to the inventory | ||
709 | |||
710 | Rest.InventoryServices.AddItem(xml); | ||
711 | |||
712 | } | ||
713 | |||
714 | rdata.Complete(); | ||
715 | rdata.Respond("Inventory " + rdata.method + ": Normal completion"); | ||
716 | |||
717 | } | ||
718 | |||
719 | /// <summary> | ||
720 | /// Arguably the most damaging REST interface. It deletes the inventory | ||
721 | /// item or folder identified by the URI. | ||
722 | /// | ||
723 | /// We only process if the URI identified node appears to exist | ||
724 | /// We do not test for success because we either get a context, | ||
725 | /// or an exception is thrown. | ||
726 | /// | ||
727 | /// Folders are deleted by moving them to another folder and then | ||
728 | /// purging that folder. We'll do that by creating a temporary | ||
729 | /// sub-folder in the TrashCan and purging that folder's | ||
730 | /// contents. If we can't can it, we don't delete it... | ||
731 | /// So, if no trashcan is available, the request does nothing. | ||
732 | /// Items are summarily deleted. | ||
733 | /// | ||
734 | /// In the interests of safety, a delete request should normally | ||
735 | /// be performed using UUID, as a name might identify several | ||
736 | /// elements. | ||
737 | /// </summary> | ||
738 | |||
739 | private void DoDelete(InventoryRequestData rdata) | ||
740 | { | ||
741 | |||
742 | Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); | ||
743 | |||
744 | if (typeof(InventoryFolderBase) == InventoryNode.GetType() || | ||
745 | typeof(InventoryFolderImpl) == InventoryNode.GetType()) | ||
746 | { | ||
747 | |||
748 | InventoryFolderBase TrashCan = GetTrashCan(rdata); | ||
749 | |||
750 | InventoryFolderBase folder = (InventoryFolderBase) InventoryNode; | ||
751 | Rest.Log.DebugFormat("{0} {1}: Folder {2} will be deleted", | ||
752 | MsgId, rdata.method, rdata.path); | ||
753 | folder.ParentID = TrashCan.ID; | ||
754 | Rest.InventoryServices.MoveFolder(folder); | ||
755 | Rest.InventoryServices.PurgeFolder(TrashCan); | ||
756 | |||
757 | } | ||
758 | |||
759 | // Deleting items is much more straight forward. | ||
760 | |||
761 | else | ||
762 | { | ||
763 | InventoryItemBase item = (InventoryItemBase) InventoryNode; | ||
764 | Rest.Log.DebugFormat("{0} {1}: Item {2} will be deleted", | ||
765 | MsgId, rdata.method, rdata.path); | ||
766 | Rest.InventoryServices.DeleteItem(item); | ||
767 | } | ||
768 | |||
769 | rdata.Complete(); | ||
770 | rdata.Respond("Inventory " + rdata.method + ": Normal completion"); | ||
771 | |||
772 | } | ||
773 | |||
774 | #endregion method-specific processing | ||
775 | |||
776 | /// <summary> | ||
777 | /// This method is called to obtain the OpenSim inventory object identified | ||
778 | /// by the supplied URI. This may be either an Item or a Folder, so a suitably | ||
779 | /// ambiguous return type is employed (Object). This method recurses as | ||
780 | /// necessary to process the designated hierarchy. | ||
781 | /// | ||
782 | /// If we reach the end of the URI then we return the contextural folder to | ||
783 | /// our caller. | ||
784 | /// | ||
785 | /// If we are not yet at the end of the URI we attempt to find a child folder | ||
786 | /// and if we succeed we recurse. | ||
787 | /// | ||
788 | /// If this is the last node, then we look to see if this is an item. If it is, | ||
789 | /// we return that item. | ||
790 | /// | ||
791 | /// Otherwise we fail the request on the ground of an invalid URI. | ||
792 | /// | ||
793 | /// <note> | ||
794 | /// This mechanism cannot detect the case where duplicate subtrees satisfy a | ||
795 | /// request. In such a case the 1st element gets processed. If this is a | ||
796 | /// problem, then UUID should be used to identify the end-node. This is basic | ||
797 | /// premise of normal inventory processing. The name is an informational, and | ||
798 | /// not a defining, attribute. | ||
799 | /// </note> | ||
800 | /// | ||
801 | /// </summary> | ||
802 | |||
803 | private Object getInventoryNode(InventoryRequestData rdata, InventoryFolderBase folder, int pi) | ||
804 | { | ||
805 | |||
806 | Rest.Log.DebugFormat("{0} Searching folder {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); | ||
807 | |||
808 | // We have just run off the end of the parameter sequence | ||
809 | |||
810 | if (pi >= rdata.parameters.Length) | ||
811 | { | ||
812 | return folder; | ||
813 | } | ||
814 | |||
815 | // More names in the sequence, look for a folder that might | ||
816 | // get us there. | ||
817 | |||
818 | if (rdata.folders != null) | ||
819 | foreach (InventoryFolderBase f in rdata.folders) | ||
820 | { | ||
821 | // Look for the present node in the directory list | ||
822 | if (f.ParentID == folder.ID && | ||
823 | (f.Name == rdata.parameters[pi] || | ||
824 | f.ID.ToString() == rdata.parameters[pi])) | ||
825 | { | ||
826 | return getInventoryNode(rdata, f, pi+1); | ||
827 | } | ||
828 | } | ||
829 | |||
830 | // No folders that match. Perhaps this parameter identifies an item? If | ||
831 | // it does, then it MUST also be the last name in the sequence. | ||
832 | |||
833 | if (pi == rdata.parameters.Length-1) | ||
834 | { | ||
835 | if (rdata.items != null) | ||
836 | { | ||
837 | int k = 0; | ||
838 | InventoryItemBase li = null; | ||
839 | foreach (InventoryItemBase i in rdata.items) | ||
840 | { | ||
841 | if (i.Folder == folder.ID && | ||
842 | (i.Name == rdata.parameters[pi] || | ||
843 | i.ID.ToString() == rdata.parameters[pi])) | ||
844 | { | ||
845 | li = i; | ||
846 | k++; | ||
847 | } | ||
848 | } | ||
849 | if (k == 1) | ||
850 | { | ||
851 | return li; | ||
852 | } | ||
853 | else | ||
854 | { | ||
855 | Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", | ||
856 | MsgId, rdata.method, rdata.path); | ||
857 | rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound); | ||
858 | } | ||
859 | } | ||
860 | } | ||
861 | |||
862 | // No, so abandon the request | ||
863 | |||
864 | Rest.Log.DebugFormat("{0} {1}: Resource {2} not found", | ||
865 | MsgId, rdata.method, rdata.path); | ||
866 | rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound); | ||
867 | |||
868 | return null; /* Never reached */ | ||
869 | |||
870 | } | ||
871 | |||
872 | /// <summary> | ||
873 | /// This routine traverse the inventory's structure until the end-point identified | ||
874 | /// in the URI is reached, the remainder of the inventory (if any) is then formatted | ||
875 | /// and returned to the requestor. | ||
876 | /// | ||
877 | /// Note that this method is only interested in those folder that match elements of | ||
878 | /// the URI supplied by the requestor, so once a match is fund, the processing does | ||
879 | /// not need to consider any further elements. | ||
880 | /// | ||
881 | /// Only the last element in the URI should identify an item. | ||
882 | /// </summary> | ||
883 | |||
884 | private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi) | ||
885 | { | ||
886 | |||
887 | Rest.Log.DebugFormat("{0} Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); | ||
888 | |||
889 | if (rdata.folders != null) | ||
890 | { | ||
891 | foreach (InventoryFolderBase f in rdata.folders) | ||
892 | { | ||
893 | if (f.ParentID == folder.ID && | ||
894 | (f.Name == rdata.parameters[pi] || | ||
895 | f.ID.ToString() == rdata.parameters[pi])) | ||
896 | { | ||
897 | if (pi < rdata.parameters.Length-1) | ||
898 | { | ||
899 | traverseInventory(rdata, f, pi+1); | ||
900 | } | ||
901 | else | ||
902 | { | ||
903 | formatInventory(rdata, f, String.Empty); | ||
904 | } | ||
905 | return; | ||
906 | } | ||
907 | } | ||
908 | } | ||
909 | |||
910 | if (pi == rdata.parameters.Length-1) | ||
911 | { | ||
912 | if (rdata.items != null) | ||
913 | { | ||
914 | foreach (InventoryItemBase i in rdata.items) | ||
915 | { | ||
916 | if (i.Folder == folder.ID && | ||
917 | (i.Name == rdata.parameters[pi] || | ||
918 | i.ID.ToString() == rdata.parameters[pi])) | ||
919 | { | ||
920 | // Fetching an Item has a special significance. In this | ||
921 | // case we also want to fetch the associated asset. | ||
922 | // To make it interesting, we'll d this via redirection. | ||
923 | string asseturl = "http://" + rdata.hostname + ":" + rdata.port + | ||
924 | "/admin/assets" + Rest.UrlPathSeparator + i.AssetID.ToString(); | ||
925 | rdata.Redirect(asseturl,Rest.PERMANENT); | ||
926 | Rest.Log.DebugFormat("{0} Never Reached"); | ||
927 | } | ||
928 | } | ||
929 | } | ||
930 | } | ||
931 | |||
932 | Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>", | ||
933 | MsgId, rdata.path); | ||
934 | rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); | ||
935 | |||
936 | } | ||
937 | |||
938 | /// <summary> | ||
939 | /// This method generates XML that describes an instance of InventoryFolderBase. | ||
940 | /// It recurses as necessary to reflect a folder hierarchy, and calls formatItem | ||
941 | /// to generate XML for any items encountered along the way. | ||
942 | /// The indentation parameter is solely for the benefit of trace record | ||
943 | /// formatting. | ||
944 | /// </summary> | ||
945 | |||
946 | private void formatInventory(InventoryRequestData rdata, InventoryFolderBase folder, string indent) | ||
947 | { | ||
948 | |||
949 | if (Rest.DEBUG) | ||
950 | { | ||
951 | Rest.Log.DebugFormat("{0} Folder : {1} {2} {3}", MsgId, folder.ID, indent, folder.Name); | ||
952 | indent += "\t"; | ||
953 | } | ||
954 | |||
955 | // Start folder item | ||
956 | |||
957 | rdata.writer.WriteStartElement(String.Empty,"Folder",String.Empty); | ||
958 | rdata.writer.WriteAttributeString("name",String.Empty,folder.Name); | ||
959 | rdata.writer.WriteAttributeString("uuid",String.Empty,folder.ID.ToString()); | ||
960 | rdata.writer.WriteAttributeString("owner",String.Empty,folder.Owner.ToString()); | ||
961 | rdata.writer.WriteAttributeString("type",String.Empty,folder.Type.ToString()); | ||
962 | rdata.writer.WriteAttributeString("version",String.Empty,folder.Version.ToString()); | ||
963 | |||
964 | if (rdata.folders != null) | ||
965 | { | ||
966 | foreach (InventoryFolderBase f in rdata.folders) | ||
967 | { | ||
968 | if (f.ParentID == folder.ID) | ||
969 | { | ||
970 | formatInventory(rdata, f, indent); | ||
971 | } | ||
972 | } | ||
973 | } | ||
974 | |||
975 | if (rdata.items != null) | ||
976 | { | ||
977 | foreach (InventoryItemBase i in rdata.items) | ||
978 | { | ||
979 | if (i.Folder == folder.ID) | ||
980 | { | ||
981 | formatItem(rdata, i, indent); | ||
982 | } | ||
983 | } | ||
984 | } | ||
985 | |||
986 | // End folder item | ||
987 | |||
988 | rdata.writer.WriteEndElement(); | ||
989 | |||
990 | } | ||
991 | |||
992 | /// <summary> | ||
993 | /// This method generates XML that describes an instance of InventoryItemBase. | ||
994 | /// </summary> | ||
995 | |||
996 | private void formatItem(InventoryRequestData rdata, InventoryItemBase i, string indent) | ||
997 | { | ||
998 | |||
999 | Rest.Log.DebugFormat("{0} Item : {1} {2} {3}", MsgId, i.ID, indent, i.Name); | ||
1000 | |||
1001 | rdata.writer.WriteStartElement(String.Empty,"Item",String.Empty); | ||
1002 | |||
1003 | rdata.writer.WriteAttributeString("name",String.Empty,i.Name); | ||
1004 | rdata.writer.WriteAttributeString("desc",String.Empty,i.Description); | ||
1005 | rdata.writer.WriteAttributeString("uuid",String.Empty,i.ID.ToString()); | ||
1006 | rdata.writer.WriteAttributeString("owner",String.Empty,i.Owner.ToString()); | ||
1007 | rdata.writer.WriteAttributeString("creator",String.Empty,i.Creator.ToString()); | ||
1008 | rdata.writer.WriteAttributeString("creationdate",String.Empty,i.CreationDate.ToString()); | ||
1009 | rdata.writer.WriteAttributeString("type",String.Empty,i.InvType.ToString()); | ||
1010 | rdata.writer.WriteAttributeString("assettype",String.Empty,i.AssetType.ToString()); | ||
1011 | rdata.writer.WriteAttributeString("groupowned",String.Empty,i.GroupOwned.ToString()); | ||
1012 | rdata.writer.WriteAttributeString("groupid",String.Empty,i.GroupID.ToString()); | ||
1013 | rdata.writer.WriteAttributeString("saletype",String.Empty,i.SaleType.ToString()); | ||
1014 | rdata.writer.WriteAttributeString("saleprice",String.Empty,i.SalePrice.ToString()); | ||
1015 | rdata.writer.WriteAttributeString("flags",String.Empty,i.Flags.ToString("X")); | ||
1016 | |||
1017 | rdata.writer.WriteStartElement(String.Empty,"Permissions",String.Empty); | ||
1018 | rdata.writer.WriteAttributeString("current",String.Empty,i.CurrentPermissions.ToString("X")); | ||
1019 | rdata.writer.WriteAttributeString("next",String.Empty,i.NextPermissions.ToString("X")); | ||
1020 | rdata.writer.WriteAttributeString("everyone",String.Empty,i.EveryOnePermissions.ToString("X")); | ||
1021 | rdata.writer.WriteAttributeString("base",String.Empty,i.BasePermissions.ToString("X")); | ||
1022 | rdata.writer.WriteEndElement(); | ||
1023 | |||
1024 | rdata.writer.WriteElementString("Asset",i.AssetID.ToString()); | ||
1025 | |||
1026 | rdata.writer.WriteEndElement(); | ||
1027 | |||
1028 | } | ||
1029 | |||
1030 | /// <summary> | ||
1031 | /// This method creates a "trashcan" folder to support folder and item | ||
1032 | /// deletions by this interface. The xisting trash folder is found and | ||
1033 | /// this folder is created within it. It is called "tmp" to indicate to | ||
1034 | /// the client that it is OK to delete this folder. The REST interface | ||
1035 | /// will recreate the folder on an as-required basis. | ||
1036 | /// If the trash can cannot be created, then by implication the request | ||
1037 | /// that required it cannot be completed, and it fails accordingly. | ||
1038 | /// </summary> | ||
1039 | |||
1040 | private InventoryFolderBase GetTrashCan(InventoryRequestData rdata) | ||
1041 | { | ||
1042 | |||
1043 | InventoryFolderBase TrashCan = null; | ||
1044 | |||
1045 | foreach (InventoryFolderBase f in rdata.folders) | ||
1046 | { | ||
1047 | if (f.Name == "Trash") | ||
1048 | { | ||
1049 | foreach (InventoryFolderBase t in rdata.folders) | ||
1050 | { | ||
1051 | if (t.Name == "tmp") | ||
1052 | { | ||
1053 | TrashCan = t; | ||
1054 | } | ||
1055 | } | ||
1056 | if (TrashCan == null) | ||
1057 | { | ||
1058 | TrashCan = new InventoryFolderBase(); | ||
1059 | TrashCan.Name = "tmp"; | ||
1060 | TrashCan.ID = LLUUID.Random(); | ||
1061 | TrashCan.Version = 1; | ||
1062 | TrashCan.Type = (short) AssetType.TrashFolder; | ||
1063 | TrashCan.ParentID = f.ID; | ||
1064 | Rest.InventoryServices.AddFolder(TrashCan); | ||
1065 | } | ||
1066 | } | ||
1067 | } | ||
1068 | |||
1069 | if (TrashCan == null) | ||
1070 | { | ||
1071 | Rest.Log.DebugFormat("{0} No Trash Can available", MsgId); | ||
1072 | rdata.Fail(Rest.HttpStatusCodeServerError, | ||
1073 | Rest.HttpStatusDescServerError); | ||
1074 | } | ||
1075 | |||
1076 | return TrashCan; | ||
1077 | |||
1078 | } | ||
1079 | |||
1080 | /// <summary> | ||
1081 | /// Make sure that an unchanged folder is not unnecessarily | ||
1082 | /// processed. | ||
1083 | /// </summary> | ||
1084 | |||
1085 | private bool FolderHasChanged(InventoryFolderBase newf, InventoryFolderBase oldf) | ||
1086 | { | ||
1087 | return ( newf.Name != oldf.Name | ||
1088 | || newf.ParentID != oldf.ParentID | ||
1089 | || newf.Owner != oldf.Owner | ||
1090 | || newf.Type != oldf.Type | ||
1091 | || newf.Version != oldf.Version | ||
1092 | ); | ||
1093 | } | ||
1094 | |||
1095 | /// <summary> | ||
1096 | /// Make sure that an unchanged item is not unnecessarily | ||
1097 | /// processed. | ||
1098 | /// </summary> | ||
1099 | |||
1100 | private bool ItemHasChanged(InventoryItemBase newf, InventoryItemBase oldf) | ||
1101 | { | ||
1102 | return ( newf.Name != oldf.Name | ||
1103 | || newf.Folder != oldf.Description | ||
1104 | || newf.Description != oldf.Description | ||
1105 | || newf.Owner != oldf.Owner | ||
1106 | || newf.Creator != oldf.Creator | ||
1107 | || newf.AssetID != oldf.AssetID | ||
1108 | || newf.GroupID != oldf.GroupID | ||
1109 | || newf.GroupOwned != oldf.GroupOwned | ||
1110 | || newf.InvType != oldf.InvType | ||
1111 | || newf.AssetType != oldf.AssetType | ||
1112 | ); | ||
1113 | } | ||
1114 | |||
1115 | /// <summary> | ||
1116 | /// This method is called by PUT and POST to create an XmlInventoryCollection | ||
1117 | /// instance that reflects the content of the entity supplied on the request. | ||
1118 | /// Any elements in the completed collection whose UUID is zero, are | ||
1119 | /// considered to be located relative to the end-point identified int he | ||
1120 | /// URI. In this way, an entire sub-tree can be conveyed in a single REST | ||
1121 | /// PUT or POST request. | ||
1122 | /// | ||
1123 | /// A new instance of XmlInventoryCollection is created and, if the request | ||
1124 | /// has an entity, it is more completely initialized. thus, if no entity was | ||
1125 | /// provided the collection is valid, but empty. | ||
1126 | /// | ||
1127 | /// The entity is then scanned and each tag is processed to produce the | ||
1128 | /// appropriate inventory elements. At the end f the scan, teh XmlInventoryCollection | ||
1129 | /// will reflect the subtree described by the entity. | ||
1130 | /// | ||
1131 | /// This is a very flexible mechanism, the entity may contain arbitrary, | ||
1132 | /// discontiguous tree fragments, or may contain single element. The caller is | ||
1133 | /// responsible for integrating this collection (and ensuring that any | ||
1134 | /// missing parent IDs are resolved). | ||
1135 | /// </summary> | ||
1136 | |||
1137 | internal XmlInventoryCollection ReconstituteEntity(InventoryRequestData rdata) | ||
1138 | { | ||
1139 | |||
1140 | Rest.Log.DebugFormat("{0} Reconstituting entity", MsgId); | ||
1141 | |||
1142 | XmlInventoryCollection ic = new XmlInventoryCollection(); | ||
1143 | |||
1144 | if (rdata.request.HasEntityBody) | ||
1145 | { | ||
1146 | |||
1147 | Rest.Log.DebugFormat("{0} Entity present", MsgId); | ||
1148 | |||
1149 | ic.init(rdata); | ||
1150 | |||
1151 | try | ||
1152 | { | ||
1153 | while (ic.xml.Read()) | ||
1154 | { | ||
1155 | switch (ic.xml.NodeType) | ||
1156 | { | ||
1157 | case XmlNodeType.Element : | ||
1158 | Rest.Log.DebugFormat("{0} StartElement: <{1}>", | ||
1159 | MsgId, ic.xml.Name); | ||
1160 | switch (ic.xml.Name) | ||
1161 | { | ||
1162 | case "Folder" : | ||
1163 | Rest.Log.DebugFormat("{0} Processing {1} element", | ||
1164 | MsgId, ic.xml.Name); | ||
1165 | CollectFolder(ic); | ||
1166 | break; | ||
1167 | case "Item" : | ||
1168 | Rest.Log.DebugFormat("{0} Processing {1} element", | ||
1169 | MsgId, ic.xml.Name); | ||
1170 | CollectItem(ic); | ||
1171 | break; | ||
1172 | case "Asset" : | ||
1173 | Rest.Log.DebugFormat("{0} Processing {1} element", | ||
1174 | MsgId, ic.xml.Name); | ||
1175 | CollectAsset(ic); | ||
1176 | break; | ||
1177 | case "Permissions" : | ||
1178 | Rest.Log.DebugFormat("{0} Processing {1} element", | ||
1179 | MsgId, ic.xml.Name); | ||
1180 | CollectPermissions(ic); | ||
1181 | break; | ||
1182 | default : | ||
1183 | Rest.Log.DebugFormat("{0} Ignoring {1} element", | ||
1184 | MsgId, ic.xml.Name); | ||
1185 | break; | ||
1186 | } | ||
1187 | // This stinks, but the ReadElement call above not only reads | ||
1188 | // the imbedded data, but also consumes the end tag for Asset | ||
1189 | // and moves the element pointer on to the containing Item's | ||
1190 | // element-end, however, if there was a permissions element | ||
1191 | // following, it would get us to the start of that.. | ||
1192 | if (ic.xml.NodeType == XmlNodeType.EndElement && | ||
1193 | ic.xml.Name == "Item") | ||
1194 | { | ||
1195 | Validate(ic); | ||
1196 | } | ||
1197 | break; | ||
1198 | case XmlNodeType.EndElement : | ||
1199 | switch (ic.xml.Name) | ||
1200 | { | ||
1201 | case "Folder" : | ||
1202 | Rest.Log.DebugFormat("{0} Completing {1} element", | ||
1203 | MsgId, ic.xml.Name); | ||
1204 | ic.Pop(); | ||
1205 | break; | ||
1206 | case "Item" : | ||
1207 | Rest.Log.DebugFormat("{0} Completing {1} element", | ||
1208 | MsgId, ic.xml.Name); | ||
1209 | Validate(ic); | ||
1210 | break; | ||
1211 | case "Asset" : | ||
1212 | Rest.Log.DebugFormat("{0} Completing {1} element", | ||
1213 | MsgId, ic.xml.Name); | ||
1214 | break; | ||
1215 | case "Permissions" : | ||
1216 | Rest.Log.DebugFormat("{0} Completing {1} element", | ||
1217 | MsgId, ic.xml.Name); | ||
1218 | break; | ||
1219 | default : | ||
1220 | Rest.Log.DebugFormat("{0} Ignoring {1} element", | ||
1221 | MsgId, ic.xml.Name); | ||
1222 | break; | ||
1223 | } | ||
1224 | break; | ||
1225 | default : | ||
1226 | Rest.Log.DebugFormat("{0} [0] Ignoring: <{1}>:<2>", | ||
1227 | MsgId, ic.xml.NodeType, ic.xml.Value); | ||
1228 | break; | ||
1229 | } | ||
1230 | } | ||
1231 | } | ||
1232 | catch (XmlException e) | ||
1233 | { | ||
1234 | Rest.Log.WarnFormat("{0} XML parsing error: {1}", MsgId, e.Message); | ||
1235 | throw e; | ||
1236 | } | ||
1237 | catch (Exception e) | ||
1238 | { | ||
1239 | Rest.Log.WarnFormat("{0} Unexpected XML parsing error: {1}", MsgId, e.Message); | ||
1240 | throw e; | ||
1241 | } | ||
1242 | |||
1243 | } | ||
1244 | else | ||
1245 | { | ||
1246 | Rest.Log.DebugFormat("{0} Entity absent", MsgId); | ||
1247 | } | ||
1248 | |||
1249 | if (Rest.DEBUG) | ||
1250 | { | ||
1251 | Rest.Log.DebugFormat("{0} Reconstituted entity", MsgId); | ||
1252 | Rest.Log.DebugFormat("{0} {1} assets", MsgId, ic.Assets.Count); | ||
1253 | Rest.Log.DebugFormat("{0} {1} folder", MsgId, ic.Folders.Count); | ||
1254 | Rest.Log.DebugFormat("{0} {1} items", MsgId, ic.Items.Count); | ||
1255 | } | ||
1256 | |||
1257 | return ic; | ||
1258 | |||
1259 | } | ||
1260 | |||
1261 | /// <summary> | ||
1262 | /// This method creates an inventory Folder from the | ||
1263 | /// information supplied in the request's entity. | ||
1264 | /// A folder instance is created and initialized to reflect | ||
1265 | /// default values. These values are then overridden | ||
1266 | /// by information supplied in the entity. | ||
1267 | /// If context was not explicitly provided, then the | ||
1268 | /// appropriate ID values are determined. | ||
1269 | /// </summary> | ||
1270 | |||
1271 | private void CollectFolder(XmlInventoryCollection ic) | ||
1272 | { | ||
1273 | |||
1274 | Rest.Log.DebugFormat("{0} Interpret folder element", MsgId); | ||
1275 | |||
1276 | InventoryFolderBase result = new InventoryFolderBase(); | ||
1277 | |||
1278 | // Default values | ||
1279 | |||
1280 | result.Name = String.Empty; | ||
1281 | result.ID = LLUUID.Zero; | ||
1282 | result.Owner = ic.UserID; | ||
1283 | result.ParentID = LLUUID.Zero; // Context | ||
1284 | result.Type = (short) AssetType.Folder; | ||
1285 | result.Version = 1; | ||
1286 | |||
1287 | if (ic.xml.HasAttributes) | ||
1288 | { | ||
1289 | for (int i = 0; i < ic.xml.AttributeCount; i++) | ||
1290 | { | ||
1291 | ic.xml.MoveToAttribute(i); | ||
1292 | switch (ic.xml.Name) | ||
1293 | { | ||
1294 | case "name" : | ||
1295 | result.Name = ic.xml.Value; | ||
1296 | break; | ||
1297 | case "uuid" : | ||
1298 | result.ID = new LLUUID(ic.xml.Value); | ||
1299 | break; | ||
1300 | case "parent" : | ||
1301 | result.ParentID = new LLUUID(ic.xml.Value); | ||
1302 | break; | ||
1303 | case "owner" : | ||
1304 | result.Owner = new LLUUID(ic.xml.Value); | ||
1305 | break; | ||
1306 | case "type" : | ||
1307 | result.Type = Int16.Parse(ic.xml.Value); | ||
1308 | break; | ||
1309 | case "version" : | ||
1310 | result.Version = UInt16.Parse(ic.xml.Value); | ||
1311 | break; | ||
1312 | default : | ||
1313 | Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}", | ||
1314 | MsgId, ic.xml.Name, ic.xml.Value); | ||
1315 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1316 | Rest.HttpStatusDescBadRequest); | ||
1317 | break; | ||
1318 | } | ||
1319 | } | ||
1320 | } | ||
1321 | |||
1322 | ic.xml.MoveToElement(); | ||
1323 | |||
1324 | // The client is relying upon the reconstitution process | ||
1325 | // to determine the parent's UUID based upon context. This | ||
1326 | // is necessary where a new folder may have been | ||
1327 | // introduced. | ||
1328 | |||
1329 | if (result.ParentID == LLUUID.Zero) | ||
1330 | { | ||
1331 | result.ParentID = ic.Parent(); | ||
1332 | } | ||
1333 | else | ||
1334 | { | ||
1335 | |||
1336 | bool found = false; | ||
1337 | |||
1338 | foreach (InventoryFolderBase parent in ic.rdata.folders) | ||
1339 | { | ||
1340 | if ( parent.ID == result.ParentID ) | ||
1341 | { | ||
1342 | found = true; | ||
1343 | break; | ||
1344 | } | ||
1345 | } | ||
1346 | |||
1347 | if (!found) | ||
1348 | { | ||
1349 | Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}", | ||
1350 | MsgId, ic.Item.Folder, result.ID); | ||
1351 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1352 | Rest.HttpStatusDescBadRequest); | ||
1353 | } | ||
1354 | |||
1355 | } | ||
1356 | |||
1357 | // This is a new folder, so no existing UUID is available | ||
1358 | // or appropriate | ||
1359 | |||
1360 | if (result.ID == LLUUID.Zero) | ||
1361 | { | ||
1362 | result.ID = LLUUID.Random(); | ||
1363 | } | ||
1364 | |||
1365 | // Treat this as a new context. Any other information is | ||
1366 | // obsolete as a consequence. | ||
1367 | |||
1368 | ic.Push(result); | ||
1369 | |||
1370 | } | ||
1371 | |||
1372 | /// <summary> | ||
1373 | /// This method is called to handle the construction of an Item | ||
1374 | /// instance from the supplied request entity. It is called | ||
1375 | /// whenever an Item start tag is detected. | ||
1376 | /// An instance of an Item is created and initialized to default | ||
1377 | /// values. These values are then overridden from values supplied | ||
1378 | /// as attributes to the Item element. | ||
1379 | /// This item is then stored in the XmlInventoryCollection and | ||
1380 | /// will be verified by Validate. | ||
1381 | /// All context is reset whenever the effective folder changes | ||
1382 | /// or an item is successfully validated. | ||
1383 | /// </summary> | ||
1384 | |||
1385 | private void CollectItem(XmlInventoryCollection ic) | ||
1386 | { | ||
1387 | |||
1388 | Rest.Log.DebugFormat("{0} Interpret item element", MsgId); | ||
1389 | |||
1390 | InventoryItemBase result = new InventoryItemBase(); | ||
1391 | |||
1392 | result.Name = String.Empty; | ||
1393 | result.Description = String.Empty; | ||
1394 | result.ID = LLUUID.Zero; | ||
1395 | result.Folder = LLUUID.Zero; | ||
1396 | result.Owner = ic.UserID; | ||
1397 | result.Creator = ic.UserID; | ||
1398 | result.AssetID = LLUUID.Zero; | ||
1399 | result.GroupID = LLUUID.Zero; | ||
1400 | result.GroupOwned = false; | ||
1401 | result.InvType = (int) InventoryType.Unknown; | ||
1402 | result.AssetType = (int) AssetType.Unknown; | ||
1403 | |||
1404 | if (ic.xml.HasAttributes) | ||
1405 | { | ||
1406 | for (int i = 0; i < ic.xml.AttributeCount; i++) | ||
1407 | { | ||
1408 | |||
1409 | ic.xml.MoveToAttribute(i); | ||
1410 | |||
1411 | switch (ic.xml.Name) | ||
1412 | { | ||
1413 | case "name" : | ||
1414 | result.Name = ic.xml.Value; | ||
1415 | break; | ||
1416 | case "desc" : | ||
1417 | result.Description = ic.xml.Value; | ||
1418 | break; | ||
1419 | case "uuid" : | ||
1420 | result.ID = new LLUUID(ic.xml.Value); | ||
1421 | break; | ||
1422 | case "folder" : | ||
1423 | result.Folder = new LLUUID(ic.xml.Value); | ||
1424 | break; | ||
1425 | case "owner" : | ||
1426 | result.Owner = new LLUUID(ic.xml.Value); | ||
1427 | break; | ||
1428 | case "invtype" : | ||
1429 | result.InvType = Int32.Parse(ic.xml.Value); | ||
1430 | break; | ||
1431 | case "creator" : | ||
1432 | result.Creator = new LLUUID(ic.xml.Value); | ||
1433 | break; | ||
1434 | case "assettype" : | ||
1435 | result.AssetType = Int32.Parse(ic.xml.Value); | ||
1436 | break; | ||
1437 | case "groupowned" : | ||
1438 | result.GroupOwned = Boolean.Parse(ic.xml.Value); | ||
1439 | break; | ||
1440 | case "groupid" : | ||
1441 | result.GroupID = new LLUUID(ic.xml.Value); | ||
1442 | break; | ||
1443 | case "flags" : | ||
1444 | result.Flags = UInt32.Parse(ic.xml.Value); | ||
1445 | break; | ||
1446 | case "creationdate" : | ||
1447 | result.CreationDate = Int32.Parse(ic.xml.Value); | ||
1448 | break; | ||
1449 | case "saletype" : | ||
1450 | result.SaleType = Byte.Parse(ic.xml.Value); | ||
1451 | break; | ||
1452 | case "saleprice" : | ||
1453 | result.SalePrice = Int32.Parse(ic.xml.Value); | ||
1454 | break; | ||
1455 | |||
1456 | default : | ||
1457 | Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}", | ||
1458 | MsgId, ic.xml.Name, ic.xml.Value); | ||
1459 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1460 | Rest.HttpStatusDescBadRequest); | ||
1461 | break; | ||
1462 | } | ||
1463 | } | ||
1464 | } | ||
1465 | |||
1466 | ic.xml.MoveToElement(); | ||
1467 | |||
1468 | ic.Push(result); | ||
1469 | |||
1470 | } | ||
1471 | |||
1472 | /// <summary> | ||
1473 | /// This method assembles an asset instance from the | ||
1474 | /// information supplied in the request's entity. It is | ||
1475 | /// called as a result of detecting a start tag for a | ||
1476 | /// type of Asset. | ||
1477 | /// The information is collected locally, and an asset | ||
1478 | /// instance is created only if the basic XML parsing | ||
1479 | /// completes successfully. | ||
1480 | /// Default values for all parts of the asset are | ||
1481 | /// established before overriding them from the supplied | ||
1482 | /// XML. | ||
1483 | /// If an asset has inline=true as an attribute, then | ||
1484 | /// the element contains the data representing the | ||
1485 | /// asset. This is saved as the data component. | ||
1486 | /// inline=false means that the element's payload is | ||
1487 | /// simply the UUID of the asset referenced by the | ||
1488 | /// item being constructed. | ||
1489 | /// An asset, if created is stored in the | ||
1490 | /// XmlInventoryCollection | ||
1491 | /// </summary> | ||
1492 | |||
1493 | private void CollectAsset(XmlInventoryCollection ic) | ||
1494 | { | ||
1495 | |||
1496 | Rest.Log.DebugFormat("{0} Interpret asset element", MsgId); | ||
1497 | |||
1498 | AssetBase asset = null; | ||
1499 | |||
1500 | string name = String.Empty; | ||
1501 | string desc = String.Empty; | ||
1502 | sbyte type = (sbyte) AssetType.Unknown; | ||
1503 | sbyte itype = (sbyte) AssetType.Unknown; | ||
1504 | bool temp = false; | ||
1505 | bool local = false; | ||
1506 | |||
1507 | // This is not a persistent attribute | ||
1508 | bool inline = true; | ||
1509 | |||
1510 | LLUUID uuid = LLUUID.Zero; | ||
1511 | |||
1512 | // Attribute is optional | ||
1513 | if (ic.xml.HasAttributes) | ||
1514 | { | ||
1515 | for (int i = 0; i < ic.xml.AttributeCount; i++) | ||
1516 | { | ||
1517 | ic.xml.MoveToAttribute(i); | ||
1518 | switch (ic.xml.Name) | ||
1519 | { | ||
1520 | |||
1521 | case "name" : | ||
1522 | name = ic.xml.Value; | ||
1523 | break; | ||
1524 | |||
1525 | case "type" : | ||
1526 | type = SByte.Parse(ic.xml.Value); | ||
1527 | break; | ||
1528 | |||
1529 | case "description" : | ||
1530 | desc = ic.xml.Value; | ||
1531 | break; | ||
1532 | |||
1533 | case "temporary" : | ||
1534 | temp = Boolean.Parse(ic.xml.Value); | ||
1535 | break; | ||
1536 | |||
1537 | case "invtype" : | ||
1538 | itype = SByte.Parse(ic.xml.Value); | ||
1539 | break; | ||
1540 | |||
1541 | case "uuid" : | ||
1542 | uuid = new LLUUID(ic.xml.Value); | ||
1543 | break; | ||
1544 | |||
1545 | case "inline" : | ||
1546 | inline = Boolean.Parse(ic.xml.Value); | ||
1547 | break; | ||
1548 | |||
1549 | case "local" : | ||
1550 | local = Boolean.Parse(ic.xml.Value); | ||
1551 | break; | ||
1552 | |||
1553 | default : | ||
1554 | Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}", | ||
1555 | MsgId, ic.xml.Name, ic.xml.Value); | ||
1556 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1557 | Rest.HttpStatusDescBadRequest); | ||
1558 | break; | ||
1559 | } | ||
1560 | } | ||
1561 | } | ||
1562 | |||
1563 | ic.xml.MoveToElement(); | ||
1564 | |||
1565 | // If this is a reference to an existing asset, just store the | ||
1566 | // asset ID into the item. | ||
1567 | |||
1568 | if (!inline) | ||
1569 | { | ||
1570 | if (ic.Item != null) | ||
1571 | { | ||
1572 | ic.Item.AssetID = new LLUUID(ic.xml.ReadElementContentAsString()); | ||
1573 | Rest.Log.DebugFormat("{0} Asset ID supplied: {1}", MsgId, ic.Item.AssetID); | ||
1574 | } | ||
1575 | else | ||
1576 | { | ||
1577 | Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId); | ||
1578 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1579 | Rest.HttpStatusDescBadRequest); | ||
1580 | } | ||
1581 | } | ||
1582 | |||
1583 | // Otherwise, generate an asset ID, store that into the item, and | ||
1584 | // create an entry in the asset list for the inlined asset. But | ||
1585 | // only if the size is non-zero. | ||
1586 | |||
1587 | else | ||
1588 | { | ||
1589 | |||
1590 | string b64string = null; | ||
1591 | |||
1592 | // Generate a UUID of none were given, and generally none should | ||
1593 | // be. Ever. | ||
1594 | |||
1595 | if (uuid == LLUUID.Zero) | ||
1596 | { | ||
1597 | uuid = LLUUID.Random(); | ||
1598 | } | ||
1599 | |||
1600 | // Create AssetBase entity to hold the inlined asset | ||
1601 | |||
1602 | asset = new AssetBase(uuid, name); | ||
1603 | |||
1604 | asset.Description = desc; | ||
1605 | asset.Type = type; // type == 0 == texture | ||
1606 | asset.InvType = itype; | ||
1607 | asset.Local = local; | ||
1608 | asset.Temporary = temp; | ||
1609 | |||
1610 | b64string = ic.xml.ReadElementContentAsString(); | ||
1611 | |||
1612 | Rest.Log.DebugFormat("{0} Data length is {1}", MsgId, b64string.Length); | ||
1613 | Rest.Log.DebugFormat("{0} Data content starts with: \n\t<{1}>", MsgId, | ||
1614 | b64string.Substring(0, b64string.Length > 132 ? 132 : b64string.Length)); | ||
1615 | |||
1616 | asset.Data = Convert.FromBase64String(b64string); | ||
1617 | |||
1618 | // Ensure the asset always has some kind of data component | ||
1619 | |||
1620 | if (asset.Data == null) | ||
1621 | { | ||
1622 | asset.Data = new byte[1]; | ||
1623 | } | ||
1624 | |||
1625 | // If this is in the context of an item, establish | ||
1626 | // a link with the item in context. | ||
1627 | |||
1628 | if (ic.Item != null && ic.Item.AssetID == LLUUID.Zero) | ||
1629 | { | ||
1630 | ic.Item.AssetID = uuid; | ||
1631 | } | ||
1632 | |||
1633 | } | ||
1634 | |||
1635 | ic.Push(asset); | ||
1636 | |||
1637 | } | ||
1638 | |||
1639 | /// <summary> | ||
1640 | /// Store any permissions information provided by the request. | ||
1641 | /// This overrides the default permissions set when the | ||
1642 | /// XmlInventoryCollection object was created. | ||
1643 | /// </summary> | ||
1644 | |||
1645 | private void CollectPermissions(XmlInventoryCollection ic) | ||
1646 | { | ||
1647 | |||
1648 | if (ic.xml.HasAttributes) | ||
1649 | { | ||
1650 | for (int i = 0; i < ic.xml.AttributeCount; i++) | ||
1651 | { | ||
1652 | ic.xml.MoveToAttribute(i); | ||
1653 | switch (ic.xml.Name) | ||
1654 | { | ||
1655 | case "current" : | ||
1656 | ic.CurrentPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); | ||
1657 | break; | ||
1658 | case "next" : | ||
1659 | ic.NextPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); | ||
1660 | break; | ||
1661 | case "everyone" : | ||
1662 | ic.EveryOnePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); | ||
1663 | break; | ||
1664 | case "base" : | ||
1665 | ic.BasePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); | ||
1666 | break; | ||
1667 | default : | ||
1668 | Rest.Log.DebugFormat("{0} Permissions: invalid attribute {1}:{2}", | ||
1669 | MsgId,ic.xml.Name, ic.xml.Value); | ||
1670 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1671 | Rest.HttpStatusDescBadRequest); | ||
1672 | break; | ||
1673 | } | ||
1674 | } | ||
1675 | } | ||
1676 | |||
1677 | ic.xml.MoveToElement(); | ||
1678 | |||
1679 | } | ||
1680 | |||
1681 | /// <summary> | ||
1682 | /// This method is called whenever an Item has been successfully | ||
1683 | /// reconstituted from the request's entity. | ||
1684 | /// It uses the information curren tin the XmlInventoryCollection | ||
1685 | /// to complete the item's specification, including any implied | ||
1686 | /// context and asset associations. | ||
1687 | /// It fails the request if any necessary item or asset information | ||
1688 | /// is missing. | ||
1689 | /// </summary> | ||
1690 | |||
1691 | private void Validate(XmlInventoryCollection ic) | ||
1692 | { | ||
1693 | |||
1694 | // There really should be an item present if we've | ||
1695 | // called validate. So fail if there is not. | ||
1696 | |||
1697 | if (ic.Item == null) | ||
1698 | { | ||
1699 | Rest.Log.ErrorFormat("{0} Unable to parse request", MsgId); | ||
1700 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1701 | Rest.HttpStatusDescBadRequest); | ||
1702 | } | ||
1703 | |||
1704 | // Every item is required to have a name (via REST anyway) | ||
1705 | |||
1706 | if (ic.Item.Name == String.Empty) | ||
1707 | { | ||
1708 | Rest.Log.ErrorFormat("{0} An item name MUST be specified", MsgId); | ||
1709 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1710 | Rest.HttpStatusDescBadRequest); | ||
1711 | } | ||
1712 | |||
1713 | // An item MUST have an asset ID. AssetID should never be zero | ||
1714 | // here. It should always get set from the information stored | ||
1715 | // when the Asset element was processed. | ||
1716 | |||
1717 | if (ic.Item.AssetID == LLUUID.Zero) | ||
1718 | { | ||
1719 | |||
1720 | Rest.Log.ErrorFormat("{0} Unable to complete request", MsgId); | ||
1721 | Rest.Log.InfoFormat("{0} Asset information is missing", MsgId); | ||
1722 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1723 | Rest.HttpStatusDescBadRequest); | ||
1724 | |||
1725 | } | ||
1726 | |||
1727 | // If the item is new, then assign it an ID | ||
1728 | |||
1729 | if (ic.Item.ID == LLUUID.Zero) | ||
1730 | { | ||
1731 | ic.Item.ID = LLUUID.Random(); | ||
1732 | } | ||
1733 | |||
1734 | // If the context is being implied, obtain the current | ||
1735 | // folder item's ID. If it was specified explicitly, make | ||
1736 | // sure that theparent folder exists. | ||
1737 | |||
1738 | if (ic.Item.Folder == LLUUID.Zero) | ||
1739 | { | ||
1740 | ic.Item.Folder = ic.Parent(); | ||
1741 | } | ||
1742 | else | ||
1743 | { | ||
1744 | |||
1745 | bool found = false; | ||
1746 | |||
1747 | foreach (InventoryFolderBase parent in ic.rdata.folders) | ||
1748 | { | ||
1749 | if ( parent.ID == ic.Item.Folder ) | ||
1750 | { | ||
1751 | found = true; | ||
1752 | break; | ||
1753 | } | ||
1754 | } | ||
1755 | |||
1756 | if (!found) | ||
1757 | { | ||
1758 | Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}", | ||
1759 | MsgId, ic.Item.Folder, ic.Item.ID); | ||
1760 | ic.Fail(Rest.HttpStatusCodeBadRequest, | ||
1761 | Rest.HttpStatusDescBadRequest); | ||
1762 | } | ||
1763 | |||
1764 | } | ||
1765 | |||
1766 | // If this is an inline asset being constructed in the context | ||
1767 | // of a new Item, then use the itm's name here too. | ||
1768 | |||
1769 | if (ic.Asset != null) | ||
1770 | { | ||
1771 | if (ic.Asset.Name == String.Empty) | ||
1772 | ic.Asset.Name = ic.Item.Name; | ||
1773 | if (ic.Asset.Description == String.Empty) | ||
1774 | ic.Asset.Description = ic.Item.Description; | ||
1775 | } | ||
1776 | |||
1777 | // Assign permissions | ||
1778 | |||
1779 | ic.Item.CurrentPermissions = ic.CurrentPermissions; | ||
1780 | ic.Item.EveryOnePermissions = ic.EveryOnePermissions; | ||
1781 | ic.Item.BasePermissions = ic.BasePermissions; | ||
1782 | ic.Item.NextPermissions = ic.NextPermissions; | ||
1783 | |||
1784 | // If no type was specified for this item, we can attempt to | ||
1785 | // infer something from the file type maybe. This is NOT as | ||
1786 | // good as having type be specified in the XML. | ||
1787 | |||
1788 | if (ic.Item.AssetType == (int) AssetType.Unknown || | ||
1789 | ic.Item.InvType == (int) AssetType.Unknown) | ||
1790 | { | ||
1791 | |||
1792 | Rest.Log.DebugFormat("{0} Attempting to infer item type", MsgId); | ||
1793 | |||
1794 | string[] parts = ic.Item.Name.Split(Rest.CA_PERIOD); | ||
1795 | |||
1796 | if (Rest.DEBUG) | ||
1797 | { | ||
1798 | for (int i = 0; i < parts.Length; i++) | ||
1799 | { | ||
1800 | Rest.Log.DebugFormat("{0} Name part {1} : {2}", | ||
1801 | MsgId, i, parts[i]); | ||
1802 | } | ||
1803 | } | ||
1804 | |||
1805 | // If the associated item name is multi-part, then maybe | ||
1806 | // the last part will indicate the item type - if we're | ||
1807 | // lucky. | ||
1808 | |||
1809 | if (parts.Length > 1) | ||
1810 | { | ||
1811 | Rest.Log.DebugFormat("{0} File type is {1}", | ||
1812 | MsgId, parts[parts.Length - 1]); | ||
1813 | switch (parts[parts.Length - 1]) | ||
1814 | { | ||
1815 | case "jpeg2000" : | ||
1816 | case "jpeg-2000" : | ||
1817 | case "jpg2000" : | ||
1818 | case "jpg-2000" : | ||
1819 | Rest.Log.DebugFormat("{0} Type {1} inferred", | ||
1820 | MsgId, parts[parts.Length-1]); | ||
1821 | if (ic.Item.AssetType == (int) AssetType.Unknown) | ||
1822 | ic.Item.AssetType = (int) AssetType.ImageJPEG; | ||
1823 | if (ic.Item.InvType == (int) AssetType.Unknown) | ||
1824 | ic.Item.InvType = (int) AssetType.ImageJPEG; | ||
1825 | break; | ||
1826 | case "jpg" : | ||
1827 | case "jpeg" : | ||
1828 | Rest.Log.DebugFormat("{0} Type {1} inferred", | ||
1829 | MsgId, parts[parts.Length - 1]); | ||
1830 | if (ic.Item.AssetType == (int) AssetType.Unknown) | ||
1831 | ic.Item.AssetType = (int) AssetType.ImageJPEG; | ||
1832 | if (ic.Item.InvType == (int) AssetType.Unknown) | ||
1833 | ic.Item.InvType = (int) AssetType.ImageJPEG; | ||
1834 | break; | ||
1835 | default : | ||
1836 | Rest.Log.DebugFormat("{0} Type was not inferred", MsgId); | ||
1837 | break; | ||
1838 | } | ||
1839 | } | ||
1840 | } | ||
1841 | |||
1842 | ic.reset(); | ||
1843 | |||
1844 | } | ||
1845 | |||
1846 | #region Inventory RequestData extension | ||
1847 | |||
1848 | internal class InventoryRequestData : RequestData | ||
1849 | { | ||
1850 | |||
1851 | /// <summary> | ||
1852 | /// These are the inventory specific request/response state | ||
1853 | /// extensions. | ||
1854 | /// </summary> | ||
1855 | |||
1856 | internal bool HaveInventory = false; | ||
1857 | internal ICollection<InventoryFolderImpl> folders = null; | ||
1858 | internal ICollection<InventoryItemBase> items = null; | ||
1859 | internal UserProfileData userProfile = null; | ||
1860 | internal InventoryFolderBase root = null; | ||
1861 | |||
1862 | internal InventoryRequestData(OSHttpRequest request, OSHttpResponse response, string prefix) | ||
1863 | : base(request, response, prefix) | ||
1864 | { | ||
1865 | } | ||
1866 | |||
1867 | /// <summary> | ||
1868 | /// This is the callback method required by inventory services. The | ||
1869 | /// requestor issues an inventory request and then blocks until this | ||
1870 | /// method signals the monitor. | ||
1871 | /// </summary> | ||
1872 | |||
1873 | internal void GetUserInventory(ICollection<InventoryFolderImpl> folders, ICollection<InventoryItemBase> items) | ||
1874 | { | ||
1875 | Rest.Log.DebugFormat("{0} Asynchronously updating inventory data", MsgId); | ||
1876 | this.folders = folders; | ||
1877 | this.items = items; | ||
1878 | this.HaveInventory = true; | ||
1879 | lock(this) | ||
1880 | { | ||
1881 | Monitor.Pulse(this); | ||
1882 | } | ||
1883 | } | ||
1884 | |||
1885 | } | ||
1886 | |||
1887 | #endregion Inventory RequestData extension | ||
1888 | |||
1889 | /// <summary> | ||
1890 | /// This class is used to record and manage the hierarchy | ||
1891 | /// constructed from the entity supplied in the request for | ||
1892 | /// PUT and POST. | ||
1893 | /// </summary> | ||
1894 | |||
1895 | internal class XmlInventoryCollection : InventoryCollection | ||
1896 | { | ||
1897 | |||
1898 | internal InventoryRequestData rdata; | ||
1899 | private Stack<InventoryFolderBase> stk; | ||
1900 | |||
1901 | internal List<AssetBase> Assets; | ||
1902 | |||
1903 | internal InventoryItemBase Item; | ||
1904 | internal AssetBase Asset; | ||
1905 | internal XmlReader xml; | ||
1906 | |||
1907 | internal /*static*/ const uint DefaultCurrent = 0x7FFFFFFF; | ||
1908 | internal /*static*/ const uint DefaultNext = 0x82000; | ||
1909 | internal /*static*/ const uint DefaultBase = 0x7FFFFFFF; | ||
1910 | internal /*static*/ const uint DefaultEveryOne = 0x0; | ||
1911 | |||
1912 | internal uint CurrentPermissions = 0x00; | ||
1913 | internal uint NextPermissions = 0x00; | ||
1914 | internal uint BasePermissions = 0x00; | ||
1915 | internal uint EveryOnePermissions = 0x00; | ||
1916 | |||
1917 | internal XmlInventoryCollection() | ||
1918 | { | ||
1919 | Folders = new List<InventoryFolderBase>(); | ||
1920 | Items = new List<InventoryItemBase>(); | ||
1921 | Assets = new List<AssetBase>(); | ||
1922 | } | ||
1923 | |||
1924 | internal void init(InventoryRequestData p_rdata) | ||
1925 | { | ||
1926 | rdata = p_rdata; | ||
1927 | UserID = rdata.uuid; | ||
1928 | stk = new Stack<InventoryFolderBase>(); | ||
1929 | rdata.initXmlReader(); | ||
1930 | xml = rdata.reader; | ||
1931 | initPermissions(); | ||
1932 | } | ||
1933 | |||
1934 | internal void initPermissions() | ||
1935 | { | ||
1936 | CurrentPermissions = DefaultCurrent; | ||
1937 | NextPermissions = DefaultNext; | ||
1938 | BasePermissions = DefaultBase; | ||
1939 | EveryOnePermissions = DefaultEveryOne; | ||
1940 | } | ||
1941 | |||
1942 | internal LLUUID Parent() | ||
1943 | { | ||
1944 | if (stk.Count != 0) | ||
1945 | { | ||
1946 | return stk.Peek().ID; | ||
1947 | } | ||
1948 | else | ||
1949 | { | ||
1950 | return LLUUID.Zero; | ||
1951 | } | ||
1952 | } | ||
1953 | |||
1954 | internal void Push(InventoryFolderBase folder) | ||
1955 | { | ||
1956 | stk.Push(folder); | ||
1957 | Folders.Add(folder); | ||
1958 | reset(); | ||
1959 | } | ||
1960 | |||
1961 | internal void Push(InventoryItemBase item) | ||
1962 | { | ||
1963 | Item = item; | ||
1964 | Items.Add(item); | ||
1965 | } | ||
1966 | |||
1967 | internal void Push(AssetBase asset) | ||
1968 | { | ||
1969 | Asset = asset; | ||
1970 | Assets.Add(asset); | ||
1971 | } | ||
1972 | |||
1973 | internal void Pop() | ||
1974 | { | ||
1975 | stk.Pop(); | ||
1976 | reset(); | ||
1977 | } | ||
1978 | |||
1979 | internal void reset() | ||
1980 | { | ||
1981 | Item = null; | ||
1982 | Asset = null; | ||
1983 | initPermissions(); | ||
1984 | } | ||
1985 | |||
1986 | internal void Fail(int code, string desc) | ||
1987 | { | ||
1988 | rdata.Fail(code, desc); | ||
1989 | } | ||
1990 | |||
1991 | } | ||
1992 | } | ||
1993 | } | ||
diff --git a/prebuild.xml b/prebuild.xml index fdc9eee..29c8a23 100644 --- a/prebuild.xml +++ b/prebuild.xml | |||
@@ -1198,6 +1198,44 @@ | |||
1198 | <Match pattern="*.cs" recurse="true"/> | 1198 | <Match pattern="*.cs" recurse="true"/> |
1199 | </Files> | 1199 | </Files> |
1200 | </Project> | 1200 | </Project> |
1201 | |||
1202 | <Project name="OpenSim.ApplicationPlugins.Rest.Inventory" | ||
1203 | path="OpenSim/ApplicationPlugins/Rest/Inventory" type="Library"> | ||
1204 | <Configuration name="Debug"> | ||
1205 | <Options> | ||
1206 | <OutputPath>../../../../bin/</OutputPath> | ||
1207 | </Options> | ||
1208 | </Configuration> | ||
1209 | <Configuration name="Release"> | ||
1210 | <Options> | ||
1211 | <OutputPath>../../../../bin/</OutputPath> | ||
1212 | </Options> | ||
1213 | </Configuration> | ||
1214 | |||
1215 | <ReferencePath>../../../../bin/</ReferencePath> | ||
1216 | <Reference name="Mono.Addins.dll" /> | ||
1217 | <Reference name="System"/> | ||
1218 | <Reference name="System.IO"/> | ||
1219 | <Reference name="System.Xml"/> | ||
1220 | <Reference name="System.Xml.Serialization"/> | ||
1221 | <Reference name="libsecondlife.dll" /> | ||
1222 | <Reference name="Nini.dll" /> | ||
1223 | <Reference name="XMLRPC.dll" /> | ||
1224 | <Reference name="OpenSim"/> | ||
1225 | <Reference name="OpenSim.Region.ClientStack"/> | ||
1226 | <Reference name="OpenSim.Region.Environment"/> | ||
1227 | <Reference name="OpenSim.Framework.Communications"/> | ||
1228 | <Reference name="OpenSim.Framework"/> | ||
1229 | <Reference name="OpenSim.Framework.Servers"/> | ||
1230 | <Reference name="OpenSim.Framework.Console"/> | ||
1231 | <Reference name="OpenSim.ApplicationPlugins.Rest"/> | ||
1232 | <Reference name="log4net"/> | ||
1233 | |||
1234 | <Files> | ||
1235 | <Match pattern="*.cs" recurse="true"/> | ||
1236 | </Files> | ||
1237 | </Project> | ||
1238 | |||
1201 | <!-- /REST plugins --> | 1239 | <!-- /REST plugins --> |
1202 | 1240 | ||
1203 | <!-- Scene Server API Example Apps --> | 1241 | <!-- Scene Server API Example Apps --> |