commit 86b1ac8f392774de2abf58c556e34966cf8b2f58 Author: Chris Ross Date: Thu Feb 6 15:30:21 2014 -0800 Initial static files port. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..2554a1fc23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution diff --git a/Microsoft.AspNet.StaticFiles.sln b/Microsoft.AspNet.StaticFiles.sln new file mode 100644 index 0000000000..1c760aa655 --- /dev/null +++ b/Microsoft.AspNet.StaticFiles.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.StaticFiles.net45", "src\Microsoft.AspNet.StaticFiles\Microsoft.AspNet.StaticFiles.net45.csproj", "{49278B83-CC12-49EA-8F61-D143863DD21B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.StaticFiles.k10", "src\Microsoft.AspNet.StaticFiles\Microsoft.AspNet.StaticFiles.k10.csproj", "{297551FE-7539-4E43-A6B8-165C7789F48D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8B21A3A9-9CA6-4857-A6E0-1A3203404B60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticFileSample.k10", "samples\StaticFileSample\StaticFileSample.k10.csproj", "{8C5384FC-24F3-47F2-897C-A151604F011C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticFileSample.net45", "samples\StaticFileSample\StaticFileSample.net45.csproj", "{742E16CD-8217-4386-AAF0-5F116E62CD82}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {49278B83-CC12-49EA-8F61-D143863DD21B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49278B83-CC12-49EA-8F61-D143863DD21B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49278B83-CC12-49EA-8F61-D143863DD21B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49278B83-CC12-49EA-8F61-D143863DD21B}.Release|Any CPU.Build.0 = Release|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Release|Any CPU.Build.0 = Release|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Release|Any CPU.Build.0 = Release|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {297551FE-7539-4E43-A6B8-165C7789F48D} = {40EE0889-960E-41B4-A3D3-9CE963EB0797} + {49278B83-CC12-49EA-8F61-D143863DD21B} = {40EE0889-960E-41B4-A3D3-9CE963EB0797} + {8C5384FC-24F3-47F2-897C-A151604F011C} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60} + {742E16CD-8217-4386-AAF0-5F116E62CD82} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60} + EndGlobalSection +EndGlobal diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..ab583b0ff7 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..7045ee1f84 --- /dev/null +++ b/build.cmd @@ -0,0 +1,23 @@ +@echo off +cd %~dp0 + +SETLOCAL +SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore +md .nuget +copy %CACHED_NUGET% .nuget\nuget.exe > nul + +:restore +IF EXIST packages\KoreBuild goto run +.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion + +:run +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/global.json b/global.json new file mode 100644 index 0000000000..840c36f6ad --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "sources": ["src"] +} \ No newline at end of file diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..6357ea2841 --- /dev/null +++ b/makefile.shade @@ -0,0 +1,7 @@ + +var VERSION='0.1' +var FULL_VERSION='0.1' +var AUTHORS='Microsoft' + +use-standard-lifecycle +k-standard-goals diff --git a/samples/StaticFileSample/Program.cs b/samples/StaticFileSample/Program.cs new file mode 100644 index 0000000000..1a7745c155 --- /dev/null +++ b/samples/StaticFileSample/Program.cs @@ -0,0 +1,28 @@ +using System; +#if NET45 +using System.Diagnostics; +using Microsoft.Owin.Hosting; +#endif + +namespace StaticFilesSample +{ + public class Program + { + const string baseUrl = "http://localhost:9001/"; + + public static void Main() + { +#if NET45 + using (WebApp.Start(new StartOptions(baseUrl))) + { + Console.WriteLine("Listening at {0}", baseUrl); + Process.Start(baseUrl); + Console.WriteLine("Press any key to exit"); + Console.ReadKey(); + } +#else + Console.WriteLine("Hello World"); +#endif + } + } +} \ No newline at end of file diff --git a/samples/StaticFileSample/Startup.cs b/samples/StaticFileSample/Startup.cs new file mode 100644 index 0000000000..37d1667ab3 --- /dev/null +++ b/samples/StaticFileSample/Startup.cs @@ -0,0 +1,32 @@ +#if NET45 +using System; +using System.IO; +using Microsoft.AspNet.Abstractions; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet; +using Owin; +using Microsoft.AspNet.StaticFiles; + +namespace StaticFilesSample +{ + public class Startup + { + public void Configuration(IAppBuilder app) + { + app.UseErrorPage(); + + // Temporary bridge from katana to Owin + app.UseBuilder(ConfigurePK); + } + + private void ConfigurePK(IBuilder builder) + { + builder.UseFileServer(new FileServerOptions() + { + EnableDirectoryBrowsing = true, + FileSystem = new PhysicalFileSystem(@"c:\temp") + }); + } + } +} +#endif \ No newline at end of file diff --git a/samples/StaticFileSample/project.json b/samples/StaticFileSample/project.json new file mode 100644 index 0000000000..39a19efd65 --- /dev/null +++ b/samples/StaticFileSample/project.json @@ -0,0 +1,21 @@ +{ + "version" : "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.FileSystems": "0.1-alpha-*", + "Microsoft.AspNet.StaticFiles": "", + "Microsoft.AspNet.Abstractions": "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin": "2.1.0", + "Microsoft.Owin.Diagnostics": "2.1.0", + "Microsoft.Owin.Hosting": "2.1.0", + "Microsoft.Owin.Host.HttpListener": "2.1.0", + "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*" + } + }, + "k10" : { } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/Constants.cs b/src/Microsoft.AspNet.StaticFiles/Constants.cs new file mode 100644 index 0000000000..df9c9fb1fc --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Constants.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.StaticFiles +{ + internal static class Constants + { + internal const string ServerCapabilitiesKey = "server.Capabilities"; + internal const string SendFileVersionKey = "sendfile.Version"; + internal const string SendFileVersion = "1.0"; + + internal const string Location = "Location"; + internal const string IfMatch = "If-Match"; + internal const string IfNoneMatch = "If-None-Match"; + internal const string IfModifiedSince = "If-Modified-Since"; + internal const string IfUnmodifiedSince = "If-Unmodified-Since"; + internal const string IfRange = "If-Range"; + internal const string Range = "Range"; + internal const string ContentRange = "Content-Range"; + internal const string LastModified = "Last-Modified"; + internal const string ETag = "ETag"; + + internal const string HttpDateFormat = "r"; + + internal const string TextHtmlUtf8 = "text/html; charset=utf-8"; + + internal const int Status200Ok = 200; + internal const int Status206PartialContent = 206; + internal const int Status304NotModified = 304; + internal const int Status412PreconditionFailed = 412; + internal const int Status416RangeNotSatisfiable = 416; + + internal static readonly Task CompletedTask = CreateCompletedTask(); + + private static Task CreateCompletedTask() + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(null); + return tcs.Task; + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs b/src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs new file mode 100644 index 0000000000..205da9adc1 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.AspNet.StaticFiles.ContentTypes +{ + /// + /// Provides a mapping between file extensions and MIME types. + /// + public class FileExtensionContentTypeProvider : IContentTypeProvider + { +#region Extension mapping table + /// + /// Creates a new provider with a set of default mappings. + /// + public FileExtensionContentTypeProvider() + : this(new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".3gpp", "video/3gpp" }, + { ".aac", "audio/aac" }, + { ".aaf", "application/octet-stream" }, + { ".aca", "application/octet-stream" }, + { ".accdb", "application/msaccess" }, + { ".accde", "application/msaccess" }, + { ".accdt", "application/msaccess" }, + { ".acx", "application/internet-property-stream" }, + { ".adt", "audio/vnd.dlna.adts" }, + { ".adts", "audio/vnd.dlna.adts" }, + { ".afm", "application/octet-stream" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/x-aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".application", "application/x-ms-application" }, + { ".art", "image/x-jg" }, + { ".asd", "application/octet-stream" }, + { ".asf", "video/x-ms-asf" }, + { ".asi", "application/octet-stream" }, + { ".asm", "text/plain" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/basic" }, + { ".avi", "video/x-msvideo" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".c", "text/plain" }, + { ".cab", "application/vnd.ms-cab-compressed" }, + { ".calx", "application/vnd.ms-office.calx" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".cdf", "application/x-cdf" }, + { ".chm", "application/octet-stream" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cnf", "text/plain" }, + { ".cod", "image/cis-cod" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/x-x509-ca-cert" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "application/octet-stream" }, + { ".cur", "application/octet-stream" }, + { ".dcr", "application/x-director" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dib", "image/bmp" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".dlm", "text/dlm" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dsp", "application/octet-stream" }, + { ".dtd", "text/xml" }, + { ".dvi", "application/x-dvi" }, + { ".dvr-ms", "video/x-ms-dvr" }, + { ".dwf", "drawing/x-dwf" }, + { ".dwp", "application/octet-stream" }, + { ".dxr", "application/x-director" }, + { ".eml", "message/rfc822" }, + { ".emz", "application/octet-stream" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".eps", "application/postscript" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".fdf", "application/vnd.fdf" }, + { ".fif", "application/fractals" }, + { ".fla", "application/octet-stream" }, + { ".flr", "x-world/x-vrml" }, + { ".flv", "video/x-flv" }, + { ".gif", "image/gif" }, + { ".gtar", "application/x-gtar" }, + { ".gz", "application/x-gzip" }, + { ".h", "text/plain" }, + { ".hdf", "application/x-hdf" }, + { ".hdml", "text/x-hdml" }, + { ".hhc", "application/x-oleobject" }, + { ".hhk", "application/octet-stream" }, + { ".hhp", "application/octet-stream" }, + { ".hlp", "application/winhlp" }, + { ".hqx", "application/mac-binhex40" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".hxt", "text/html" }, + { ".ical", "text/calendar" }, + { ".icalendar", "text/calendar" }, + { ".ico", "image/x-icon" }, + { ".ics", "text/calendar" }, + { ".ief", "image/ief" }, + { ".ifb", "text/calendar" }, + { ".iii", "application/x-iphone" }, + { ".inf", "application/octet-stream" }, + { ".ins", "application/x-internet-signup" }, + { ".isp", "application/x-internet-signup" }, + { ".IVF", "video/x-ivf" }, + { ".jar", "application/java-archive" }, + { ".java", "application/octet-stream" }, + { ".jck", "application/liquidmotion" }, + { ".jcz", "application/liquidmotion" }, + { ".jfif", "image/pjpeg" }, + { ".jpb", "application/octet-stream" }, + { ".jpe", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "application/javascript" }, + { ".jsx", "text/jscript" }, + { ".latex", "application/x-latex" }, + { ".lit", "application/x-ms-reader" }, + { ".lpk", "application/octet-stream" }, + { ".lsf", "video/x-la-asf" }, + { ".lsx", "video/x-la-asf" }, + { ".lzh", "application/octet-stream" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2ts", "video/vnd.dlna.mpeg-tts" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m4a", "audio/mp4" }, + { ".m4v", "video/mp4" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "text/plain" }, + { ".mdb", "application/x-msaccess" }, + { ".mdp", "application/octet-stream" }, + { ".me", "application/x-troff-me" }, + { ".mht", "message/rfc822" }, + { ".mhtml", "message/rfc822" }, + { ".mid", "audio/mid" }, + { ".midi", "audio/mid" }, + { ".mix", "application/octet-stream" }, + { ".mmf", "application/x-smaf" }, + { ".mno", "text/xml" }, + { ".mny", "application/x-msmoney" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpv2", "video/mpeg" }, + { ".ms", "application/x-troff-ms" }, + { ".msi", "application/octet-stream" }, + { ".mso", "application/octet-stream" }, + { ".mvb", "application/x-msmediaview" }, + { ".mvc", "application/x-miva-compiled" }, + { ".nc", "application/x-netcdf" }, + { ".nsc", "video/x-ms-asf" }, + { ".nws", "message/rfc822" }, + { ".ocx", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".odc", "text/x-ms-odc" }, + { ".ods", "application/oleobject" }, + { ".oga", "audio/ogg" }, + { ".ogg", "video/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".one", "application/onenote" }, + { ".onea", "application/onenote" }, + { ".onetoc", "application/onenote" }, + { ".onetoc2", "application/onenote" }, + { ".onetmp", "application/onenote" }, + { ".onepkg", "application/onenote" }, + { ".osdx", "application/opensearchdescription+xml" }, + { ".otf", "font/otf" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/x-pkcs12" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcx", "application/octet-stream" }, + { ".pcz", "application/octet-stream" }, + { ".pdf", "application/pdf" }, + { ".pfb", "application/octet-stream" }, + { ".pfm", "application/octet-stream" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pnz", "image/png" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".prf", "application/pics-rules" }, + { ".prm", "application/octet-stream" }, + { ".prx", "application/octet-stream" }, + { ".ps", "application/postscript" }, + { ".psd", "application/octet-stream" }, + { ".psm", "application/octet-stream" }, + { ".psp", "application/octet-stream" }, + { ".pub", "application/x-mspublisher" }, + { ".qt", "video/quicktime" }, + { ".qtl", "application/x-quicktimeplayer" }, + { ".qxd", "application/octet-stream" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rar", "application/octet-stream" }, + { ".ras", "image/x-cmu-raster" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".roff", "application/x-troff" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rtf", "application/rtf" }, + { ".rtx", "text/richtext" }, + { ".scd", "application/x-msschedule" }, + { ".sct", "text/scriptlet" }, + { ".sea", "application/octet-stream" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".sgml", "text/sgml" }, + { ".sh", "application/x-sh" }, + { ".shar", "application/x-shar" }, + { ".sit", "application/x-stuffit" }, + { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, + { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ".smd", "audio/x-smd" }, + { ".smi", "application/octet-stream" }, + { ".smx", "audio/x-smd" }, + { ".smz", "audio/x-smd" }, + { ".snd", "audio/basic" }, + { ".snp", "application/octet-stream" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".spx", "audio/ogg" }, + { ".src", "application/x-wais-source" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".stl", "application/vnd.ms-pki.stl" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svg", "image/svg+xml" }, + { ".svgz", "image/svg+xml" }, + { ".swf", "application/x-shockwave-flash" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tcl", "application/x-tcl" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".tgz", "application/x-compressed" }, + { ".thmx", "application/vnd.ms-officetheme" }, + { ".thn", "application/octet-stream" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".toc", "application/octet-stream" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".ts", "video/vnd.dlna.mpeg-tts" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttf", "application/octet-stream" }, + { ".tts", "video/vnd.dlna.mpeg-tts" }, + { ".txt", "text/plain" }, + { ".u32", "application/octet-stream" }, + { ".uls", "text/iuls" }, + { ".ustar", "application/x-ustar" }, + { ".vbs", "text/vbscript" }, + { ".vcf", "text/x-vcard" }, + { ".vcs", "text/plain" }, + { ".vdx", "application/vnd.ms-visio.viewer" }, + { ".vml", "text/xml" }, + { ".vsd", "application/vnd.visio" }, + { ".vss", "application/vnd.visio" }, + { ".vst", "application/vnd.visio" }, + { ".vsto", "application/x-ms-vsto" }, + { ".vsw", "application/vnd.visio" }, + { ".vsx", "application/vnd.visio" }, + { ".vtx", "application/vnd.visio" }, + { ".wav", "audio/wav" }, + { ".wax", "audio/x-ms-wax" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".webm", "video/webm" }, + { ".wks", "application/vnd.ms-works" }, + { ".wm", "video/x-ms-wm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmd", "application/x-ms-wmd" }, + { ".wmf", "application/x-msmetafile" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".wmz", "application/x-ms-wmz" }, + { ".woff", "application/font-woff" }, + { ".wps", "application/vnd.ms-works" }, + { ".wri", "application/x-mswrite" }, + { ".wrl", "x-world/x-vrml" }, + { ".wrz", "x-world/x-vrml" }, + { ".wsdl", "text/xml" }, + { ".wtv", "video/x-ms-wtv" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x", "application/directx" }, + { ".xaf", "x-world/x-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "text/plain" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xlc", "application/vnd.ms-excel" }, + { ".xlm", "application/vnd.ms-excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlw", "application/vnd.ms-excel" }, + { ".xml", "text/xml" }, + { ".xof", "x-world/x-vrml" }, + { ".xpm", "image/x-xpixmap" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".xsd", "text/xml" }, + { ".xsf", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsn", "application/octet-stream" }, + { ".xtp", "application/octet-stream" }, + { ".xwd", "image/x-xwindowdump" }, + { ".z", "application/x-compress" }, + { ".zip", "application/x-zip-compressed" }, + }) + { + } +#endregion + + /// + /// Creates a lookup engine using the provided mapping. + /// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase. + /// + /// + public FileExtensionContentTypeProvider(IDictionary mapping) + { + if (mapping == null) + { + throw new ArgumentNullException("mapping"); + } + Mappings = mapping; + } + + /// + /// The cross reference table of file extensions and content-types. + /// + public IDictionary Mappings { get; private set; } + + /// + /// Given a file path, determine the MIME type + /// + /// A file path + /// The resulting MIME type + /// True if MIME type could be determined + public bool TryGetContentType(string subpath, out string contentType) + { + string extension = Path.GetExtension(subpath); + if (extension == null) + { + contentType = null; + return false; + } + return Mappings.TryGetValue(extension, out contentType); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs b/src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs new file mode 100644 index 0000000000..cd0263c1ab --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.StaticFiles.ContentTypes +{ + /// + /// Used to look up MIME types given a file path + /// + public interface IContentTypeProvider + { + /// + /// Given a file path, determine the MIME type + /// + /// A file path + /// The resulting MIME type + /// True if MIME type could be determined + bool TryGetContentType(string subpath, out string contentType); + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml b/src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml new file mode 100644 index 0000000000..78a76142f7 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml @@ -0,0 +1,10 @@ + + + + + Owin + + + + + diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs new file mode 100644 index 0000000000..ac89ad46ca --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods for the DefaultFilesMiddleware + /// + public static class DefaultFilesExtensions + { + /// + /// Enables default file mapping on the current path from the current directory + /// + /// + /// + public static IBuilder UseDefaultFiles(this IBuilder builder) + { + return builder.UseDefaultFiles(new DefaultFilesOptions()); + } + + /// + /// Enables default file mapping for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseDefaultFiles(this IBuilder builder, string requestPath) + { + return UseDefaultFiles(builder, new DefaultFilesOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enables default file mapping with the given options + /// + /// + /// + /// + public static IBuilder UseDefaultFiles(this IBuilder builder, DefaultFilesOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.Use(next => new DefaultFilesMiddleware(next, options).Invoke); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs new file mode 100644 index 0000000000..cccaeb047f --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// This examines a directory path and determines if there is a default file present. + /// If so the file name is appended to the path and execution continues. + /// Note we don't just serve the file because it may require interpretation. + /// + public class DefaultFilesMiddleware + { + private readonly DefaultFilesOptions _options; + private readonly PathString _matchUrl; + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the DefaultFilesMiddleware. + /// + /// The next middleware in the pipeline. + /// The configuration options for this middleware. + public DefaultFilesMiddleware(RequestDelegate next, DefaultFilesOptions options) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.FileSystem == null) + { + options.FileSystem = new PhysicalFileSystem("." + options.RequestPath.Value); + } + + _next = next; + _options = options; + _matchUrl = options.RequestPath; + } + + /// + /// This examines the request to see if it matches a configured directory, and if there are any files with the + /// configured default names in that directory. If so this will append the corresponding file name to the request + /// path for a later middleware to handle. + /// + /// + /// + public Task Invoke(HttpContext context) + { + IEnumerable dirContents; + PathString subpath; + if (Helpers.IsGetOrHeadMethod(context.Request.Method) + && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out subpath) + && _options.FileSystem.TryGetDirectoryContents(subpath.Value, out dirContents)) + { + // Check if any of our default files exist. + for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++) + { + string defaultFile = _options.DefaultFileNames[matchIndex]; + IFileInfo file; + // TryMatchPath will make sure subpath always ends with a "/" by adding it if needed. + if (_options.FileSystem.TryGetFileInfo(subpath + defaultFile, out file)) + { + // If the path matches a directory but does not end in a slash, redirect to add the slash. + // This prevents relative links from breaking. + if (!Helpers.PathEndsInSlash(context.Request.Path)) + { + context.Response.StatusCode = 301; + context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/"; + return Constants.CompletedTask; + } + + // Match found, re-write the url. A later middleware will actually serve the file. + context.Request.Path = new PathString(context.Request.Path.Value + defaultFile); + break; + } + } + } + + return _next(context); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs new file mode 100644 index 0000000000..d7d5069a8c --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Options for selecting default file names. + /// + public class DefaultFilesOptions : SharedOptionsBase + { + /// + /// Configuration for the DefaultFilesMiddleware. + /// + public DefaultFilesOptions() + : this(new SharedOptions()) + { + } + + /// + /// Configuration for the DefaultFilesMiddleware. + /// + /// + public DefaultFilesOptions(SharedOptions sharedOptions) + : base(sharedOptions) + { + // Prioritized list + DefaultFileNames = new List() + { + "default.htm", + "default.html", + "index.htm", + "index.html", + }; + } + + /// + /// An ordered list of file names to select by default. List length and ordering may affect performance. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Improves usability")] + public IList DefaultFileNames { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs new file mode 100644 index 0000000000..74bf255f45 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods for the DirectoryBrowserMiddleware + /// + public static class DirectoryBrowserExtensions + { + /// + /// Enable directory browsing on the current path for the current directory + /// + /// + /// + public static IBuilder UseDirectoryBrowser(this IBuilder builder) + { + return builder.UseDirectoryBrowser(new DirectoryBrowserOptions()); + } + + /// + /// Enables directory browsing for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseDirectoryBrowser(this IBuilder builder, string requestPath) + { + return UseDirectoryBrowser(builder, new DirectoryBrowserOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enable directory browsing with the given options + /// + /// + /// + /// + public static IBuilder UseDirectoryBrowser(this IBuilder builder, DirectoryBrowserOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.Use(next => new DirectoryBrowserMiddleware(next, options).Invoke); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs new file mode 100644 index 0000000000..87d56cb3a0 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Enables directory browsing + /// + public class DirectoryBrowserMiddleware + { + private readonly DirectoryBrowserOptions _options; + private readonly PathString _matchUrl; + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the SendFileMiddleware. + /// + /// The next middleware in the pipeline. + /// The configuration for this middleware. + public DirectoryBrowserMiddleware(RequestDelegate next, DirectoryBrowserOptions options) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.Formatter == null) + { + throw new ArgumentException(Resources.Args_NoFormatter); + } + if (options.FileSystem == null) + { + options.FileSystem = new PhysicalFileSystem("." + options.RequestPath.Value); + } + + _next = next; + _options = options; + _matchUrl = options.RequestPath; + } + + /// + /// Examines the request to see if it matches a configured directory. If so, a view of the directory contents is returned. + /// + /// + /// + public Task Invoke(HttpContext context) + { + // Check if the URL matches any expected paths + PathString subpath; + IEnumerable contents; + if (Helpers.IsGetOrHeadMethod(context.Request.Method) + && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out subpath) + && TryGetDirectoryInfo(subpath, out contents)) + { + // If the path matches a directory but does not end in a slash, redirect to add the slash. + // This prevents relative links from breaking. + if (!Helpers.PathEndsInSlash(context.Request.Path)) + { + context.Response.StatusCode = 301; + context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/"; + return Constants.CompletedTask; + } + + return _options.Formatter.GenerateContentAsync(context, contents); + } + + return _next(context); + } + + private bool TryGetDirectoryInfo(PathString subpath, out IEnumerable contents) + { + return _options.FileSystem.TryGetDirectoryContents(subpath.Value, out contents); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs new file mode 100644 index 0000000000..8c24bb11c8 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.StaticFiles.DirectoryFormatters; +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Directory browsing options + /// + public class DirectoryBrowserOptions : SharedOptionsBase + { + /// + /// Enabled directory browsing in the current physical directory for all request paths + /// + public DirectoryBrowserOptions() + : this(new SharedOptions()) + { + } + + /// + /// Enabled directory browsing in the current physical directory for all request paths + /// + /// + public DirectoryBrowserOptions(SharedOptions sharedOptions) + : base(sharedOptions) + { + Formatter = new HtmlDirectoryFormatter(); + } + + /// + /// The component that generates the view. + /// + public IDirectoryFormatter Formatter { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs new file mode 100644 index 0000000000..8a5056d189 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.DirectoryFormatters +{ + /// + /// Generates an HTML view for a directory. + /// + public class HtmlDirectoryFormatter : IDirectoryFormatter + { + /// + /// Generates an HTML view for a directory. + /// + public virtual Task GenerateContentAsync(HttpContext context, IEnumerable contents) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + if (contents == null) + { + throw new ArgumentNullException("contents"); + } + + context.Response.ContentType = Constants.TextHtmlUtf8; + + if (Helpers.IsHeadMethod(context.Request.Method)) + { + // HEAD, no response body + return Constants.CompletedTask; + } + + PathString requestPath = context.Request.PathBase + context.Request.Path; + + var builder = new StringBuilder(); + + builder.AppendFormat( +@" +", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName); + + builder.AppendFormat(@" + + {0} {1}", HtmlEncode(Resources.HtmlDir_IndexOf), HtmlEncode(requestPath.Value)); + + builder.Append(@" + + + +
"); + builder.AppendFormat(@" +

{0} /", HtmlEncode(Resources.HtmlDir_IndexOf)); + + string cumulativePath = "/"; + foreach (var segment in requestPath.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)) + { + cumulativePath = cumulativePath + segment + "/"; + builder.AppendFormat(@"{1}/", + HtmlEncode(cumulativePath), HtmlEncode(segment)); + } + + builder.AppendFormat(CultureInfo.CurrentUICulture, + @"

+ + + + + ", + HtmlEncode(Resources.HtmlDir_TableSummary), + HtmlEncode(Resources.HtmlDir_Name), + HtmlEncode(Resources.HtmlDir_Size), + HtmlEncode(Resources.HtmlDir_Modified), + HtmlEncode(Resources.HtmlDir_LastModified)); + + foreach (var subdir in contents.Where(info => info.IsDirectory)) + { + builder.AppendFormat(@" + + + + + ", + HtmlEncode(subdir.Name), + HtmlEncode(subdir.LastModified.ToString(CultureInfo.CurrentCulture))); + } + + foreach (var file in contents.Where(info => !info.IsDirectory)) + { + builder.AppendFormat(@" + + + + + ", + HtmlEncode(file.Name), + HtmlEncode(file.Length.ToString("n0", CultureInfo.CurrentCulture)), + HtmlEncode(file.LastModified.ToString(CultureInfo.CurrentCulture))); + } + + builder.Append(@" + +
{1}{2}{4}
{0}/{1}
{0}{1}{2}
+
+ +"); + string data = builder.ToString(); + byte[] bytes = Encoding.UTF8.GetBytes(data); + context.Response.ContentLength = bytes.Length; + return context.Response.Body.WriteAsync(bytes, 0, bytes.Length); + } + + private static string HtmlEncode(string body) + { + return WebUtility.HtmlEncode(body); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs new file mode 100644 index 0000000000..48c2b833a6 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.DirectoryFormatters +{ + /// + /// Generates the view for a directory + /// + public interface IDirectoryFormatter + { + /// + /// Generates the view for a directory. + /// Implementers should properly handle HEAD requests. + /// Implementers should set all necessary response headers (e.g. Content-Type, Content-Length, etc.). + /// + Task GenerateContentAsync(HttpContext context, IEnumerable contents); + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs b/src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs new file mode 100644 index 0000000000..f647704465 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods that combine all of the static file middleware components: + /// Default files, directory browsing, send file, and static files + /// + public static class FileServerExtensions + { + /// + /// Enable all static file middleware (except directory browsing) for the current request path in the current directory. + /// + /// + /// + public static IBuilder UseFileServer(this IBuilder builder) + { + return UseFileServer(builder, new FileServerOptions()); + } + + /// + /// Enable all static file middleware on for the current request path in the current directory. + /// + /// + /// Should directory browsing be enabled? + /// + public static IBuilder UseFileServer(this IBuilder builder, bool enableDirectoryBrowsing) + { + return UseFileServer(builder, new FileServerOptions() { EnableDirectoryBrowsing = enableDirectoryBrowsing }); + } + + /// + /// Enables all static file middleware (except directory browsing) for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseFileServer(this IBuilder builder, string requestPath) + { + return UseFileServer(builder, new FileServerOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enable all static file middleware with the given options + /// + /// + /// + /// + public static IBuilder UseFileServer(this IBuilder builder, FileServerOptions options) + { + if (options == null) + { + throw new ArgumentNullException("options"); + } + + if (options.EnableDefaultFiles) + { + builder = builder.UseDefaultFiles(options.DefaultFilesOptions); + } + + if (options.EnableDirectoryBrowsing) + { + builder = builder.UseDirectoryBrowser(options.DirectoryBrowserOptions); + } + + return builder + .UseSendFileFallback() + .UseStaticFiles(options.StaticFileOptions); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs b/src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs new file mode 100644 index 0000000000..22f4e17184 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Options for all of the static file middleware components + /// + public class FileServerOptions : SharedOptionsBase + { + /// + /// Creates a combined options class for all of the static file middleware components. + /// + public FileServerOptions() + : base(new SharedOptions()) + { + StaticFileOptions = new StaticFileOptions(SharedOptions); + DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions); + DefaultFilesOptions = new DefaultFilesOptions(SharedOptions); + EnableDefaultFiles = true; + } + + /// + /// Options for configuring the StaticFileMiddleware. + /// + public StaticFileOptions StaticFileOptions { get; private set; } + + /// + /// Options for configuring the DirectoryBrowserMiddleware. + /// + public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; } + + /// + /// Options for configuring the DefaultFilesMiddleware. + /// + public DefaultFilesOptions DefaultFilesOptions { get; private set; } + + /// + /// Directory browsing is disabled by default. + /// + public bool EnableDirectoryBrowsing { get; set; } + + /// + /// Default files are enabled by default. + /// + public bool EnableDefaultFiles { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Helpers.cs b/src/Microsoft.AspNet.StaticFiles/Helpers.cs new file mode 100644 index 0000000000..040ebf1dfd --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Helpers.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + internal static class Helpers + { + internal static bool IsGetOrHeadMethod(string method) + { + return IsGetMethod(method) || IsHeadMethod(method); + } + + internal static bool IsGetMethod(string method) + { + return string.Equals("GET", method, StringComparison.OrdinalIgnoreCase); + } + + internal static bool IsHeadMethod(string method) + { + return string.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase); + } + + internal static bool PathEndsInSlash(PathString path) + { + return path.Value.EndsWith("/", StringComparison.Ordinal); + } + + internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath) + { + var path = context.Request.Path; + + if (forDirectory && !PathEndsInSlash(path)) + { + path += new PathString("/"); + } + + if (path.StartsWithSegments(matchUrl, out subpath)) + { + return true; + } + return false; + } + + internal static bool TryParseHttpDate(string dateString, out DateTime parsedDate) + { + return DateTime.TryParseExact(dateString, Constants.HttpDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs new file mode 100644 index 0000000000..c5c355cbde --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.AspNet.StaticFiles.Infrastructure +{ + internal static class RangeHelpers + { + // Examples: + // bytes=0-499 + // bytes=500- + // bytes=-500 + // bytes=0-0,-1 + // bytes=500-600,601-999 + // Any individual bad range fails the whole parse and the header should be ignored. + internal static bool TryParseRanges(string rangeHeader, out IList> parsedRanges) + { + parsedRanges = null; + if (string.IsNullOrWhiteSpace(rangeHeader) + || !rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string[] subRanges = rangeHeader.Substring("bytes=".Length).Replace(" ", string.Empty).Split(','); + + List> ranges = new List>(); + + for (int i = 0; i < subRanges.Length; i++) + { + long? first = null, second = null; + string subRange = subRanges[i]; + int dashIndex = subRange.IndexOf('-'); + if (dashIndex < 0) + { + return false; + } + else if (dashIndex == 0) + { + // -500 + string remainder = subRange.Substring(1); + if (!TryParseLong(remainder, out second)) + { + return false; + } + } + else if (dashIndex == (subRange.Length - 1)) + { + // 500- + string remainder = subRange.Substring(0, subRange.Length - 1); + if (!TryParseLong(remainder, out first)) + { + return false; + } + } + else + { + // 0-499 + string firstString = subRange.Substring(0, dashIndex); + string secondString = subRange.Substring(dashIndex + 1, subRange.Length - dashIndex - 1); + if (!TryParseLong(firstString, out first) || !TryParseLong(secondString, out second) + || first.Value > second.Value) + { + return false; + } + } + + ranges.Add(new Tuple(first, second)); + } + + if (ranges.Count > 0) + { + parsedRanges = ranges; + return true; + } + return false; + } + + private static bool TryParseLong(string input, out long? result) + { + int temp; + if (!string.IsNullOrWhiteSpace(input) + && int.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out temp)) + { + result = temp; + return true; + } + result = null; + return false; + } + + // 14.35.1 Byte Ranges - If a syntactically valid byte-range-set includes at least one byte-range-spec whose + // first-byte-pos is less than the current length of the entity-body, or at least one suffix-byte-range-spec + // with a non-zero suffix-length, then the byte-range-set is satisfiable. + // Adjusts ranges to be absolute and within bounds. + internal static IList> NormalizeRanges(IList> ranges, long length) + { + IList> normalizedRanges = new List>(ranges.Count); + for (int i = 0; i < ranges.Count; i++) + { + Tuple range = ranges[i]; + long? start = range.Item1, end = range.Item2; + + // X-[Y] + if (start.HasValue) + { + if (start.Value >= length) + { + // Not satisfiable, skip/discard. + continue; + } + if (!end.HasValue || end.Value >= length) + { + end = length - 1; + } + } + else + { + // suffix range "-X" e.g. the last X bytes, resolve + if (end.Value == 0) + { + // Not satisfiable, skip/discard. + continue; + } + + long bytes = Math.Min(end.Value, length); + start = length - bytes; + end = start + bytes - 1; + } + normalizedRanges.Add(new Tuple(start.Value, end.Value)); + } + return normalizedRanges; + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs new file mode 100644 index 0000000000..06ff75c429 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.Infrastructure +{ + /// + /// Options common to several middleware components + /// + public class SharedOptions + { + private PathString _requestPath; + + /// + /// Defaults to all request paths and the current physical directory. + /// + public SharedOptions() + { + RequestPath = PathString.Empty; + } + + /// + /// The request path that maps to static resources + /// + public PathString RequestPath + { + get { return _requestPath; } + set + { + if (value.HasValue && value.Value.EndsWith("/", StringComparison.Ordinal)) + { + throw new ArgumentException("Request path must not end in a slash"); + } + _requestPath = value; + } + } + + /// + /// The file system used to locate resources + /// + public IFileSystem FileSystem { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs new file mode 100644 index 0000000000..7715dcc6ce --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.Infrastructure +{ + /// + /// Options common to several middleware components + /// + /// The type of the subclass + public abstract class SharedOptionsBase + { + /// + /// Creates an new instance of the SharedOptionsBase. + /// + /// + protected SharedOptionsBase(SharedOptions sharedOptions) + { + if (sharedOptions == null) + { + throw new ArgumentNullException("sharedOptions"); + } + + SharedOptions = sharedOptions; + } + + /// + /// Options common to several middleware components + /// + protected SharedOptions SharedOptions { get; private set; } + + /// + /// The relative request path that maps to static resources. + /// + public PathString RequestPath + { + get { return SharedOptions.RequestPath; } + set { SharedOptions.RequestPath = value; } + } + + /// + /// The file system used to locate resources + /// + public IFileSystem FileSystem + { + get { return SharedOptions.FileSystem; } + set { SharedOptions.FileSystem = value; } + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a373b20da5 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Microsoft.AspNet.StaticFiles")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("310c92f5-1719-4616-9ca8-a5788fcb86f8")] +[assembly: CLSCompliant(true)] +[assembly: NeutralResourcesLanguage("en-US")] diff --git a/src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs b/src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs new file mode 100644 index 0000000000..d6a4919ce7 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34006 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.StaticFiles { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.StaticFiles.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to No IContentTypeProvider was specified.. + /// + internal static string Args_NoContentTypeProvider { + get { + return ResourceManager.GetString("Args_NoContentTypeProvider", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No formatter provided.. + /// + internal static string Args_NoFormatter { + get { + return ResourceManager.GetString("Args_NoFormatter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This server does not support the sendfile.SendAsync extension.. + /// + internal static string Exception_SendFileNotSupported { + get { + return ResourceManager.GetString("Exception_SendFileNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Index of. + /// + internal static string HtmlDir_IndexOf { + get { + return ResourceManager.GetString("HtmlDir_IndexOf", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last Modified. + /// + internal static string HtmlDir_LastModified { + get { + return ResourceManager.GetString("HtmlDir_LastModified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modified. + /// + internal static string HtmlDir_Modified { + get { + return ResourceManager.GetString("HtmlDir_Modified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + internal static string HtmlDir_Name { + get { + return ResourceManager.GetString("HtmlDir_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Size. + /// + internal static string HtmlDir_Size { + get { + return ResourceManager.GetString("HtmlDir_Size", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The list of files in the given directory. Column headers are listed in the first row.. + /// + internal static string HtmlDir_TableSummary { + get { + return ResourceManager.GetString("HtmlDir_TableSummary", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Resources.resx b/src/Microsoft.AspNet.StaticFiles/Resources.resx new file mode 100644 index 0000000000..6c58583e87 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Resources.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No IContentTypeProvider was specified. + + + No formatter provided. + + + This server does not support the sendfile.SendAsync extension. + + + Index of + + + Last Modified + + + Modified + + + Name + + + Size + + + The list of files in the given directory. Column headers are listed in the first row. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs b/src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs new file mode 100644 index 0000000000..5b62726b9d --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Extension methods for the SendFileMiddleware + /// + public static class SendFileExtensions + { + /// + /// Provide a SendFile fallback if another component does not. + /// + /// + /// + public static IBuilder UseSendFileFallback(this IBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + /* TODO builder.GetItem(typeof(ISendFile)) + + // Check for advertised support + if (IsSendFileSupported(builder.Properties)) + { + return builder; + } + + // Otherwise, insert a fallback SendFile middleware and advertise support + SetSendFileCapability(builder.Properties); + */ + return builder.Use(next => new SendFileMiddleware(next).Invoke); + } + + private static bool IsSendFileSupported(IDictionary properties) + { + object obj; + if (properties.TryGetValue(Constants.ServerCapabilitiesKey, out obj)) + { + var capabilities = (IDictionary)obj; + if (capabilities.TryGetValue(Constants.SendFileVersionKey, out obj) + && Constants.SendFileVersion.Equals((string)obj, StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + + private static void SetSendFileCapability(IDictionary properties) + { + object obj; + if (properties.TryGetValue(Constants.ServerCapabilitiesKey, out obj)) + { + var capabilities = (IDictionary)obj; + capabilities[Constants.SendFileVersionKey] = Constants.SendFileVersion; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs new file mode 100644 index 0000000000..18901d981b --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// This middleware provides an efficient fallback mechanism for sending static files + /// when the server does not natively support such a feature. + /// The caller is responsible for setting all headers in advance. + /// The caller is responsible for performing the correct impersonation to give access to the file. + /// + public class SendFileMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the SendFileMiddleware. + /// + /// The next middleware in the pipeline. + public SendFileMiddleware(RequestDelegate next) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + + _next = next; + } + + public Task Invoke(HttpContext context) + { + // Check if there is a SendFile feature already present + if (context.GetFeature() == null) + { + context.SetFeature(new SendFileWrapper(context.Response.Body)); + } + + return _next(context); + } + + private class SendFileWrapper : IHttpSendFile + { + private readonly Stream _output; + + internal SendFileWrapper(Stream output) + { + _output = output; + } + + // Not safe for overlapped writes. + public async Task SendFileAsync(string fileName, long offset, long? length, CancellationToken cancel) + { + cancel.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException("fileName"); + } + if (!File.Exists(fileName)) + { + throw new FileNotFoundException(string.Empty, fileName); + } + + var fileInfo = new FileInfo(fileName); + if (offset < 0 || offset > fileInfo.Length) + { + throw new ArgumentOutOfRangeException("offset", offset, string.Empty); + } + + if (length.HasValue && + (length.Value < 0 || length.Value > fileInfo.Length - offset)) + { + throw new ArgumentOutOfRangeException("length", length, string.Empty); + } + + Stream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1024 * 64, +#if NET45 + FileOptions.Asynchronous | FileOptions.SequentialScan); +#else + useAsync: true); +#endif + try + { + fileStream.Seek(offset, SeekOrigin.Begin); + await StreamCopyOperation.CopyToAsync(fileStream, _output, length, cancel); + } + finally + { + fileStream.Dispose(); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs b/src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs new file mode 100644 index 0000000000..4ea40268d3 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.Owin +{ + /// + /// Provides extensions for HttpResponse exposing the SendFile extension. + /// + public static class SendFileResponseExtensions + { + /// + /// Checks if the SendFile extension is supported. + /// + /// + /// True if sendfile.SendAsync is defined in the environment. + public static bool SupportsSendFile(this HttpResponse response) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + return response.HttpContext.GetFeature() != null; + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// + /// + public static Task SendFileAsync(this HttpResponse response, string fileName) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + return response.SendFileAsync(fileName, 0, null, CancellationToken.None); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The full or relative path to the file. + /// The offset in the file. + /// The number of types to send, or null to send the remainder of the file. + /// + /// + public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + IHttpSendFile sendFile = response.HttpContext.GetFeature(); + if (sendFile == null) + { + throw new NotSupportedException(Resources.Exception_SendFileNotSupported); + } + + return sendFile.SendFileAsync(fileName, offset, count, cancellationToken); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs new file mode 100644 index 0000000000..6cbd30a93e --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.StaticFiles.Infrastructure; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.StaticFiles +{ + internal struct StaticFileContext + { + private readonly HttpContext _context; + private readonly StaticFileOptions _options; + private readonly PathString _matchUrl; + private readonly HttpRequest _request; + private readonly HttpResponse _response; + private string _method; + private bool _isGet; + private bool _isHead; + private PathString _subPath; + private string _contentType; + private IFileInfo _fileInfo; + private long _length; + private DateTime _lastModified; + private string _lastModifiedString; + private string _etag; + private string _etagQuoted; + + private PreconditionState _ifMatchState; + private PreconditionState _ifNoneMatchState; + private PreconditionState _ifModifiedSinceState; + private PreconditionState _ifUnmodifiedSinceState; + + private IList> _ranges; + + public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl) + { + _context = context; + _options = options; + _matchUrl = matchUrl; + _request = context.Request; + _response = context.Response; + + _method = null; + _isGet = false; + _isHead = false; + _subPath = PathString.Empty; + _contentType = null; + _fileInfo = null; + _length = 0; + _lastModified = new DateTime(); + _etag = null; + _etagQuoted = null; + _lastModifiedString = null; + _ifMatchState = PreconditionState.Unspecified; + _ifNoneMatchState = PreconditionState.Unspecified; + _ifModifiedSinceState = PreconditionState.Unspecified; + _ifUnmodifiedSinceState = PreconditionState.Unspecified; + _ranges = null; + } + + internal enum PreconditionState + { + Unspecified, + NotModified, + ShouldProcess, + PreconditionFailed, + } + + public bool IsHeadMethod + { + get { return _isHead; } + } + + public bool IsRangeRequest + { + get { return _ranges != null; } + } + + public bool ValidateMethod() + { + _method = _request.Method; + _isGet = Helpers.IsGetMethod(_method); + _isHead = Helpers.IsHeadMethod(_method); + return _isGet || _isHead; + } + + // Check if the URL matches any expected paths + public bool ValidatePath() + { + return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath); + } + + public bool LookupContentType() + { + if (_options.ContentTypeProvider.TryGetContentType(_subPath.Value, out _contentType)) + { + return true; + } + + if (_options.ServeUnknownFileTypes) + { + _contentType = _options.DefaultContentType; + return true; + } + + return false; + } + + public bool LookupFileInfo() + { + bool found = _options.FileSystem.TryGetFileInfo(_subPath.Value, out _fileInfo); + if (found) + { + _length = _fileInfo.Length; + + DateTime last = _fileInfo.LastModified; + // Truncate to the second. + _lastModified = new DateTime(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Kind); + _lastModifiedString = _lastModified.ToString(Constants.HttpDateFormat, CultureInfo.InvariantCulture); + + long etagHash = _lastModified.ToFileTimeUtc() ^ _length; + _etag = Convert.ToString(etagHash, 16); + _etagQuoted = '\"' + _etag + '\"'; + } + return found; + } + + public void ComprehendRequestHeaders() + { + ComputeIfMatch(); + + ComputeIfModifiedSince(); + + ComputeRange(); + } + + private void ComputeIfMatch() + { + // 14.24 If-Match + IList ifMatch = _request.Headers.GetCommaSeparatedValues(Constants.IfMatch); // Removes quotes + if (ifMatch != null) + { + _ifMatchState = PreconditionState.PreconditionFailed; + foreach (var segment in ifMatch) + { + if (segment.Equals("*", StringComparison.Ordinal) + || segment.Equals(_etag, StringComparison.Ordinal)) + { + _ifMatchState = PreconditionState.ShouldProcess; + break; + } + } + } + + // 14.26 If-None-Match + IList ifNoneMatch = _request.Headers.GetCommaSeparatedValues(Constants.IfNoneMatch); + if (ifNoneMatch != null) + { + _ifNoneMatchState = PreconditionState.ShouldProcess; + foreach (var segment in ifNoneMatch) + { + if (segment.Equals("*", StringComparison.Ordinal) + || segment.Equals(_etag, StringComparison.Ordinal)) + { + _ifNoneMatchState = PreconditionState.NotModified; + break; + } + } + } + } + + private void ComputeIfModifiedSince() + { + // 14.25 If-Modified-Since + string ifModifiedSinceString = _request.Headers.Get(Constants.IfModifiedSince); + DateTime ifModifiedSince; + if (Helpers.TryParseHttpDate(ifModifiedSinceString, out ifModifiedSince)) + { + bool modified = ifModifiedSince < _lastModified; + _ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified; + } + + // 14.28 If-Unmodified-Since + string ifUnmodifiedSinceString = _request.Headers.Get(Constants.IfUnmodifiedSince); + DateTime ifUnmodifiedSince; + if (Helpers.TryParseHttpDate(ifUnmodifiedSinceString, out ifUnmodifiedSince)) + { + bool unmodified = ifUnmodifiedSince >= _lastModified; + _ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; + } + } + + private void ComputeRange() + { + // 14.35 Range + // http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-24 + + // A server MUST ignore a Range header field received with a request method other + // than GET. + if (!_isGet) + { + return; + } + + string rangeHeader = _request.Headers.Get(Constants.Range); + IList> ranges; + if (!RangeHelpers.TryParseRanges(rangeHeader, out ranges)) + { + return; + } + + if (ranges.Count > 1) + { + // multiple range headers not yet supported + return; + } + + // 14.27 If-Range + string ifRangeHeader = _request.Headers.Get(Constants.IfRange); + if (!string.IsNullOrWhiteSpace(ifRangeHeader)) + { + // If the validator given in the If-Range header field matches the + // current validator for the selected representation of the target + // resource, then the server SHOULD process the Range header field as + // requested. If the validator does not match, the server MUST ignore + // the Range header field. + DateTime ifRangeLastModified; + bool ignoreRangeHeader = false; + if (Helpers.TryParseHttpDate(ifRangeHeader, out ifRangeLastModified)) + { + if (_lastModified > ifRangeLastModified) + { + ignoreRangeHeader = true; + } + } + else + { + if (!_etagQuoted.Equals(ifRangeHeader)) + { + ignoreRangeHeader = true; + } + } + if (ignoreRangeHeader) + { + return; + } + } + + _ranges = RangeHelpers.NormalizeRanges(ranges, _length); + } + + public void ApplyResponseHeaders(int statusCode) + { + _response.StatusCode = statusCode; + if (statusCode < 400) + { + // these headers are returned for 200, 206, and 304 + // they are not returned for 412 and 416 + if (!string.IsNullOrEmpty(_contentType)) + { + _response.ContentType = _contentType; + } + _response.Headers.Set(Constants.LastModified, _lastModifiedString); + _response.Headers.Set(Constants.ETag, _etagQuoted); + } + if (statusCode == Constants.Status200Ok) + { + // this header is only returned here for 200 + // it already set to the returned range for 206 + // it is not returned for 304, 412, and 416 + _response.ContentLength = _length; + } + _options.OnPrepareResponse(new StaticFileResponseContext() + { + Context = _context, + File = _fileInfo, + }); + } + + public PreconditionState GetPreconditionState() + { + return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, + _ifModifiedSinceState, _ifUnmodifiedSinceState); + } + + private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states) + { + PreconditionState max = PreconditionState.Unspecified; + for (int i = 0; i < states.Length; i++) + { + if (states[i] > max) + { + max = states[i]; + } + } + return max; + } + + public Task SendStatusAsync(int statusCode) + { + ApplyResponseHeaders(statusCode); + + return Constants.CompletedTask; + } + + public async Task SendAsync() + { + ApplyResponseHeaders(Constants.Status200Ok); + + string physicalPath = _fileInfo.PhysicalPath; + IHttpSendFile sendFile = _context.GetFeature(); + if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) + { + await sendFile.SendFileAsync(physicalPath, 0, _length, _request.CallCanceled); + return; + } + + Stream readStream = _fileInfo.CreateReadStream(); + try + { + await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, _request.CallCanceled); + } + finally + { + readStream.Dispose(); + } + } + + // When there is only a single range the bytes are sent directly in the body. + internal async Task SendRangeAsync() + { + bool rangeNotSatisfiable = false; + if (_ranges.Count == 0) + { + rangeNotSatisfiable = true; + } + + if (rangeNotSatisfiable) + { + // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable) + // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies + // the current length of the selected resource. e.g. */length + _response.Headers[Constants.ContentRange] = "bytes */" + _length.ToString(CultureInfo.InvariantCulture); + ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable); + return; + } + + // Multi-range is not supported. + Debug.Assert(_ranges.Count == 1); + + long start, length; + _response.Headers[Constants.ContentRange] = ComputeContentRange(_ranges[0], out start, out length); + _response.ContentLength = length; + ApplyResponseHeaders(Constants.Status206PartialContent); + + string physicalPath = _fileInfo.PhysicalPath; + IHttpSendFile sendFile = _context.GetFeature(); + if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) + { + await sendFile.SendFileAsync(physicalPath, start, length, _request.CallCanceled); + return; + } + + Stream readStream = _fileInfo.CreateReadStream(); + try + { + readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek? + await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _request.CallCanceled); + } + finally + { + readStream.Dispose(); + } + } + + // Note: This assumes ranges have been normalized to absolute byte offsets. + private string ComputeContentRange(Tuple range, out long start, out long length) + { + start = range.Item1; + long end = range.Item2; + length = end - start + 1; + return string.Format(CultureInfo.InvariantCulture, "bytes {0}-{1}/{2}", start, end, _length); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs new file mode 100644 index 0000000000..be625dcc60 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods for the StaticFileMiddleware + /// + public static class StaticFileExtensions + { + /// + /// Enables static file serving for the current request path from the current directory + /// + /// + /// + public static IBuilder UseStaticFiles(this IBuilder builder) + { + return UseStaticFiles(builder, new StaticFileOptions()); + } + + /// + /// Enables static file serving for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseStaticFiles(this IBuilder builder, string requestPath) + { + return UseStaticFiles(builder, new StaticFileOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enables static file serving with the given options + /// + /// + /// + /// + public static IBuilder UseStaticFiles(this IBuilder builder, StaticFileOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + return builder.Use(next => new StaticFileMiddleware(next, options).Invoke); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs new file mode 100644 index 0000000000..f1bc8aa17f --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Enables serving static files for a given request path + /// + public class StaticFileMiddleware + { + private readonly StaticFileOptions _options; + private readonly PathString _matchUrl; + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the StaticFileMiddleware. + /// + /// The next middleware in the pipeline. + /// The configuration options. + public StaticFileMiddleware(RequestDelegate next, StaticFileOptions options) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.ContentTypeProvider == null) + { + throw new ArgumentException(Resources.Args_NoContentTypeProvider); + } + if (options.FileSystem == null) + { + options.FileSystem = new PhysicalFileSystem("." + options.RequestPath.Value); + } + + _next = next; + _options = options; + _matchUrl = options.RequestPath; + } + + /// + /// Processes a request to determine if it matches a known file, and if so, serves it. + /// + /// + /// + public Task Invoke(HttpContext context) + { + var fileContext = new StaticFileContext(context, _options, _matchUrl); + if (fileContext.ValidateMethod() + && fileContext.ValidatePath() + && fileContext.LookupContentType() + && fileContext.LookupFileInfo()) + { + fileContext.ComprehendRequestHeaders(); + + switch (fileContext.GetPreconditionState()) + { + case StaticFileContext.PreconditionState.Unspecified: + case StaticFileContext.PreconditionState.ShouldProcess: + if (fileContext.IsHeadMethod) + { + return fileContext.SendStatusAsync(Constants.Status200Ok); + } + if (fileContext.IsRangeRequest) + { + return fileContext.SendRangeAsync(); + } + return fileContext.SendAsync(); + + case StaticFileContext.PreconditionState.NotModified: + return fileContext.SendStatusAsync(Constants.Status304NotModified); + + case StaticFileContext.PreconditionState.PreconditionFailed: + return fileContext.SendStatusAsync(Constants.Status412PreconditionFailed); + + default: + throw new NotImplementedException(fileContext.GetPreconditionState().ToString()); + } + } + + return _next(context); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs new file mode 100644 index 0000000000..3e6b0ee7d5 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles.ContentTypes; +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Options for serving static files + /// + public class StaticFileOptions : SharedOptionsBase + { + /// + /// Defaults to all request paths in the current physical directory + /// + public StaticFileOptions() : this(new SharedOptions()) + { + } + + /// + /// Defaults to all request paths in the current physical directory + /// + /// + public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions) + { + ContentTypeProvider = new FileExtensionContentTypeProvider(); + + OnPrepareResponse = _ => { }; + } + + /// + /// Used to map files to content-types. + /// + public IContentTypeProvider ContentTypeProvider { get; set; } + + /// + /// The default content type for a request if the ContentTypeProvider cannot determine one. + /// None is provided by default, so the client must determine the format themselves. + /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7 + /// + public string DefaultContentType { get; set; } + + /// + /// If the file is not a recognized content-type should it be served? + /// Default: false. + /// + public bool ServeUnknownFileTypes { get; set; } + + /// + /// Called after the status code and headers have been set, but before the body has been written. + /// This can be used to add or change the response headers. + /// + public Action OnPrepareResponse { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs new file mode 100644 index 0000000000..2f681722be --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Contains information about the request and the file that will be served in response. + /// + public class StaticFileResponseContext + { + /// + /// The request and response information. + /// + public HttpContext Context { get; internal set; } + + /// + /// The file to be served. + /// + public IFileInfo File { get; internal set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs b/src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs new file mode 100644 index 0000000000..aed649e5f8 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.StaticFiles +{ + // FYI: In most cases the source will be a FileStream and the destination will be to the network. + internal static class StreamCopyOperation + { + private const int DefaultBufferSize = 1024 * 16; + + internal static async Task CopyToAsync(Stream source, Stream destination, long? length, CancellationToken cancel) + { + long? bytesRemaining = length; + byte[] buffer = new byte[DefaultBufferSize]; + + Contract.Assert(source != null); + Contract.Assert(destination != null); + Contract.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Contract.Assert(buffer != null); + + while (true) + { + // The natural end of the range. + if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + int readLength = buffer.Length; + if (bytesRemaining.HasValue) + { + readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + } + int count = await source.ReadAsync(buffer, 0, readLength, cancel); + + if (bytesRemaining.HasValue) + { + bytesRemaining -= count; + } + + // End of the source stream. + if (count == 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, count, cancel); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/project.json b/src/Microsoft.AspNet.StaticFiles/project.json new file mode 100644 index 0000000000..eed3dc1834 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/project.json @@ -0,0 +1,16 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Abstractions" : "0.1-alpha-*", + "Microsoft.AspNet.HttpFeature" : "0.1-alpha-*", + "Microsoft.AspNet.FileSystems" : "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin" : "1.0" + } + }, + "k10" : { } + } +} \ No newline at end of file