-
-
Notifications
You must be signed in to change notification settings - Fork 210
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Sentry.Tunnel to add official tunnel middleware (#1133)
* Add Sentry.Tunnel to add official tunnel middleware * Add Tunnel version user agent * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: Bruno Garcia <bruno@brunogarcia.com> * Remove .NET testing stuff, cache the assembly version * Add trailing newline * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> Co-authored-by: Bruno Garcia <bruno@brunogarcia.com> Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>
- Loading branch information
1 parent
3f8ffa1
commit cd03fe1
Showing
7 changed files
with
361 additions
and
15 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>net5.0;netcoreapp3.0;netstandard2.0</TargetFrameworks> | ||
<PackageTags>$(PackageTags);AspNetCore;MVC</PackageTags> | ||
<PackageId>Sentry.Tunnel</PackageId> | ||
<AssemblyName>Sentry.Tunnel</AssemblyName> | ||
<RootNamespace>Sentry.Tunnel</RootNamespace> | ||
<Description>Official Tunnel middleware for Sentry.</Description> | ||
</PropertyGroup> | ||
|
||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> | ||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.0" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" Version="2.1.0" /> | ||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" /> | ||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.0" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Routing.Abstractions" Version="2.1.0" /> | ||
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.0" /> | ||
<PackageReference Include="System.Text.Json" Version="4.6.0" /> | ||
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.5.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0'"> | ||
<FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
</ItemGroup> | ||
|
||
</Project> |
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,96 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Reflection; | ||
using System.Text.Json; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Net.Http.Headers; | ||
|
||
namespace Sentry.Tunnel | ||
{ | ||
/// <summary> | ||
/// Middleware that can forward Sentry envelopes. | ||
/// </summary> | ||
/// <seealso href="https://docs.sentry.io/platforms/javascript/troubleshooting/#dealing-with-ad-blockers"/> | ||
public class SentryTunnelMiddleware : IMiddleware | ||
{ | ||
private readonly string[] _allowedHosts; | ||
private string? _version; | ||
private string Version => _version ??= (GetType().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? string.Empty); | ||
|
||
/// <summary> | ||
/// Middleware that can forward Sentry envelopes. | ||
/// </summary> | ||
/// <seealso href="https://docs.sentry.io/platforms/javascript/troubleshooting/#dealing-with-ad-blockers"/> | ||
public SentryTunnelMiddleware(string[] allowedHosts) | ||
{ | ||
_allowedHosts = new[] {"sentry.io"}.Concat(allowedHosts).ToArray(); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public async Task InvokeAsync(HttpContext context, RequestDelegate next) | ||
{ | ||
if (context.Request.Method == "OPTIONS") | ||
{ | ||
context.Response.Headers.Add("Access-Control-Allow-Origin", new[] {(string) context.Request.Headers["Origin"]}); | ||
context.Response.Headers.Add("Access-Control-Allow-Headers", new[] {"Origin, X-Requested-With, Content-Type, Accept"}); | ||
context.Response.Headers.Add("Access-Control-Allow-Methods", new[] {"POST, OPTIONS"}); | ||
context.Response.Headers.Add("Access-Control-Allow-Credentials", new[] {"true"}); | ||
context.Response.StatusCode = 200; | ||
return; | ||
} | ||
|
||
var httpClientFactory = context.RequestServices.GetRequiredService<IHttpClientFactory>(); | ||
var client = httpClientFactory.CreateClient("SentryTunnel"); | ||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Sentry.NET_Tunnel", Version)); | ||
var ms = new MemoryStream(); | ||
await context.Request.Body.CopyToAsync(ms); | ||
ms.Position = 0; | ||
using (var reader = new StreamReader(ms)) | ||
{ | ||
var header = await reader.ReadLineAsync().ConfigureAwait(false); | ||
if (string.IsNullOrWhiteSpace(header)) | ||
{ | ||
context.Response.StatusCode = StatusCodes.Status400BadRequest; | ||
return; | ||
} | ||
|
||
try | ||
{ | ||
var headerJson = JsonSerializer.Deserialize<Dictionary<string, object>>(header); | ||
if (headerJson == null) | ||
{ | ||
context.Response.StatusCode = StatusCodes.Status400BadRequest; | ||
await context.Response.WriteAsync("Invalid DSN JSON supplied").ConfigureAwait(false); | ||
return; | ||
} | ||
if (headerJson.TryGetValue("dsn", out var dsnString) && Uri.TryCreate(dsnString.ToString(), UriKind.Absolute, out var dsn) && _allowedHosts.Contains(dsn.Host)) | ||
{ | ||
var projectId = dsn.AbsolutePath.Trim('/'); | ||
ms.Position = 0; | ||
var responseMessage = await client.PostAsync($"https://{dsn.Host}/api/{projectId}/envelope/", | ||
new StreamContent(ms)).ConfigureAwait(false); | ||
context.Response.Headers["content-type"] = "application/json"; | ||
context.Response.StatusCode = StatusCodes.Status200OK; | ||
await responseMessage.Content.CopyToAsync(context.Response.Body).ConfigureAwait(false); | ||
} | ||
} | ||
catch(JsonException) | ||
{ | ||
context.Response.StatusCode = StatusCodes.Status400BadRequest; | ||
await context.Response.WriteAsync("Invalid DSN JSON supplied").ConfigureAwait(false); | ||
} | ||
catch(ArgumentNullException) | ||
{ | ||
context.Response.StatusCode = StatusCodes.Status400BadRequest; | ||
await context.Response.WriteAsync("Received empty body").ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/Sentry.Tunnel/SentryTunnelingApplicationBuilderExtensions.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,26 @@ | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Sentry.Tunnel | ||
{ | ||
public static class SentryTunnelingApplicationBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds and configures the Sentry tunneling middleware | ||
/// </summary> | ||
/// <param name="services"></param> | ||
/// <param name="hostnames">The extra hostnames to be allowed for the tunneling. sentry.io is allowed by default; add your own Sentry domain if you use a self-hosted Sentry or Relay.</param> | ||
public static void AddSentryTunneling(this IServiceCollection services, params string[] hostnames) | ||
{ | ||
services.AddScoped<SentryTunnelMiddleware>((s) => new SentryTunnelMiddleware(hostnames)); | ||
} | ||
|
||
public static void UseSentryTunneling(this IApplicationBuilder builder, string path = "/tunnel") | ||
{ | ||
builder.Map(path, applicationBuilder => | ||
{ | ||
applicationBuilder.UseMiddleware<SentryTunnelMiddleware>(); | ||
}); | ||
} | ||
} | ||
} |
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,95 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Microsoft.AspNetCore.TestHost; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using NSubstitute; | ||
using Xunit; | ||
|
||
namespace Sentry.Tunnel.Tests | ||
{ | ||
[Collection("SentryTunnelCollection")] | ||
public partial class IntegrationsTests | ||
{ | ||
private readonly TestServer _server; | ||
private HttpClient _httpClient; | ||
private MockHttpMessageHandler _httpMessageHander; | ||
|
||
public IntegrationsTests() | ||
{ | ||
var builder = new WebHostBuilder() | ||
.ConfigureServices(s => | ||
{ | ||
s.AddSentryTunneling("sentry.mywebsite.com"); | ||
_httpMessageHander = new MockHttpMessageHandler("{}", HttpStatusCode.OK); | ||
_httpClient = new HttpClient(_httpMessageHander); | ||
var factory = Substitute.For<IHttpClientFactory>(); | ||
factory.CreateClient(Arg.Any<string>()).Returns(_httpClient); | ||
s.AddSingleton<IHttpClientFactory>(factory); | ||
}) | ||
.Configure(app => { app.UseSentryTunneling(); }); | ||
_server = new TestServer(builder); | ||
} | ||
|
||
[Fact] | ||
public async Task TunnelMiddleware_CanForwardValidEnvelope() | ||
{ | ||
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel"); | ||
requestMessage.Content = new StringContent( | ||
@"{""sent_at"":""2021-01-01T00:00:00.000Z"",""sdk"":{""name"":""sentry.javascript.browser"",""version"":""6.8.0""},""dsn"":""https://dns@sentry.io/1""} | ||
{""type"":""session""} | ||
{""sid"":""fda00e933162466c849962eaea0cfaff""}"); | ||
var responseMessage = await _server.CreateClient().SendAsync(requestMessage); | ||
|
||
Assert.Equal(1, _httpMessageHander.NumberOfCalls); | ||
} | ||
|
||
[Fact] | ||
public async Task TunnelMiddleware_DoesNotForwardEnvelopeWithoutDsn() | ||
{ | ||
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel"); | ||
requestMessage.Content = new StringContent(@"{} | ||
{""type"":""session""} | ||
{""sid"":""fda00e933162466c849962eaea0cfaff""}"); | ||
var responseMessage = await _server.CreateClient().SendAsync(requestMessage); | ||
|
||
Assert.Equal(0, _httpMessageHander.NumberOfCalls); | ||
} | ||
|
||
[Fact] | ||
public async Task TunnelMiddleware_DoesNotForwardEnvelopeToArbitraryHost() | ||
{ | ||
{ | ||
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel"); | ||
requestMessage.Content = new StringContent( | ||
@"{""sent_at"":""2021-01-01T00:00:00.000Z"",""sdk"":{""name"":""sentry.javascript.browser"",""version"":""6.8.0""},""dsn"":""https://dns@evil.com/1""} | ||
{""type"":""session""} | ||
{""sid"":""fda00e933162466c849962eaea0cfaff""}"); | ||
var responseMessage = await _server.CreateClient().SendAsync(requestMessage); | ||
|
||
Assert.Equal(0, _httpMessageHander.NumberOfCalls); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task TunnelMiddleware_CanForwardEnvelopeToWhiteListedHost() | ||
{ | ||
{ | ||
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel"); | ||
requestMessage.Content = new StringContent( | ||
@"{""sent_at"":""2021-01-01T00:00:00.000Z"",""sdk"":{""name"":""sentry.javascript.browser"",""version"":""6.8.0""},""dsn"":""https://dns@sentry.mywebsite.com/1""} | ||
{""type"":""session""} | ||
{""sid"":""fda00e933162466c849962eaea0cfaff""}"); | ||
var responseMessage = await _server.CreateClient().SendAsync(requestMessage); | ||
|
||
Assert.Equal(1, _httpMessageHander.NumberOfCalls); | ||
} | ||
} | ||
} | ||
} |
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,41 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Sentry.Tunnel.Tests | ||
{ | ||
public class MockHttpMessageHandler : HttpMessageHandler | ||
{ | ||
private readonly string _response; | ||
private readonly HttpStatusCode _statusCode; | ||
|
||
public string Input { get; private set; } | ||
public int NumberOfCalls { get; private set; } | ||
|
||
public MockHttpMessageHandler(string response, HttpStatusCode statusCode) | ||
{ | ||
_response = response; | ||
_statusCode = statusCode; | ||
} | ||
|
||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | ||
CancellationToken cancellationToken) | ||
{ | ||
NumberOfCalls++; | ||
if (request.Content != null) // Could be a GET-request without a body | ||
{ | ||
Input = await request.Content.ReadAsStringAsync(); | ||
} | ||
return new HttpResponseMessage | ||
{ | ||
StatusCode = _statusCode, | ||
Content = new StringContent(_response) | ||
}; | ||
} | ||
} | ||
} |
Oops, something went wrong.