Azure / azure-relay-dotnet

☁️ .NET Standard client library for Azure Relay Hybrid Connections

Home Page:https://docs.microsoft.com/en-us/azure/service-bus-relay/relay-what-is-it

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Acceptor needs access to client request headers (node.js parity)

marcschier opened this issue · comments

Hi,

In node.js hc sdk, acceptor has access to the connect headers, can query them, and update them to reply in upgrade response. We will need the same functionality in the .net SDK eventually. The reason is, while client/server can negotiate information through the websocket stream, in reconnection scenarios, it is easier to communicate a session id or equivalent through the headers, rather than renegotiate through the stream, and then just to resume where you left off. Also, it will be more secure to reject connections on header content (string), rather than parsing a binary or framed stream. That, plus the ability to select the protocol during handshake...

It might be enough to have an overload of AcceptConnectionAsync that takes a lambda predicate that allows an acceptor to query, filter, modify the connect headers before returning the connection stream.

Any plans to support this functionality in an upcoming release?

Thx.

I believe we had been planning on doing something like this when time permitted. I kind of like the idea of passing a lambda to AcceptConnectionAsync. However up to this point HybridConnectionListener abstracts away all details of the underlying protocol, even the fact that it's websocket-based. Dealing with HTTP headers, accepting the websocket in the lambda would be very websocket specific.

// Potential new API #1
var hybridConnectionStream = await hybridConnectionListener.AcceptConnectionAsync(
    (requestContext) =>
    {
        // check the request headers, pick the desired sub-protocol, and other stuff.​​​
        if (requestContext.Request.Headers[...)
        {
            // reject the request with a status code/description
            requestContext.Response.StatusCode = 401;
            requestContext.Response.StatusDescription = "Some reason here";
            requestContext.Response.Headers["YourCustomHeader"] = ...
            return false;
        }
        else
        {
             // return true to accept the websocket?
             return true;
        }
    });

We had also discussed having a RelayedHttpListener API modeled after HttpListener which was lower level than HybridConnectionListener (which would then be built on top of RelayedHttpListener). RelayedHttpListener would be the class users wanting this level of control would use:

// Potential new API #2
/// <summary>Provides a listener for accepting Relayed Http requests from remote clients.</summary>
public class RelayedHttpListener : IConnectionStatus, IDisposable
{
	/// <summary>
	/// Create a new <see cref="RelayedHttpListener"/>  instance for accepting Relayed Http requests from remote clients.
	/// </summary>
	public RelayedHttpListener(Uri address, TokenProvider tokenProvider);

	/// <summary>Starts the <see cref="RelayedHttpListener"/> and registers it as a listener in ServiceBus.</summary>
	public void Start();
	public Task StartAsync(CancellationToken cancellationToken);

	/// <summary>Stops the <see cref="RelayedHttpListener"/> and removes the listener instance from ServiceBus.</summary>
	public void Stop();
	public Task StopAsync(CancellationToken cancellationToken);

	/// <summary>Accepts a Relayed Http request from a remote client.</summary>
	public Task<RelayedHttpListenerContext> GetContextAsync();
}


RelayedHttpListenerContext requestContext = await relayedHttpListener.GetContextAsync();
// check the request headers, pick the desired sub-protocol, and other stuff.​​​
if (requestContext.Request.Headers[...)
{
    // reject the request with a status code/description
    requestContext.Response.StatusCode = 401;
    requestContext.Response.StatusDescription = "Some reason here";
    requestContext.Response.Headers["YourCustomHeader"] = ...
    requestContext.Response.Close();
}
else
{
    var hybridConnectionStream = await requestContext.AcceptWebSocketAsync(subprotocol);
}

The "Potential new API # 1" takes a lot of re-plumbing in order to get it to work. If ones listener is going to have 10 or however many pending calls to AcceptClientAsync "Potential API # 1" has to keep track of 10 (potentially different, though not likely) delegate instances (lambdas).

Having an API to register the acceptor method just once might be simpler.

// Potential new API #3
hybridConnectionListener.AcceptHandler =
    async (requestContext) =>
    {
        // check the request headers, pick the desired sub-protocol, and other stuff.​​​
        if (requestContext.Request.Headers[...)
        {
            // reject the request with a status code/description
            requestContext.Response.StatusCode = HttpStatusCode.Unauthorized;
            requestContext.Response.StatusDescription = "Some reason here";
            requestContext.Response.Headers["YourCustomHeader"] = ...
            return false;
        }
        else
        {
             // return true to accept the websocket
             return true;
        }
    };

while(true)
{
    var hybridConnectionStream = await hybridConnectionListener.AcceptConnectionAsync();
    ... Process the accepted client.
}

Thanks a lot! Yes, I like this last option. This would work very well for us.

Fixed with PR #37.

Thanks guys! Looking forward to the new nuget release.

@marcschier - I'm planning to publish this by the end of the week.