Skip to content

Commit 39e68b3

Browse files
authored
Validate request protocol (#475)
1 parent e1c13b1 commit 39e68b3

File tree

7 files changed

+79
-13
lines changed

7 files changed

+79
-13
lines changed

src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using Microsoft.AspNetCore.Http.Features;
2424
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
2525
using Microsoft.Extensions.Logging;
26+
using Microsoft.Net.Http.Headers;
2627

2728
namespace Grpc.AspNetCore.Server.Internal.CallHandlers
2829
{
@@ -55,7 +56,14 @@ public Task HandleCallAsync(HttpContext httpContext)
5556
{
5657
if (GrpcProtocolHelpers.IsInvalidContentType(httpContext, out var error))
5758
{
58-
GrpcProtocolHelpers.SendHttpError(httpContext.Response, StatusCodes.Status415UnsupportedMediaType, StatusCode.Internal, error!);
59+
GrpcProtocolHelpers.SendHttpError(httpContext.Response, StatusCodes.Status415UnsupportedMediaType, StatusCode.Internal, error);
60+
return Task.CompletedTask;
61+
}
62+
if (httpContext.Request.Protocol != GrpcProtocolConstants.Http2Protocol)
63+
{
64+
var protocolError = $"Request protocol '{httpContext.Request.Protocol}' is not supported.";
65+
GrpcProtocolHelpers.SendHttpError(httpContext.Response, StatusCodes.Status426UpgradeRequired, StatusCode.Internal, protocolError);
66+
httpContext.Response.Headers[HeaderNames.Upgrade] = GrpcProtocolConstants.Http2Protocol;
5967
return Task.CompletedTask;
6068
}
6169

src/Grpc.AspNetCore.Server/Internal/GrpcProtocolConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ namespace Grpc.AspNetCore.Server.Internal
2525
internal static class GrpcProtocolConstants
2626
{
2727
internal const string GrpcContentType = "application/grpc";
28+
internal const string Http2Protocol = "HTTP/2";
2829

2930
internal const string TimeoutHeader = "grpc-timeout";
3031
internal const string MessageEncodingHeader = "grpc-encoding";

test/FunctionalTests/Client/CompressionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public async Task SendCompressedMessage_ServiceCompressionConfigured_ResponseGzi
4141

4242
string? requestMessageEncoding = null;
4343
string? responseMessageEncoding = null;
44-
using var httpClient = Fixture.CreateClient(new TestDelegateHandler(
44+
using var httpClient = Fixture.CreateClient(messageHandler: new TestDelegateHandler(
4545
r =>
4646
{
4747
requestMessageEncoding = r.Headers.GetValues(GrpcProtocolConstants.MessageEncodingHeader).Single();

test/FunctionalTests/Infrastructure/GrpcTestFixture.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System;
2020
using System.Net.Http;
2121
using Grpc.Net.Client;
22+
using Microsoft.AspNetCore.Server.Kestrel.Core;
2223
using Microsoft.Extensions.DependencyInjection;
2324
using Microsoft.Extensions.Logging;
2425

@@ -58,7 +59,7 @@ public GrpcTestFixture(Action<IServiceCollection>? initialConfigureServices = nu
5859

5960
public HttpClient Client { get; }
6061

61-
public HttpClient CreateClient(DelegatingHandler? messageHandler = null)
62+
public HttpClient CreateClient(HttpProtocols? httpProtocol = null, DelegatingHandler? messageHandler = null)
6263
{
6364
HttpClient client;
6465
if (messageHandler != null)
@@ -71,8 +72,18 @@ public HttpClient CreateClient(DelegatingHandler? messageHandler = null)
7172
client = new HttpClient();
7273
}
7374

74-
client.DefaultRequestVersion = new Version(2, 0);
75-
client.BaseAddress = new Uri(_server.Url!);
75+
switch (httpProtocol ?? HttpProtocols.Http2)
76+
{
77+
case HttpProtocols.Http1:
78+
client.BaseAddress = new Uri(_server.GetUrl(HttpProtocols.Http1));
79+
break;
80+
case HttpProtocols.Http2:
81+
client.DefaultRequestVersion = new Version(2, 0);
82+
client.BaseAddress = new Uri(_server.GetUrl(HttpProtocols.Http2));
83+
break;
84+
default:
85+
throw new ArgumentException("Unexpected value.", nameof(httpProtocol));
86+
}
7687

7788
return client;
7889
}

test/FunctionalTests/Infrastructure/InProcessTestServer.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@
1919
using System;
2020
using System.Collections.Generic;
2121
using System.IO;
22-
using System.Linq;
2322
using System.Text;
2423
using System.Threading.Tasks;
2524
using Microsoft.AspNetCore.Hosting;
26-
using Microsoft.AspNetCore.Hosting.Server.Features;
2725
using Microsoft.AspNetCore.Server.Kestrel.Core;
2826
using Microsoft.Extensions.DependencyInjection;
2927
using Microsoft.Extensions.Hosting;
@@ -35,7 +33,7 @@ public abstract class InProcessTestServer : IDisposable
3533
{
3634
internal abstract event Action<LogRecord> ServerLogged;
3735

38-
public abstract string? Url { get; }
36+
public abstract string GetUrl(HttpProtocols httpProtocol);
3937

4038
public abstract IWebHost? Host { get; }
4139

@@ -53,15 +51,23 @@ public class InProcessTestServer<TStartup> : InProcessTestServer
5351
private readonly Action<IServiceCollection>? _initialConfigureServices;
5452
private IWebHost? _host;
5553
private IHostApplicationLifetime? _lifetime;
56-
private string? _url;
54+
private Dictionary<HttpProtocols, string>? _urls;
5755

5856
internal override event Action<LogRecord> ServerLogged
5957
{
6058
add => _logSinkProvider.RecordLogged += value;
6159
remove => _logSinkProvider.RecordLogged -= value;
6260
}
6361

64-
public override string? Url => _url;
62+
public override string GetUrl(HttpProtocols httpProtocol)
63+
{
64+
if (_urls == null)
65+
{
66+
throw new InvalidOperationException();
67+
}
68+
69+
return _urls[httpProtocol];
70+
}
6571

6672
public override IWebHost? Host => _host;
6773

@@ -78,7 +84,7 @@ public InProcessTestServer(Action<IServiceCollection>? initialConfigureServices)
7884
public override void StartServer()
7985
{
8086
// We're using 127.0.0.1 instead of localhost to ensure that we use IPV4 across different OSes
81-
var url = "http://127.0.0.1:50050";
87+
var url = "http://127.0.0.1";
8288

8389
_host = new WebHostBuilder()
8490
.ConfigureLogging(builder => builder
@@ -95,6 +101,10 @@ public override void StartServer()
95101
{
96102
listenOptions.Protocols = HttpProtocols.Http2;
97103
});
104+
options.ListenLocalhost(50040, listenOptions =>
105+
{
106+
listenOptions.Protocols = HttpProtocols.Http1;
107+
});
98108
})
99109
.UseUrls(url)
100110
.UseContentRoot(Directory.GetCurrentDirectory())
@@ -119,7 +129,11 @@ public override void StartServer()
119129
_logger.LogInformation("Test Server started");
120130

121131
// Get the URL from the server
122-
_url = _host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.Single();
132+
_urls = new Dictionary<HttpProtocols, string>
133+
{
134+
[HttpProtocols.Http2] = url + ":50050",
135+
[HttpProtocols.Http1] = url + ":50040"
136+
};
123137

124138
_lifetime.ApplicationStopped.Register(() =>
125139
{

test/FunctionalTests/Server/UnaryMethodTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,37 @@ public async Task InvalidContentType_Return415Response(string contentType, strin
397397
response.AssertTrailerStatus(StatusCode.Internal, responseMessage);
398398
}
399399

400+
[Test]
401+
public async Task InvalidProtocol_Return426Response()
402+
{
403+
// Arrange
404+
var requestMessage = new HelloRequest
405+
{
406+
Name = "World"
407+
};
408+
409+
var ms = new MemoryStream();
410+
MessageHelpers.WriteMessage(ms, requestMessage);
411+
var streamContent = new StreamContent(ms);
412+
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc");
413+
414+
var client = Fixture.CreateClient(Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1);
415+
416+
// Act
417+
var response = await client.PostAsync(
418+
"Greet.Greeter/SayHello",
419+
streamContent).DefaultTimeout();
420+
421+
// Assert
422+
Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode);
423+
424+
var upgradeValue = response.Headers.Upgrade.Single();
425+
Assert.AreEqual("HTTP", upgradeValue.Name);
426+
Assert.AreEqual("2", upgradeValue.Version);
427+
428+
response.AssertTrailerStatus(StatusCode.Internal, "Request protocol 'HTTP/1.1' is not supported.");
429+
}
430+
400431
[TestCase("application/grpc")]
401432
[TestCase("APPLICATION/GRPC")]
402433
[TestCase("application/grpc+proto")]

test/Grpc.AspNetCore.Server.Tests/CallHandlerTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ private static ServerCallHandlerBase<TestService, TestMessage, TestMessage> Crea
136136
private static HttpContext CreateContext(bool isMaxRequestBodySizeFeatureReadOnly = false)
137137
{
138138
var httpContext = new DefaultHttpContext();
139-
httpContext.Request.ContentType = "application/grpc";
139+
httpContext.Request.Protocol = GrpcProtocolConstants.Http2Protocol;
140+
httpContext.Request.ContentType = GrpcProtocolConstants.GrpcContentType;
140141
httpContext.Features.Set<IHttpMinRequestBodyDataRateFeature>(new TestMinRequestBodyDataRateFeature());
141142
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(new TestMaxRequestBodySizeFeature(isMaxRequestBodySizeFeatureReadOnly, 100));
142143

0 commit comments

Comments
 (0)