/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSim Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ using System; using System.IO; using System.Reflection; using System.Text; using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Collections.Generic; using System.Collections.Specialized; using OpenSim.Framework; using OpenSim.Framework.Servers; using libsecondlife; using System.Xml; namespace OpenSim.ApplicationPlugins.Rest.Inventory { /// /// This class represents the current REST request. It /// encapsulates the request/response state and takes care /// of response generation without exposing the REST handler /// to the actual mechanisms involved. /// /// This structure is created on entry to the Handler /// method and is disposed of upon return. It is part of /// the plug-in infrastructure, rather than the functionally /// specific REST handler, and fundamental changes to /// this should be reflected in the Rest HandlerVersion. The /// object is instantiated, and may be extended by, any /// given handler. See the inventory handler for an example /// of this. /// /// If possible, the underlying request/response state is not /// changed until the handler explicitly issues a Respond call. /// This ensures that the request/response pair can be safely /// processed by subsequent, unrelated, handlers even id the /// agent handler had completed much of its processing. Think /// of it as a transactional req/resp capability. /// internal class RequestData { // HTTP Server interface data internal OSHttpRequest request = null; internal OSHttpResponse response = null; internal string qprefix = null; // Request lifetime values internal byte[] buffer = null; internal string body = null; internal string html = null; internal string entity = null; internal string path = null; internal string method = null; internal string statusDescription = null; internal string redirectLocation = null; internal string[] pathNodes = null; internal string[] parameters = null; internal int statusCode = 0; internal bool handled = false; internal LLUUID uuid = LLUUID.Zero; internal Encoding encoding = Rest.Encoding; internal Uri uri = null; internal string query = null; internal bool fail = false; internal string hostname = "localhost"; internal int port = 80; internal string prefix = Rest.UrlPathSeparator; internal bool keepAlive = false; internal bool chunked = false; // Authentication related state internal bool authenticated = false; // internal string scheme = Rest.AS_DIGEST; // internal string scheme = Rest.AS_BASIC; internal string scheme = null; internal string realm = Rest.Realm; internal string domain = null; internal string nonce = null; internal string cnonce = null; internal string qop = Rest.Qop_Auth; internal string opaque = null; internal string stale = null; internal string algorithm = Rest.Digest_MD5; internal string authParms = null; internal string authPrefix = null; internal string userName = String.Empty; internal string userPass = String.Empty; internal LLUUID client = LLUUID.Zero; // XML related state internal XmlWriter writer = null; internal XmlReader reader = null; // Internal working state private StringBuilder sbuilder = new StringBuilder(1024); private MemoryStream xmldata = null; private static readonly string[] EmptyPath = { String.Empty }; // Session related tables. These are only needed if QOP is set to "auth-sess" // and for now at least, it is not. Session related authentication is of // questionable merit in the context of REST anyway, but it is, arguably, more // secure. private static Dictionary cntable = new Dictionary(); private static Dictionary sktable = new Dictionary(); // This dictionary is used to keep track fo all of the parameters discovered // when the authorisation header is anaylsed. private Dictionary authparms = new Dictionary(); // These regular expressions are used to decipher the various header entries. private static Regex schema = new Regex("^\\s*(?\\w+)\\s*.*", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex basicParms = new Regex("^\\s*(?:\\w+)\\s+(?\\S+)\\s*", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex digestParm1 = new Regex("\\s*(?\\w+)\\s*=\\s*\"(?[^\"]+)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex digestParm2 = new Regex("\\s*(?\\w+)\\s*=\\s*(?[^\\p{P}\\s]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex reuserPass = new Regex("\\s*(?[^:]+)\\s*:\\s*(?\\S*)", RegexOptions.Compiled | RegexOptions.IgnoreCase); // For efficiency, we create static instances of these objects private static MD5 md5hash = MD5.Create(); private static StringComparer sc = StringComparer.OrdinalIgnoreCase; // Constructor internal RequestData(OSHttpRequest p_request, OSHttpResponse p_response, string p_qprefix) { request = p_request; response = p_response; qprefix = p_qprefix; sbuilder.Length = 0; encoding = request.ContentEncoding; if (encoding == null) { encoding = Rest.Encoding; } method = request.HttpMethod.ToLower(); initUrl(); initParameters(p_qprefix.Length); } // Just for convenience... internal string MsgId { get { return Rest.MsgId; } } // Defer authentication check until requested internal bool IsAuthenticated { get { if (Rest.Authenticate) { if (!authenticated) { authenticate(); } return authenticated; } else return true; } } /// /// The REST handler has requested authentication. Authentication /// is considered to be with respect to the current values for /// Realm, domain, etc. /// /// This method checks to see if the current request is already /// authenticated for this domain. If it is, then it returns /// true. If it is not, then it issues a challenge to the client /// and responds negatively to the request. /// private void authenticate() { string authdata = request.Headers.Get("Authorization"); string reqscheme = String.Empty; // If we don't have an authorization header, then this // user is certainly not authorized. This is the typical // pivot for the 1st request by a client. if (authdata == null) { Rest.Log.DebugFormat("{0} Challenge reason: No authorization data", MsgId); DoChallenge(); } // So, we have authentication data, now we have to check to // see what we got and whether or not it is valid for the // current domain. To do this we need to interpret the data // provided in the Authorization header. First we need to // identify the scheme being used and route accordingly. MatchCollection matches = schema.Matches(authdata); foreach (Match m in matches) { Rest.Log.DebugFormat("{0} Scheme matched : {1}", MsgId, m.Groups["scheme"].Value); reqscheme = m.Groups["scheme"].Value.ToLower(); } // If we want a specific authentication mechanism, make sure // we get it. null indicates we don't care. non-null indicates // a specific scheme requirement. if (scheme != null && scheme.ToLower() != reqscheme) { Rest.Log.DebugFormat("{0} Challenge reason: Requested scheme not acceptable", MsgId); DoChallenge(); } // In the future, these could be made into plug-ins... // But for now at least we have no reason to use anything other // then MD5. TLS/SSL are taken care of elsewhere. switch (reqscheme) { case "digest" : Rest.Log.DebugFormat("{0} Digest authentication offered", MsgId); DoDigest(authdata); break; case "basic" : Rest.Log.DebugFormat("{0} Basic authentication offered", MsgId); DoBasic(authdata); break; } // If the current header is invalid, then a challenge is still needed. if (!authenticated) { Rest.Log.DebugFormat("{0} Challenge reason: Authentication failed", MsgId); DoChallenge(); } } /// /// Construct the necessary WWW-Authenticate headers and fail the request /// with a NOT AUTHORIZED response. The parameters are the union of values /// required by the supported schemes. /// private void DoChallenge() { Flush(); nonce = Rest.NonceGenerator(); // should be unique per 401 (and it is) Challenge(scheme, realm, domain, nonce, opaque, stale, algorithm, qop, authParms); Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); } /// /// Interpret a BASIC authorization claim /// This is here for completeness, it is not used. /// private void DoBasic(string authdata) { string response = null; MatchCollection matches = basicParms.Matches(authdata); // In the case of basic authentication there is // only expected to be a single argument. foreach (Match m in matches) { authparms.Add("response",m.Groups["pval"].Value); Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", MsgId, "response", m.Groups["pval"].Value); } // Did we get a valid response? if (authparms.TryGetValue("response", out response)) { // Decode response = Rest.Base64ToString(response); Rest.Log.DebugFormat("{0} Auth response is: <{1}>", MsgId, response); // Extract user & password Match m = reuserPass.Match(response); userName = m.Groups["user"].Value; userPass = m.Groups["pass"].Value; // Validate against user database authenticated = Validate(userName,userPass); } } /// /// This is an RFC2617 compliant HTTP MD5 Digest authentication /// implementation. It has been tested with Firefox, Java HTTP client, /// and Miscrosoft's Internet Explorer V7. /// private void DoDigest(string authdata) { string response = null; MatchCollection matches = digestParm1.Matches(authdata); // Collect all of the supplied parameters and store them // in a dictionary (for ease of access) foreach (Match m in matches) { authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); } // And pick up any tokens too matches = digestParm2.Matches(authdata); foreach (Match m in matches) { authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); Rest.Log.DebugFormat("{0} Tokenized Parameter matched : {1} = {2}", MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); } // A response string MUST be returned, otherwise we are // NOT authenticated. Rest.Log.DebugFormat("{0} Validating authorization parameters", MsgId); if (authparms.TryGetValue("response", out response)) { string temp = null; do { string nck = null; string ncl = null; // The userid is sent in clear text. Needed for the // verification. authparms.TryGetValue("username", out userName); // All URI's of which this is a prefix are // optimistically considered to be authenticated by the // client. This is also needed to verify the response. authparms.TryGetValue("uri", out authPrefix); // There MUST be a nonce string present. We're not preserving any server // side state and we can't validate the MD5 unless the client returns it // to us, as it should. if (!authparms.TryGetValue("nonce", out nonce) || nonce == null) { Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); break; } // If there is an opaque string present, it had better // match what we sent. if (authparms.TryGetValue("opaque", out temp)) { if (temp != opaque) { Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); break; } } // If an algorithm string is present, it had better // match what we sent. if (authparms.TryGetValue("algorithm", out temp)) { if (temp != algorithm) { Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); break; } } // Quality of protection considerations... if (authparms.TryGetValue("qop", out temp)) { qop = temp.ToLower(); // replace with actual value used // if QOP was specified then // these MUST be present. if (!authparms.ContainsKey("cnonce")) { Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); break; } cnonce = authparms["cnonce"]; if (!authparms.TryGetValue("nc", out nck) || nck == null) { Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); break; } Rest.Log.DebugFormat("{0} Comparing nonce indices", MsgId); if (cntable.TryGetValue(nonce, out ncl)) { Rest.Log.DebugFormat("{0} nonce values: Verify that request({1}) > Reference({2})", MsgId, nck, ncl); if (Rest.Hex2Int(ncl) >= Rest.Hex2Int(nck)) { Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); break; } cntable[nonce] = nck; } else { lock (cntable) cntable.Add(nonce, nck); } } else { qop = String.Empty; // if QOP was not specified then // these MUST NOT be present. if (authparms.ContainsKey("cnonce")) { Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce", MsgId); break; } if (authparms.ContainsKey("nc")) { Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce counter[2]", MsgId); break; } } // Validate the supplied userid/password info authenticated = ValidateDigest(userName, nonce, cnonce, nck, authPrefix, response); } while (false); } } // Indicate that authentication is required internal void Challenge(string scheme, string realm, string domain, string nonce, string opaque, string stale, string alg, string qop, string auth) { sbuilder.Length = 0; if (scheme == null || scheme == Rest.AS_BASIC) { sbuilder.Append(Rest.AS_BASIC); if (realm != null) { sbuilder.Append(" realm=\""); sbuilder.Append(realm); sbuilder.Append("\""); } AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); } sbuilder.Length = 0; if (scheme == null || scheme == Rest.AS_DIGEST) { sbuilder.Append(Rest.AS_DIGEST); sbuilder.Append(" "); if (realm != null) { sbuilder.Append("realm="); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(realm); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(Rest.CS_COMMA); } if (nonce != null) { sbuilder.Append("nonce="); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(nonce); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(Rest.CS_COMMA); } if (opaque != null) { sbuilder.Append("opaque="); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(opaque); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(Rest.CS_COMMA); } if (stale != null) { sbuilder.Append("stale="); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(stale); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(Rest.CS_COMMA); } if (alg != null) { sbuilder.Append("algorithm="); sbuilder.Append(alg); sbuilder.Append(Rest.CS_COMMA); } if (qop != String.Empty) { sbuilder.Append("qop="); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(qop); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(Rest.CS_COMMA); } if (auth != null) { sbuilder.Append(auth); sbuilder.Append(Rest.CS_COMMA); } // We don;t know the userid that will be used // so we cannot make any authentication domain // assumptions. So the prefix will determine // this. sbuilder.Append("domain="); sbuilder.Append(Rest.CS_DQUOTE); sbuilder.Append(qprefix); sbuilder.Append(Rest.CS_DQUOTE); AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); } } /// /// This method provides validation in support of the BASIC /// authentication method. This is not normaly expected to be /// used, but is included for completeness (and because I tried /// it first). /// private bool Validate(string user, string pass) { Rest.Log.DebugFormat("{0} Simple User Validation", MsgId); // Both values are required if (user == null || pass == null) return false; // Eliminate any leading or trailing spaces user = user.Trim(); return vetPassword(user, pass); } /// /// This mechanism is used by the digest authetnication mechanism /// to return the user's password. In fact, because the OpenSim /// user's passwords are already hashed, and the HTTP mechanism /// does not supply an open password, the hashed passwords cannot /// be used unless the cliemt has used the same salting mechanism /// to has the password before using it in the authentication /// algorithn. This is not inconceivable... /// private string getPassword(string user) { int x; string first; string last; // Distinguish the parts, if necessary if ((x=user.IndexOf(Rest.C_SPACE)) != -1) { first = user.Substring(0,x); last = user.Substring(x+1); } else { first = user; last = String.Empty; } UserProfileData udata = Rest.UserServices.GetUserProfile(first, last); // If we don;t recognize the user id, perhaps it is god? if (udata == null) { Rest.Log.DebugFormat("{0} Administrator", MsgId); return Rest.GodKey; } else { Rest.Log.DebugFormat("{0} Normal User {1}", MsgId, user); return udata.PasswordHash; } } /// /// This is used by the BASIC authentication scheme to calculate /// the double hash used by OpenSim to encode user's passwords. /// It returns true, if the supplied password is actually correct. /// If the specified user-id is not recognized, but the password /// matches the God password, then this is accepted as an admin /// session. /// private bool vetPassword(string user, string pass) { int x; string HA1; string first; string last; // Distinguish the parts, if necessary if ((x=user.IndexOf(Rest.C_SPACE)) != -1) { first = user.Substring(0,x); last = user.Substring(x+1); } else { first = user; last = String.Empty; } UserProfileData udata = Rest.UserServices.GetUserProfile(first, last); // If we don;t recognize the user id, perhaps it is god? if (udata == null) return pass == Rest.GodKey; HA1 = HashToString(pass); HA1 = HashToString(String.Format("{0}:{1}",HA1,udata.PasswordSalt)); return (0 == sc.Compare(HA1, udata.PasswordHash)); } // Validate the request-digest private bool ValidateDigest(string user, string nonce, string cnonce, string nck, string uri, string response) { string patt = null; string payl = String.Empty; string KDS = null; string HA1 = null; string HA2 = null; string pass = getPassword(user); // Generate H(A1) if (algorithm == Rest.Digest_MD5Sess) { if (!sktable.ContainsKey(cnonce)) { patt = String.Format("{0}:{1}:{2}:{3}:{4}", user, realm, pass, nonce, cnonce); HA1 = HashToString(patt); sktable.Add(cnonce, HA1); } else { HA1 = sktable[cnonce]; } } else { patt = String.Format("{0}:{1}:{2}", user, realm, pass); HA1 = HashToString(patt); } // Generate H(A2) if (qop == "auth-int") { patt = String.Format("{0}:{1}:{2}", request.HttpMethod, uri, HashToString(payl)); } else { patt = String.Format("{0}:{1}", request.HttpMethod, uri); } HA2 = HashToString(patt); // Generate Digest if (qop != String.Empty) { patt = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, nonce, nck, cnonce, qop, HA2); } else { patt = String.Format("{0}:{1}:{2}", HA1, nonce, HA2); } KDS = HashToString(patt); // Compare the generated sequence with the original return (0 == sc.Compare(KDS, response)); } private string HashToString(string pattern) { Rest.Log.DebugFormat("{0} Generate <{1}>", MsgId, pattern); byte[] hash = md5hash.ComputeHash(encoding.GetBytes(pattern)); sbuilder.Length = 0; for (int i = 0; i < hash.Length; i++) { sbuilder.Append(hash[i].ToString("x2")); } Rest.Log.DebugFormat("{0} Hash = <{1}>", MsgId, sbuilder.ToString()); return sbuilder.ToString(); } internal void Complete() { statusCode = Rest.HttpStatusCodeOK; statusDescription = Rest.HttpStatusDescOK; } internal void Redirect(string Url, bool temp) { redirectLocation = Url; if (temp) { statusCode = Rest.HttpStatusCodeTemporaryRedirect; statusDescription = Rest.HttpStatusDescTemporaryRedirect; } else { statusCode = Rest.HttpStatusCodePermanentRedirect; statusDescription = Rest.HttpStatusDescPermanentRedirect; } Fail(statusCode, statusDescription, true); } // Fail for an arbitrary reason. Just a failure with // headers. internal void Fail(int code, string message) { Fail(code, message, true); } // More adventurous. This failure also includes a // specified entity. internal void Fail(int code, string message, string data) { buffer = null; body = data; Fail(code, message, false); } internal void Fail(int code, string message, bool reset) { statusCode = code; statusDescription = message; if (reset) { buffer = null; SendHtml(message); body = html; } if (Rest.DEBUG) { Rest.Log.DebugFormat("{0} Request Failure State Dump", MsgId); Rest.Log.DebugFormat("{0} Scheme = {1}", MsgId, scheme); Rest.Log.DebugFormat("{0} Realm = {1}", MsgId, realm); Rest.Log.DebugFormat("{0} Domain = {1}", MsgId, domain); Rest.Log.DebugFormat("{0} Nonce = {1}", MsgId, nonce); Rest.Log.DebugFormat("{0} CNonce = {1}", MsgId, cnonce); Rest.Log.DebugFormat("{0} Opaque = {1}", MsgId, opaque); Rest.Log.DebugFormat("{0} Stale = {1}", MsgId, stale); Rest.Log.DebugFormat("{0} Algorithm = {1}", MsgId, algorithm); Rest.Log.DebugFormat("{0} QOP = {1}", MsgId, qop); Rest.Log.DebugFormat("{0} AuthPrefix = {1}", MsgId, authPrefix); Rest.Log.DebugFormat("{0} UserName = {1}", MsgId, userName); Rest.Log.DebugFormat("{0} UserPass = {1}", MsgId, userPass); } fail = true; Respond("Failure response"); RestException re = new RestException(message+" <"+code+">"); re.statusCode = code; re.statusDesc = message; re.httpmethod = method; re.httppath = path; throw re; } // Reject this request internal void Reject() { Fail(Rest.HttpStatusCodeNotImplemented, Rest.HttpStatusDescNotImplemented); } // This MUST be called by an agent handler before it returns // control to Handle, otherwise the request will be ignored. // This is called implciitly for the REST stream handlers and // is harmless if it is called twice. internal virtual bool Respond(string reason) { Rest.Log.DebugFormat("{0} Respond ENTRY, handled = {1}, reason = {2}", MsgId, handled, reason); if (!handled) { Rest.Log.DebugFormat("{0} Generating Response", MsgId); Rest.Log.DebugFormat("{0} Method is {1}", MsgId, method); // A Head request can NOT have a body! if (method != Rest.HEAD) { Rest.Log.DebugFormat("{0} Response is not abbreviated", MsgId); // If the writer is non-null then we know that an XML // data component exists. Flush and close the writer and // then convert the result to the expected buffer format // unless the request has already been failed for some // reason. if (writer != null) { Rest.Log.DebugFormat("{0} XML Response handler extension ENTRY", MsgId); Rest.Log.DebugFormat("{0} XML Response exists", MsgId); writer.Flush(); writer.Close(); if (!fail) { buffer = xmldata.ToArray(); AddHeader("Content-Type","application/xml"); } xmldata.Close(); Rest.Log.DebugFormat("{0} XML Response encoded", MsgId); Rest.Log.DebugFormat("{0} XML Response handler extension EXIT", MsgId); } // If buffer != null, then we assume that // this has already been done some other // way. For example, transfer encoding might // have been done. if (buffer == null) { if (body != null && body.Length > 0) { Rest.Log.DebugFormat("{0} String-based entity", MsgId); buffer = encoding.GetBytes(body); } } // OK, if the buffer contains something, regardless of how // it got there, set various response headers accordingly. if (buffer != null) { Rest.Log.DebugFormat("{0} Buffer-based entity", MsgId); response.ContentLength64 = buffer.Length; } else { response.ContentLength64 = 0; } if (response.Headers.Get("Content-Encoding") == null) response.ContentEncoding = encoding; response.SendChunked = chunked; response.KeepAlive = keepAlive; } // Set the status code & description. If nothing has been stored, // we consider that a success. if (statusCode == 0) { Complete(); } // Set the response code in the actual carrier response.StatusCode = statusCode; // For a redirect we need to set the relocation header accordingly if (response.StatusCode == (int) Rest.HttpStatusCodeTemporaryRedirect || response.StatusCode == (int) Rest.HttpStatusCodePermanentRedirect) { Rest.Log.DebugFormat("{0} Re-direct location is {1}", MsgId, redirectLocation); response.RedirectLocation = redirectLocation; } // And include the status description if provided. if (statusDescription != null) { Rest.Log.DebugFormat("{0} Status description is {1}", MsgId, statusDescription); response.StatusDescription = statusDescription; } // Finally we send back our response. // We've left the setting of handled' until the // last minute because the header settings included // above are pretty harmless. But everything from // here on down probably leaves the response // element unusable by anyone else. handled = true; DumpHeaders(); // if (request.InputStream != null) // { // Rest.Log.DebugFormat("{0} Closing input stream", MsgId); // request.InputStream.Close(); // } if (buffer != null && buffer.Length != 0) { Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", MsgId, buffer.Length, encoding.GetString(buffer)); response.OutputStream.Write(buffer, 0, buffer.Length); } // Closing the outputstream should complete the transmission process Rest.Log.DebugFormat("{0} Closing output stream", MsgId); response.OutputStream.Close(); } Rest.Log.DebugFormat("{0} Respond EXIT, handled = {1}, reason = {2}", MsgId, handled, reason); return handled; } // Add a header to the table. We need to allow // multiple instances of many of the headers. // If the internal void AddHeader(string hdr, string data) { if (Rest.DEBUG) { Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", MsgId, hdr, data); if (response.Headers.Get(hdr) != null) { Rest.Log.DebugFormat("{0} Multipe {1} headers will be generated>", MsgId, hdr); } } response.Headers.Add(hdr, data); } // Keep explicit track of any headers which // are to be removed. internal void RemoveHeader(string hdr) { if (Rest.DEBUG) { Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, hdr); if (response.Headers.Get(hdr) == null) { Rest.Log.DebugFormat("{0} No such header existed", MsgId, hdr); } } response.Headers.Remove(hdr); } /// /// Dump headers that will be generated in the response /// internal void DumpHeaders() { if (Rest.DEBUG) { for (int i=0;i /// Helper methods for deconstructing and reconstructing /// URI path data. /// private void initUrl() { uri = request.Url; if (query == null) { query = uri.Query; } // If the path has not been previously initialized, // do so now. if (path == null) { path = uri.AbsolutePath; if (path.EndsWith(Rest.UrlPathSeparator)) path = path.Substring(0,path.Length-1); } // If we succeeded in getting a path, perform any // additional pre-processing required. if (path != null) { if (Rest.ExtendedEscape) { // Handle "+". Not a standard substitution, but // common enough... path = path.Replace(Rest.C_PLUS,Rest.C_SPACE); } pathNodes = path.Split(Rest.CA_PATHSEP); } else { pathNodes = EmptyPath; } // Elimiate any %-escaped values. This is left until here // so that escaped "+' are not mistakenly replaced. path = Uri.UnescapeDataString(path); // Request server context info hostname = uri.Host; port = uri.Port; } internal int initParameters(int prfxlen) { if (prfxlen < path.Length-1) { parameters = path.Substring(prfxlen+1).Split(Rest.CA_PATHSEP); } else { parameters = new string[0]; } // Generate a debug list of the decoded parameters if (Rest.DEBUG && prfxlen < path.Length-1) { Rest.Log.DebugFormat("{0} URI: Parameters: {1}", MsgId, path.Substring(prfxlen)); for (int i = 0; i < parameters.Length; i++) { Rest.Log.DebugFormat("{0} Parameter[{1}]: {2}", MsgId, i, parameters[i]); } } return parameters.Length; } internal string[] PathNodes { get { if (pathNodes == null) { initUrl(); } return pathNodes; } } internal string BuildUrl(int first, int last) { if (pathNodes == null) { initUrl(); } if (first < 0) { first = first + pathNodes.Length; } if (last < 0) { last = last + pathNodes.Length; if (last < 0) { return Rest.UrlPathSeparator; } } sbuilder.Length = 0; sbuilder.Append(Rest.UrlPathSeparator); if (first <= last) { for (int i = first; i <= last; i++) { sbuilder.Append(pathNodes[i]); sbuilder.Append(Rest.UrlPathSeparator); } } else { for (int i = last; i >= first; i--) { sbuilder.Append(pathNodes[i]); sbuilder.Append(Rest.UrlPathSeparator); } } return sbuilder.ToString(); } // Setup the XML writer for output internal void initXmlWriter() { XmlWriterSettings settings = new XmlWriterSettings(); xmldata = new MemoryStream(); settings.Indent = true; settings.IndentChars = " "; settings.Encoding = encoding; settings.CloseOutput = false; settings.OmitXmlDeclaration = true; settings.ConformanceLevel = ConformanceLevel.Fragment; writer = XmlWriter.Create(xmldata, settings); } internal void initXmlReader() { XmlReaderSettings settings = new XmlReaderSettings(); settings.ConformanceLevel = ConformanceLevel.Fragment; settings.IgnoreComments = true; settings.IgnoreWhitespace = true; settings.IgnoreProcessingInstructions = true; settings.ValidationType = ValidationType.None; reader = XmlReader.Create(request.InputStream,settings); } private void Flush() { byte[] dbuffer = new byte[8192]; while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0); return; } // This allows us to make errors a bit more apparent in REST internal void SendHtml(string text) { SendHtml("OpenSim REST Interface 1.0", text); } internal void SendHtml(string title, string text) { AddHeader(Rest.HttpHeaderContentType, "text/html"); sbuilder.Length = 0; sbuilder.Append(""); sbuilder.Append(""); sbuilder.Append(""); sbuilder.Append(title); sbuilder.Append(""); sbuilder.Append(""); sbuilder.Append(""); sbuilder.Append("
"); sbuilder.Append("

"); sbuilder.Append(text); sbuilder.Append("

"); sbuilder.Append(""); sbuilder.Append(""); html = sbuilder.ToString(); } } }