diff --git a/src/Agent/NewRelic/Agent/Core/Utilities/ExtensionsLoader.cs b/src/Agent/NewRelic/Agent/Core/Utilities/ExtensionsLoader.cs index a5b4df9290..d97693468b 100644 --- a/src/Agent/NewRelic/Agent/Core/Utilities/ExtensionsLoader.cs +++ b/src/Agent/NewRelic/Agent/Core/Utilities/ExtensionsLoader.cs @@ -39,7 +39,7 @@ public static void Initialize(string installPathExtensionsDirectory) { "BuildCommonServicesWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") }, { "GenericHostWebHostBuilderExtensionsWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") }, - { "InvokeActionMethodAsync6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") }, + { "InvokeActionMethodAsyncWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") }, { "ResolveAppWrapper", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.Owin.dll") }, diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/Instrumentation.xml b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/Instrumentation.xml index b598e001f9..abff9ddff5 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/Instrumentation.xml +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/Instrumentation.xml @@ -19,6 +19,9 @@ SPDX-License-Identifier: Apache-2.0 + + + diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/InvokeActionMethodAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/InvokeActionMethodAsyncWrapper6Plus.cs similarity index 96% rename from src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/InvokeActionMethodAsyncWrapper.cs rename to src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/InvokeActionMethodAsyncWrapper6Plus.cs index b387ef9a41..9a81efd8c7 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/InvokeActionMethodAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/InvokeActionMethodAsyncWrapper6Plus.cs @@ -21,7 +21,7 @@ public class InvokeActionMethodAsyncWrapper6Plus : IWrapper public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) { - return new CanWrapResponse(nameof(InvokeActionMethodAsyncWrapper6Plus).Equals(methodInfo.RequestedWrapperName)); + return new CanWrapResponse("InvokeActionMethodAsyncWrapper6Plus".Equals(methodInfo.RequestedWrapperName)); } public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/ResponseStreamWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/ResponseStreamWrapper.cs index b3b1e936d2..1cf763ff4a 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/ResponseStreamWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/ResponseStreamWrapper.cs @@ -8,17 +8,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using NewRelic.Agent.Api; +using NewRelic.Agent.Extensions.Logging; namespace NewRelic.Providers.Wrapper.AspNetCore6Plus { /// - /// Wrapper for the response stream, handles checking for response content type and injecting the browser script if appropriate + /// Wrapper for the response stream, handles injecting the browser script if appropriate /// public class ResponseStreamWrapper : Stream { private readonly IAgent _agent; private Stream _baseStream; private HttpContext _context; + private bool _isContentLengthSet; public ResponseStreamWrapper(IAgent agent, Stream baseStream, HttpContext context) @@ -30,57 +32,83 @@ public ResponseStreamWrapper(IAgent agent, Stream baseStream, HttpContext contex CanWrite = true; } - public override void Flush() + public override Task FlushAsync(CancellationToken cancellationToken) { - _baseStream.Flush(); + if (!_isContentLengthSet && IsHtmlResponse()) + { + _context.Response.Headers.ContentLength = null; + _isContentLengthSet = true; + } + + return _baseStream.FlushAsync(cancellationToken); } - public override int Read(byte[] buffer, int offset, int count) + public override void Flush() { - return _baseStream.Read(buffer, offset, count); + _baseStream.Flush(); } + public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin); public override void SetLength(long value) { _baseStream.SetLength(value); - } - public override void Write(ReadOnlySpan buffer) - { - _baseStream.Write(buffer); + IsHtmlResponse(forceReCheck: true); } - public override void WriteByte(byte value) - { - _baseStream.WriteByte(value); - } + public override void Write(ReadOnlySpan buffer) => _baseStream.Write(buffer); + + public override void WriteByte(byte value) => _baseStream.WriteByte(value); public override void Write(byte[] buffer, int offset, int count) { - var curBuf = buffer.AsMemory(offset, count).ToArray(); - _agent.TryInjectBrowserScriptAsync(_context.Response.ContentType, _context.Request.Path.Value, curBuf, _baseStream) + if (IsHtmlResponse()) + { + var curBuf = buffer.AsMemory(offset, count).ToArray(); + _agent.TryInjectBrowserScriptAsync(_context.Response.ContentType, _context.Request.Path.Value, curBuf, + _baseStream) .GetAwaiter().GetResult(); - } + } + else + { + _agent.CurrentTransaction.LogFinest("ResponseStreamWrapper: Not an HTML response so no attempt to inject RUM."); + _baseStream?.Write(buffer, offset, count); + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { - await _agent.TryInjectBrowserScriptAsync(_context.Response.ContentType, _context.Request.Path.Value, buffer.ToArray(), _baseStream); + if (IsHtmlResponse()) + { + await _agent.TryInjectBrowserScriptAsync(_context.Response.ContentType, _context.Request.Path.Value, + buffer.ToArray(), _baseStream); + } + else + { + _agent.Logger.Log(Level.Finest, "ResponseStreamWrapper: Not an HTML response so no attempt to inject RUM."); + if (_baseStream != null) + await _baseStream.WriteAsync(buffer, cancellationToken); + } } - protected override void Dispose(bool disposing) + public override async ValueTask DisposeAsync() { - _baseStream = null; + // TODO: Debugging only + _agent.CurrentTransaction.LogFinest($"ResponseStreamWrapper.DisposeAsync starting "); _context = null; - base.Dispose(disposing); + await _baseStream.DisposeAsync(); + _baseStream = null; + + // TODO: Debugging only + _agent.CurrentTransaction.LogFinest($"ResponseStreamWrapper.DisposeAsync complete "); } public override bool CanRead { get; } @@ -88,5 +116,47 @@ protected override void Dispose(bool disposing) public override bool CanWrite { get; } public override long Length { get; } public override long Position { get; set; } + + private bool? _isHtmlResponse = null; + private bool IsHtmlResponse(bool forceReCheck = false) + { + if (!forceReCheck && _isHtmlResponse != null) + return _isHtmlResponse.Value; + + // we need to check if the active request is still valid + // this can fail if we're in the middle of an error response + // or url rewrite in which case we can't intercept + if (_context?.Response == null) + return false; + + // Requirements for script injection: + // * has to have result body + // * 200 or 500 response + // * text/html response + // * UTF-8 formatted (explicit or no charset) + + _isHtmlResponse = + _context.Response?.Body != null && + (_context.Response.StatusCode == 200 || _context.Response.StatusCode == 500) && + _context.Response.ContentType != null && + _context.Response.ContentType.Contains("text/html", StringComparison.OrdinalIgnoreCase) && + (_context.Response.ContentType.Contains("utf-8", StringComparison.OrdinalIgnoreCase) || + !_context.Response.ContentType.Contains("charset=", StringComparison.OrdinalIgnoreCase)); + + if (!_isHtmlResponse.Value) + return false; + + // Make sure we force dynamic content type since we're + // rewriting the content - static content will set the header explicitly + // and fail when it doesn't matchif (_isHtmlResponse.Value) + if (!_isContentLengthSet && _context.Response.ContentLength != null) + { + _context.Response.Headers.ContentLength = null; + _isContentLengthSet = true; + } + + return _isHtmlResponse.Value; + } + } } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/WrapPipelineMiddleware.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/WrapPipelineMiddleware.cs index 89bf8353d3..1da7d2c001 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/WrapPipelineMiddleware.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore6Plus/WrapPipelineMiddleware.cs @@ -148,7 +148,7 @@ private ISegment SetupSegment(ITransaction transaction, HttpContext context) { // Seems like it would be cool to not require all of this for a segment??? var method = new Method(typeof(WrapPipelineMiddleware), nameof(Invoke), nameof(context)); - var methodCall = new MethodCall(method, this, new object[] { context }, false); + var methodCall = new MethodCall(method, this, new object[] { context }, true); var segment = transaction.StartTransactionSegment(methodCall, "Middleware Pipeline"); return segment; diff --git a/tests/Agent/IntegrationTests/Applications/BasicMvcApplication/BasicMvcApplication.csproj b/tests/Agent/IntegrationTests/Applications/BasicMvcApplication/BasicMvcApplication.csproj index a97512bfcb..8fd1e2fed2 100644 --- a/tests/Agent/IntegrationTests/Applications/BasicMvcApplication/BasicMvcApplication.csproj +++ b/tests/Agent/IntegrationTests/Applications/BasicMvcApplication/BasicMvcApplication.csproj @@ -1,4 +1,4 @@ - + Debug @@ -165,4 +165,4 @@ - + \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/ConsoleAsyncApplication/ConsoleAsyncApplication.csproj b/tests/Agent/IntegrationTests/Applications/ConsoleAsyncApplication/ConsoleAsyncApplication.csproj index 476817835b..a3c857358a 100644 --- a/tests/Agent/IntegrationTests/Applications/ConsoleAsyncApplication/ConsoleAsyncApplication.csproj +++ b/tests/Agent/IntegrationTests/Applications/ConsoleAsyncApplication/ConsoleAsyncApplication.csproj @@ -1,4 +1,4 @@ - + @@ -62,4 +62,4 @@ - + \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/MvcAsyncApplication/MvcAsyncApplication.csproj b/tests/Agent/IntegrationTests/Applications/MvcAsyncApplication/MvcAsyncApplication.csproj index 2583736ff7..282bc40a3f 100644 --- a/tests/Agent/IntegrationTests/Applications/MvcAsyncApplication/MvcAsyncApplication.csproj +++ b/tests/Agent/IntegrationTests/Applications/MvcAsyncApplication/MvcAsyncApplication.csproj @@ -1,4 +1,4 @@ - + @@ -172,4 +172,4 @@ - + \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/OwinRemotingClient/OwinRemotingClient.csproj b/tests/Agent/IntegrationTests/Applications/OwinRemotingClient/OwinRemotingClient.csproj index e8974e846f..5413526c8d 100644 --- a/tests/Agent/IntegrationTests/Applications/OwinRemotingClient/OwinRemotingClient.csproj +++ b/tests/Agent/IntegrationTests/Applications/OwinRemotingClient/OwinRemotingClient.csproj @@ -1,4 +1,4 @@ - + @@ -106,4 +106,4 @@ - + \ No newline at end of file diff --git a/tests/Agent/IntegrationTests/Applications/WebApiAsyncApplication/WebApiAsyncApplication.csproj b/tests/Agent/IntegrationTests/Applications/WebApiAsyncApplication/WebApiAsyncApplication.csproj index 86be265dd5..6961ee4a01 100644 --- a/tests/Agent/IntegrationTests/Applications/WebApiAsyncApplication/WebApiAsyncApplication.csproj +++ b/tests/Agent/IntegrationTests/Applications/WebApiAsyncApplication/WebApiAsyncApplication.csproj @@ -1,4 +1,4 @@ - + @@ -175,4 +175,4 @@ - + \ No newline at end of file