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