ASP.NET Core 1.0 custom compression middleware and static files issue - asp.net-core

My startup's middleware configuration looks something like this:
public void Configure(IApplicationBuilder app)
{
app.UseCompression();
app.UseIISPlatformHandler();
app.UseApplicationInsightsRequestTelemetry();
app.UseCors("CorsPolicy");
app.UseStaticFiles();
app.UseMvc();
app.UseApplicationInsightsExceptionTelemetry();
}
Since adding the app.UseCompression() middleware, static html files in wwwroot aren't loading correctly anymore. They don't resolve and tend to load indefinitely.
The compression middleware looks as follows and was sourced from here:
public class CompressionMiddleware
{
private readonly RequestDelegate nextDelegate;
public CompressionMiddleware(RequestDelegate next)
{
nextDelegate = next;
}
public async Task Invoke(HttpContext httpContext)
{
var acceptEncoding = httpContext.Request.Headers["Accept-Encoding"];
//Checking that we have been given the correct header before moving forward
if (!(String.IsNullOrEmpty(acceptEncoding)))
{
//Checking that it is gzip
if (acceptEncoding.ToString().IndexOf("gzip", StringComparison.CurrentCultureIgnoreCase) >= 0)
{
using (var memoryStream = new MemoryStream())
{
var stream = httpContext.Response.Body;
httpContext.Response.Body = memoryStream;
await nextDelegate(httpContext);
using (var compressedStream = new GZipStream(stream, CompressionLevel.Optimal))
{
httpContext.Response.Headers.Add("Content-Encoding", new string[] { "gzip" });
memoryStream.Seek(0, SeekOrigin.Begin);
await memoryStream.CopyToAsync(compressedStream);
}
}
}
else
{
await nextDelegate(httpContext);
}
}
//If we have are given to Accept Encoding header or it is blank
else
{
await nextDelegate(httpContext);
}
}
}
Does anyone know why this could be happening?
Note: I am using DNX 1.0.0-rc1-update1 and the 1.0.0-rc1-final libraries.

I just enabled compression in web.config in /wwwroot folder (like we used to do in IIs applicationHost.config), and it works. There's no need to add a compression middleware.
http://www.brandonmartinez.com/2015/08/20/enable-gzip-compression-for-azure-web-apps/

Compression defined in web.config only works with IIS NOT the self hosted web server.
I tested the CompressionMiddleware example and the problem You are seeing is caused by the Content-Length header.
If the Content-Length is already set, the browser gets confused as the actual size of the response doesn't match the value defined in the header as the content is zipped.
Removing the content-length header solved the problem:
httpContext.Response.Headers.Remove("Content-Length");
httpContext.Response.Headers.Add("Content-Encoding", new string[] { "gzip" });
...even better you could try to specify the actual content-length when the content is zipped.

Related

How to configure Anti-Forgery Protection in a view-less Web API

I'm implementing a REST API using ASP.NET Core. It is stateless except for the fact that is uses cookies for authentication and therefore is vulnerable to cross-site request forgery (CSRF) attacks.
Luckily, ASP.NET Core provides means as a protection against that: Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core.
As my application does not have any views or pages, I'm only configuring my controllers using services.AddControllers() in my Startup.
When hitting a REST endpoint that is attributed with [ValidateAntiForgeryToken], I get the following exception:
System.InvalidOperationException: No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter' has been registered.
Registering my controllers using services.AddControllersWithViews() makes this error go away as it internally registers the appropriate service.
According to the docs:
Antiforgery middleware is added to the Dependency injection container when one of the following APIs is called in Startup.ConfigureServices:
AddMvc
MapRazorPages
MapControllerRoute
MapBlazorHub
All of these method seem to me to be view-centric (except MapControllerRoute which I'm doing in the Configure method in my Startup but it doesn't help) and part of the namespace of the missing service is ViewFeatures. This confuses me because in my understanding, and need to take care of CSRF although I'm developing a pure Web API without views.
Is my understanding wrong? How is CSRF protection configured when no views are involved?
I will suggest move away from the default ValidateAntiForgeryToken attribute
All the harder work is done by services.AddAntiforgery(), and the ValidateAntiForgeryToken just calls antiforgery.ValidateRequestAsync()
You can create your own filter for it and register it etc. but take a look at this neat implementation, you can simply inject an instance of IAntiforgery in all the POST api methods
https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/anti-request-forgery/sample/AngularSample/Startup.cs
Here are what I believe to be bits of the Microsoft docs you link to on how to handle this. They say that "using local storage to store the antiforgery token on the client and sending the token as a request header is a recommended approach." They also go on to say that the approach is to use middleware to generate an antiforgery token and send it in the response as a cookie. In short they are saying if you have an API put the antiforgery token in a cookie.
As you say with just AddControllers you can't use the [ValidateAntiForgeryToken]. As LarryX says the thing to do is create your own filter.
In case it helps anyone I have created a demo app that uses some custom middleware to check for the antiforgery token if the request is not a GET.
Note that the CORS code is just there so that I could make a post from another domain to test the code works (I tested with https://localhost:44302).
Standard Program.cs (nothing interesting here)
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace SpaAntiforgery
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Startup.cs
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace SpaAntiforgery
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
}
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
//CORS code that is needed if you want another domain to access your API
app.UseCors(
options => options.WithOrigins("https://localhost:44302")
.AllowAnyMethod()
.AllowCredentials()
.WithHeaders("x-csrf-token", "content-type"));
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
//this bit is straight form the Microsoft docs. See the link reference at the start of my answer
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// The request token can be sent as a JavaScript-readable cookie,
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
//this is my custom middleware that will test for the antiforgery token if the request is not a GET
app.EnsureAntiforgeryTokenPresentOnPosts();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapFallbackToController("Index", "Home");
});
}
}
}
Here is the custommiddleware code that is needed for app.EnsureAntiforgeryTokenPresentOnPosts();
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;
namespace SpaAntiforgery
{
public class AppEnsureAntiforgeryTokenPresentOnPostsMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
public AppEnsureAntiforgeryTokenPresentOnPostsMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
_next = next;
_antiforgery = antiforgery;
}
public async Task Invoke(HttpContext httpContext)
{
var notAGetRerquest = !string.Equals("GET", httpContext.Request.Method, StringComparison.OrdinalIgnoreCase);
if (notAGetRerquest)
{
// This will throw if the token is invalid.
await _antiforgery.ValidateRequestAsync(httpContext);
}
await _next(httpContext);
}
}
public static class AppEnsureAntiforgeryTokenPresentOnPostsExtension
{
public static IApplicationBuilder EnsureAntiforgeryTokenPresentOnPosts(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<AppEnsureAntiforgeryTokenPresentOnPostsMiddleware>();
}
}
}
HomeController.cs
The idea is to make a get to this endpoint so that your client code can retrieve the antiforgery token.
using Microsoft.AspNetCore.Mvc;
namespace SpaAntiforgery.Controllers
{
[Route("[controller]")]
[ApiController]
public class HomeController: ControllerBase
{
public IActionResult Index()
{
return Ok();
}
}
}
I also included a controller to test out a post.
using Microsoft.AspNetCore.Mvc;
namespace SpaAntiforgery.Controllers
{
[Route("[controller]")]
[ApiController]
public class TestAntiforgeryController : ControllerBase
{
[HttpPost]
public IActionResult Index()
{
return Ok();
}
}
}
Sending a post request to /testantiforgery using something like Postman results in an error because the post does not include the antiforgery token. This is what we want.
In order to test that a successful post can be made I created another website with the following code. Note the getCookie method comes straight from the Microsoft docs that I linked to at the start of my answer.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="MyButton">
Test
</button>
<script>
const getCookie = cookieName => {
var name = cookieName + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(";");
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
};
const getCsrfToken = () => {
return getCookie("CSRF-TOKEN");
};
const getHeadersIncludingCsrfToken = () => {
const defaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json"
};
return { ...defaultHeaders, "X-CSRF-TOKEN": getCsrfToken()};
};
const sendRequest = async (url, settings, done) => {
const baseUrl = "https://localhost:44333";
const response = await fetch(baseUrl + url, settings);
if (response.status !== 200) {
console.log("there was an api error");
return;
}
done();
};
const sendGet = async (url, done) => {
const settings = {
method: "GET"
};
await sendRequest(url, settings, done);
};
const sendPost = async (url, done) => {
const settings = {
method: "POST",
headers: getHeadersIncludingCsrfToken()
};
settings.credentials = "include";
await sendRequest(url, settings, done);
};
const sendAPost = () => {
sendPost("/testantiforgery", () => console.log("post succeeded!"));
}
const onTest = () => {
//sending a get to / means the antiforgery cookie is sent back
sendGet("/", sendAPost);
};
const MyButton = document.getElementById("MyButton");
MyButton.addEventListener("click", onTest);
</script>
</body>
</html>
As you can see from the javascript code, after clicking the button, the code sends a GET, this is just to retreive the antiforgery token. The GET is followed by a post. The CSRF-TOKEN is retreived from the cookies and included in the request headers. Note if trying this code out for yourself you will need to set your own baseUrl in the javascript code and also set your own url in the UseCors method in the Configure of Startup.

How to return link to file in ASP Net Core?

I do not know how to serve links to files in ASP Net Core.I have tried using the PhyisicalFileProvider class to no avail.
What i want
Given a folder root on the hard disk , on receiving a querystring
like : /localFolder/test.txt i want the server to send a Link so
the user can click and get the file test.txt
Important
I do not want to send the file but a link to it , so he can click it and download it.
What i have tried:
1.Using the extension method IApplicationBuilder.Map in order to direct the requests directly to the file.
2.Using the extension method IApplicationBuilder.Map + Adding a middleware ,though i do not know how to i serve the link ? (add it
in the response body?)
Startup
public void ConfigureServices(IServiceCollection collection)
{
//i have also added the provider to the service collection to further inject it in the middleware
var phyisicalFileProvider = new PhysicalFileProvider(config.Storage.DocxFileRoot);
services.AddSingleton<IFileProvider>(phyisicalFileProvider);
}
public void Configure()
{
//scenario without middleware
app.Map("/localfiles", x =>
x.UseStaticFiles(new StaticFileOptions {
FileProvider = new PhysicalFileProvider([some file root]),RequestPath ="/localfiles"}
));
//scenario with middleware
app.Map("/localfiles",x=>
x.UseMiddleware<FileWare>()
);
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=File}/{action=Index}/{id?}");
});
}
Middleware
public class FileWare
{
private IFileProvider provider;
private RequestDelegate next;
public FileWare(RequestDelegate next,IFileProvider provider)
{
this.provider=provider;
this.next=next;
}
public async Task Invoke(HttpContext context)
{
var query = context.Request.Query;
var path=query.First(x => x.Key == "path").Value;
var fileInfo=this.provider.GetFileInfo(path);
await fileInfo.CreateReadStream().CopyToAsync(context.Response.Body);
}
}
If you just want to have a link in razor view,click the link to download a file in the local disk(for example C:\MyFolder), you could follow below steps:
1.Razor View
<a asp-controller="File" asp-action="download" asp-route-path="C:\MyFolder\test.txt">Download</a>
2.FileController
public IActionResult Download([FromQuery]string path)
{
string fileName = "test.txt";
byte[] fileBytes = System.IO.File.ReadAllBytes(path);
return File(fileBytes, "application/force-download", fileName);
}
Code below helps to serve files to users in ASP NET Core:
in program.cs write this:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "RealFilePath")),
RequestPath = "/NickNameForPath"
});
then in yourpage.cshtml, code this link:
Get File
And do not forget place folders under wwwroot

Stripped header in request from client to server

Recently I have transferred my simple application which consists of angular 7 client and asp .net core web api (.net core v2.2) to production environment and I encountered a strange problem. When I tried to make an action which should do a post request with authentication token in header i got a 401 - Unauthorized server error.
My production environment is Ubuntu 18.04 with Apache2 server with proxy set to redirect from port 80 to port 5000 (this is the place where API is).
On development environment everything worked perfectly without any error so I guess that there is something with transferring request header from apache to kestrel.
I tried to google this problem and on Microsoft's website I found that I need to use forwarded headers method in my startup.cs file like this:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseAuthentication();
I configured my startup class like that but without any success.
Any help or direction where to try to find the solution would be appreciated.
Thank you very much in advance.
try to add a custome header throgh middleware.
namespace Middleware
{
public class CustomHeader
{
private readonly RequestDelegate _next;
public CustomHeader(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context,
{
var dictionary = new Dictionary<string, string[]>()
{
{
"Access-Control-Allow-Headers",new string[]{ "authorization" }
}
};
//To add Headers AFTER everything you need to do this
context.Response.OnStarting(state => {
var httpContext = (HttpContext)state;
foreach (var item in dictionary)
{
httpContext.Response.Headers.Add(item.Key, item.Value);
}
return Task.FromResult(0);
}, context);
await _next(context);
}
}
}

Serving images from Azure Blob Storage in dot net core

I am attempting to write some Middleware to serve Azure Blobs via proxy.  The handler is being called, the blob is being retrieved, but my image is not being displayed.
I wrote a service to connect to the Storage Account and create a Blob Client. I wrote middleware that consumes the service and then downloads the requested blob and writes it to the Response. Normally, I would expect to download the blob as a byte array or a stream and write it to the OutputStream and that does not seem to be an option using the new httpContext in .net core.
My Middleware:
namespace SampleApp1.WebApp.Middleware
{
public class BlobFileViewHandler
{
public BlobFileViewHandler(RequestDelegate next)
{
}
public async Task Invoke(HttpContext httpContext, IBlobService svc)
{
string container = httpContext.Request.Query["container"];
string itemPath = httpContext.Request.Query["path"];
Blob cbb = await svc.GetBlobAsync(container, itemPath);
httpContext.Response.ContentType = cbb.ContentType;
await httpContext.Response.Body.WriteAsync(cbb.Contents, 0, cbb.Contents.Length);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class BlobFileViewHandlerExtensions
{
public static IApplicationBuilder UseBlobFileViewHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<BlobFileViewHandler>();
}
}
}
I call the middleware using the Map function in Startup as below:
app.Map(new PathString("/thumbs"), a => a.UseBlobFileHandler());
And finally, I attempt to use that handler on a test page as follows:
    <img src="~/thumbs?qs=1" alt="thumbtest" />
When I debug I can see all the correct parts being hit, but the image never loads, I just get the following:
I feel like I'm missing something simple, but I'm not sure what that is. I am using NetCoreApp Version 1.1.
I guess I jumped the gun a little early, because it appears you CAN write to the OutputStream, it's just referenced a little differently. Below is the working implementation of what I was attempting in the middleware:
public class BlobFileHandler
{
public BlobFileHandler(RequestDelegate next)
{
}
public async Task Invoke(HttpContext httpContext)
{
string container = "<static container reference>";
string itemPath = "<static blob reference>";
//string response;
IBlobService svc = (IBlobService)httpContext.RequestServices.GetService(typeof(IBlobService));
CloudBlockBlob cbb = svc.GetBlob(container, itemPath);
httpContext.Response.ContentType = "image/jpeg";//cbb.Properties.ContentType;
await cbb.DownloadToStreamAsync(httpContext.Response.Body);
}
}

Multipart body length limit exceeded exception

Although having set the MaxRequestLength and maxAllowedContentLength to the maximum possible values in the web.config section, ASP.Net Core does not allow me to upload files larger than 134,217,728 Bytes. The exact error coming from the web server is:
An unhandled exception occurred while processing the request.
InvalidDataException: Multipart body length limit 134217728 exceeded.
Is there any way to work this around? (ASP.Net Core)
I found the solution for this problem after reading some posts in GitHub. Conclusion is that they have to be set in the Startup class. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<FormOptions>(x => {
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = int.MaxValue; // In case of multipart
})
}
This will solve the problem. However they also indicated that there is a [RequestFormSizeLimit] attribute, but I have been unable to reference it yet.
Alternatively use the attribute, so the equivalent for an action as resolved by Transcendant would be:
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = int.MaxValue)]
If you use int.MaxValue (2,147,483,647) for the value of MultipartBodyLengthLimit as suggested in other answers, you'll be allowing file uploads of approx. 2Gb, which could quickly fill up disk space on a server. I recommend instead setting a constant to limit file uploads to a more sensible value e.g. in Startup.cs
using MyNamespace.Constants;
public void ConfigureServices(IServiceCollection services)
{
... other stuff
services.Configure<FormOptions>(options => {
options.MultipartBodyLengthLimit = Files.MaxFileUploadSizeKiloBytes;
})
}
And in a separate constants class:
namespace MyNamespace.Constants
{
public static class Files
{
public const int MaxFileUploadSizeKiloBytes = 250000000; // max length for body of any file uploaded
}
}
in case some one still face this problem i've created a middle-ware which intercept the request and create another body
public class FileStreamUploadMiddleware
{
private readonly RequestDelegate _next;
public FileStreamUploadMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.ContentType != null)
{
if (context.Request.Headers.Any(x => x.Key == "Content-Disposition"))
{
var v = ContentDispositionHeaderValue.Parse(
new StringSegment(context.Request.Headers.First(x => x.Key == "Content-Disposition").Value));
if (HasFileContentDisposition(v))
{
using (var memoryStream = new MemoryStream())
{
context.Request.Body.CopyTo(memoryStream);
var length = memoryStream.Length;
var formCollection = context.Request.Form =
new FormCollection(new Dictionary<string, StringValues>(),
new FormFileCollection()
{new FormFile(memoryStream, 0, length, v.Name.Value, v.FileName.Value)});
}
}
}
}
await _next.Invoke(context);
}
private static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// this part of code from https://github.com/aspnet/Mvc/issues/7019#issuecomment-341626892
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
and in the controller we can fetch the files form the request
[HttpPost("/api/file")]
public IActionResult GetFile([FromServices] IHttpContextAccessor contextAccessor,
[FromServices] IHostingEnvironment environment)
{
//save the file
var files = Request.Form.Files;
foreach (var file in files)
{
var memoryStream = new MemoryStream();
file.CopyTo(memoryStream);
var fileStream = File.Create(
$"{environment.WebRootPath}/images/background/{file.FileName}", (int) file.Length,
FileOptions.None);
fileStream.Write(memoryStream.ToArray(), 0, (int) file.Length);
fileStream.Flush();
fileStream.Dispose();
memoryStream.Flush();
memoryStream.Dispose();
}
return Ok();
}
you can improve the code for your needs eg: add form parameters in the body of the request and deserialize it.
its a workaround i guess but it gets the work done.