1 //------------------------------------------------------------------------------
2 // <copyright file="UrlPath.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
10 * Copyright (c) 1999 Microsoft Corporation
13 namespace System.Web.Util {
14 using System.Security.Permissions;
16 using System.Runtime.Serialization.Formatters;
17 using System.Runtime.InteropServices;
18 using System.Collections;
19 using System.Globalization;
21 using System.Web.Hosting;
24 internal struct FileTimeInfo {
25 internal long LastWriteTime;
28 internal static readonly FileTimeInfo MinValue = new FileTimeInfo(0, 0);
30 internal FileTimeInfo(long lastWriteTime, long size) {
31 LastWriteTime = lastWriteTime;
35 public override bool Equals(object obj) {
38 if (obj is FileTimeInfo) {
39 fti = (FileTimeInfo) obj;
40 return (LastWriteTime == fti.LastWriteTime) && (Size == fti.Size);
47 public static bool operator == (FileTimeInfo value1, FileTimeInfo value2)
49 return (value1.LastWriteTime == value2.LastWriteTime) &&
50 (value1.Size == value2.Size);
53 public unsafe static bool operator != (FileTimeInfo value1, FileTimeInfo value2)
55 return !(value1 == value2);
58 public override int GetHashCode(){
59 return HashCodeCombiner.CombineHashCodes(LastWriteTime.GetHashCode(), Size.GetHashCode());
66 * Helper methods relating to file operations
68 internal class FileUtil {
73 [FileIOPermission(SecurityAction.Assert, AllFiles = FileIOPermissionAccess.Read)]
74 internal static bool FileExists(String filename) {
78 exists = File.Exists(filename);
86 // For a given path, if its beneath the app root, return the first existing directory
87 internal static string GetFirstExistingDirectory(string appRoot, string fileName) {
88 if (IsBeneathAppRoot(appRoot, fileName)) {
89 string existingDir = appRoot;
91 int nextSeparator = fileName.IndexOf(Path.DirectorySeparatorChar, existingDir.Length + 1);
92 if (nextSeparator > -1) {
93 string nextDir = fileName.Substring(0, nextSeparator);
94 if (FileUtil.DirectoryExists(nextDir, false)) {
95 existingDir = nextDir;
107 internal static bool IsBeneathAppRoot(string appRoot, string filePath) {
108 if (filePath.Length > appRoot.Length + 1
109 && filePath.IndexOf(appRoot, StringComparison.OrdinalIgnoreCase) > -1
110 && filePath[appRoot.Length] == Path.DirectorySeparatorChar) {
116 // Remove the final backslash from a directory path, unless it's something like c:\
117 internal static String RemoveTrailingDirectoryBackSlash(String path) {
122 int length = path.Length;
123 if (length > 3 && path[length - 1] == '\\')
124 path = path.Substring(0, length - 1);
129 private static int _maxPathLength = 259;
130 // If the path is longer than the maximum length
131 // Trim the end and append the hashcode to it.
132 internal static String TruncatePathIfNeeded(string path, int reservedLength) {
133 int maxPathLength = _maxPathLength - reservedLength;
134 if (path.Length > maxPathLength) {
137 path = path.Substring(0, maxPathLength - 13) +
138 path.GetHashCode().ToString(CultureInfo.InvariantCulture);
145 * Canonicalize the directory, and makes sure it ends with a '\'
147 internal static string FixUpPhysicalDirectory(string dir) {
151 dir = Path.GetFullPath(dir);
153 // Append '\' to the directory if necessary.
154 if (!StringUtil.StringEndsWith(dir, @"\"))
160 // Fail if the physical path is not canonical
161 static internal void CheckSuspiciousPhysicalPath(string physicalPath) {
162 if (IsSuspiciousPhysicalPath(physicalPath)) {
163 throw new HttpException(404, String.Empty);
167 // Check whether the physical path is not canonical
168 // NOTE: this API throws if we don't have permission to the file.
169 // NOTE: The compare needs to be case insensitive (VSWhidbey 444513)
170 static internal bool IsSuspiciousPhysicalPath(string physicalPath) {
173 if (!IsSuspiciousPhysicalPath(physicalPath, out pathTooLong)) {
181 // physical path too long -> not good because we still need to make
182 // it work for virtual path provider scenarios
184 // first a few simple checks:
185 if (physicalPath.IndexOf('/') >= 0) {
189 string slashDots = "\\..";
190 int idxSlashDots = physicalPath.IndexOf(slashDots, StringComparison.Ordinal);
191 if (idxSlashDots >= 0
192 && (physicalPath.Length == idxSlashDots + slashDots.Length
193 || physicalPath[idxSlashDots + slashDots.Length] == '\\')) {
197 // the real check is to go right to left until there is no longer path-too-long
198 // and see if the canonicalization check fails then
200 int pos = physicalPath.LastIndexOf('\\');
203 string path = physicalPath.Substring(0, pos);
205 if (!IsSuspiciousPhysicalPath(path, out pathTooLong)) {
206 // reached a non-suspicious path that is not too long
211 // reached a suspicious path that is not too long
215 // trim the path some more
216 pos = physicalPath.LastIndexOf('\\', pos-1);
219 // backtracted to the end without reaching a non-suspicious path
220 // this is suspicious (should happen because app root at least should be ok)
224 private static readonly char[] s_invalidPathChars = Path.GetInvalidPathChars();
226 // VSWhidbey 609102 - Medium trust apps may hit this method, and if the physical path exists,
227 // Path.GetFullPath will seek PathDiscovery permissions and throw an exception.
228 [FileIOPermissionAttribute(SecurityAction.Assert, AllFiles=FileIOPermissionAccess.PathDiscovery)]
229 static internal bool IsSuspiciousPhysicalPath(string physicalPath, out bool pathTooLong) {
232 // DevDiv 340712: GetConfigPathData generates n^2 exceptions where n is number of incorrectly placed '/'
233 // Explicitly prevent frequent exception cases since this method is called a few times per url segment
234 if ((physicalPath != null) &&
235 (physicalPath.Length > _maxPathLength ||
236 physicalPath.IndexOfAny(s_invalidPathChars) != -1 ||
237 // Contains ':' at any position other than 2nd char
238 (physicalPath.Length > 0 && physicalPath[0] == ':') ||
239 (physicalPath.Length > 2 && physicalPath.IndexOf(':', 2) > 0))) {
247 isSuspicious = !String.IsNullOrEmpty(physicalPath) &&
248 String.Compare(physicalPath, Path.GetFullPath(physicalPath),
249 StringComparison.OrdinalIgnoreCase) != 0;
252 catch (PathTooLongException) {
256 catch (NotSupportedException) {
257 // see comment below -- we do the same for ':'
261 catch (ArgumentException) {
262 // DevDiv Bugs 152256: Illegal characters {",|} in path prevent configuration system from working.
263 // We need to catch this exception and conservatively assume that the path is suspicious in
265 // We also set pathTooLong to true because at this point we do not know if the path is too long
266 // or not. If we assume that pathTooLong is false, it means that our path length enforcement
267 // is bypassed by using URLs with illegal characters. We do not want that. Moreover, returning
268 // pathTooLong = true causes the current logic to peel of URL fragments, which can also find a
269 // path without illegal characters to retrieve the config.
277 static bool HasInvalidLastChar(string physicalPath) {
278 // see VSWhidbey #108945
279 // We need to filter out directory names which end
280 // in " " or ".". We want to treat path names that
281 // end in these characters as files - however, Windows
282 // will strip these characters off the end of the name,
283 // which may result in the name being treated as a
284 // directory instead.
286 if (String.IsNullOrEmpty(physicalPath)) {
290 char lastChar = physicalPath[physicalPath.Length - 1];
291 return lastChar == ' ' || lastChar == '.';
294 internal static bool DirectoryExists(String dirname) {
296 dirname = RemoveTrailingDirectoryBackSlash(dirname);
297 if (HasInvalidLastChar(dirname))
301 exists = Directory.Exists(dirname);
309 internal static bool DirectoryAccessible(String dirname) {
310 bool accessible = false;
311 dirname = RemoveTrailingDirectoryBackSlash(dirname);
312 if (HasInvalidLastChar(dirname))
316 accessible = (new DirectoryInfo(dirname)).Exists;
324 private static Char[] _invalidFileNameChars = Path.GetInvalidFileNameChars();
325 internal static bool IsValidDirectoryName(String name) {
326 if (String.IsNullOrEmpty(name)) {
330 if (name.IndexOfAny(_invalidFileNameChars, 0) != -1) {
334 if (name.Equals(".") || name.Equals("..")) {
342 // Given a physical path, determine if it exists, and whether it is a directory or file.
344 // If directoryExistsOnError is set, set exists=true and isDirectory=true if we cannot confirm that the path does not exist.
345 // If fileExistsOnError is set, set exists=true and isDirectory=false if we cannot confirm that the path does not exist.
348 // this code is called by config that doesn't have AspNetHostingPermission
349 internal static void PhysicalPathStatus(string physicalPath, bool directoryExistsOnError, bool fileExistsOnError, out bool exists, out bool isDirectory) {
353 Debug.Assert(!(directoryExistsOnError && fileExistsOnError), "!(directoryExistsOnError && fileExistsOnError)");
355 if (String.IsNullOrEmpty(physicalPath))
358 using (new ApplicationImpersonationContext()) {
359 UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data;
360 bool ok = UnsafeNativeMethods.GetFileAttributesEx(physicalPath, UnsafeNativeMethods.GetFileExInfoStandard, out data);
363 isDirectory = ((data.fileAttributes & (int) FileAttributes.Directory) == (int) FileAttributes.Directory);
364 if (isDirectory && HasInvalidLastChar(physicalPath)) {
369 if (directoryExistsOnError || fileExistsOnError) {
370 // Set exists to true if we cannot confirm that the path does NOT exist.
371 int hr = Marshal.GetHRForLastWin32Error();
372 if (!(hr == HResults.E_FILENOTFOUND || hr == HResults.E_PATHNOTFOUND)) {
374 isDirectory = directoryExistsOnError;
382 // Use to avoid the perf hit of a Demand when the Demand is not necessary for security.
384 // If trueOnError is set, then return true if we cannot confirm that the file does NOT exist.
386 internal static bool DirectoryExists(string filename, bool trueOnError) {
387 filename = RemoveTrailingDirectoryBackSlash(filename);
388 if (HasInvalidLastChar(filename)) {
392 UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data;
393 bool ok = UnsafeNativeMethods.GetFileAttributesEx(filename, UnsafeNativeMethods.GetFileExInfoStandard, out data);
395 // The path exists. Return true if it is a directory, false if a file.
396 return (data.fileAttributes & (int) FileAttributes.Directory) == (int) FileAttributes.Directory;
403 // Return true if we cannot confirm that the file does NOT exist.
404 int hr = Marshal.GetHRForLastWin32Error();
405 if (hr == HResults.E_FILENOTFOUND || hr == HResults.E_PATHNOTFOUND) {
418 // Wraps the Win32 API FindFirstFile
420 sealed class FindFileData {
422 private FileAttributesData _fileAttributesData;
423 private string _fileNameLong;
424 private string _fileNameShort;
426 internal string FileNameLong { get { return _fileNameLong; } }
427 internal string FileNameShort { get { return _fileNameShort; } }
428 internal FileAttributesData FileAttributesData { get { return _fileAttributesData; } }
430 // FindFile - given a file name, gets the file attributes and short form (8.3 format) of a file name.
431 static internal int FindFile(string path, out FindFileData data) {
433 UnsafeNativeMethods.WIN32_FIND_DATA wfd;
437 // Remove trailing slash if any, otherwise FindFirstFile won't work correctly
438 path = FileUtil.RemoveTrailingDirectoryBackSlash(path);
440 Debug.Assert(Path.GetDirectoryName(path) != null, "Path.GetDirectoryName(path) != null");
441 Debug.Assert(Path.GetFileName(path) != null, "Path.GetFileName(path) != null");
444 hFindFile = UnsafeNativeMethods.FindFirstFile(path, out wfd);
445 int lastError = Marshal.GetLastWin32Error(); // FXCOP demands that this preceed the ==
446 if (hFindFile == UnsafeNativeMethods.INVALID_HANDLE_VALUE) {
447 return HttpException.HResultFromLastError(lastError);
450 UnsafeNativeMethods.FindClose(hFindFile);
453 string file = Path.GetFileName(path);
454 file = file.TrimEnd(' ', '.');
455 Debug.Assert(StringUtil.EqualsIgnoreCase(file, wfd.cFileName) ||
456 StringUtil.EqualsIgnoreCase(file, wfd.cAlternateFileName),
457 "Path to FindFile is not for a single file: " + path);
460 data = new FindFileData(ref wfd);
461 return HResults.S_OK;
464 // FindFile - takes a full-path and a root-directory-path, and is used to get the
465 // short form (8.3 format) of the relative-path. A FindFileData structure is returned
466 // with FileNameLong and FileNameShort relative to the specified root-directory-path.
468 // For example, if full-path is "c:\vdir\subdirectory\t.aspx" and root-directory-path
469 // is "c:\vdir", then the relative-path will be "subdirectory\t.aspx" and it's short
470 // form will be something like "subdir~1\t~1.ASP".
472 // This is used by FileChangesMonitor to support the ability to monitor all files and
473 // directories at any depth beneath the application root directory.
474 internal static int FindFile(string fullPath, string rootDirectoryPath, out FindFileData data) {
476 int hr = FindFileData.FindFile(fullPath, out data);
477 if (hr != HResults.S_OK || String.IsNullOrEmpty(rootDirectoryPath)) {
482 // The trailing slash should have been removed already, unless the root is "c:\"
483 Debug.Assert(rootDirectoryPath.Length < 4 || rootDirectoryPath[rootDirectoryPath.Length-1] != '\\', "Trailing slash unexpected: " + rootDirectoryPath);
486 // remove it just in case
487 rootDirectoryPath = FileUtil.RemoveTrailingDirectoryBackSlash(rootDirectoryPath);
490 Debug.Assert(fullPath.IndexOf(rootDirectoryPath, StringComparison.OrdinalIgnoreCase) == 0,
491 "fullPath (" + fullPath + ") is not within rootDirectoryPath (" + rootDirectoryPath + ")");
494 // crawl backwards along the subdirectories of fullPath until we get to the specified rootDirectoryPath
495 string relativePathLong = String.Empty;
496 string relativePathShort = String.Empty;
497 string currentParentDir = Path.GetDirectoryName(fullPath);
498 while (currentParentDir != null
499 && currentParentDir.Length > rootDirectoryPath.Length+1
500 && currentParentDir.IndexOf(rootDirectoryPath, StringComparison.OrdinalIgnoreCase) == 0) {
502 UnsafeNativeMethods.WIN32_FIND_DATA fd;
503 IntPtr hFindFile = UnsafeNativeMethods.FindFirstFile(currentParentDir, out fd);
504 int lastError = Marshal.GetLastWin32Error(); // FXCOP demands that this preceed the ==
505 if (hFindFile == UnsafeNativeMethods.INVALID_HANDLE_VALUE) {
506 return HttpException.HResultFromLastError(lastError);
508 UnsafeNativeMethods.FindClose(hFindFile);
511 Debug.Assert(!String.IsNullOrEmpty(fd.cFileName), "!String.IsNullOrEmpty(fd.cFileName)");
514 // build the long and short versions of the relative path
515 relativePathLong = fd.cFileName + Path.DirectorySeparatorChar + relativePathLong;
516 if (!String.IsNullOrEmpty(fd.cAlternateFileName)) {
517 relativePathShort = fd.cAlternateFileName + Path.DirectorySeparatorChar + relativePathShort;
520 relativePathShort = fd.cFileName + Path.DirectorySeparatorChar + relativePathShort;
523 currentParentDir = Path.GetDirectoryName(currentParentDir);
526 if (!String.IsNullOrEmpty(relativePathLong)) {
527 data.PrependRelativePath(relativePathLong, relativePathShort);
531 Debug.Trace("FindFile", "fullPath=" + fullPath + ", rootDirectoryPath=" + rootDirectoryPath);
532 Debug.Trace("FindFile", "relativePathLong=" + relativePathLong + ", relativePathShort=" + relativePathShort);
533 string fileNameShort = data.FileNameShort == null ? "<null>" : data.FileNameShort;
534 Debug.Trace("FindFile", "FileNameLong=" + data.FileNameLong + ", FileNameShrot=" + fileNameShort);
540 internal FindFileData(ref UnsafeNativeMethods.WIN32_FIND_DATA wfd) {
541 _fileAttributesData = new FileAttributesData(ref wfd);
542 _fileNameLong = wfd.cFileName;
543 if (wfd.cAlternateFileName != null
544 && wfd.cAlternateFileName.Length > 0
545 && !StringUtil.EqualsIgnoreCase(wfd.cFileName, wfd.cAlternateFileName)) {
546 _fileNameShort = wfd.cAlternateFileName;
550 private void PrependRelativePath(string relativePathLong, string relativePathShort) {
551 _fileNameLong = relativePathLong + _fileNameLong;
553 // if the short form is null or empty, prepend the short relative path to the long form
554 string fileName = String.IsNullOrEmpty(_fileNameShort) ? _fileNameLong : _fileNameShort;
555 _fileNameShort = relativePathShort + fileName;
557 // if the short form is the same as the long form, set the short form to null
558 if (StringUtil.EqualsIgnoreCase(_fileNameShort, _fileNameLong)) {
559 _fileNameShort = null;
565 // Wraps the Win32 API GetFileAttributesEx
566 // We use this api in addition to FindFirstFile because FindFirstFile
567 // does not work for volumes (e.g. "c:\")
569 sealed class FileAttributesData {
570 internal readonly FileAttributes FileAttributes;
571 internal readonly DateTime UtcCreationTime;
572 internal readonly DateTime UtcLastAccessTime;
573 internal readonly DateTime UtcLastWriteTime;
574 internal readonly long FileSize;
576 static internal FileAttributesData NonExistantAttributesData {
578 return new FileAttributesData();
582 static internal int GetFileAttributes(string path, out FileAttributesData fad) {
585 UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data;
586 if (!UnsafeNativeMethods.GetFileAttributesEx(path, UnsafeNativeMethods.GetFileExInfoStandard, out data)) {
587 return HttpException.HResultFromLastError(Marshal.GetLastWin32Error());
590 fad = new FileAttributesData(ref data);
591 return HResults.S_OK;
594 FileAttributesData() {
598 FileAttributesData(ref UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data) {
599 FileAttributes = (FileAttributes) data.fileAttributes;
600 UtcCreationTime = DateTimeUtil.FromFileTimeToUtc(((long)data.ftCreationTimeHigh) << 32 | (long)data.ftCreationTimeLow);
601 UtcLastAccessTime = DateTimeUtil.FromFileTimeToUtc(((long)data.ftLastAccessTimeHigh) << 32 | (long)data.ftLastAccessTimeLow);
602 UtcLastWriteTime = DateTimeUtil.FromFileTimeToUtc(((long)data.ftLastWriteTimeHigh) << 32 | (long)data.ftLastWriteTimeLow);
603 FileSize = (long)(uint)data.fileSizeHigh << 32 | (long)(uint)data.fileSizeLow;
606 internal FileAttributesData(ref UnsafeNativeMethods.WIN32_FIND_DATA wfd) {
607 FileAttributes = (FileAttributes) wfd.dwFileAttributes;
608 UtcCreationTime = DateTimeUtil.FromFileTimeToUtc(((long)wfd.ftCreationTime_dwHighDateTime) << 32 | (long)wfd.ftCreationTime_dwLowDateTime);
609 UtcLastAccessTime = DateTimeUtil.FromFileTimeToUtc(((long)wfd.ftLastAccessTime_dwHighDateTime) << 32 | (long)wfd.ftLastAccessTime_dwLowDateTime);
610 UtcLastWriteTime = DateTimeUtil.FromFileTimeToUtc(((long)wfd.ftLastWriteTime_dwHighDateTime) << 32 | (long)wfd.ftLastWriteTime_dwLowDateTime);
611 FileSize = (long)wfd.nFileSizeHigh << 32 | (long)wfd.nFileSizeLow;
615 internal string DebugDescription(string indent) {
616 StringBuilder sb = new StringBuilder(200);
617 string i2 = indent + " ";
619 sb.Append(indent + "FileAttributesData\n");
620 sb.Append(i2 + "FileAttributes: " + FileAttributes + "\n");
621 sb.Append(i2 + " CreationTime: " + Debug.FormatUtcDate(UtcCreationTime) + "\n");
622 sb.Append(i2 + "LastAccessTime: " + Debug.FormatUtcDate(UtcLastAccessTime) + "\n");
623 sb.Append(i2 + " LastWriteTime: " + Debug.FormatUtcDate(UtcLastWriteTime) + "\n");
624 sb.Append(i2 + " FileSize: " + FileSize.ToString("n0", NumberFormatInfo.InvariantInfo) + "\n");
626 return sb.ToString();