Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.WebUtilities;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal class HttpRequestAdapterFeature : IHttpRequestAdapterFeature, IHttpRequestFeature, IDisposable
{
private readonly int _bufferThreshold;
private readonly long? _bufferLimit;
private readonly IHttpRequestFeature _other;

private Stream? _bufferedStream;

public HttpRequestAdapterFeature(IHttpRequestFeature other, int bufferThreshold, long? bufferLimit)
{
_bufferThreshold = bufferThreshold;
_bufferLimit = bufferLimit;
_other = other;
}

public ReadEntityBodyMode Mode { get; private set; }

public Stream GetBufferedInputStream()
{
if (Mode is ReadEntityBodyMode.Buffered)
{
Debug.Assert(_bufferedStream is not null);
return _bufferedStream;
}

if (Mode is ReadEntityBodyMode.None)
{
Mode = ReadEntityBodyMode.Buffered;

return _bufferedStream = new FileBufferingReadStream(_other.Body, _bufferThreshold, _bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory);
}

throw new InvalidOperationException("GetBufferlessInputStream cannot be called after other stream access");
}

Stream IHttpRequestAdapterFeature.GetBufferlessInputStream()
{
if (Mode is ReadEntityBodyMode.Bufferless or ReadEntityBodyMode.None)
{
Mode = ReadEntityBodyMode.Bufferless;
return GetBody();
}

throw new InvalidOperationException("GetBufferlessInputStream cannot be called after other stream access");
}

Stream IHttpRequestAdapterFeature.InputStream
{
get
{
if (Mode is ReadEntityBodyMode.Classic && _bufferedStream is not null)
{
return _bufferedStream;
}

throw new InvalidOperationException("InputStream must be prebuffered");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could remove this requirement and provide an API similar to what is described in #164 for HttpRequest.GetInputStreamAsync() that would do the awaiting there. This then could block and wait with the recommendation people move to the async version

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, this blocks on framework so is not a real change. Non-blocking APIs are the now available GetBufferedStream and GetBufferlessStream

}
}

async Task<Stream> IHttpRequestAdapterFeature.GetInputStreamAsync(CancellationToken token)
{
await BufferInputStreamAsync(token);
return GetBody();
}

public async Task BufferInputStreamAsync(CancellationToken token)
{
if (Mode is ReadEntityBodyMode.Classic)
{
return;
}

if (Mode is not ReadEntityBodyMode.None)
{
throw new InvalidOperationException("InputStream cannot be called after other stream access");
}

var stream = GetBufferedInputStream();
await stream.DrainAsync(token);
stream.Position = 0;

Mode = ReadEntityBodyMode.Classic;
}

public void Dispose() => _bufferedStream?.Dispose();

string IHttpRequestFeature.Protocol
{
get => _other.Protocol;
set => _other.Protocol = value;
}

string IHttpRequestFeature.Scheme
{
get => _other.Scheme;
set => _other.Scheme = value;
}

string IHttpRequestFeature.Method
{
get => _other.Method;
set => _other.Method = value;
}

string IHttpRequestFeature.PathBase
{
get => _other.PathBase;
set => _other.PathBase = value;
}

string IHttpRequestFeature.Path
{
get => _other.Path;
set => _other.Path = value;
}

string IHttpRequestFeature.QueryString
{
get => _other.QueryString;
set => _other.QueryString = value;
}

string IHttpRequestFeature.RawTarget
{
get => _other.RawTarget;
set => _other.RawTarget = value;
}

IHeaderDictionary IHttpRequestFeature.Headers
{
get => _other.Headers;
set => _other.Headers = value;
}

Stream IHttpRequestFeature.Body
{
get
{
var body = GetBody();

if (Mode is ReadEntityBodyMode.None)
{
Mode = body.CanSeek ? ReadEntityBodyMode.Buffered : ReadEntityBodyMode.Bufferless;
}

return body;
}
set => _other.Body = value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Mode be updated if the stream is replaced?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_bufferedStream as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought about it, but wasn't sure how necessary it would be. Pushed a PR to reset in that case

}

private Stream GetBody() => _bufferedStream ?? _other.Body;

internal static class AspNetCoreTempDirectory
{
private static string? _tempDirectory;

public static string TempDirectory
{
get
{
if (_tempDirectory == null)
{
// Look for folders in the following order.
var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location.
Path.GetTempPath(); // Fall back.

if (!Directory.Exists(temp))
{
throw new DirectoryNotFoundException(temp);
}

_tempDirectory = temp;
}

return _tempDirectory;
}
}

public static Func<string> TempDirectoryFactory => () => TempDirectory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,31 @@

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal partial class PreBufferRequestStreamMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<PreBufferRequestStreamMiddleware> _logger;
private readonly PreBufferRequestStreamAttribute _defaultMetadata = new() { IsDisabled = true };

[LoggerMessage(Level = LogLevel.Trace, Message = "Prebuffering request stream")]
private partial void LogMessage();
public PreBufferRequestStreamMiddleware(RequestDelegate next) => _next = next;

public PreBufferRequestStreamMiddleware(RequestDelegate next, ILogger<PreBufferRequestStreamMiddleware> logger)
public async Task InvokeAsync(HttpContextCore context)
{
_next = next;
_logger = logger;
}

public Task InvokeAsync(HttpContextCore context)
=> context.GetEndpoint()?.Metadata.GetMetadata<PreBufferRequestStreamAttribute>() is { IsDisabled: false } metadata
? PreBufferAsync(context, metadata)
: _next(context);


private async Task PreBufferAsync(HttpContextCore context, PreBufferRequestStreamAttribute metadata)
{
// TODO: Should this enforce MaxRequestBodySize? https://github.com/aspnet/AspLabs/pull/447#discussion_r827314309
LogMessage();

context.Request.EnableBuffering(metadata.BufferThreshold, metadata.BufferLimit ?? long.MaxValue);

await context.Request.Body.DrainAsync(context.RequestAborted);
context.Request.Body.Position = 0;
var metadata = context.GetEndpoint()?.Metadata.GetMetadata<PreBufferRequestStreamAttribute>() ?? _defaultMetadata;
var existing = context.Features.GetRequired<IHttpRequestFeature>();
var requestFeature = new HttpRequestAdapterFeature(existing, metadata.BufferThreshold, metadata.BufferLimit);

if(!metadata.IsDisabled)
{
await requestFeature.BufferInputStreamAsync(context.RequestAborted);
}

context.Response.RegisterForDispose(requestFeature);
context.Features.Set<IHttpRequestFeature>(requestFeature);
context.Features.Set<IHttpRequestAdapterFeature>(requestFeature);

await _next(context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NETCOREAPP

using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal interface IHttpRequestAdapterFeature
{
ReadEntityBodyMode Mode { get; }

Task<Stream> GetInputStreamAsync(CancellationToken token);

Stream InputStream { get; }

Stream GetBufferedInputStream();

Stream GetBufferlessInputStream();
}

#endif
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET6_0_OR_GREATER
#if NETCOREAPP

using System.Threading.Tasks;

Expand Down
17 changes: 13 additions & 4 deletions src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ public class HttpRequest
private NameValueCollection? _params;
private HttpBrowserCapabilities? _browser;

private FeatureReference<IHttpRequestAdapterFeature> _requestFeature;

internal HttpRequest(HttpRequestCore request)
{
_request = request;
_requestFeature = FeatureReference<IHttpRequestAdapterFeature>.Default;
}

private IHttpRequestAdapterFeature RequestFeature => _requestFeature.Fetch(_request.HttpContext.Features) ?? throw new InvalidOperationException("Please ensure you have added the System.Web adapters middleware.");

internal RequestHeaders TypedHeaders => _typedHeaders ??= new(_request.Headers);

public string? Path => _request.Path.Value;
Expand All @@ -49,6 +54,12 @@ internal HttpRequest(HttpRequestCore request)

public Uri Url => new(_request.GetEncodedUrl());

public ReadEntityBodyMode ReadEntityBodyMode => RequestFeature.Mode;

public Stream GetBufferlessInputStream() => RequestFeature.GetBufferlessInputStream();

public Stream GetBufferedInputStream() => RequestFeature.GetBufferedInputStream();

[SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = Constants.ApiFromAspNet)]
public string? RawUrl => _request.HttpContext.Features.Get<IHttpRequestFeature>()?.RawTarget;

Expand Down Expand Up @@ -139,9 +150,7 @@ public string? ContentType
set => _request.ContentType = value;
}

public Stream InputStream => _request.Body.CanSeek
? _request.Body
: throw new InvalidOperationException("Input stream must be seekable. Ensure your endpoints are either annotated with BufferRequestStreamAttribute or you've called .RequireRequestStreamBuffering() on them.");
public Stream InputStream => RequestFeature.InputStream;

public NameValueCollection ServerVariables => _serverVariables ??= _request.HttpContext.Features.GetRequired<IServerVariablesFeature>().ToNameValueCollection();

Expand Down Expand Up @@ -268,7 +277,7 @@ public void SaveAs(string filename, bool includeHeaders)
w.WriteLine();
}

WriteTo(InputStream, f);
WriteTo(GetBufferedInputStream(), f);
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public virtual string? ContentType

public virtual Stream InputStream => throw new NotImplementedException();

public virtual Stream GetBufferedInputStream() => throw new NotImplementedException();

public virtual Stream GetBufferlessInputStream() => throw new NotImplementedException();

public virtual NameValueCollection ServerVariables => throw new NotImplementedException();

public virtual bool IsSecureConnection => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public override string? ContentType

public override Stream InputStream => _request.InputStream;

public override Stream GetBufferedInputStream() => _request.GetBufferedInputStream();

public override Stream GetBufferlessInputStream() => _request.GetBufferlessInputStream();

public override bool IsAuthenticated => _request.IsAuthenticated;

public override bool IsLocal => _request.IsLocal;
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/ReadEntityBodyMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Web
{
public enum ReadEntityBodyMode
{
None,
Classic, // BinaryRead, Form, Files, InputStream
Bufferless, // GetBufferlessInputStream
Buffered // GetBufferedInputStream
}
}
Loading