1464 lines
51 KiB
C#
1464 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.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.
|
|
|
|
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} Closing output stream", MsgId);
|
|
response.OutputStream.Close();
|
|
|
|
}
|
|
|
|
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);
|
|
if (response.Headers.Get(hdr) != null)
|
|
{
|
|
Rest.Log.DebugFormat("{0} Multipe {1} headers will be generated>",
|
|
MsgId, hdr);
|
|
}
|
|
}
|
|
response.Headers.Add(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
|
|
|
|
}
|
|
}
|