|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
// generate a certificate and hash to be shared with the client
var certificate = GenerateManualCertificate();
var hash = SHA256.HashData(certificate.RawData);
var certStr = Convert.ToBase64String(hash);
// configure the ports
builder.WebHost.ConfigureKestrel((context, options) =>
{
// website configured port
options.Listen(IPAddress.Any, 5001, listenOptions =>
{
listenOptions.UseHttps();
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
// webtransport configured port
options.Listen(IPAddress.Any, 5002, listenOptions =>
{
listenOptions.UseHttps(certificate);
listenOptions.UseConnectionLogging();
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
});
var app = builder.Build();
// make index.html accessible
app.UseFileServer();
app.Use(async (context, next) =>
{
// configure /certificate.js to inject the certificate hash
if (context.Request.Path.Value?.Equals("/certificate.js") ?? false)
{
context.Response.ContentType = "application/javascript";
await context.Response.WriteAsync($"var CERTIFICATE = '{certStr}';");
}
// configure the serverside application
else
{
var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();
if (!feature.IsWebTransportRequest)
{
await next(context);
}
var session = await feature.AcceptAsync(CancellationToken.None);
if (session is null)
{
return;
}
while (true)
{
ConnectionContext? stream = null;
IStreamDirectionFeature? direction = null;
// wait until we get a stream
stream = await session.AcceptStreamAsync(CancellationToken.None);
if (stream is not null)
{
direction = stream.Features.GetRequiredFeature<IStreamDirectionFeature>();
if (direction.CanRead && direction.CanWrite)
{
_ = handleBidirectionalStream(session, stream);
}
else
{
_ = handleUnidirectionalStream(session, stream);
}
}
}
}
});
await app.RunAsync();
static async Task handleUnidirectionalStream(IWebTransportSession session, ConnectionContext stream)
{
var inputPipe = stream.Transport.Input;
// read some data from the stream into the memory
var memory = new Memory<byte>(new byte[4096]);
while (!stream.ConnectionClosed.IsCancellationRequested)
{
var length = await inputPipe.AsStream().ReadAsync(memory);
var message = Encoding.Default.GetString(memory[..length].ToArray());
await ApplySpecialCommands(session, message);
Console.WriteLine("RECEIVED FROM CLIENT:");
Console.WriteLine(message);
}
}
static async Task handleBidirectionalStream(IWebTransportSession session, ConnectionContext stream)
{
var inputPipe = stream.Transport.Input;
var outputPipe = stream.Transport.Output;
// read some data from the stream into the memory
var memory = new Memory<byte>(new byte[4096]);
while (!stream.ConnectionClosed.IsCancellationRequested)
{
var length = await inputPipe.AsStream().ReadAsync(memory);
// slice to only keep the relevant parts of the memory
var outputMemory = memory[..length];
// handle special commands
await ApplySpecialCommands(session, Encoding.Default.GetString(outputMemory.ToArray()));
// do some operations on the contents of the data
outputMemory.Span.Reverse();
// write back the data to the stream
await outputPipe.WriteAsync(outputMemory);
memory.Span.Clear();
}
}
static async Task ApplySpecialCommands(IWebTransportSession session, string message)
{
switch (message)
{
case "Initiate Stream":
var stream = await session.OpenUnidirectionalStreamAsync();
if (stream is not null)
{
await stream.Transport.Output.WriteAsync(new("Created a new stream from the client and sent this message then closing the stream."u8.ToArray()));
}
break;
case "Abort":
session.Abort(256 /*No error*/);
break;
default:
break; // in all other cases the string is not a special command
}
}
// Adapted from: https://github.com/wegylexy/webtransport
// We will need to eventually merge this with existing Kestrel certificate generation
// tracked in issue #41762
static X509Certificate2 GenerateManualCertificate()
{
X509Certificate2 cert;
var store = new X509Store("KestrelSampleWebTransportCertificates", StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
if (store.Certificates.Count > 0)
{
cert = store.Certificates[^1];
// rotate key after it expires
if (DateTime.Parse(cert.GetExpirationDateString(), null) >= DateTimeOffset.UtcNow)
{
store.Close();
return cert;
}
}
// generate a new cert
var now = DateTimeOffset.UtcNow;
SubjectAlternativeNameBuilder sanBuilder = new();
sanBuilder.AddDnsName("localhost");
using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
CertificateRequest req = new("CN=localhost", ec, HashAlgorithmName.SHA256);
// Adds purpose
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection
{
new("1.3.6.1.5.5.7.3.1") // serverAuth
}, false));
// Adds usage
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
// Adds subject alternate names
req.CertificateExtensions.Add(sanBuilder.Build());
// Sign
using var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for this
cert = new(crt.Export(X509ContentType.Pfx));
// Save
store.Add(cert);
store.Close();
return cert;
}
|