Skip to content

Instantly share code, notes, and snippets.

@grumpydev
Created October 30, 2013 12:12
Show Gist options
  • Save grumpydev/7231607 to your computer and use it in GitHub Desktop.
Save grumpydev/7231607 to your computer and use it in GitHub Desktop.
namespace Nancy.Embedded.Conventions
{
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Helpers;
using Responses;
public class EmbeddedStaticContentConventionBuilder
{
private static readonly ConcurrentDictionary<string, Func<Response>> ResponseFactoryCache;
private static readonly Regex PathReplaceRegex = new Regex(@"[/\\]", RegexOptions.Compiled);
static EmbeddedStaticContentConventionBuilder()
{
ResponseFactoryCache = new ConcurrentDictionary<string, Func<Response>>();
}
/// <summary>
/// Adds a directory-based convention for embedded static convention.
/// </summary>
/// <param name="requestedPath">The path that should be matched with the request.</param>
/// <param name="contentPath">The path to where the content is stored in your application, relative to the root. If this is <see langword="null" /> then it will be the same as <paramref name="requestedPath"/>.</param>
/// <param name="allowedExtensions">A list of extensions that is valid for the conventions. If not supplied, all extensions are valid.</param>
/// <returns>A <see cref="EmbeddedFileResponse"/> instance for the requested embedded static contents if it was found, otherwise <see langword="null"/>.</returns>
public static Func<NancyContext, string, Response> AddDirectory(string requestedPath, Assembly assembly, string contentPath = null, params string[] allowedExtensions)
{
if (!requestedPath.StartsWith("/"))
{
requestedPath = string.Concat("/", requestedPath);
}
return (ctx, root) =>
{
var path =
HttpUtility.UrlDecode(ctx.Request.Path);
var fileName =
Path.GetFileName(path);
if (string.IsNullOrEmpty(fileName))
{
return null;
}
var pathWithoutFilename =
GetPathWithoutFilename(fileName, path);
if (!pathWithoutFilename.StartsWith(requestedPath, StringComparison.OrdinalIgnoreCase))
{
ctx.Trace.TraceLog.WriteLog(x => x.AppendLine(string.Concat("[EmbeddedStaticContentConventionBuilder] The requested resource '", path, "' does not match convention mapped to '", requestedPath, "'")));
return null;
}
contentPath =
GetContentPath(requestedPath, contentPath);
var responseFactory =
ResponseFactoryCache.GetOrAdd(path, BuildContentDelegate(ctx, requestedPath, contentPath, assembly, allowedExtensions));
return responseFactory.Invoke();
};
}
private static Func<string, Func<Response>> BuildContentDelegate(NancyContext context, string requestedPath, string contentPath, Assembly assembly, string[] allowedExtensions)
{
return requestPath =>
{
context.Trace.TraceLog.WriteLog(x => x.AppendLine(string.Concat("[EmbeddedStaticContentConventionBuilder] Attempting to resolve embedded static content '", requestPath, "'")));
var extension = Path.GetExtension(requestPath);
if (allowedExtensions.Length != 0 && !allowedExtensions.Any(e => string.Equals(e, extension, StringComparison.OrdinalIgnoreCase)))
{
context.Trace.TraceLog.WriteLog(x => x.AppendLine(string.Concat("[EmbeddedStaticContentConventionBuilder] The requested extension '", extension, "' does not match any of the valid extensions for the convention '", string.Join(",", allowedExtensions), "'")));
return () => null;
}
var transformedRequestPath =
GetSafeRequestPath(requestPath, requestedPath, contentPath);
transformedRequestPath =
GetEncodedPath(transformedRequestPath);
// Resolve relative paths by using c:\ as a fake root path
var fileName =
Path.GetFullPath(Path.Combine("c:\\", transformedRequestPath));
var contentRootPath =
Path.GetFullPath(Path.Combine("c:\\", GetEncodedPath(contentPath)));
if (!IsWithinContentFolder(contentRootPath, fileName))
{
context.Trace.TraceLog.WriteLog(x => x.AppendLine(string.Concat("[EmbeddedStaticContentConventionBuilder] The request '", fileName, "' is trying to access a path outside the content folder '", contentPath, "'")));
return () => null;
}
var resourceName =
Path.GetDirectoryName(assembly.GetName().Name + fileName.Substring(2)).Replace('\\', '.').Replace('-', '_');
fileName =
Path.GetFileName(fileName);
if (!assembly.GetManifestResourceNames().Any(x => string.Equals(x, resourceName + "." + fileName, StringComparison.OrdinalIgnoreCase)))
{
context.Trace.TraceLog.WriteLog(x => x.AppendLine(string.Concat("[EmbeddedStaticContentConventionBuilder] The requested resource '", requestPath, "' was not found in assembly '", assembly.GetName().Name, "'")));
return () => null;
}
context.Trace.TraceLog.WriteLog(x => x.AppendLine(string.Concat("[EmbeddedStaticContentConventionBuilder] Returning file '", fileName, "'")));
return () => new EmbeddedFileResponse(assembly, resourceName, fileName);
};
}
private static string GetEncodedPath(string path)
{
return PathReplaceRegex.Replace(path.TrimStart(new[] { '/' }), Path.DirectorySeparatorChar.ToString());
}
private static string GetSafeRequestPath(string requestPath, string requestedPath, string contentPath)
{
var actualContentPath =
(contentPath.Equals("/") ? string.Empty : contentPath);
if (requestedPath.Equals("/"))
{
return string.Concat(actualContentPath, requestPath);
}
var expression =
new Regex(Regex.Escape(requestedPath), RegexOptions.IgnoreCase);
return expression.Replace(requestPath, actualContentPath, 1);
}
private static string GetContentPath(string requestedPath, string contentPath)
{
contentPath =
contentPath ?? requestedPath;
if (!contentPath.StartsWith("/"))
{
contentPath = string.Concat("/", contentPath);
}
return contentPath;
}
private static string GetPathWithoutFilename(string fileName, string path)
{
var pathWithoutFileName =
path.Replace(fileName, string.Empty);
return (pathWithoutFileName.Equals("/")) ?
pathWithoutFileName :
pathWithoutFileName.TrimEnd(new[] { '/' });
}
/// <summary>
/// Returns whether the given filename is contained within the content folder
/// </summary>
/// <param name="contentRootPath">Content root path</param>
/// <param name="fileName">Filename requested</param>
/// <returns>True if contained within the content root, false otherwise</returns>
private static bool IsWithinContentFolder(string contentRootPath, string fileName)
{
return fileName.StartsWith(contentRootPath, StringComparison.Ordinal);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment