Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ public static partial class HostingEnvironment
public static bool IsHosted { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public static string SiteName { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public static System.Web.Hosting.VirtualPathProvider VirtualPathProvider { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public static string MapPath(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
public static void RegisterVirtualPathProvider(System.Web.Hosting.VirtualPathProvider provider) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
}
public abstract partial class VirtualDirectory : System.Web.Hosting.VirtualFileBase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Web.Caching;
using System.Web.Util;
using Microsoft.Extensions.DependencyInjection;

namespace System.Web.Hosting;

Expand All @@ -26,5 +28,29 @@ public static void RegisterVirtualPathProvider(VirtualPathProvider provider)
HostingEnvironmentAccessor.Current.Options.VirtualPathProvider = provider;
}

public static string MapPath(string? virtualPath)
{
if (string.IsNullOrEmpty(virtualPath))
{
throw new ArgumentNullException(nameof(virtualPath));
}

// original implementation disallows paths that are not virtual or do not begin with a forward slash, e.g. file.txt.
if (!VirtualPathUtilityImpl.IsAppRelative(virtualPath) && UrlPath.FixVirtualPathSlashes(virtualPath)[0] != '/')
{
throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.");
}

// original implementation allows paths starting with // and \\ but MapPathUtility.MapPath() throws
// an error that the path is a physical path. to avoid the error, collapsing multiple leading slash characters
// to a single slash.
if (UrlPath.IsUncSharePath(virtualPath))
{
virtualPath = $"/{virtualPath.TrimStart('/', '\\')}";
}

return HttpRuntime.WebObjectActivator.GetRequiredService<IMapPathUtility>().MapPath("/", virtualPath);
}

public static Cache Cache => HttpRuntime.Cache;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@ public string MapPath(string requestPath, string? path)
return rootPath;
}

return Path.Combine(
var hasTrailingSlash = !string.IsNullOrEmpty(path) && (path.EndsWith('/') || path.EndsWith('\\'));

var combined = Path.Combine(
rootPath,
appPath[1..]
.Replace('/', Path.DirectorySeparatorChar))
.TrimEnd(Path.DirectorySeparatorChar);
.Replace('/', Path.DirectorySeparatorChar));

// mirror the input to include or exclude a trailing slash.
if (hasTrailingSlash && !combined.EndsWith(Path.DirectorySeparatorChar))
combined += Path.DirectorySeparatorChar;
else if (!hasTrailingSlash && combined.EndsWith(Path.DirectorySeparatorChar))
combined = combined.TrimEnd(Path.DirectorySeparatorChar);

return combined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ internal static string GetDirectory(string path)
private static bool IsDirectorySeparatorChar(char ch) => ch == '\\' || ch == '/';

// e.g \\server\share\foo or //server/share/foo
private static bool IsUncSharePath(string path) => path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]);
internal static bool IsUncSharePath(string path) => path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]);

private static bool IsAbsolutePhysicalPath(string path)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Web;
using System.Web.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.SystemWebAdapters;

public class HostingEnvironmentTests
{
[InlineData("/DownOneLevel/DownLevelPage.aspx", "DownOneLevel\\DownLevelPage.aspx")]
[InlineData("~/MyUploadedFiles", "MyUploadedFiles")]
[InlineData("/UploadedFiles", "UploadedFiles")]
[InlineData("/NotRealFolder", "NotRealFolder")]
[InlineData("/TrailingSlash/", "TrailingSlash\\")]
[InlineData("\\TrailingSlash2\\", "TrailingSlash2\\")]
[InlineData("\\\\SomeServer\\Share\\Path", "SomeServer\\Share\\Path")]
[InlineData("//SomeServer/Share/Path", "SomeServer\\Share\\Path")]
[Theory]
public void MapPath(string? virtualPath, string expectedRelativePath)
{
// Arrange
var options = new SystemWebAdaptersOptions
{
AppDomainAppVirtualPath = "/",
AppDomainAppPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\ExampleSites\TestMapPath" : "/apps/test-map-path"
};
var ioptions = Options.Create(options);
var utility = new MapPathUtility(ioptions, new(ioptions));

var services = new ServiceCollection();
services.AddSingleton<IMapPathUtility>(utility);
var serviceProvider = services.BuildServiceProvider();

string result;

// using ensures Dispose() runs which clears the static "Current" property.
using (var hostingEnvironmentAccessor = new HostingEnvironmentAccessor(serviceProvider, ioptions))
{
// Act
result = HostingEnvironment.MapPath(virtualPath);
}

var expected = Path.Combine(options.AppDomainAppPath, expectedRelativePath);

// for Linux/MacOS
expected = expected.Replace('\\', Path.DirectorySeparatorChar);

// Assert
Assert.Equal(expected, result);
}

[InlineData("../OutsideApplication", typeof(ArgumentException))]
[InlineData("C:\\OutsideApplication", typeof(ArgumentException))]
[InlineData("../RootLevelPage.aspx", typeof(ArgumentException))]
[InlineData("/../OutsideApplication.aspx", typeof(HttpException))]
[InlineData("File", typeof(ArgumentException))]
[InlineData("", typeof(ArgumentNullException))]
[InlineData(null, typeof(ArgumentNullException))]
[Theory]
public void MapPathException(string? virtualPath, Type expected)
{
// Arrange
var options = new SystemWebAdaptersOptions
{
AppDomainAppVirtualPath = "/",
AppDomainAppPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\ExampleSites\TestMapPath" : "/apps/test-map-path"
};
var ioptions = Options.Create(options);
var utility = new MapPathUtility(ioptions, new(ioptions));

var services = new ServiceCollection();
services.AddSingleton<IMapPathUtility>(utility);
var serviceProvider = services.BuildServiceProvider();

// using ensures Dispose() runs which clears the static "Current" property.
using (var hostingEnvironmentAccessor = new HostingEnvironmentAccessor(serviceProvider, ioptions))
{
// Assert
Assert.Throws(expected, () => HostingEnvironment.MapPath(virtualPath));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Web;
Expand Down Expand Up @@ -85,6 +86,8 @@ public void UrlTokenRoundtrip(string input, string expected)
[InlineData("/api/test/request/info", "/UploadedFiles", "UploadedFiles")]
[InlineData("/api/test/request/info", "UploadedFiles", "api", "test", "request", "UploadedFiles")]
[InlineData("/api/test/request/info", "~/MyUploadedFiles", "MyUploadedFiles")]
[InlineData("/api/test/request/info", "~/TrailingSlash/", "TrailingSlash\\")]
[InlineData("/api/test/request/info", "path/file", "api", "test", "request", "path", "file")]
[Theory]
public void MapPath(string page, string? path, params string[] segments)
{
Expand All @@ -101,8 +104,11 @@ public void MapPath(string page, string? path, params string[] segments)
// Act
var result = utility.MapPath(page, path);

var relative = System.IO.Path.Combine(segments);
var expected = System.IO.Path.Combine(options.AppDomainAppPath, relative);
var relative = Path.Combine(segments);
var expected = Path.Combine(options.AppDomainAppPath, relative);

// for Linux/MacOS
expected = expected.Replace('\\', Path.DirectorySeparatorChar);

// Assert
Assert.Equal(expected, result);
Expand Down