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 /OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs | |
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.
Diffstat (limited to '')
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs | 1201 |
1 files changed, 1201 insertions, 0 deletions
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 | } | ||