From 5e83a758157520d48b15c725f5be2b196d2414e3 Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Wed, 20 Aug 2008 10:11:11 +0000 Subject: From: Alan Webb cleanups of the REST inventory code. --- .../Rest/Inventory/RequestData.cs | 1106 ++++++++++++-------- 1 file changed, 647 insertions(+), 459 deletions(-) (limited to 'OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs') diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs index d08f830..6742402 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs @@ -23,6 +23,7 @@ * 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; @@ -40,16 +41,17 @@ using System.Xml; namespace OpenSim.ApplicationPlugins.Rest.Inventory { + /// /// This class represents the current REST request. It - /// encapsulates the request/response state and takes care + /// 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 + /// 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 @@ -63,44 +65,109 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// of it as a transactional req/resp capability. /// - internal class RequestData + public class RequestData { - // HTTP Server interface data + + // HTTP Server interface data (Received values) internal OSHttpRequest request = null; internal OSHttpResponse response = null; internal string qprefix = null; // Request lifetime values + // buffer is global because it is referenced by the handler + // in supported of streamed requests. + // If a service provider wants to construct the message + // body explicitly it can use body to do this. The value + // in body is used if the buffer is still null when a response + // is generated. + // Storing information in body will suppress the return of + // statusBody which is only intended to report status on + // requests which do not themselves ordinarily generate + // an informational response. All of this is handled in + // Respond(). internal byte[] buffer = null; - internal string body = null; - internal string html = null; - internal string entity = null; + internal string body = null; + internal string bodyType = "text/html"; + + // The encoding in effect is set to a server default. It may + // subsequently be overridden by a Content header. This + // value is established during construction and is used + // wherever encoding services are needed. + + internal Encoding encoding = Rest.Encoding; + + // These values are derived from the supplied URL. They + // are initialized during construction. + 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; + + // The path part of the URI is decomposed. pathNodes + // is an array of every element in the URI. Parameters + // is an array that contains only those nodes that + // are not a part of the authority prefix + + private string[] pathNodes = null; + private string[] parameters = null; + private static readonly string[] EmptyPath = { String.Empty }; + + // The status code gets set during the course of processing + // and is the HTTP completion code. The status body is + // initialized during construction, is appended to during the + // course of execution, and is finalized during Respond + // processing. + // + // Fail processing marks the request as failed and this is + // then used to inhibit processing during Response processing. + + internal int statusCode = 0; + internal string statusBody = String.Empty; + internal bool fail = false; + + // This carries the URL to which the client should be redirected. + // It is set by the service provider using the Redirect call. + + internal string redirectLocation = null; + + // These values influence response processing. They can be set by + // service providers according to need. The defaults are generally + // good. + internal bool keepAlive = false; internal bool chunked = false; - // Authentication related state + // XML related state + internal XmlWriter writer = null; + internal XmlReader reader = null; + + // Internal working state + + private StringBuilder sbuilder = new StringBuilder(1024); + private MemoryStream xmldata = null; + + // This is used to make the response mechanism idempotent. + + internal bool handled = false; + + // Authentication related state + // + // Two supported authentication mechanisms are: + // scheme = Rest.AS_BASIC; + // scheme = Rest.AS_DIGEST; + // Presented in that order (as required by spec) + // A service provider can set the scheme variable to + // force selection of a particular authentication model + // (choosing from amongst those supported of course) + // + 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; @@ -114,22 +181,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 + // 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. @@ -145,46 +199,26 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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]*)", 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); - } +#region properties // Just for convenience... @@ -193,12 +227,16 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory get { return Rest.MsgId; } } - // Defer authentication check until requested + /// + /// Return a boolean indication of whether or no an authenticated user is + /// associated with this request. This could be wholly integrated, but + /// that would make authentication mandatory. + /// internal bool IsAuthenticated { get - { + { if (Rest.Authenticate) { if (!authenticated) @@ -208,24 +246,87 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory return authenticated; } - else - return true; + else return true; + } + } + + /// + /// Access to all 'nodes' in the supplied URI as an + /// array of strings. + /// + + internal string[] PathNodes + { + get + { + return pathNodes; + } + } + + /// + /// Access to all non-prefix 'nodes' in the supplied URI as an + /// array of strings. These identify a specific resource that + /// is managed by the authority (the prefix). + /// + + internal string[] Parameters + { + get + { + return parameters; + } + } + +#endregion properties + +#region constructors + + // 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); + } +#endregion constructors + +#region authentication_common + /// /// 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 + /// 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. + /// + /// As soon as authentication failure is detected the method calls + /// DoChallenge() which terminates the request with REST exception + /// for unauthroized access. /// private void authenticate() { + string authdata = request.Headers.Get("Authorization"); string reqscheme = String.Empty; @@ -238,7 +339,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 @@ -287,6 +388,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Challenge reason: Authentication failed", MsgId); DoChallenge(); } + } /// @@ -300,16 +402,193 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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); + Fail(Rest.HttpStatusCodeNotAuthorized); + } + + /// + /// The Flush() call is here to support a problem encountered with the + /// client where an authentication rejection was lost because the rejection + /// may flow before the clienthas finished sending us the inbound data stream, + /// in which case the client responds to the socket error on out put, and + /// never sees the authentication challenge. The client should be fixed, + /// because this solution leaves the server prone to DOS attacks. A message + /// will be issued whenever flushing occurs. It can be enabled/disabled from + /// the configuration file. + /// + + private void Flush() + { + if (Rest.FlushEnabled) + { + byte[] dbuffer = new byte[8192]; + Rest.Log.WarnFormat("{0} REST server is flushing the inbound data stream", MsgId); + while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0); + } + return; + } + + // Indicate that authentication is required + + private void Challenge(string scheme, string realm, string domain, string nonce, + string opaque, string stale, string alg, + string qop, string auth) + { + + sbuilder.Length = 0; + + // The service provider can force a particular scheme by + // assigning a value to scheme. + + // Basic authentication is pretty simple. + // Just specify the realm in question. + + 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; + + // Digest authentication takes somewhat more + // to express. + + if (scheme == null || scheme == Rest.AS_DIGEST) + { + + sbuilder.Append(Rest.AS_DIGEST); + sbuilder.Append(" "); + + // Specify the effective realm. This should + // never be null if we are uthenticating, as it is required for all + // authentication schemes. It defines, in conjunction with the + // absolute URI information, the domain to which the authentication + // applies. It is an arbitrary string. I *believe* this allows an + // authentication to apply to disjoint resources within the same + // server. + + if (realm != null) + { + sbuilder.Append("realm="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(realm); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // Share our nonce. This is *uniquely* generated each time a 401 is + // returned. We do not generate a very sophisticated nonce at the + // moment (it's simply a base64 encoded UUID). + + if (nonce != null) + { + sbuilder.Append("nonce="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(nonce); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // The opaque string should be returned by the client unchanged in all + // subsequent requests. + + if (opaque != null) + { + sbuilder.Append("opaque="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(opaque); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // This flag indicates that the authentication was rejected because the + // included nonce was stale. The server might use timestamp information + // in the nonce to determine this. We do not. + + if (stale != null) + { + sbuilder.Append("stale="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(stale); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // Identifies the algorithm used to produce the digest and checksum. + // The default is MD5. + + if (alg != null) + { + sbuilder.Append("algorithm="); + sbuilder.Append(alg); + sbuilder.Append(Rest.CS_COMMA); + } + + // Theoretically QOP is optional, but it is required by a compliant + // with current versions of the scheme. In fact IE requires that QOP + // be specified and will refuse to authenticate otherwise. + + 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); + } + + // This parameter allows for arbitrary extensions to the protocol. + // Unrecognized values should be simply ignored. + + 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); + + // Generate the authenticate header and we're basically + // done. + + AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); + + } + } +#endregion authentication_common + +#region authentication_basic + /// - /// Interpret a BASIC authorization claim - /// This is here for completeness, it is not used. + /// Interpret a BASIC authorization claim. Some clients can only + /// understand this and also expect it to be the first one + /// offered. So we do. + /// OpenSim also needs this, as it is the only scheme that allows + /// authentication using the hashed passwords stored in the + /// user database. /// private void DoBasic(string authdata) { + string response = null; MatchCollection matches = basicParms.Matches(authdata); @@ -320,7 +599,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory foreach (Match m in matches) { authparms.Add("response",m.Groups["pval"].Value); - Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", + Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", MsgId, "response", m.Groups["pval"].Value); } @@ -340,38 +619,111 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Validate against user database authenticated = Validate(userName,userPass); } + + } + + /// + /// 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 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)); + } +#endregion authentication_basic + +#region authentication_digest + /// /// 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. + /// and Microsoft's Internet Explorer V7. /// private void DoDigest(string authdata) { + string response = null; - MatchCollection matches = digestParm1.Matches(authdata); + // Find all of the values of the for x = "y" - // Collect all of the supplied parameters and store them - // in a dictionary (for ease of access) + MatchCollection matches = digestParm1.Matches(authdata); foreach (Match m in matches) { authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); - Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", + Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); } - // And pick up any tokens too + // Find all of the values of the for x = y 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}", + Rest.Log.DebugFormat("{0} Tokenized Parameter matched : {1} = {2}", MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); } @@ -382,10 +734,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (authparms.TryGetValue("response", out response)) { + string temp = null; do { + string nck = null; string ncl = null; @@ -406,7 +760,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!authparms.TryGetValue("nonce", out nonce) || nonce == null) { - Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); break; } @@ -417,7 +771,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { if (temp != opaque) { - Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); break; } } @@ -429,7 +783,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { if (temp != algorithm) { - Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); break; } } @@ -438,6 +792,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (authparms.TryGetValue("qop", out temp)) { + qop = temp.ToLower(); // replace with actual value used // if QOP was specified then @@ -445,7 +800,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!authparms.ContainsKey("cnonce")) { - Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); break; } @@ -453,7 +808,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!authparms.TryGetValue("nc", out nck) || nck == null) { - Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); break; } @@ -465,7 +820,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (Rest.Hex2Int(ncl) >= Rest.Hex2Int(nck)) { - Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); break; } cntable[nonce] = nck; @@ -474,179 +829,62 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { 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; - } } + else + { - // 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()); - } - } + qop = String.Empty; - /// - /// 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). - /// + // 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; + } + } - private bool Validate(string user, string pass) - { - Rest.Log.DebugFormat("{0} Simple User Validation", MsgId); + // Validate the supplied userid/password info - // Both values are required + authenticated = ValidateDigest(userName, nonce, cnonce, nck, authPrefix, response); - if (user == null || pass == null) - return false; + } + while (false); - // Eliminate any leading or trailing spaces - user = user.Trim(); + } - return vetPassword(user, pass); } /// - /// This mechanism is used by the digest authetnication mechanism + /// This mechanism is used by the digest authentication 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 - /// algorithm. This is not inconceivable... + /// user's passwords are already hashed, and the HTTP mechanism + /// does not supply an open password, the hashed passwords cannot + /// be used unless the client 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; @@ -667,53 +905,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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; @@ -756,7 +955,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory HA2 = HashToString(patt); // Generate Digest - + if (qop != String.Empty) { patt = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, nonce, nck, cnonce, qop, HA2); @@ -771,10 +970,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // 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)); @@ -789,62 +990,98 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Hash = <{1}>", MsgId, sbuilder.ToString()); return sbuilder.ToString(); + } +#endregion authentication_digest + +#region service_interface + + /// + /// Conditionally set a normal completion code. This allows a normal + /// execution path to default. + /// + internal void Complete() { - statusCode = Rest.HttpStatusCodeOK; - statusDescription = Rest.HttpStatusDescOK; + if (statusCode == 0) + { + statusCode = Rest.HttpStatusCodeOK; + } + } + + /// + /// Indicate a functionally-dependent conclusion to the + /// request. See Rest.cs for a list of possible values. + /// + + internal void Complete(int code) + { + statusCode = code; } + /// + /// Indicate that a request should be redirected, using + /// the HTTP completion codes. Permanent and temporary + /// redirections may be indicated. The supplied URL is + /// the new location of the resource. + /// + 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(statusCode, String.Empty, true); + } - // Fail for an arbitrary reason. Just a failure with - // headers. + /// + /// Fail for an arbitrary reason. Just a failure with + /// headers. The supplied message will be returned in the + /// message body. + /// - internal void Fail(int code, string message) + internal void Fail(int code) { - Fail(code, message, true); + Fail(code, String.Empty, false); } - // More adventurous. This failure also includes a - // specified entity. + /// + /// For the more adventurous. This failure also includes a + /// specified entity to be appended to the code-related + /// status string. + /// - internal void Fail(int code, string message, string data) + internal void Fail(int code, string addendum) { - buffer = null; - body = data; - Fail(code, message, false); + Fail(code, addendum, false); } - internal void Fail(int code, string message, bool reset) + internal void Fail(int code, string addendum, bool reset) { + statusCode = code; - statusDescription = message; + appendStatus(String.Format("({0}) : {1}", code, Rest.HttpStatusDesc[code])); - if (reset) + // Add any final addendum to the status information + + if (addendum != String.Empty) { - buffer = null; - SendHtml(message); - body = html; + appendStatus(String.Format(addendum)); } + // Help us understand why the request is being rejected + if (Rest.DEBUG) { Rest.Log.DebugFormat("{0} Request Failure State Dump", MsgId); @@ -864,42 +1101,61 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory fail = true; - Respond("Failure response"); + // Respond to the client's request, tag the response (for the + // benefit of trace) to indicate the reason. - RestException re = new RestException(message+" <"+code+">"); + Respond(String.Format("Failure response: ({0}) : {1}", + code, Rest.HttpStatusDesc[code])); + + // Finally initialize and the throw a RestException. All of the + // handler's infrastructure knows that this is a "normal" + // completion from a code point-of-view. + + RestException re = new RestException(Rest.HttpStatusDesc[code]+" <"+code+">"); re.statusCode = code; - re.statusDesc = message; + re.statusDesc = Rest.HttpStatusDesc[code]; re.httpmethod = method; re.httppath = path; throw re; + } // Reject this request internal void Reject() { - Fail(Rest.HttpStatusCodeNotImplemented, Rest.HttpStatusDescNotImplemented); + Fail(Rest.HttpStatusCodeNotImplemented, "request rejected (not implemented)"); } - // This MUST be called by an agent handler before it returns + // 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); + // We do this to try and make multiple Respond requests harmless, + // as it is sometimes convenient to isse a response without + // certain knowledge that it has not previously been done. + 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! + // A Head request can NOT have a body! So don't waste time on + // formatting if we're going to reject it anyway! + 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 @@ -924,18 +1180,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 (buffer == null && body != null) { - if (body != null && body.Length > 0) - { - Rest.Log.DebugFormat("{0} String-based entity", MsgId); - buffer = encoding.GetBytes(body); - } + buffer = encoding.GetBytes(body); + AddHeader("Content-Type",bodyType); } // OK, if the buffer contains something, regardless of how @@ -944,21 +1192,37 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (buffer != null) { Rest.Log.DebugFormat("{0} Buffer-based entity", MsgId); - response.ContentLength64 = buffer.Length; } else { - response.ContentLength64 = 0; + if (statusBody != String.Empty) + { + statusBody += Rest.statusTail; + buffer = encoding.GetBytes(statusBody); + AddHeader("Content-Type","text/html"); + } + else + { + statusBody = Rest.statusHead; + appendStatus(String.Format(": ({0}) {1}", + statusCode, Rest.HttpStatusDesc[statusCode])); + statusBody += Rest.statusTail; + buffer = encoding.GetBytes(statusBody); + AddHeader("Content-Type","text/html"); + } } + response.ContentLength64 = buffer.Length; + 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, + // Set the status code & description. If nothing has been stored, // we consider that a success. if (statusCode == 0) @@ -972,7 +1236,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // For a redirect we need to set the relocation header accordingly - if (response.StatusCode == (int) Rest.HttpStatusCodeTemporaryRedirect || + if (response.StatusCode == (int) Rest.HttpStatusCodeTemporaryRedirect || response.StatusCode == (int) Rest.HttpStatusCodePermanentRedirect) { Rest.Log.DebugFormat("{0} Re-direct location is {1}", MsgId, redirectLocation); @@ -981,18 +1245,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // And include the status description if provided. - if (statusDescription != null) - { - Rest.Log.DebugFormat("{0} Status description is {1}", MsgId, statusDescription); - response.StatusDescription = statusDescription; - } + response.StatusDescription = Rest.HttpStatusDesc[response.StatusCode]; // 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 + // here on down probably leaves the response // element unusable by anyone else. handled = true; @@ -1007,7 +1267,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (buffer != null && buffer.Length != 0) { - Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", + Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", MsgId, buffer.Length, encoding.GetString(buffer)); response.OutputStream.Write(buffer, 0, buffer.Length); } @@ -1016,35 +1276,36 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 + /// + /// These methods allow a service provider to manipulate the + /// request/response headers. The DumpHeaders method is intended + /// for problem diagnosis. + /// internal void AddHeader(string hdr, string data) { if (Rest.DEBUG) { - Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", + 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>", + 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) @@ -1052,29 +1313,69 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, hdr); if (response.Headers.Get(hdr) == null) { - Rest.Log.DebugFormat("{0} No such header existed", + 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. @@ -1082,6 +1383,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private void initUrl() { + uri = request.Url; if (query == null) @@ -1096,19 +1398,19 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { path = uri.AbsolutePath; if (path.EndsWith(Rest.UrlPathSeparator)) - path = path.Substring(0, path.Length-1); + path = path.Substring(0,path.Length-1); } // If we succeeded in getting a path, perform any // additional pre-processing required. - if (path != null) + if (path != null) { if (Rest.ExtendedEscape) { // Handle "+". Not a standard substitution, but // common enough... - path = path.Replace(Rest.C_PLUS, Rest.C_SPACE); + path = path.Replace(Rest.C_PLUS,Rest.C_SPACE); } pathNodes = path.Split(Rest.CA_PATHSEP); } @@ -1126,10 +1428,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory hostname = uri.Host; port = uri.Port; + } - internal int initParameters(int prfxlen) + private int initParameters(int prfxlen) { + if (prfxlen < path.Length-1) { parameters = path.Substring(prfxlen+1).Split(Rest.CA_PATHSEP); @@ -1138,139 +1442,23 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { 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); - } - - // 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; +#endregion internal_methods - 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(); - } } } -- cgit v1.1