FileSystemXmlRepository.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Runtime.InteropServices;
  9. using System.Xml.Linq;
  10. using Microsoft.Extensions.Logging;
  11. namespace Microsoft.AspNetCore.DataProtection.Repositories
  12. {
  13. /// <summary>
  14. /// An XML repository backed by a file system.
  15. /// </summary>
  16. public class FileSystemXmlRepository : IXmlRepository
  17. {
  18. private static readonly Lazy<DirectoryInfo> _defaultDirectoryLazy = new Lazy<DirectoryInfo>(GetDefaultKeyStorageDirectory);
  19. private readonly ILogger _logger;
  20. /// <summary>
  21. /// Creates a <see cref="FileSystemXmlRepository"/> with keys stored at the given directory.
  22. /// </summary>
  23. /// <param name="directory">The directory in which to persist key material.</param>
  24. /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
  25. public FileSystemXmlRepository(DirectoryInfo directory, ILoggerFactory loggerFactory)
  26. {
  27. if (directory == null)
  28. {
  29. throw new ArgumentNullException(nameof(directory));
  30. }
  31. Directory = directory;
  32. _logger = loggerFactory.CreateLogger<FileSystemXmlRepository>();
  33. }
  34. /// <summary>
  35. /// The default key storage directory.
  36. /// On Windows, this currently corresponds to "Environment.SpecialFolder.LocalApplication/ASP.NET/DataProtection-Keys".
  37. /// On Linux and macOS, this currently corresponds to "$HOME/.aspnet/DataProtection-Keys".
  38. /// </summary>
  39. /// <remarks>
  40. /// This property can return null if no suitable default key storage directory can
  41. /// be found, such as the case when the user profile is unavailable.
  42. /// </remarks>
  43. public static DirectoryInfo DefaultKeyStorageDirectory => _defaultDirectoryLazy.Value;
  44. /// <summary>
  45. /// The directory into which key material will be written.
  46. /// </summary>
  47. public DirectoryInfo Directory { get; }
  48. private const string DataProtectionKeysFolderName = "DataProtection-Keys";
  49. private static DirectoryInfo GetKeyStorageDirectoryFromBaseAppDataPath(string basePath)
  50. {
  51. return new DirectoryInfo(Path.Combine(basePath, "ASP.NET", DataProtectionKeysFolderName));
  52. }
  53. public virtual IReadOnlyCollection<XElement> GetAllElements()
  54. {
  55. // forces complete enumeration
  56. return GetAllElementsCore().ToList().AsReadOnly();
  57. }
  58. private IEnumerable<XElement> GetAllElementsCore()
  59. {
  60. Directory.Create(); // won't throw if the directory already exists
  61. // Find all files matching the pattern "*.xml".
  62. // Note: Inability to read any file is considered a fatal error (since the file may contain
  63. // revocation information), and we'll fail the entire operation rather than return a partial
  64. // set of elements. If a file contains well-formed XML but its contents are meaningless, we
  65. // won't fail that operation here. The caller is responsible for failing as appropriate given
  66. // that scenario.
  67. foreach (var fileSystemInfo in Directory.EnumerateFileSystemInfos("*.xml", SearchOption.TopDirectoryOnly))
  68. {
  69. yield return ReadElementFromFile(fileSystemInfo.FullName);
  70. }
  71. }
  72. private static DirectoryInfo GetDefaultKeyStorageDirectory()
  73. {
  74. DirectoryInfo retVal;
  75. // Environment.GetFolderPath returns null if the user profile isn't loaded.
  76. var localAppDataFromSystemPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
  77. var localAppDataFromEnvPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
  78. var userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE");
  79. var homePath = Environment.GetEnvironmentVariable("HOME");
  80. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(localAppDataFromSystemPath))
  81. {
  82. // To preserve backwards-compatibility with 1.x, Environment.SpecialFolder.LocalApplicationData
  83. // cannot take precedence over $LOCALAPPDATA and $HOME/.aspnet on non-Windows platforms
  84. retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromSystemPath);
  85. }
  86. else if (localAppDataFromEnvPath != null)
  87. {
  88. retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromEnvPath);
  89. }
  90. else if (userProfilePath != null)
  91. {
  92. retVal = GetKeyStorageDirectoryFromBaseAppDataPath(Path.Combine(userProfilePath, "AppData", "Local"));
  93. }
  94. else if (homePath != null)
  95. {
  96. // If LOCALAPPDATA and USERPROFILE are not present but HOME is,
  97. // it's a good guess that this is a *NIX machine. Use *NIX conventions for a folder name.
  98. retVal = new DirectoryInfo(Path.Combine(homePath, ".aspnet", DataProtectionKeysFolderName));
  99. }
  100. else if (!string.IsNullOrEmpty(localAppDataFromSystemPath))
  101. {
  102. // Starting in 2.x, non-Windows platforms may use Environment.SpecialFolder.LocalApplicationData
  103. // but only after checking for $LOCALAPPDATA, $USERPROFILE, and $HOME.
  104. retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromSystemPath);
  105. }
  106. else
  107. {
  108. return null;
  109. }
  110. Debug.Assert(retVal != null);
  111. try
  112. {
  113. retVal.Create(); // throws if we don't have access, e.g., user profile not loaded
  114. return retVal;
  115. }
  116. catch
  117. {
  118. return null;
  119. }
  120. }
  121. internal static DirectoryInfo GetKeyStorageDirectoryForAzureWebSites()
  122. {
  123. // Azure Web Sites needs to be treated specially, as we need to store the keys in a
  124. // correct persisted location. We use the existence of the %WEBSITE_INSTANCE_ID% env
  125. // variable to determine if we're running in this environment, and if so we then use
  126. // the %HOME% variable to build up our base key storage path.
  127. if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")))
  128. {
  129. var homeEnvVar = Environment.GetEnvironmentVariable("HOME");
  130. if (!String.IsNullOrEmpty(homeEnvVar))
  131. {
  132. return GetKeyStorageDirectoryFromBaseAppDataPath(homeEnvVar);
  133. }
  134. }
  135. // nope
  136. return null;
  137. }
  138. private static bool IsSafeFilename(string filename)
  139. {
  140. // Must be non-empty and contain only a-zA-Z0-9, hyphen, and underscore.
  141. return (!String.IsNullOrEmpty(filename) && filename.All(c =>
  142. c == '-'
  143. || c == '_'
  144. || ('0' <= c && c <= '9')
  145. || ('A' <= c && c <= 'Z')
  146. || ('a' <= c && c <= 'z')));
  147. }
  148. private XElement ReadElementFromFile(string fullPath)
  149. {
  150. _logger.ReadingDataFromFile(fullPath);
  151. using (var fileStream = File.OpenRead(fullPath))
  152. {
  153. return XElement.Load(fileStream);
  154. }
  155. }
  156. public virtual void StoreElement(XElement element, string friendlyName)
  157. {
  158. if (element == null)
  159. {
  160. throw new ArgumentNullException(nameof(element));
  161. }
  162. if (!IsSafeFilename(friendlyName))
  163. {
  164. var newFriendlyName = Guid.NewGuid().ToString();
  165. _logger.NameIsNotSafeFileName(friendlyName, newFriendlyName);
  166. friendlyName = newFriendlyName;
  167. }
  168. StoreElementCore(element, friendlyName);
  169. }
  170. private void StoreElementCore(XElement element, string filename)
  171. {
  172. // We're first going to write the file to a temporary location. This way, another consumer
  173. // won't try reading the file in the middle of us writing it. Additionally, if our process
  174. // crashes mid-write, we won't end up with a corrupt .xml file.
  175. Directory.Create(); // won't throw if the directory already exists
  176. var tempFilename = Path.Combine(Directory.FullName, Guid.NewGuid().ToString() + ".tmp");
  177. var finalFilename = Path.Combine(Directory.FullName, filename + ".xml");
  178. try
  179. {
  180. using (var tempFileStream = File.OpenWrite(tempFilename))
  181. {
  182. element.Save(tempFileStream);
  183. }
  184. // Once the file has been fully written, perform the rename.
  185. // Renames are atomic operations on the file systems we support.
  186. _logger.WritingDataToFile(finalFilename);
  187. File.Move(tempFilename, finalFilename);
  188. }
  189. finally
  190. {
  191. File.Delete(tempFilename); // won't throw if the file doesn't exist
  192. }
  193. }
  194. }
  195. }