-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
summary: ASP.NET Core 6+ Browser Injectionand Razor Pages Instrumenta…
…tion feat: Add automatic browser agent injection for ASP.NET Core v6+ web applications. feat: Add automatic instrumentation for ASP.NET Core v6+ Razor Pages.
- Loading branch information
1 parent
3ac75a0
commit 14c6bb1
Showing
50 changed files
with
1,746 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
src/Agent/NewRelic/Agent/Core/BrowserMonitoring/BrowserScriptInjectionHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Copyright 2020 New Relic, Inc. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
using System; | ||
using System.IO; | ||
using System.Threading.Tasks; | ||
using NewRelic.Agent.Api; | ||
using NewRelic.Core.Logging; | ||
|
||
namespace NewRelic.Agent.Core.BrowserMonitoring | ||
{ | ||
public static class BrowserScriptInjectionHelper | ||
{ | ||
/// <summary> | ||
/// Determine where to inject the RUM script and write the buffer to the base stream. | ||
/// </summary> | ||
/// <param name="buffer">UTF-8 encoded buffer representing the current page</param> | ||
/// <param name="baseStream"></param> | ||
/// <param name="rumBytes"></param> | ||
/// <param name="transaction"></param> | ||
/// <returns></returns> | ||
public static async Task InjectBrowserScriptAsync(byte[] buffer, Stream baseStream, byte[] rumBytes, ITransaction transaction) | ||
{ | ||
var index = BrowserScriptInjectionIndexHelper.TryFindInjectionIndex(buffer); | ||
|
||
if (index == -1) | ||
{ | ||
// not found, can't inject anything | ||
transaction?.LogFinest("Skipping RUM Injection: No suitable location found to inject script."); | ||
await baseStream.WriteAsync(buffer, 0, buffer.Length); | ||
return; | ||
} | ||
|
||
transaction?.LogFinest($"Injecting RUM script at byte index {index}."); | ||
|
||
|
||
// Write everything up to the insertion index | ||
await baseStream.WriteAsync(buffer, 0, index); | ||
|
||
// Write the RUM script | ||
await baseStream.WriteAsync(rumBytes, 0, rumBytes.Length); | ||
|
||
// Write the rest of the doc, starting after the insertion index | ||
await baseStream.WriteAsync(buffer, index, buffer.Length - index); | ||
} | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
src/Agent/NewRelic/Agent/Core/BrowserMonitoring/BrowserScriptInjectionIndexHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Copyright 2020 New Relic, Inc. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
using System; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace NewRelic.Agent.Core.BrowserMonitoring | ||
{ | ||
internal static class BrowserScriptInjectionIndexHelper | ||
{ | ||
|
||
private static readonly Regex XUaCompatibleFilter = new Regex(@"(<\s*meta[^>]+http-equiv[\s]*=[\s]*['""]x-ua-compatible['""][^>]*>)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase); | ||
private static readonly Regex CharsetFilter = new Regex(@"(<\s*meta[^>]+charset\s*=[^>]*>)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase); | ||
|
||
/// <summary> | ||
/// Returns the index into the (UTF-8 encoded) buffer where the RUM script should be injected, or -1 if no suitable location is found | ||
/// </summary> | ||
/// <param name="content"></param> | ||
/// <returns></returns> | ||
/// <remarks> | ||
/// Specification for Javascript insertion: https://newrelic.atlassian.net/wiki/spaces/eng/pages/50299103/BAM+Agent+Auto-Instrumentation | ||
/// </remarks> | ||
public static int TryFindInjectionIndex(byte[] content) | ||
{ | ||
var contentAsString = Encoding.UTF8.GetString(content); | ||
|
||
var openingHeadTagIndex = FindFirstOpeningHeadTag(contentAsString); | ||
|
||
// No <HEAD> tag. Attempt to insert before <BODY> tag (not a great fallback option). | ||
if (openingHeadTagIndex == -1) | ||
{ | ||
return FindIndexBeforeBodyTag(content, contentAsString); | ||
} | ||
|
||
// Since we have a head tag (top of 'page'), search for <X_UA_COMPATIBLE> and for <CHARSET> tags in Head section | ||
var xUaCompatibleFilterMatch = XUaCompatibleFilter.Match(contentAsString, openingHeadTagIndex); | ||
var charsetFilterMatch = CharsetFilter.Match(contentAsString, openingHeadTagIndex); | ||
|
||
// Try to find </HEAD> tag. (It's okay if we don't find it!) | ||
var closingHeadTagIndex = contentAsString.IndexOf("</head>", StringComparison.InvariantCultureIgnoreCase); | ||
|
||
// Find which of the two tags occurs latest (if at all) and ensure that at least | ||
// one of the matches occurs prior to the closing head tag | ||
if ((xUaCompatibleFilterMatch.Success || charsetFilterMatch.Success) && | ||
(xUaCompatibleFilterMatch.Index < closingHeadTagIndex || charsetFilterMatch.Index < closingHeadTagIndex)) | ||
{ | ||
var match = charsetFilterMatch; | ||
if (xUaCompatibleFilterMatch.Index > charsetFilterMatch.Index) | ||
{ | ||
match = xUaCompatibleFilterMatch; | ||
} | ||
|
||
// find the index just after the end of the regex match in the UTF-8 buffer | ||
var contentSubString = contentAsString.Substring(match.Index, match.Length); | ||
var utf8HeadMatchIndex = IndexOfByteArray(content, contentSubString, out var substringBytesLength); | ||
|
||
return utf8HeadMatchIndex + substringBytesLength; | ||
} | ||
|
||
// found opening head tag but no meta tags, insert immediately after the opening head tag | ||
// Find first '>' after the opening head tag, which will be end of head opening tag. | ||
var indexOfEndHeadOpeningTag = contentAsString.IndexOf('>', openingHeadTagIndex); | ||
|
||
// The <HEAD> tag may be malformed or simply be another type of tag, if so do not use it | ||
if (!(indexOfEndHeadOpeningTag > openingHeadTagIndex)) | ||
return -1; | ||
|
||
// Get the whole open HEAD tag string | ||
var headOpeningTag = contentAsString.Substring(openingHeadTagIndex, (indexOfEndHeadOpeningTag - openingHeadTagIndex) + 1); | ||
var utf8HeadOpeningTagIndex = IndexOfByteArray(content, headOpeningTag, out var headOpeningTagBytesLength); | ||
return utf8HeadOpeningTagIndex + headOpeningTagBytesLength; | ||
} | ||
|
||
private static int FindIndexBeforeBodyTag(byte[] content, string contentAsString) | ||
{ | ||
const string bodyOpenTag = "<body"; | ||
|
||
var indexOfBodyTag = contentAsString.IndexOf(bodyOpenTag, StringComparison.InvariantCultureIgnoreCase); | ||
if (indexOfBodyTag < 0) | ||
return -1; | ||
|
||
// find the body tag start index in the UTF-8 buffer | ||
var bodyFromContent = contentAsString.Substring(indexOfBodyTag, bodyOpenTag.Length); | ||
var utf8BodyTagIndex = IndexOfByteArray(content, bodyFromContent, out _); | ||
return utf8BodyTagIndex; | ||
} | ||
|
||
private static int FindFirstOpeningHeadTag(string content) | ||
{ | ||
int indexOpeningHead = -1; | ||
|
||
var indexTemp = content.IndexOf("<head", StringComparison.InvariantCultureIgnoreCase); | ||
if (indexTemp < 0) | ||
return -1; | ||
|
||
if (content[indexTemp + 5] == '>' || content[indexTemp + 5] == ' ') | ||
{ | ||
indexOpeningHead = indexTemp; | ||
} | ||
|
||
return indexOpeningHead; | ||
} | ||
|
||
/// <summary> | ||
/// Returns an index into a byte array to find a string in the byte array. | ||
/// Exact match using the encoding provided or UTF-8 by default. | ||
/// </summary> | ||
/// <param name="buffer"></param> | ||
/// <param name="stringToFind"></param> | ||
/// <param name="stringToFindBytesLength"></param> | ||
/// <param name="encoding"></param> | ||
/// <returns></returns> | ||
private static int IndexOfByteArray(byte[] buffer, string stringToFind, out int stringToFindBytesLength, Encoding encoding = null) | ||
{ | ||
stringToFindBytesLength = 0; | ||
encoding ??= Encoding.UTF8; | ||
|
||
if (buffer.Length == 0 || string.IsNullOrEmpty(stringToFind)) | ||
return -1; | ||
|
||
var stringToFindBytes = encoding.GetBytes(stringToFind); | ||
stringToFindBytesLength = stringToFindBytes.Length; | ||
|
||
return buffer.AsSpan().IndexOf(stringToFindBytes); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ public enum WebTransactionType | |
ASP, | ||
MVC, | ||
WCF, | ||
Razor, | ||
WebAPI, | ||
WebService, | ||
MonoRail, | ||
|
Oops, something went wrong.