Files
opensim/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs
Dr Scofield ad04626737 cleaning up OSHttpResponse: note that read access to extra header
fields is GONE (HttpServer does not support that), you can read the
"normal" HTTP headers available via properties, and you can add
headers. also, it is now possible to set a timeout for KeepAlive (for
those clients that pay attention to it).

this also fixes the broken REST inventory/assets/appearance services,
they should be working again.

testcase for OSHttpResponse will follow.
2008-10-06 21:59:43 +00:00

1456 lines
51 KiB
C#

/*
* 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 OpenMetaverse;
using System.Xml;
namespace OpenSim.ApplicationPlugins.Rest.Inventory
{
/// <summary>
/// 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.
/// </summary>
public class RequestData
{
// 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 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 Uri uri = null;
internal string query = null;
internal string hostname = "localhost";
internal int port = 80;
// 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;
// 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 = 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;
// 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<string,string> cntable = new Dictionary<string,string>();
private static Dictionary<string,string> sktable = new Dictionary<string,string>();
// This dictionary is used to keep track fo all of the parameters discovered
// when the authorisation header is anaylsed.
private Dictionary<string,string> authparms = new Dictionary<string,string>();
// These regular expressions are used to decipher the various header entries.
private static Regex schema = new Regex("^\\s*(?<scheme>\\w+)\\s*.*",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex basicParms = new Regex("^\\s*(?:\\w+)\\s+(?<pval>\\S+)\\s*",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex digestParm1 = new Regex("\\s*(?<parm>\\w+)\\s*=\\s*\"(?<pval>[^\"]+)\"",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex digestParm2 = new Regex("\\s*(?<parm>\\w+)\\s*=\\s*(?<pval>[^\\p{P}\\s]+)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex reuserPass = new Regex("(?<user>[^:]+):(?<pass>[\\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;
#region properties
// Just for convenience...
internal string MsgId
{
get { return Rest.MsgId; }
}
/// <summary>
/// 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.
/// </summary>
internal bool IsAuthenticated
{
get
{
if (Rest.Authenticate)
{
if (!authenticated)
{
authenticate();
}
return authenticated;
}
else return true;
}
}
/// <summary>
/// Access to all 'nodes' in the supplied URI as an
/// array of strings.
/// </summary>
internal string[] PathNodes
{
get
{
return pathNodes;
}
}
/// <summary>
/// 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).
/// </summary>
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
/// <summary>
/// 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.
///
/// As soon as authentication failure is detected the method calls
/// DoChallenge() which terminates the request with REST exception
/// for unauthroized access.
/// </summary>
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();
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// 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).
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// This is an RFC2617 compliant HTTP MD5 Digest authentication
/// implementation. It has been tested with Firefox, Java HTTP client,
/// and Microsoft's Internet Explorer V7.
/// </summary>
private void DoDigest(string authdata)
{
string response = null;
// Find all of the values of the for x = "y"
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}",
MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value);
}
// 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}",
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);
}
}
/// <summary>
/// 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 client has used the same salting mechanism
/// to has the password before using it in the authentication
/// algorithn. This is not inconceivable...
/// </summary>
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;
}
}
// 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();
}
#endregion authentication_digest
#region service_interface
/// <summary>
/// Conditionally set a normal completion code. This allows a normal
/// execution path to default.
/// </summary>
internal void Complete()
{
if (statusCode == 0)
{
statusCode = Rest.HttpStatusCodeOK;
}
}
/// <summary>
/// Indicate a functionally-dependent conclusion to the
/// request. See Rest.cs for a list of possible values.
/// </summary>
internal void Complete(int code)
{
statusCode = code;
}
/// <summary>
/// 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.
/// </summary>
internal void Redirect(string Url, bool temp)
{
redirectLocation = Url;
if (temp)
{
statusCode = Rest.HttpStatusCodeTemporaryRedirect;
}
else
{
statusCode = Rest.HttpStatusCodePermanentRedirect;
}
Fail(statusCode, String.Empty, true);
}
/// <summary>
/// Fail for an arbitrary reason. Just a failure with
/// headers. The supplied message will be returned in the
/// message body.
/// </summary>
internal void Fail(int code)
{
Fail(code, String.Empty, false);
}
/// <summary>
/// For the more adventurous. This failure also includes a
/// specified entity to be appended to the code-related
/// status string.
/// </summary>
internal void Fail(int code, string addendum)
{
Fail(code, addendum, false);
}
internal void Fail(int code, string addendum, bool reset)
{
statusCode = code;
appendStatus(String.Format("({0}) : {1}", code, Rest.HttpStatusDesc[code]));
// Add any final addendum to the status information
if (addendum != String.Empty)
{
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);
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 to the client's request, tag the response (for the
// benefit of trace) to indicate the reason.
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 = Rest.HttpStatusDesc[code];
re.httpmethod = method;
re.httppath = path;
throw re;
}
// Reject this request
internal void Reject()
{
Fail(Rest.HttpStatusCodeNotImplemented, "request rejected (not implemented)");
}
// 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! 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
// 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 && body != null)
{
buffer = encoding.GetBytes(body);
AddHeader("Content-Type",bodyType);
}
// 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);
}
else
{
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.ContentEncoding == 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.
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
// 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} Sending response", MsgId);
// response.OutputStream.Close();
response.Send();
}
Rest.Log.DebugFormat("{0} Respond EXIT, handled = {1}, reason = {2}", MsgId, handled, reason);
return handled;
}
/// <summary>
/// These methods allow a service provider to manipulate the
/// request/response headers. The DumpHeaders method is intended
/// for problem diagnosis.
/// </summary>
internal void AddHeader(string hdr, string data)
{
if (Rest.DEBUG) Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", MsgId, hdr, data);
response.AddHeader(hdr, data);
}
// 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);
// }
// internal void DumpHeaders()
// {
// if (Rest.DEBUG)
// {
// for (int i=0;i<response.Headers.Count;i++)
// {
// Rest.Log.DebugFormat("{0} Header[{1}] : {2}", MsgId, i,
// response.Headers.Get(i));
// }
// }
// }
// 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);
}
internal void appendStatus(string msg)
{
if (statusBody == String.Empty)
{
statusBody = String.Format(Rest.statusHead, request.HttpMethod);
}
statusBody = String.Format("{0} {1}", statusBody, msg);
}
#endregion service_interface
#region internal_methods
/// <summary>
/// Helper methods for deconstructing and reconstructing
/// URI path data.
/// </summary>
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;
}
private 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;
}
#endregion internal_methods
}
}