File: System\Web\Services\Protocols\ServerProtocol.cs
Project: ndp\cdf\src\NetFx20\System.Web.Services\System.Web.Services.csproj (System.Web.Services)
//------------------------------------------------------------------------------
// <copyright file="ServerProtocol.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>                                                                
//------------------------------------------------------------------------------
 
namespace System.Web.Services.Protocols {
    using System;
    using System.Diagnostics;
    using System.Collections;
    using System.IO;
    using System.Reflection;
    using System.Xml.Serialization;
    using System.Web.Caching;
    using System.ComponentModel;
    using System.Text;
    using System.Net;
    using System.Web.Services;
    using System.Threading;
    using System.Security.Permissions;
    using System.Web.Services.Diagnostics;
 
    [PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")]
    [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
    public abstract class ServerProtocol {
        Type type;
        HttpRequest request;
        HttpResponse response;
        HttpContext context;
        object target;
        WebMethodAttribute methodAttr;
 
        private static Object s_InternalSyncObject;
        internal static Object InternalSyncObject {
            get {
                if (s_InternalSyncObject == null) {
                    Object o = new Object();
                    Interlocked.CompareExchange(ref s_InternalSyncObject, o, null);
                }
                return s_InternalSyncObject;
            }
        }
 
        internal void SetContext(Type type, HttpContext context, HttpRequest request, HttpResponse response) {
            PartialTrustHelpers.FailIfInPartialTrustOutsideAspNet(); 
            this.type = type;
            this.context = context;
            this.request = request;
            this.response = response;
            Initialize();
        }
 
        internal virtual void CreateServerInstance() {
            target = Activator.CreateInstance(ServerType.Type);
            WebService service = target as WebService;
            if (service != null)
                service.SetContext(context);
        }
 
        internal virtual void DisposeServerInstance() {
            if (target == null) return;
            IDisposable disposable = target as IDisposable;
            if (disposable != null)
                disposable.Dispose();
            target = null;
        }
 
        protected internal HttpContext Context {
            get { return context; }
        }
 
        protected internal HttpRequest Request {
            get { return request; }
        }
 
        protected internal HttpResponse Response {
            get { return response; }
        }
 
        internal Type Type {
            get { return type; }
        }
 
        protected virtual internal object Target {
            get { return target; }
        }
 
        internal virtual bool WriteException(Exception e, Stream outputStream) {
            // return true if exception should not be re-thrown to ASP.NET
            return false;
        }
 
        internal abstract bool Initialize();
        internal abstract object[] ReadParameters();
        internal abstract void WriteReturns(object[] returns, Stream outputStream);
        internal abstract LogicalMethodInfo MethodInfo { get; }
        internal abstract ServerType ServerType { get; }
        internal abstract bool IsOneWay { get; }
        internal virtual Exception OnewayInitException { get { return null; } }
 
        internal WebMethodAttribute MethodAttribute {
            get {
                if (methodAttr == null)
                    methodAttr = MethodInfo.MethodAttribute;
                return methodAttr;
            }
        }
 
        internal string GenerateFaultString(Exception e) {
            return GenerateFaultString(e, false);
        }
 
        internal static void SetHttpResponseStatusCode(HttpResponse httpResponse, int statusCode) {
            // We skip IIS custom errors for HTTP requests.
            httpResponse.TrySkipIisCustomErrors = true;
            httpResponse.StatusCode = statusCode;
        }
 
        // 
 
        internal string GenerateFaultString(Exception e, bool htmlEscapeMessage) {
            bool isDevelopmentServer = Context != null && !Context.IsCustomErrorEnabled;
            if (isDevelopmentServer && !htmlEscapeMessage) {
                //If the user has specified it's a development server (versus a production server) in ASP.NET config,
                //then we should just return e.ToString instead of extracting the list of messages.            
                return e.ToString();
            }
            StringBuilder builder = new StringBuilder();
            if (isDevelopmentServer) {
                //  we are dumping the ecseption directly to IE, need to encode
                GenerateFaultString(e, builder);
            }
            else {
                for (Exception inner = e; inner != null; inner = inner.InnerException) {
                    string text = htmlEscapeMessage ? HttpUtility.HtmlEncode(inner.Message) : inner.Message;
                    if (text.Length == 0) text = e.GetType().Name;
                    builder.Append(text);
                    if (inner.InnerException != null) builder.Append(" ---> ");
                }
            }
            return builder.ToString();
        }
 
        static void GenerateFaultString(Exception e, StringBuilder builder) {
            builder.Append(e.GetType().FullName);
            if (e.Message != null && e.Message.Length > 0) {
                builder.Append(": ");
                builder.Append(HttpUtility.HtmlEncode(e.Message));
            }
            if (e.InnerException != null) {
                builder.Append(" ---> ");
                GenerateFaultString(e.InnerException, builder);
                builder.Append(Environment.NewLine);
                builder.Append("   ");
                builder.Append(Res.GetString(Res.StackTraceEnd));
            }
            if (e.StackTrace != null) {
                builder.Append(Environment.NewLine);
                builder.Append(e.StackTrace);
            }
        }
 
        internal void WriteOneWayResponse() {
            context.Response.ContentType = null;
            Response.StatusCode = (int)HttpStatusCode.Accepted;
        }
 
        delegate string CreateCustomKeyForAspNetWebServiceMetadataCache(Type protocolType, Type serverType, string originalKey);
 
        static string DefaultCreateCustomKeyForAspNetWebServiceMetadataCache(Type protocolType, Type serverType, string originalKey) {
            return originalKey;
        }
 
        static CreateCustomKeyForAspNetWebServiceMetadataCache GetCreateCustomKeyForAspNetWebServiceMetadataCacheDelegate(Type serverType) {
            PartialTrustHelpers.FailIfInPartialTrustOutsideAspNet();
            string key = "CreateCustomKeyForAspNetWebServiceMetadataCache-" + serverType.FullName;
            CreateCustomKeyForAspNetWebServiceMetadataCache result = (CreateCustomKeyForAspNetWebServiceMetadataCache)HttpRuntime.Cache.Get(key);
            if (result == null) {
                MethodInfo createKeyMethod = serverType.GetMethod(
                    "CreateCustomKeyForAspNetWebServiceMetadataCache",
                    BindingFlags.Public | BindingFlags.Static | BindingFlags.ExactBinding | BindingFlags.FlattenHierarchy,
                    null,
                    new Type[] { typeof(Type), typeof(Type), typeof(string) },
                    null);
 
                if (createKeyMethod == null) {
                    result = ServerProtocol.DefaultCreateCustomKeyForAspNetWebServiceMetadataCache;
 
                } else {
                    result = delegate(Type pt, Type st, string originalString)
                    {
                        return (string)createKeyMethod.Invoke(null, new object[] { pt, st, originalString });
                    };
                }
 
                HttpRuntime.Cache.Add(key, result, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
            }
 
            return result;
        }
 
        string CreateKey(Type protocolType, Type serverType, bool excludeSchemeHostPort = false, string keySuffix = null) {
            //
            // we want to use the hostname to cache since for documentation, WSDL
            // contains the cache hostname, but we definitely don't want to cache the query string!
            //            
            string protocolTypeName = protocolType.FullName;
            string serverTypeName = serverType.FullName;
            string typeHandleString = serverType.TypeHandle.Value.ToString();
            string url = excludeSchemeHostPort ? Request.Url.AbsolutePath : Request.Url.GetLeftPart(UriPartial.Path);
            int length = protocolTypeName.Length + url.Length + serverTypeName.Length + typeHandleString.Length;
            StringBuilder sb = new StringBuilder(length);
            sb.Append(protocolTypeName);
            sb.Append(url);
            sb.Append(serverTypeName);
            sb.Append(typeHandleString);
            if (keySuffix != null) {
                sb.Append(keySuffix);
            }
            
            CreateCustomKeyForAspNetWebServiceMetadataCache createKey = ServerProtocol.GetCreateCustomKeyForAspNetWebServiceMetadataCacheDelegate(serverType);
 
            return createKey(protocolType, serverType, sb.ToString());
        }
 
        protected void AddToCache(Type protocolType, Type serverType, object value) {
            this.AddToCache(protocolType, serverType, value, false);
        }
 
        // See comment on the ServerProtocol.IsCacheUnderPressure method for explanation of the excludeSchemeHostPort logic.
        internal void AddToCache(Type protocolType, Type serverType, object value, bool excludeSchemeHostPort) {
            PartialTrustHelpers.FailIfInPartialTrustOutsideAspNet();
            HttpRuntime.Cache.Insert(CreateKey(protocolType, serverType, excludeSchemeHostPort),
                value,
                null,
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration,
                CacheItemPriority.NotRemovable,
                null);
        }
 
        protected object GetFromCache(Type protocolType, Type serverType) {
            return this.GetFromCache(protocolType, serverType, false);
        }
 
        internal object GetFromCache(Type protocolType, Type serverType, bool excludeSchemeHostPort) {
            PartialTrustHelpers.FailIfInPartialTrustOutsideAspNet();
            return HttpRuntime.Cache.Get(CreateKey(protocolType, serverType, excludeSchemeHostPort));
        }
 
        // IsCacheUnderPressure is part of a DOS mitigation mechanism addressing CSDMain#195148. Original problem: when a large number of 
        // HTTP requests for WSDL or the ASMX documentation page is made, each request with a unique value of the HOST header, a unique response
        // is generated and cached for each of the requests (responses contain the scheme/host/port of the request). 
        // This leads to ever growing memory consumption and eventual crash of the process.
        // The mitigation for this DOS attack uses the following mechanism:
        // 1. The behavior of the system remains unchanged for the first 10 requests for WSDL of a given ASMX service that have differing 
        //    scheme/host/port combination of the request URI. This is to avoid breaking behavioral changes in the 99.99% case, 
        //    since the DOS attack cannot be generically fixed without breaking behavioral changes. The value of 10 is baked in, 
        //    and we consider it a reasonable default based on the assumption that ASMX services in most circumstances cannot be 
        //    reached using more than 10 different values of the scheme/host/port. 
        // 2. For any requests for WSDL going beyond the 10 limit of scheme/host/port combination, we go into a “DOS mitigation mode”. 
        //    The mode prevents the eventual process crash while introducing marginal breaking behavioral changes:
        //    a. We create a single service description and cache it using the AbsolutePath of the request URI alone 
        //       (as opposed to scheme/host/port + AbsolutePath).
        //    b. For every request for WSDL/disco/documentation document, we fix up the URLs in the returned document to match the 
        //       scheme/host/port of the actual request for WSDL/disco. This fixup only applies to the WSDL extensions we have shipped in .NET 
        //       and does not apply to custom extensions implemented externally, hence the breaking behavioral change. 
        // This mechamism affects the DiscoveryServerProtocol and DocumentationServerProtocol.
        internal bool IsCacheUnderPressure(Type protocolType, Type serverType) {
            PartialTrustHelpers.FailIfInPartialTrustOutsideAspNet();
 
            const int threshold = 10;
            string key = this.CreateKey(protocolType, serverType, true, "CachePressure");
            ServerProtocolCachePressure item = (ServerProtocolCachePressure)HttpRuntime.Cache.Get(key);
 
            // There is a potential race condition in creating a new entry or increasing the value of an existing entry, 
            // but it is acceptable since DOS threshold enforcement need not be exact.
 
            if (item != null) {
                return item.Pressure < threshold ? Interlocked.Increment(ref item.Pressure) >= threshold : false;
            }
            else {
                HttpRuntime.Cache.Insert(
                    key,
                    new ServerProtocolCachePressure { Pressure = 1 },
                    null,
                    Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.NotRemovable,
                    null);
 
                return false;
            }
        }
 
        class ServerProtocolCachePressure {
            public int Pressure;
        }
    }
 
    [PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")]
    [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
    public abstract class ServerProtocolFactory {
        internal ServerProtocol Create(Type type, HttpContext context, HttpRequest request, HttpResponse response, out bool abortProcessing) {
            ServerProtocol serverProtocol = null;
            abortProcessing = false;
            serverProtocol = CreateIfRequestCompatible(request);
            try {
                if (serverProtocol != null)
                    serverProtocol.SetContext(type, context, request, response);
                return serverProtocol;
            }
            catch (Exception e) {
                abortProcessing = true;
                if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) {
                    throw;
                }
                if (Tracing.On) Tracing.ExceptionCatch(TraceEventType.Warning, this, "Create", e);
                if (serverProtocol != null) {
                    // give the protocol a shot at handling the error in a custom way
                    if (!serverProtocol.WriteException(e, serverProtocol.Response.OutputStream))
                        throw new InvalidOperationException(Res.GetString(Res.UnableToHandleRequest0), e);
                }
                return null;
            }
 
        }
 
        protected abstract ServerProtocol CreateIfRequestCompatible(HttpRequest request);
 
    }
 
}