diff --git a/src/FileCache/BasicFileCacheManager.cs b/src/FileCache/BasicFileCacheManager.cs index a6fb4e6..07bf7a1 100644 --- a/src/FileCache/BasicFileCacheManager.cs +++ b/src/FileCache/BasicFileCacheManager.cs @@ -74,7 +74,5 @@ public override string GetPolicyPath(string key, string regionName = null) } return filePath; } - - } } diff --git a/src/FileCache/FileCache.cs b/src/FileCache/FileCache.cs index 89c832e..fbe9bda 100644 --- a/src/FileCache/FileCache.cs +++ b/src/FileCache/FileCache.cs @@ -31,7 +31,6 @@ public class FileCache : ObjectCache // this is a file used to prevent multiple processes from trying to "clean" at the same time private const string SemaphoreFile = "cache.sem"; private long _currentCacheSize = 0; - private PayloadMode _readMode = PayloadMode.Serializable; public string CacheDir { get; protected set; } /// @@ -88,16 +87,7 @@ public enum PayloadMode /// /// Specified whether the payload is deserialized or just the file name. /// - public PayloadMode PayloadReadMode { - get => _readMode; - set { - if (value == PayloadMode.RawBytes) - { - throw new ArgumentException("The read mode cannot be set to RawBytes. Use the file name please."); - } - _readMode = value; - } - } + public PayloadMode PayloadReadMode { get; set; } = PayloadMode.Serializable; /// /// Specified how the payload is to be handled on add operations. @@ -144,12 +134,11 @@ public long CurrentCacheSize if (_currentCacheSize == 0) { // Read the system file for cache size - object cacheSizeObj = CacheManager.ReadSysFile(CacheSizeFile); - - // Did we successfully get data from the file? - if (cacheSizeObj != null) + long cacheSize; + if (CacheManager.ReadSysValue(CacheSizeFile, out cacheSize)) { - _currentCacheSize = (long) cacheSizeObj; + // Did we successfully get data from the file? Write it to our member var. + _currentCacheSize = cacheSize; } } @@ -160,7 +149,7 @@ private set // no need to do a pointless re-store of the same value if (_currentCacheSize != value || value == 0) { - CacheManager.WriteSysFile(CacheSizeFile, value); + CacheManager.WriteSysValue(CacheSizeFile, value); _currentCacheSize = value; } } @@ -395,11 +384,10 @@ private bool ShouldClean() try { // if the file can't be found, or is corrupt this will throw an exception - DateTime? lastClean = CacheManager.ReadSysFile(LastCleanedDateFile) as DateTime?; - - //AC: rewrote to be safer in null cases - if (lastClean == null) + DateTime lastClean; + if (!CacheManager.ReadSysValue(LastCleanedDateFile, out lastClean)) { + //AC: rewrote to be safer in cases where no value obtained. return true; } @@ -514,7 +502,7 @@ public long CleanCache(string regionName = null) } // mark that we've cleaned the cache - CacheManager.WriteSysFile(LastCleanedDateFile, DateTime.Now); + CacheManager.WriteSysValue(LastCleanedDateFile, DateTime.Now); // unlock cLock.Close(); diff --git a/src/FileCache/FileCacheManager.cs b/src/FileCache/FileCacheManager.cs index 28121af..e662b61 100644 --- a/src/FileCache/FileCacheManager.cs +++ b/src/FileCache/FileCacheManager.cs @@ -12,6 +12,11 @@ namespace System.Runtime.Caching { public abstract class FileCacheManager { + // Magic version for new sysfiles: 3.3.0 packed into a long. + protected const ulong CACHE_VERSION = ( 3 << 16 + + 3 << 8 + + 0 << 0); + public string CacheDir { get; set; } public string CacheSubFolder { get; set; } public string PolicySubFolder { get; set; } @@ -24,6 +29,40 @@ public abstract class FileCacheManager /// public TimeSpan AccessTimeout { get; set; } + + /// + /// Differentiate outdated cache formats from newer. + /// + /// Older caches use "BinaryFormatter", which is a security risk: + /// https://docs.microsoft.com/nl-nl/dotnet/standard/serialization/binaryformatter-security-guide#preferred-alternatives + /// + /// The newer caches have a 'magic' header we'll look for. + /// + /// BinaryReader opened to stream containing the file contents. + /// boolean indicating validity + protected bool HeaderVersionValid(BinaryReader reader) + { + // Don't much care about exceptions here - let them bubble up. + ulong version = reader.ReadUInt64(); + // Valid if magic header version matches. + return (version == CACHE_VERSION); + } + + /// + /// Differentiate outdated cache formats from newer. + /// + /// Older caches use "BinaryFormatter", which is a security risk: + /// https://docs.microsoft.com/nl-nl/dotnet/standard/serialization/binaryformatter-security-guide#preferred-alternatives + /// + /// The newer caches have a 'magic' header we'll look for. + /// + /// BinaryWriter opened to stream that will contain the file contents. + protected void HeaderVersionWrite(BinaryWriter writer) + { + // Don't much care about exceptions here - let them bubble up. + writer.Write(CACHE_VERSION); + } + protected virtual object DeserializePayloadData(string fileName, SerializationBinder objectBinder = null) { object data = null; @@ -98,9 +137,30 @@ public virtual FileCachePayload ReadFile(FileCache.PayloadMode mode, string key, return payload; } - private object LoadRawPayloadData(string cachePath) + private byte[] LoadRawPayloadData(string fileName) { - throw new NotSupportedException("Reading raw payload is not currently supported."); + byte[] data = null; + if (File.Exists(fileName)) + { + using (FileStream stream = GetStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (BinaryReader reader = new BinaryReader(stream)) + { + // Check if it's valid version first. + if (!HeaderVersionValid(reader)) + { + // Failure - return invalid data. + return null; + + // `using` statements will clean up for us. + } + + // Valid - read entire file. + data = reader.ReadBytes(int.MaxValue); + } + } + } + return data; } protected virtual object Deserialize(string fileName, SerializationBinder objectBinder = null) @@ -165,7 +225,6 @@ public virtual long WriteFile(FileCache.PayloadMode mode, string key, FileCacheP case FileCache.PayloadMode.Serializable: using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None)) { - BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, data.Payload); } @@ -173,17 +232,20 @@ public virtual long WriteFile(FileCache.PayloadMode mode, string key, FileCacheP case FileCache.PayloadMode.RawBytes: using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None)) { - - if (data.Payload is byte[]) - { - byte[] dataPayload = (byte[])data.Payload; - stream.Write(dataPayload, 0, dataPayload.Length); - } - else if (data.Payload is Stream) + using (BinaryWriter writer = new BinaryWriter(stream)) { - Stream dataPayload = (Stream)data.Payload; - dataPayload.CopyTo(stream); - // no close or the like, we are not the owner + if (data.Payload is byte[]) + { + byte[] dataPayload = (byte[])data.Payload; + writer.Write(dataPayload); + } + else if (data.Payload is Stream) + { + Stream dataPayload = (Stream)data.Payload; + byte[] bytePayload = new byte[dataPayload.Length - dataPayload.Position]; + dataPayload.Read(bytePayload, (int)dataPayload.Position, bytePayload.Length); + // no close or the like for data.Payload - we are not the owner + } } } break; @@ -261,17 +323,56 @@ public IEnumerable GetRegions() public abstract string GetPolicyPath(string key, string regionName = null); /// - /// Reads data in from a system file. System files are not part of the - /// cache itself, but serve as a way for the cache to store data it - /// needs to operate. + /// Generic version of ReadSysValue just throws an ArgumentException to error on unknown new types. /// /// The name of the sysfile (without directory) - /// The data from the file - public object ReadSysFile(string filename) + /// The value read + /// success/failure boolean + public bool ReadSysValue(string filename, out T value) where T : struct { + throw new ArgumentException(string.Format("Type is currently unsupported: {0}", typeof(T).ToString()), "value"); + + // These types could be easily implemented following the `long` function as a template: + // - bool: + // + reader.ReadBoolean(); + // - byte: + // + reader.ReadByte(); + // - char: + // + reader.ReadChar(); + // - decimal: + // + reader.ReadDecimal(); + // - double: + // + reader.ReadDouble(); + // - short: + // + reader.ReadInt16(); + // - int: + // + reader.ReadInt32(); + // - long: + // + reader.ReadInt64(); + // - sbyte: + // + reader.ReadSbyte(); + // - ushort: + // + reader.ReadUInt16(); + // - uint: + // + reader.ReadUInt32(); + // - ulong: + // + reader.ReadUInt64(); + } + + /// + /// Read a `long` (64 bit signed int) from a sysfile. + /// + /// The name of the sysfile (without directory) + /// The value read or long.MinValue + /// success/failure boolean + public bool ReadSysValue(string filename, out long value) + { + // Return min value on fail. Success/fail will be either exception or bool return. + value = long.MinValue; + bool success = false; + // sys files go in the root directory string path = Path.Combine(CacheDir, filename); - object data = null; if (File.Exists(path)) { @@ -281,21 +382,33 @@ public object ReadSysFile(string filename) { using (FileStream stream = GetStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { - BinaryFormatter formatter = new BinaryFormatter(); - try - { - data = formatter.Deserialize(stream); - } - catch (Exception) + using(BinaryReader reader = new BinaryReader(stream)) { - data = null; - } - finally - { - stream.Close(); + try + { + // The old "BinaryFormatter" sysfiles will fail this check. + if (HeaderVersionValid(reader)) + { + value = reader.ReadInt64(); + } + else + { + // Invalid version - return invalid value & failure. + value = long.MinValue; + success = false; + break; + } + } + catch (Exception) + { + value = long.MinValue; + // DriveCommerce: Need to rethrow to get the IOException caught. + throw; + } } + success = true; + break; } - break; } catch (IOException) { @@ -304,16 +417,47 @@ public object ReadSysFile(string filename) } } - return data; + // `value` already set correctly. + return success; } /// - /// Writes data to a system file that is not part of the cache itself, + /// Read a `DateTime` struct from a sysfile using `DateTime.FromBinary()`. + /// + /// The name of the sysfile (without directory) + /// The value read or DateTime.MinValue + /// success/failure boolean + public bool ReadSysValue(string filename, out DateTime value) + { + // DateTime is serialized as a long, so use that `ReadSysValue()` function. + long serialized; + if (ReadSysValue(filename, out serialized)) + { + value = DateTime.FromBinary(serialized); + return true; + } + // else failed: + value = DateTime.MinValue; + return false; + } + + /// + /// Generic version of `WriteSysValue` just throws an ArgumentException on unknown new types. + /// + /// The name of the sysfile (without directory) + /// The data to write to the sysfile + public void WriteSysValue(string filename, T data) where T : struct + { + throw new ArgumentException(string.Format("Type is currently unsupported: {0}", typeof(T).ToString()), "data"); + } + + /// + /// Writes a long to a system file that is not part of the cache itself, /// but is used to help it function. /// /// The name of the sysfile (without directory) - /// The data to write to the file - public void WriteSysFile(string filename, object data) + /// The long to write to the file + public void WriteSysValue(string filename, long data) { // sys files go in the root directory string path = Path.Combine(CacheDir, filename); @@ -321,9 +465,13 @@ public void WriteSysFile(string filename, object data) // write the data to the file using (FileStream stream = GetStream(path, FileMode.Create, FileAccess.Write, FileShare.Write)) { - BinaryFormatter formatter = new BinaryFormatter(); - formatter.Serialize(stream, data); - stream.Close(); + using (BinaryWriter writer = new BinaryWriter(stream)) + { + // Must write the magic version header first. + HeaderVersionWrite(writer); + + writer.Write(data); + } } }