File: net\System\Net\Cache\FtpRequestCacheValidator.cs
Project: ndp\fx\src\System.csproj (System)
/*++
Copyright (c) Microsoft Corporation
 
Module Name:
 
    FtpRequestCacheValidator.cs
 
Abstract:
    The class implements FTP Caching validators
 
Author:
 
    Alexei Vopilov    3-Aug-2004
 
Revision History:
 
--*/
namespace System.Net.Cache {
    using System;
    using System.Net;
    using System.IO;
    using System.Collections;
    using System.Text;
    using System.Collections.Specialized;
    using System.Globalization;
    using System.Threading;
 
 
    // The class represents an adavanced way for an application to control caching protocol
    internal class FtpRequestCacheValidator: HttpRequestCacheValidator {
 
        DateTime       m_LastModified;
        bool           m_HttpProxyMode;
 
        private bool                    HttpProxyMode    {get{return m_HttpProxyMode;}}
        internal new RequestCachePolicy Policy           {get {return ((RequestCacheValidator)this).Policy;}}
 
        //
        private void ZeroPrivateVars()
        {
            m_LastModified      = DateTime.MinValue;
            m_HttpProxyMode     = false;
        }
 
        //public
        internal override RequestCacheValidator CreateValidator()
        {
            return new FtpRequestCacheValidator(StrictCacheErrors, UnspecifiedMaxAge);
        }
 
        //public
        internal FtpRequestCacheValidator(bool strictCacheErrors, TimeSpan unspecifiedMaxAge): base(strictCacheErrors, unspecifiedMaxAge)
        {
        }
 
        //
        // This validation method is called first and before any Cache access is done.
        // Given the request instance the code has to decide whether the request is ever suitable for caching.
        //
        // Returns:
        // Continue           = Proceed to the next protocol stage.
        // DoNotTakeFromCache = Don't used caches value for this request
        // DoNotUseCache      = Cache is not used for this request and response is not cached.
        protected internal override CacheValidationStatus ValidateRequest()
        {
            // cleanup context after previous  request
            ZeroPrivateVars();
 
            if (Request is HttpWebRequest)
            {
                m_HttpProxyMode = true;
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_ftp_proxy_doesnt_support_partial));
                return base.ValidateRequest();
            }
 
            if (Policy.Level == RequestCacheLevel.BypassCache)
                return CacheValidationStatus.DoNotUseCache;
 
            string method = Request.Method.ToUpper(CultureInfo.InvariantCulture);
            if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_ftp_method, method));
 
            switch (method) {
                case WebRequestMethods.Ftp.DownloadFile:  RequestMethod = HttpMethod.Get;   break;
                case WebRequestMethods.Ftp.UploadFile:    RequestMethod = HttpMethod.Put;break;
                case WebRequestMethods.Ftp.AppendFile:    RequestMethod = HttpMethod.Put;break;
                case WebRequestMethods.Ftp.Rename:        RequestMethod = HttpMethod.Put;break;
                case WebRequestMethods.Ftp.DeleteFile:    RequestMethod = HttpMethod.Delete;break;
 
                default:        RequestMethod = HttpMethod.Other;    break;
            }
 
            if ((RequestMethod != HttpMethod.Get || !((FtpWebRequest)Request).UseBinary) && Policy.Level == RequestCacheLevel.CacheOnly)
            {
                // Throw because the request must hit the wire and it's cache-only policy
                FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy);
            }
 
            if (method != WebRequestMethods.Ftp.DownloadFile)
                return CacheValidationStatus.DoNotTakeFromCache;
 
            if (!((FtpWebRequest)Request).UseBinary)
            {
                if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_ftp_supports_bin_only));
                return CacheValidationStatus.DoNotUseCache;
            }
 
            if (Policy.Level >= RequestCacheLevel.Reload)
                return CacheValidationStatus.DoNotTakeFromCache;
 
            return CacheValidationStatus.Continue;
        }
 
        //
        // This validation method is called after caching protocol has retrieved the metadata of a cached entry.
        // Given the cached entry context, the request instance and the effective caching policy,
        // the handler has to decide whether a cached item can be considered as fresh.
        protected internal override CacheFreshnessStatus ValidateFreshness()
        {
 
            if (HttpProxyMode)
            {
                if (CacheStream != Stream.Null)
                {
                    if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_replacing_entry_with_HTTP_200));
 
                    // HTTP validator cannot parse FTP status code and other metadata
                    if (CacheEntry.EntryMetadata == null)
                        CacheEntry.EntryMetadata = new StringCollection();
 
                    CacheEntry.EntryMetadata.Clear();
                    CacheEntry.EntryMetadata.Add("HTTP/1.1 200 OK");
                }
                return base.ValidateFreshness();
            }
 
            DateTime nowDate = DateTime.UtcNow;
 
            if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_now_time, nowDate.ToString("r", CultureInfo.InvariantCulture)));
 
            // If absolute Expires can be recovered
            if (CacheEntry.ExpiresUtc != DateTime.MinValue)
            {
                //Take absolute Expires value
                if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_max_age_absolute, CacheEntry.ExpiresUtc.ToString("r", CultureInfo.InvariantCulture)));
                if (CacheEntry.ExpiresUtc < nowDate) {
                    return CacheFreshnessStatus.Stale;
                }
                return CacheFreshnessStatus.Fresh;
            }
 
            TimeSpan age  = TimeSpan.MaxValue;
 
            if(CacheEntry.LastSynchronizedUtc != DateTime.MinValue)
            {
                age = nowDate - CacheEntry.LastSynchronizedUtc;
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age1, ((int)age.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo), CacheEntry.LastSynchronizedUtc.ToString("r", CultureInfo.InvariantCulture)));
            }
 
            //
            // Heruistic expiration
            //
            if (CacheEntry.LastModifiedUtc != DateTime.MinValue)
            {
                TimeSpan span = (nowDate - CacheEntry.LastModifiedUtc);
                int maxAgeSeconds = (int)(span.TotalSeconds/10);
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_no_max_age_use_10_percent, maxAgeSeconds.ToString(NumberFormatInfo.InvariantInfo), CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture)));
                if (age.TotalSeconds < maxAgeSeconds) {
                    return CacheFreshnessStatus.Fresh;
                }
                return CacheFreshnessStatus.Stale;
            }
 
            // Else we can only rely on UnspecifiedMaxAge hint
            if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_no_max_age_use_default, ((int)(UnspecifiedMaxAge.TotalSeconds)).ToString(NumberFormatInfo.InvariantInfo)));
            if (UnspecifiedMaxAge >= age)
            {
                return CacheFreshnessStatus.Fresh;
            }
 
            return CacheFreshnessStatus.Stale;
            //return OnValidateFreshness(this);
        }
 
        // This method may add headers under the "Warning" header name
        protected internal override CacheValidationStatus ValidateCache()
        {
            if (HttpProxyMode)
                return base.ValidateCache();
 
            if (Policy.Level >= RequestCacheLevel.Reload)
            {
                // For those policies cache is never returned
                GlobalLog.Assert("OnValidateCache()", "This validator should not be called for policy = " + Policy.ToString());
                if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_validator_invalid_for_policy, Policy.ToString()));
                return CacheValidationStatus.DoNotTakeFromCache;
            }
 
            // First check is do we have a cached entry at all?
            if (CacheStream == Stream.Null || CacheEntry.IsPartialEntry)
            {
                if (Policy.Level == RequestCacheLevel.CacheOnly)
                {
                    // Throw because entry was not found and it's cache-only policy
                    FailRequest(WebExceptionStatus.CacheEntryNotFound);
                }
                if (CacheStream == Stream.Null)
                {
                    return CacheValidationStatus.DoNotTakeFromCache;
                }
                // Otherwise it's a partial entry and we can go on the wire
            }
 
            CacheStreamOffset = 0L;
            CacheStreamLength = CacheEntry.StreamSize;
 
            //
            // Before request submission validation
            //
            if (Policy.Level == RequestCacheLevel.Revalidate || CacheEntry.IsPartialEntry)
            {
                return TryConditionalRequest();
            }
 
            long contentOffset = Request is FtpWebRequest ? ((FtpWebRequest)Request).ContentOffset: 0L;
 
            if (CacheFreshnessStatus == CacheFreshnessStatus.Fresh || Policy.Level == RequestCacheLevel.CacheOnly || Policy.Level == RequestCacheLevel.CacheIfAvailable)
            {
                if (contentOffset != 0)
                {
                    if (contentOffset >= CacheStreamLength)
                    {
                        if (Policy.Level == RequestCacheLevel.CacheOnly)
                        {
                            // Throw because request is outside of cached size and it's cache-only policy
                            FailRequest(WebExceptionStatus.CacheEntryNotFound);
                        }
                        return CacheValidationStatus.DoNotTakeFromCache;
                    }
                    CacheStreamOffset = contentOffset;
                }
                return CacheValidationStatus.ReturnCachedResponse;
            }
 
            return CacheValidationStatus.DoNotTakeFromCache;
        }
        //
        // This is (optionally) called after receiveing a live response
        //
        protected internal override CacheValidationStatus RevalidateCache()
        {
            if (HttpProxyMode)
                return base.RevalidateCache();
 
 
            if (Policy.Level >= RequestCacheLevel.Reload)
            {
                // For those policies cache is never returned
                GlobalLog.Assert("RevalidateCache()", "This validator should not be called for policy = " + Policy.ToString());
                if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_validator_invalid_for_policy, Policy.ToString()));
                return CacheValidationStatus.DoNotTakeFromCache;
            }
 
            // First check is do we still hold on a cached entry?
            if (CacheStream == Stream.Null)
            {
                return CacheValidationStatus.DoNotTakeFromCache;
            }
 
            //
            // This is a second+ time validation after receiving at least one response
            //
 
            CacheValidationStatus result = CacheValidationStatus.DoNotTakeFromCache;
 
            FtpWebResponse resp = Response as FtpWebResponse;
            if (resp == null)
            {
                // This will result to an application error
                return CacheValidationStatus.DoNotTakeFromCache;
            }
 
            if (resp.StatusCode == FtpStatusCode.FileStatus)
            {
                if(Logging.On) Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_response_last_modified, resp.LastModified.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture), resp.ContentLength));
                if(Logging.On) Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_cache_last_modified, CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture), CacheEntry.StreamSize));
 
                if (CacheStreamOffset != 0L && CacheEntry.IsPartialEntry)
                {
                    //should never happen
                    if(Logging.On) Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_partial_and_non_zero_content_offset, CacheStreamOffset.ToString(CultureInfo.InvariantCulture))); 
                    result = CacheValidationStatus.DoNotTakeFromCache;
                }
 
                if (resp.LastModified.ToUniversalTime() == CacheEntry.LastModifiedUtc)
                {
                    if (CacheEntry.IsPartialEntry)
                    {
                        // A caller will need to use Validator.CacheEntry.StreamSize to figure out what the restart point is
 
                        if (resp.ContentLength > 0)
                            this.CacheStreamLength = resp.ContentLength;
                        else
                            this.CacheStreamLength = -1;
 
                        result = CacheValidationStatus.CombineCachedAndServerResponse;
                    }
                    else if (resp.ContentLength == CacheEntry.StreamSize)
                    {
                        result = CacheValidationStatus.ReturnCachedResponse;
                    }
                    else
                        result = CacheValidationStatus.DoNotTakeFromCache;
                }
                else
                    result = CacheValidationStatus.DoNotTakeFromCache;
            }
            else
            {
                result =  CacheValidationStatus.DoNotTakeFromCache;
            }
 
            return result;
        }
 
        //
        // This validation method is responsible to answer whether the live response is sufficient to make
        // the final decision for caching protocol.
        // This is useful in case of possible failure or inconsistent results received from
        // the remote cache.
        //
        /// Invalid response from this method means the request was internally modified and should be retried </remarks>
        protected internal override CacheValidationStatus ValidateResponse()
        {
            if (HttpProxyMode)
                return base.ValidateResponse();
 
            if (Policy.Level != RequestCacheLevel.Default && Policy.Level != RequestCacheLevel.Revalidate)
            {
                // Those policy levels do not modify requests
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_response_valid_based_on_policy, Policy.ToString()));
                return CacheValidationStatus.Continue;
            }
 
            FtpWebResponse resp = Response as FtpWebResponse;
 
            if (resp == null) {
                if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_null_response_failure));
                return CacheValidationStatus.Continue;
            }
 
            if(Logging.On) Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_ftp_response_status, ((int)resp.StatusCode).ToString(CultureInfo.InvariantCulture), resp.StatusCode.ToString()));
 
            // If there was a retry already, it should go with cache disabled so by default we won't retry it again
            if (ResponseCount > 1) {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_valid_based_on_retry, ResponseCount));
                return CacheValidationStatus.Continue;
            }
 
            if (resp.StatusCode != FtpStatusCode.OpeningData && resp.StatusCode != FtpStatusCode.FileStatus)
            {
                return CacheValidationStatus.RetryResponseFromServer;
            }
            return CacheValidationStatus.Continue;
        }
 
        ///This action handler is responsible for making final decision on whether
        // a received response can be cached.
        // Invalid result from this method means the response must not be cached
        protected internal override CacheValidationStatus UpdateCache()
        {
            if (HttpProxyMode)
                return base.UpdateCache();
 
            // An combined cace+wire response is not supported if user has specified a restart offset.
            CacheStreamOffset = 0L;
 
            if (RequestMethod == HttpMethod.Other)
            {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_not_updated_based_on_policy, Request.Method));
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            if (ValidationStatus == CacheValidationStatus.RemoveFromCache) {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_removed_existing_invalid_entry));
                return CacheValidationStatus.RemoveFromCache;
            }
 
            if (Policy.Level == RequestCacheLevel.CacheOnly) {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_not_updated_based_on_policy, Policy.ToString()));
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            FtpWebResponse resp = Response as FtpWebResponse;
 
            if (resp == null)
            {
                if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_not_updated_because_no_response));
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            //
            // Check on cache removal based on the request method
            //
            if (RequestMethod == HttpMethod.Delete || RequestMethod == HttpMethod.Put)
            {
                if (RequestMethod == HttpMethod.Delete ||
                    resp.StatusCode == FtpStatusCode.OpeningData ||
                    resp.StatusCode == FtpStatusCode.DataAlreadyOpen ||
                    resp.StatusCode == FtpStatusCode.FileActionOK ||
                    resp.StatusCode == FtpStatusCode.ClosingData)
                {
                    if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_removed_existing_based_on_method, Request.Method));
                    return CacheValidationStatus.RemoveFromCache;
                }
                if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_existing_not_removed_because_unexpected_response_status, (int)resp.StatusCode, resp.StatusCode.ToString()));
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            if (Policy.Level == RequestCacheLevel.NoCacheNoStore) {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_removed_existing_based_on_policy, Policy.ToString()));
                return CacheValidationStatus.RemoveFromCache;
            }
 
            if (ValidationStatus == CacheValidationStatus.ReturnCachedResponse)
            {
                // have a response still returning from cache means just revalidated the entry.
                return UpdateCacheEntryOnRevalidate();
            }
 
            if (resp.StatusCode != FtpStatusCode.OpeningData 
                && resp.StatusCode != FtpStatusCode.DataAlreadyOpen
                && resp.StatusCode != FtpStatusCode.ClosingData)
            {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_not_updated_based_on_ftp_response_status, FtpStatusCode.OpeningData.ToString() + "|" + FtpStatusCode.DataAlreadyOpen.ToString() + "|" + FtpStatusCode.ClosingData.ToString(), resp.StatusCode.ToString()));
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            // Check on no-update or cache removal if restart action has invalidated existing cache entry
            if (((FtpWebRequest)Request).ContentOffset != 0L)
            {
                if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_update_not_supported_for_ftp_restart, ((FtpWebRequest)Request).ContentOffset.ToString(CultureInfo.InvariantCulture)));
                if (CacheEntry.LastModifiedUtc != DateTime.MinValue && resp.LastModified.ToUniversalTime() != CacheEntry.LastModifiedUtc)
                {
                    if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_removed_entry_because_ftp_restart_response_changed, CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture), resp.LastModified.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)));
                    return CacheValidationStatus.RemoveFromCache;
                }
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            return UpdateCacheEntryOnStore();
        }
 
        //
        //
        //
        private CacheValidationStatus UpdateCacheEntryOnStore()
        {
            CacheEntry.EntryMetadata  = null;
            CacheEntry.SystemMetadata = null;
 
            FtpWebResponse resp = Response as FtpWebResponse;
            if (resp.LastModified != DateTime.MinValue)
            {
                CacheEntry.LastModifiedUtc = resp.LastModified.ToUniversalTime();
            }
 
            ResponseEntityLength = Response.ContentLength;
            CacheEntry.StreamSize = ResponseEntityLength;       //This is passed down to cache on what size to expect
            CacheEntry.LastSynchronizedUtc = DateTime.UtcNow;
            return CacheValidationStatus.CacheResponse;
        }
        //
        //
        private CacheValidationStatus UpdateCacheEntryOnRevalidate()
        {
            if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_last_synchronized, CacheEntry.LastSynchronizedUtc.ToString("r", CultureInfo.InvariantCulture)));
 
            DateTime nowUtc = DateTime.UtcNow;
            if (CacheEntry.LastSynchronizedUtc + TimeSpan.FromMinutes(1) >= nowUtc)
            {
                if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_suppress_update_because_synched_last_minute));
                return CacheValidationStatus.DoNotUpdateCache;
            }
 
            CacheEntry.EntryMetadata  = null;
            CacheEntry.SystemMetadata = null;
 
            CacheEntry.LastSynchronizedUtc = nowUtc;
 
            if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_updating_last_synchronized, CacheEntry.LastSynchronizedUtc.ToString("r", CultureInfo.InvariantCulture)));
 
            return CacheValidationStatus.UpdateResponseInformation;
        }
 
        //
        private CacheValidationStatus TryConditionalRequest()
        {
            FtpWebRequest request = Request as FtpWebRequest;
            if (request == null || !request.UseBinary)
                return CacheValidationStatus.DoNotTakeFromCache;
 
            if (request.ContentOffset != 0L)
            {
                if (CacheEntry.IsPartialEntry || request.ContentOffset >= CacheStreamLength)
                    return CacheValidationStatus.DoNotTakeFromCache;
                CacheStreamOffset = request.ContentOffset;
            }
            return CacheValidationStatus.Continue;
        }
 
    }
}