diff --git a/Rest.Net.Tests/ServeFileTests.cs b/Rest.Net.Tests/ServeFileTests.cs
index 9035abe..77478fa 100644
--- a/Rest.Net.Tests/ServeFileTests.cs
+++ b/Rest.Net.Tests/ServeFileTests.cs
@@ -12,29 +12,98 @@ namespace Rest.Net.Tests
{
public class ServeFileTests
{
+ public string BaseDirectory = null;
+
public string TestDirectory = null;
public string TestFile = null;
public ServeFileTests()
{
- this.TestDirectory = Path.Combine(Environment.CurrentDirectory, "test/");
+ this.BaseDirectory = Path.Combine(Environment.CurrentDirectory, "test");
+
+ if (!Directory.Exists(this.BaseDirectory))
+ Directory.CreateDirectory(this.BaseDirectory);
+
+ this.TestDirectory = Path.Combine(this.BaseDirectory, "test2");
if (!Directory.Exists(this.TestDirectory))
Directory.CreateDirectory(this.TestDirectory);
- this.TestFile = Path.Combine(this.TestDirectory, "test.html");
+ this.TestFile = Path.Combine(this.BaseDirectory, "test.html");
if (!File.Exists(this.TestFile))
File.WriteAllText(this.TestFile, "hello world");
}
[Fact]
- public void ServeMultipleShouldWorkForFiles()
+ public void ServeMultiple_File_ShouldWork()
{
- HttpListenerResponseExtensions.ResolveMultiPagePath(Path.Combine(Environment.CurrentDirectory, "test"), "../../test.html", null, out string resolvedPath, out bool isDirecotry).Should().BeTrue();
+ HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, "/test.html", null, out string resolvedPath, out bool isDirectory).Should().BeTrue();
- isDirecotry.Should().BeFalse();
+ isDirectory.Should().BeFalse();
+
+ resolvedPath.Should().BeEquivalentTo(this.TestFile);
+ }
+
+ [Fact]
+ public void ServeMultiple_Directory_ShouldWork()
+ {
+ HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, "/test2", null, out string resolvedPath, out bool isDirectory).Should().BeTrue();
+
+ isDirectory.Should().BeTrue();
+
+ resolvedPath.Should().BeEquivalentTo(this.TestDirectory);
+ }
+
+ [Fact]
+ public void ServeMultiple_NavigatingUp_Should_NotWork()
+ {
+ HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, "../test.html", null, out string resolvedPath, out bool isDirectory).Should().BeFalse();
+ }
+
+ [Fact]
+ public void ServeMultiple_Correct_NavigatingUp_Should_Work()
+ {
+ HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, "a/b/../../test.html", null, out string resolvedPath, out bool isDirectory).Should().BeTrue();
+
+ isDirectory.Should().BeFalse();
+
+ resolvedPath.Should().BeEquivalentTo(this.TestFile);
+ }
+
+ [Fact]
+ public void ServeMultiple_NavigatingUp_Multiple_Should_NotWork()
+ {
+ HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, "test/../../test.html", null, out string resolvedPath, out bool isDirectory).Should().BeFalse();
+ }
+
+ [Fact]
+ public void ServeSingle_File_ShouldWork()
+ {
+ HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, "/test.html", null, out string resolvedPath, out bool isDirectory).Should().BeTrue();
+
+ isDirectory.Should().BeFalse();
+
+ resolvedPath.Should().BeEquivalentTo(this.TestFile);
+ }
+
+ [Fact]
+ public void ServeSingle_Directory_ShouldWork()
+ {
+ HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, "/test2", null, out string resolvedPath, out bool isDirectory).Should().BeTrue();
+
+ isDirectory.Should().BeTrue();
+
+ resolvedPath.Should().BeEquivalentTo(this.TestDirectory);
+ }
+
+ [Fact]
+ public void ServeSingle_File_Route_ShouldWork()
+ {
+ HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, "/a/b/test.html", null, out string resolvedPath, out bool isDirectory).Should().BeTrue();
+
+ isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
diff --git a/Rest.Net/HttpListenerResponseExtensions.cs b/Rest.Net/HttpListenerResponseExtensions.cs
index 96a7a8e..bbff058 100644
--- a/Rest.Net/HttpListenerResponseExtensions.cs
+++ b/Rest.Net/HttpListenerResponseExtensions.cs
@@ -505,8 +505,10 @@ namespace MontoyaTech.Rest.Net
/// The base path where to serve files from
/// The request to serve
/// The name of the index file, default is index.html
+ /// Whether or not to compress files served. Default is false.
+ /// A collection of file extensions that should be compressed, example: .jpg, default is null. If and compress is true, all files will be compressed.
/// The modified response
- public static HttpListenerResponse ServeMultiPage(this HttpListenerResponse response, string basePath, HttpListenerRequest request, string indexFile = "index.html")
+ public static HttpListenerResponse ServeMultiPage(this HttpListenerResponse response, string basePath, HttpListenerRequest request, string indexFile = "index.html", bool compress = false, HashSet compressExtensions = null)
{
if (ResolveMultiPagePath(basePath, request.Url.LocalPath, indexFile, out string resolvedPath, out bool isDirectory))
{
@@ -517,9 +519,16 @@ namespace MontoyaTech.Rest.Net
else
{
if (request.HttpMethod.Equals("head", StringComparison.CurrentCultureIgnoreCase))
+ {
return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
+ }
else
- return response.WithStatus(HttpStatusCode.OK).WithFile(resolvedPath);
+ {
+ if (compress && (compressExtensions == null || compressExtensions.Contains(Path.GetExtension(resolvedPath))))
+ return response.WithStatus(HttpStatusCode.OK).WithCompressedFile(resolvedPath);
+ else
+ return response.WithStatus(HttpStatusCode.OK).WithFile(resolvedPath);
+ }
}
}
else
@@ -534,12 +543,48 @@ namespace MontoyaTech.Rest.Net
isDirectory = false;
- var relativePath = Path.GetRelativePath(basePath, requestPath);
+ //If the requestPath is pointing to nothing change that to the index file.
+ if (string.IsNullOrWhiteSpace(requestPath) || requestPath == "/" || requestPath == ".")
+ requestPath = indexFile;
- if (string.IsNullOrWhiteSpace(relativePath) || relativePath == ".")
- relativePath = indexFile;
+ //Break th erequest path into it's components so we can enfore staying in the base path.
+ var components = requestPath.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
- var absolutePath = Path.Combine(basePath, relativePath);
+ for (int i = 0; i < components.Count; i++)
+ {
+ if (components[i].Trim() == "..")
+ {
+ components.RemoveAt(i--);
+
+ if (i >= 0)
+ components.RemoveAt(i--);
+ else
+ return false; //Trying to jump outside of basePath
+ }
+ else if (components[i].Trim() == "...")
+ {
+ components.RemoveAt(i--);
+
+ if (i >= 0)
+ components.RemoveAt(i--);
+ else
+ return false; //Trying to jump outside of basePath
+
+ if (i >= 0)
+ components.RemoveAt(i--);
+ else
+ return false; //Trying to jump outside of basePath
+ }
+ else if (components[i].Trim() == ".")
+ {
+ components.RemoveAt(i--);
+ }
+ }
+
+ if (components.Count == 0)
+ return false;
+
+ var absolutePath = Path.Combine(basePath, components.Separate(Path.DirectorySeparatorChar));
if (File.Exists(absolutePath))
{
@@ -568,31 +613,104 @@ namespace MontoyaTech.Rest.Net
/// The base path where to serve files from
/// The request to serve
/// The name of the index file, default is index.html
+ /// Whether or not to compress files served. Default is false.
+ /// A collection of file extensions that should be compressed, example: .jpg, default is null. If and compress is true, all files will be compressed.
/// The modified response
- public static HttpListenerResponse ServeSinglePage(this HttpListenerResponse response, string basePath, HttpListenerRequest request, string indexFile = "index.html")
+ public static HttpListenerResponse ServeSinglePage(this HttpListenerResponse response, string basePath, HttpListenerRequest request, string indexFile = "index.html", bool compress = false, HashSet compressExtensions = null)
{
- var relativePath = Path.GetRelativePath(basePath, request.Url.LocalPath);
-
- if (string.IsNullOrWhiteSpace(relativePath) || relativePath == ".")
- relativePath = indexFile;
-
- var absolutePath = Path.Combine(basePath, relativePath);
-
- if (File.Exists(absolutePath))
+ if (ResolveSinglePagePath(basePath, request.Url.LocalPath, indexFile, out string resolvedPath, out bool isDirectory))
{
- if (request.HttpMethod.Equals("head", StringComparison.CurrentCultureIgnoreCase))
+ if (isDirectory)
+ {
return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
+ }
else
- return response.WithStatus(HttpStatusCode.OK).WithFile(absolutePath);
- }
- else if (Directory.Exists(absolutePath))
- {
- return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
+ {
+ if (request.HttpMethod.Equals("head", StringComparison.CurrentCultureIgnoreCase))
+ {
+ return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
+ }
+ else
+ {
+ if (compress && (compressExtensions == null || compressExtensions.Contains(Path.GetExtension(resolvedPath))))
+ return response.WithStatus(HttpStatusCode.OK).WithCompressedFile(resolvedPath);
+ else
+ return response.WithStatus(HttpStatusCode.OK).WithFile(resolvedPath);
+ }
+ }
}
else
{
- var components = relativePath.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
+ return response.WithStatus(HttpStatusCode.NotFound);
+ }
+ }
+ internal static bool ResolveSinglePagePath(string basePath, string requestPath, string indexFile, out string resolvedPath, out bool isDirectory)
+ {
+ resolvedPath = null;
+
+ isDirectory = false;
+
+ //If the requestPath is pointing to nothing change that to the index file.
+ if (string.IsNullOrWhiteSpace(requestPath) || requestPath == "/" || requestPath == ".")
+ requestPath = indexFile;
+
+ //Break th erequest path into it's components so we can enfore staying in the base path.
+ var components = requestPath.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
+
+ for (int i = 0; i < components.Count; i++)
+ {
+ if (components[i].Trim() == "..")
+ {
+ components.RemoveAt(i--);
+
+ if (i >= 0)
+ components.RemoveAt(i--);
+ else
+ return false; //Trying to jump outside of basePath
+ }
+ else if (components[i].Trim() == "...")
+ {
+ components.RemoveAt(i--);
+
+ if (i >= 0)
+ components.RemoveAt(i--);
+ else
+ return false; //Trying to jump outside of basePath
+
+ if (i >= 0)
+ components.RemoveAt(i--);
+ else
+ return false; //Trying to jump outside of basePath
+ }
+ else if (components[i].Trim() == ".")
+ {
+ components.RemoveAt(i--);
+ }
+ }
+
+ if (components.Count == 0)
+ return false;
+
+ var absolutePath = Path.Combine(basePath, components.Separate(Path.DirectorySeparatorChar));
+
+ if (File.Exists(absolutePath))
+ {
+ resolvedPath = absolutePath;
+
+ return true;
+ }
+ else if (Directory.Exists(absolutePath))
+ {
+ resolvedPath = absolutePath;
+
+ isDirectory = true;
+
+ return true;
+ }
+ else
+ {
+ //Try to components that don't exist and try again
while (components.Count > 0)
{
string path = Path.Combine(basePath, components[0]);
@@ -604,25 +722,26 @@ namespace MontoyaTech.Rest.Net
}
if (components.Count == 0)
- return response.WithStatus(HttpStatusCode.NotFound);
+ return false;
- var combined = Path.Combine(basePath, components.Separate(Path.PathSeparator));
+ absolutePath = Path.Combine(basePath, components.Separate(Path.PathSeparator));
- if (File.Exists(combined))
+ if (File.Exists(absolutePath))
{
- if (request.HttpMethod.Equals("head", StringComparison.CurrentCultureIgnoreCase))
- return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
- else
- return response.WithStatus(HttpStatusCode.OK).WithFile(absolutePath);
+ resolvedPath = absolutePath;
+
+ return true;
}
- else if (Directory.Exists(combined))
+ else if (Directory.Exists(absolutePath))
{
- return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
- }
- else
- {
- return response.WithStatus(HttpStatusCode.NotFound);
+ resolvedPath = absolutePath;
+
+ isDirectory = true;
+
+ return true;
}
+
+ return false;
}
}
}
diff --git a/Rest.Net/Rest.Net.csproj b/Rest.Net/Rest.Net.csproj
index 31923dd..a77cf3b 100644
--- a/Rest.Net/Rest.Net.csproj
+++ b/Rest.Net/Rest.Net.csproj
@@ -17,7 +17,7 @@
MontoyaTech.Rest.Net
MontoyaTech.Rest.Net
True
- 1.6.2
+ 1.6.3
Logo_Symbol_Black_Outline.png
diff --git a/Rest.Net/StringExtensions.cs b/Rest.Net/StringExtensions.cs
index 35c771e..c51e144 100644
--- a/Rest.Net/StringExtensions.cs
+++ b/Rest.Net/StringExtensions.cs
@@ -22,7 +22,7 @@ namespace MontoyaTech.Rest.Net
public static string Separate(this IList input, char separator)
{
if (input == null || input.Count == 0)
- return null;
+ return "";
else if (input.Count < 2)
return input[0];
@@ -39,7 +39,7 @@ namespace MontoyaTech.Rest.Net
public static string Separate(this IList input, string separator)
{
if (input == null || input.Count == 0)
- return null;
+ return "";
else if (input.Count < 2)
return input[0];