Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sentry.Tunnel to add official tunnel middleware #1133

Merged
merged 16 commits into from
Jul 25, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 29 additions & 15 deletions Sentry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -101,35 +101,39 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F267
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{E1800E08-72E6-45AE-91EC-69B4FDBA7555}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\danger.yml = .github\workflows\danger.yml
.github\workflows\docs.yml = .github\workflows\docs.yml
.github\workflows\vulnerabilities.yml = .github\workflows\vulnerabilities.yml
EndProjectSection
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\danger.yml = .github\workflows\danger.yml
.github\workflows\docs.yml = .github\workflows\docs.yml
.github\workflows\vulnerabilities.yml = .github\workflows\vulnerabilities.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.AspNetCore.Grpc", "src\Sentry.AspNetCore.Grpc\Sentry.AspNetCore.Grpc.csproj", "{720811C8-29C7-4368-86F2-D61DF415AC2A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.AspNetCore.Grpc.Tests", "test\Sentry.AspNetCore.Grpc.Tests\Sentry.AspNetCore.Grpc.Tests.csproj", "{3818FA5C-8649-427D-8E68-0C44558CA0DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.AspNetCore.Grpc", "samples\Sentry.Samples.AspNetCore.Grpc\Sentry.Samples.AspNetCore.Grpc.csproj", "{21599C29-C3D4-4DAC-A2D6-6C194600478F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.AspNetCore.Blazor.Wasm", "samples\Sentry.Samples.AspNetCore.Blazor.Wasm\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "{73FDCE53-75D1-4DCC-933A-8AB93A0E86EA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.AspNetCore.Blazor.Wasm", "samples\Sentry.Samples.AspNetCore.Blazor.Wasm\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "{73FDCE53-75D1-4DCC-933A-8AB93A0E86EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.Aws.Lambda.AspNetCoreServer", "samples\Sentry.Samples.Aws.Lambda.AspNetCoreServer\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", "{274CEDC2-2129-469D-B269-649EFA2EF5E0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.EntityFramework", "src\Sentry.EntityFramework\Sentry.EntityFramework.csproj", "{8B38F62E-0DD5-486F-96F5-2025AFB9B491}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Aws.Lambda.AspNetCoreServer", "samples\Sentry.Samples.Aws.Lambda.AspNetCoreServer\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", "{274CEDC2-2129-469D-B269-649EFA2EF5E0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.EntityFramework.Tests", "test\Sentry.EntityFramework.Tests\Sentry.EntityFramework.Tests.csproj", "{840B220E-68EC-4ECB-AEA1-67B0151F17FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.EntityFramework", "src\Sentry.EntityFramework\Sentry.EntityFramework.csproj", "{8B38F62E-0DD5-486F-96F5-2025AFB9B491}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.EntityFramework", "samples\Sentry.Samples.EntityFramework\Sentry.Samples.EntityFramework.csproj", "{8E4BA4C7-413C-4668-8F41-32F484FFB7AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.EntityFramework.Tests", "test\Sentry.EntityFramework.Tests\Sentry.EntityFramework.Tests.csproj", "{840B220E-68EC-4ECB-AEA1-67B0151F17FC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.Google.Cloud.Functions", "samples\Sentry.Samples.Google.Cloud.Functions\Sentry.Samples.Google.Cloud.Functions.csproj", "{88269A52-A0BA-41B2-8DF3-505B66B17243}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.EntityFramework", "samples\Sentry.Samples.EntityFramework\Sentry.Samples.EntityFramework.csproj", "{8E4BA4C7-413C-4668-8F41-32F484FFB7AA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Google.Cloud.Functions", "src\Sentry.Google.Cloud.Functions\Sentry.Google.Cloud.Functions.csproj", "{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Google.Cloud.Functions", "samples\Sentry.Samples.Google.Cloud.Functions\Sentry.Samples.Google.Cloud.Functions.csproj", "{88269A52-A0BA-41B2-8DF3-505B66B17243}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Google.Cloud.Functions.Tests", "test\Sentry.Google.Cloud.Functions.Tests\Sentry.Google.Cloud.Functions.Tests.csproj", "{066522A4-8380-4D29-8DD0-973B1EDF0B39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Google.Cloud.Functions", "src\Sentry.Google.Cloud.Functions\Sentry.Google.Cloud.Functions.csproj", "{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Tunnel", "src\Sentry.Tunnel\Sentry.Tunnel.csproj", "{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Google.Cloud.Functions.Tests", "test\Sentry.Google.Cloud.Functions.Tests\Sentry.Google.Cloud.Functions.Tests.csproj", "{066522A4-8380-4D29-8DD0-973B1EDF0B39}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Tunnel.Tests", "test\Sentry.Tunnel.Tests\Sentry.Tunnel.Tests.csproj", "{BB54EF08-2FA1-498B-827B-32D905FB0F9F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -289,6 +293,14 @@ Global
{066522A4-8380-4D29-8DD0-973B1EDF0B39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{066522A4-8380-4D29-8DD0-973B1EDF0B39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{066522A4-8380-4D29-8DD0-973B1EDF0B39}.Release|Any CPU.Build.0 = Release|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Release|Any CPU.Build.0 = Release|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -335,6 +347,8 @@ Global
{88269A52-A0BA-41B2-8DF3-505B66B17243} = {77454495-55EE-4B40-A089-71B9E8F82E89}
{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB}
{066522A4-8380-4D29-8DD0-973B1EDF0B39} = {83263231-1A2A-4733-B759-EEFF14E8C5D5}
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB}
{BB54EF08-2FA1-498B-827B-32D905FB0F9F} = {83263231-1A2A-4733-B759-EEFF14E8C5D5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0C652B1A-DF72-4EE5-A98B-194FE2C054F6}
Expand Down
27 changes: 27 additions & 0 deletions src/Sentry.Tunnel/Sentry.Tunnel.csproj
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>
Copy link
Member

Choose a reason for hiding this comment

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

What about just netcoreapp3.1 and netstandard2.0?

Suggested change
<TargetFrameworks>net5.0;netcoreapp3.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;netstandard2.0</TargetFrameworks>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Never bothered with 2.1, only 3.0, not sure if there is a point to do 3.1 instead. I added net5.0 just to ensure we have a version that binds against ASP.NET Core 5, just in case. Not sure if we need it but gotta make sure it doesn't pull in weird, old dependencies.

<PackageTags>$(PackageTags);AspNetCore;MVC</PackageTags>
<PackageId>Sentry.Tunnel</PackageId>
<AssemblyName>Sentry.Tunnel</AssemblyName>
<RootNamespace>Sentry.Tunnel</RootNamespace>
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
<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>
96 changes: 96 additions & 0 deletions src/Sentry.Tunnel/SentryTunnelMiddleware.cs
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
kanadaj marked this conversation as resolved.
Show resolved Hide resolved
{
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)
kanadaj marked this conversation as resolved.
Show resolved Hide resolved
{
_allowedHosts = new[] {"sentry.io"}.Concat(allowedHosts).ToArray();
}

/// <inheritdoc />
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
kanadaj marked this conversation as resolved.
Show resolved Hide resolved
{
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))
{
Comment on lines +54 to +55
Copy link
Member

Choose a reason for hiding this comment

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

What about?:

Suggested change
using (var reader = new StreamReader(ms))
{
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);
}
}
}
}
}
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
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
{
/// <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")
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
{
builder.Map(path, applicationBuilder =>
{
applicationBuilder.UseMiddleware<SentryTunnelMiddleware>();
});
}
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
}
}
95 changes: 95 additions & 0 deletions test/Sentry.Tunnel.Tests/IntegrationsTests.cs
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);
}
}
}
}
41 changes: 41 additions & 0 deletions test/Sentry.Tunnel.Tests/MockHttpMessageHandler.cs
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)
};
}
}
}