File: System\Data\Services\HttpContextServiceHost.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="HttpContextServiceHost.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides an HttpContext-based implementation.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services
{
    #region Namespaces.
 
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Net;
    using System.ServiceModel;
    using System.ServiceModel.Web;
 
    #endregion Namespaces.
 
    /// <summary>
    /// Provides access to the environment for a DataService, including information about the current request, based
    /// on the current WebOperationContext.
    /// </summary>
    internal class HttpContextServiceHost : IDataServiceHost2
    {
        #region Private fields.
 
        /// <summary>Message sent to server.</summary>
        private readonly Stream incomingMessageBody;
 
        /// <summary>The WCF-based operation context.</summary>
        private readonly WebOperationContext operationContext;
 
        /// <summary>Whether an error was found when processing this request.</summary>
        private bool errorFound;
 
        /// <summary>Gets the absolute URI to the resource upon which to apply the request.</summary>
        private Uri absoluteRequestUri;
 
        /// <summary>Gets the absolute URI to the service.</summary>
        private Uri absoluteServiceUri;
 
        #endregion Private fields.
 
        #region Constructors.
 
        /// <summary>
        /// Initializes a new System.Data.Services.HttpContextServiceHost instance.
        /// </summary>
        /// <param name='messageBody'>Incoming message body to process.</param>
        internal HttpContextServiceHost(Stream messageBody)
        {
            // We capture the current context at initialization time rather
            // than accessing it repeatedly.
            this.incomingMessageBody = messageBody;
            this.operationContext = WebOperationContext.Current;
            if (this.operationContext == null)
            {
                throw new InvalidOperationException(Strings.HttpContextServiceHost_WebOperationContextCurrentMissing);
            }
        }
 
        #endregion Constructors.
 
        #region Properties.
 
        /// <summary>
        /// Gets the character set encoding that the client requested,
        /// possibly null.
        /// </summary>
        string IDataServiceHost.RequestAcceptCharSet
        {
            get
            {
                // Returns a string that contains the comma-separated list of values 
                // associated with the specified key, if found; otherwise, null.
                return this.operationContext.IncomingRequest.Headers[HttpRequestHeader.AcceptCharset]; 
            }
        }
 
        /// <summary>Gets or sets the HTTP MIME type of the output stream.</summary>
        string IDataServiceHost.ResponseContentType
        {
            get
            {
                return this.operationContext.OutgoingResponse.ContentType;
            }
            
            set
            {
                this.operationContext.OutgoingResponse.ContentType = value;
            }
        }
 
        /// <summary>Gets the HTTP MIME type of the input stream.</summary>
        string IDataServiceHost.RequestContentType
        {
            get
            {
                return this.operationContext.IncomingRequest.ContentType;
            }
        }
 
        /// <summary>
        /// Gets a comma-separated list of client-supported MIME Accept types.
        /// </summary>
        string IDataServiceHost.RequestAccept
        {
            get
            {
                return this.operationContext.IncomingRequest.Accept;
            }
        }
 
        /// <summary>
        /// Gets the HTTP data transfer method (such as GET, POST, or HEAD) used by the client.
        /// </summary>
        string IDataServiceHost.RequestHttpMethod
        {
            get
            {
                string result;
                string[] methodValues = this.operationContext.IncomingRequest.Headers.GetValues(XmlConstants.HttpXMethod);
                if (methodValues == null || methodValues.Length == 0)
                {
                    result = this.operationContext.IncomingRequest.Method;
                }
                else if (methodValues.Length == 1)
                {
                    result = methodValues[0];
                    if (this.operationContext.IncomingRequest.Method != XmlConstants.HttpMethodPost)
                    {
                        throw DataServiceException.CreateBadRequestError(Strings.HttpContextServiceHost_XMethodNotUsingPost);
                    }
                    
                    if (result != XmlConstants.HttpMethodDelete && result != XmlConstants.HttpMethodPut && result != XmlConstants.HttpMethodMerge)
                    {
                        throw DataServiceException.CreateBadRequestError(Strings.HttpContextServiceHost_XMethodIncorrectValue(result));
                    }
                }
                else
                {
                    throw DataServiceException.CreateBadRequestError(Strings.HttpContextServiceHost_XMethodIncorrectCount(methodValues.Length));
                }
 
                return result;
            }
        }
 
        /// <summary>Gets the value of the If-Match header from the request made</summary>
        string IDataServiceHost.RequestIfMatch
        {
            get
            {
                return this.operationContext.IncomingRequest.Headers[HttpRequestHeader.IfMatch];
            }
        }
 
        /// <summary>Gets the value of the If-None-Match header from the request made</summary>
        string IDataServiceHost.RequestIfNoneMatch
        {
            get
            {
                return this.operationContext.IncomingRequest.Headers[HttpRequestHeader.IfNoneMatch];
            }
        }
 
        /// <summary>Gets the value for the MaxDataServiceVersion request header.</summary>
        string IDataServiceHost.RequestMaxVersion
        {
            get
            {
                return this.operationContext.IncomingRequest.Headers[XmlConstants.HttpMaxDataServiceVersion];
            }
        }
 
        /// <summary>Gets the value for the DataServiceVersion request header.</summary>
        string IDataServiceHost.RequestVersion
        {
            get
            {
                return this.operationContext.IncomingRequest.Headers[XmlConstants.HttpDataServiceVersion];
            }
        }
 
        /// <summary>Gets the absolute URI to the resource upon which to apply the request.</summary>
        Uri IDataServiceHost.AbsoluteRequestUri
        {
            get
            {
                if (this.absoluteRequestUri == null)
                {
                    object property;
                    if (OperationContext.Current.IncomingMessageProperties.TryGetValue(XmlConstants.MicrosoftDataServicesRequestUri, out property))
                    {
                        this.absoluteRequestUri = property as Uri;
                        if (this.absoluteRequestUri == null)
                        {
                            throw new InvalidOperationException(Strings.HttpContextServiceHost_IncomingMessagePropertyMustBeValidUriInstance(XmlConstants.MicrosoftDataServicesRequestUri));
                        }
                    }
 
                    if (this.absoluteRequestUri == null)
                    {
                        UriTemplateMatch match = this.operationContext.IncomingRequest.UriTemplateMatch;
                        this.absoluteRequestUri = WebUtil.ApplyHostHeader(match.RequestUri, this.HostHeader);
                    }
                }
 
                return this.absoluteRequestUri;
            }
        }
 
        /// <summary>Gets or sets the Cache-Control header on the response.</summary>
        string IDataServiceHost.ResponseCacheControl
        {
            get { return this.operationContext.OutgoingResponse.Headers[HttpResponseHeader.CacheControl]; }
            set { this.operationContext.OutgoingResponse.Headers[HttpResponseHeader.CacheControl] = value; }
        }
 
        /// <summary>Gets/Sets the value of the ETag header on the outgoing response</summary>
        string IDataServiceHost.ResponseETag
        {
            get
            {
                return this.operationContext.OutgoingResponse.ETag;
            }
 
            set
            {
                this.operationContext.OutgoingResponse.ETag = value;
            }
        }
 
        /// <summary>Gets or sets the Location header on the response.</summary>
        string IDataServiceHost.ResponseLocation
        {
            get { return this.operationContext.OutgoingResponse.Headers[HttpResponseHeader.Location]; }
            set { this.operationContext.OutgoingResponse.Headers[HttpResponseHeader.Location] = value; }
        }
 
        /// <summary>
        /// Gets/Sets the status code for the request made.
        /// </summary>
        int IDataServiceHost.ResponseStatusCode
        {
            get
            {
                return (int) this.operationContext.OutgoingResponse.StatusCode;
            }
 
            set
            {
                HttpStatusCode statusCode = (HttpStatusCode)value;
                this.operationContext.OutgoingResponse.StatusCode = statusCode;
 
                // Some status codes such as NoContent or NotModified MUST NOT include a message-body in the response.
                // We need to set SupressEntityBody to true so that in the case of chuncked encoding WCF won't write
                // a '0' in the message body to indicate there is no more content.
                // Note that not setting this will result in a HTTP Protocol Violation exception when chuncked encoding is on.
                this.operationContext.OutgoingResponse.SuppressEntityBody = MustNotReturnMessageBody(statusCode);
            }
        }
 
        /// <summary>
        /// Gets the <see cref="Stream"/> to be written to send a response
        /// to the client.
        /// </summary>
        Stream IDataServiceHost.ResponseStream
        {
            get
            {
                // The ResponseStream is not directly accessible - this would
                // prevent WCF from streaming results back. For the WCF host,
                // a Message subclass should write directly in the OnBodyWrite
                // method.
                throw Error.NotSupported();
            }
        }
 
        /// <summary>Gets or sets the value for the DataServiceVersion response header.</summary>
        string IDataServiceHost.ResponseVersion
        {
            get { return this.operationContext.OutgoingResponse.Headers[XmlConstants.HttpDataServiceVersion]; }
            set { this.operationContext.OutgoingResponse.Headers[XmlConstants.HttpDataServiceVersion] = value; }
        }
 
        /// <summary>Gets the absolute URI to the service.</summary>
        Uri IDataServiceHost.AbsoluteServiceUri
        {
            get
            {
                if (this.absoluteServiceUri == null)
                {
                    object property;
                    if (OperationContext.Current.IncomingMessageProperties.TryGetValue(XmlConstants.MicrosoftDataServicesRootUri, out property))
                    {
                        this.absoluteServiceUri = property as Uri;
                        if (this.absoluteServiceUri == null)
                        {
                            throw new InvalidOperationException(Strings.HttpContextServiceHost_IncomingMessagePropertyMustBeValidUriInstance(XmlConstants.MicrosoftDataServicesRootUri));
                        }
                    }
 
                    if (this.absoluteServiceUri == null)
                    {
                        UriTemplateMatch match = this.operationContext.IncomingRequest.UriTemplateMatch;
 
                        // We never want to consider the last segment of the base URI a 'document' type
                        // of segment to be replaced, ie, http://foo/svc.svc should never remove svc.svc
                        // from the path.
                        this.absoluteServiceUri = WebUtil.ApplyHostHeader(match.BaseUri, this.HostHeader);
                    }
 
                    if (!String.IsNullOrEmpty(this.absoluteServiceUri.Fragment))
                    {
                        throw new InvalidOperationException(Strings.HttpContextServiceHost_IncomingTemplateMatchFragment(this.absoluteServiceUri));
                    }
 
                    if (!String.IsNullOrEmpty(this.absoluteServiceUri.Query))
                    {
                        throw new InvalidOperationException(Strings.HttpContextServiceHost_IncomingTemplateMatchQuery(this.absoluteServiceUri));
                    }
 
                    this.absoluteServiceUri = WebUtil.EnsureLastSegmentEmpty(this.absoluteServiceUri);
                }
 
                return this.absoluteServiceUri;
            }
        }
 
        /// <summary>
        /// Gets the <see cref="Stream"/> from which the request data can be read from
        /// to the client.
        /// </summary>
        Stream IDataServiceHost.RequestStream
        {
            [DebuggerStepThrough]
            get { return this.incomingMessageBody; }
        }
 
        #region IDataServiceHost2 Properties
 
        /// <summary>Dictionary of all request headers from the host.</summary>
        WebHeaderCollection IDataServiceHost2.RequestHeaders
        {
            [DebuggerStepThrough]
            get { return this.operationContext.IncomingRequest.Headers; }
        }
 
        /// <summary>Enumerates all response headers that has been set.</summary>
        WebHeaderCollection IDataServiceHost2.ResponseHeaders
        {
            [DebuggerStepThrough]
            get { return this.operationContext.OutgoingResponse.Headers; }
        }
 
        #endregion IDataServiceHost2 Properties
 
        /// <summary>Whether an error was found when processing this request.</summary>
        internal bool ErrorFound
        {
            get { return this.errorFound; }
        }
 
        /// <summary>The value for the Host header in the request, possibly null.</summary>
        private string HostHeader
        {
            get { return this.operationContext.IncomingRequest.Headers[HttpRequestHeader.Host]; }
        }
 
        #endregion Properties.
 
        #region Methods.
 
        /// <summary>Gets the value for the specified item in the request query string.</summary>
        /// <param name="item">Item to return.</param>
        /// <returns>
        /// The value for the specified item in the request query string;
        /// null if <paramref name="item"/> is not found.
        /// </returns>
        string IDataServiceHost.GetQueryStringItem(string item)
        {
            Debug.Assert(item != null, "item != null");
            Debug.Assert(item.Trim() == item, "item.Trim() == item - otherwise, there are leading/trailing spaces in the name");
 
            System.Collections.Specialized.NameValueCollection collection = this.operationContext.IncomingRequest.UriTemplateMatch.QueryParameters;
            string[] values = collection.GetValues(item);
            if (values == null || values.Length == 0)
            {
                // Do a scan of arguments ignoring whitespace (SQLBUDT #555944).
                string keyFound = null;
                foreach (string key in collection.Keys)
                {
                    if (key != null && StringComparer.OrdinalIgnoreCase.Equals(key.Trim(), item))
                    {
                        if (keyFound != null)
                        {
                            throw DataServiceException.CreateBadRequestError(Strings.HttpContextServiceHost_AmbiguousItemName(item, keyFound, key));
                        }
 
                        keyFound = key;
                        values = collection.GetValues(key);
                    }
                }
 
                if (values == null || values.Length == 0)
                {
                    return null;
                }
            }
 
            Debug.Assert(values != null && values.Length > 0, "values != null && values.Length > 0 - otherwise we should have returned already");
            if (values.Length == 1)
            {
                return values[0];
            }
            else
            {
                throw DataServiceException.CreateSyntaxError();
            }
        }
 
        /// <summary>
        /// Method to handle a data service exception during processing.
        /// </summary>
        /// <param name="args">Exception handling description.</param>
        void IDataServiceHost.ProcessException(HandleExceptionArgs args)
        {
            Debug.Assert(this.operationContext != null, "this.operationContext != null");
            this.errorFound = true;
            if (!args.ResponseWritten)
            {
                ((IDataServiceHost)this).ResponseStatusCode = args.ResponseStatusCode;
                ((IDataServiceHost)this).ResponseContentType = args.ResponseContentType;
                if (args.ResponseAllowHeader != null)
                {
                    this.operationContext.OutgoingResponse.Headers[HttpResponseHeader.Allow] = args.ResponseAllowHeader;
                }
            }
        }
 
        /// <summary>Verifies that query parameters are valid.</summary>
        internal void VerifyQueryParameters()
        {
            HashSet<string> namesFound = new HashSet<string>(StringComparer.Ordinal);
            System.Collections.Specialized.NameValueCollection collection = this.operationContext.IncomingRequest.UriTemplateMatch.QueryParameters;
            for (int i = 0; i < collection.Count; i++)
            {
                string name = collection.GetKey(i);
                if (name == null)
                {
                    // These are values of the form a&b&c, without '='. We just make sure they aren't system
                    // values at all.
                    string[] values = collection.GetValues(i);
                    if (values != null)
                    {
                        for (int j = 0; j < values.Length; j++)
                        {
                            string value = values[j].Trim();
                            if (value.Length > 0 && value[0] == '$')
                            {
                                throw DataServiceException.CreateBadRequestError(
                                    Strings.HttpContextServiceHost_QueryParameterMustBeSpecifiedOnce(value));
                            }
                        }
                    }
 
                    continue;
                }
 
                name = name.Trim();
                if (!namesFound.Add(name))
                {
                    throw DataServiceException.CreateBadRequestError(
                        Strings.HttpContextServiceHost_QueryParameterMustBeSpecifiedOnce(name));
                }
 
                if (name.Length > 0 && name[0] == '$')
                {
                    if (name != XmlConstants.HttpQueryStringExpand &&
                        name != XmlConstants.HttpQueryStringFilter &&
                        name != XmlConstants.HttpQueryStringOrderBy &&
                        name != XmlConstants.HttpQueryStringSkip &&
                        name != XmlConstants.HttpQueryStringSkipToken &&
                        name != XmlConstants.HttpQueryStringInlineCount &&
                        name != XmlConstants.HttpQueryStringTop &&
                        name != XmlConstants.HttpQueryStringSelect)
                    {
                        throw DataServiceException.CreateBadRequestError(
                            Strings.HttpContextServiceHost_UnknownQueryParameter(name));
                    }
 
                    string[] values = collection.GetValues(i);
                    if (values == null || values.Length != 1)
                    {
                        throw DataServiceException.CreateBadRequestError(
                            Strings.HttpContextServiceHost_QueryParameterMustBeSpecifiedOnce(name));
                    }
                }
            }
        }
 
        /// <summary>
        /// Check to see if the given status code expects an empty message-body.
        /// </summary>
        /// <param name="statusCode">Http status code</param>
        /// <returns>True if the message-body must be empty for the given status code, false otherwise.</returns>
        private static bool MustNotReturnMessageBody(HttpStatusCode statusCode)
        {
            // Both 204 and 304 must not include a message-body in the response.
            switch (statusCode)
            {
                case HttpStatusCode.NoContent:    // 204
                case HttpStatusCode.ResetContent: // 205
                case HttpStatusCode.NotModified:  // 304
                    return true;
 
                default:
                    return false;
            }
        }
 
        #endregion Methods.
    }
}