Skip to content

Instantly share code, notes, and snippets.

@kfrancis
Created November 23, 2021 18:00
Show Gist options
  • Save kfrancis/c585a0651d8b4f13ecef397f424c96be to your computer and use it in GitHub Desktop.
Save kfrancis/c585a0651d8b4f13ecef397f424c96be to your computer and use it in GitHub Desktop.
LoopbackHttpListener from Identity with some changes that make it work up to net6.0
/// <summary>
/// The HTTP Loopback Listener used to receive commands via HTTP.
/// </summary>
internal class LoopbackHttpListener : IDisposable
{
/// <summary>
/// The <see cref="TaskCompletionSource{TResult}"/>.
/// </summary>
private readonly TaskCompletionSource<string> _completionSource = new TaskCompletionSource<string>();
/// <summary>
/// The <see cref="IWebHost"/>.
/// </summary>
private readonly IWebHost _host;
/// <summary>
/// Gets the default <see cref="TimeSpan"/> to wait before timing out.
/// </summary>
public static TimeSpan DefaultTimeOut => TimeSpan.FromMinutes(5);
/// <summary>
/// Gets the URL which the current <see cref="LoopbackHttpListener"/> is listening on.
/// </summary>
public string Url { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LoopbackHttpListener"/> class.
/// </summary>
/// <param name="host">
/// The hostname or IP address to use.
/// </param>
/// <param name="port">
/// The port to use.
/// </param>
/// <param name="path">
/// The URL path after the address and port (http://127.0.0.1:42069/{PATH}).
/// </param>
public LoopbackHttpListener(string host, int port, string path = null)
{
if (string.IsNullOrEmpty(host)) throw new ArgumentNullException(nameof(host));
// Assign the path to an empty string if nothing was provided
path ??= string.Empty;
// Trim any excess slashes from the path
if (path.StartsWith("/")) path = path.Substring(1);
// Build the URL
Url = $"http://{host}:{port}/{path}";
// Build and start the web host
_host = new WebHostBuilder()
.UseKestrel()
.UseUrls(Url)
.Configure(Configure)
.Build();
_host.Start();
}
/// <summary>
/// Waits until a callback has been received, then returns the result as an asynchronous operation.
/// </summary>
/// <param name="timeout">
/// The <see cref="TimeSpan"/> to wait before timing out.
/// </param>
/// <returns>
/// The <see cref="Task{T}"/> representing the asynchronous operation.
/// The <see cref="Task{TResult}.Result"/> contains the result.
/// </returns>
public Task<string> WaitForCallbackAsync(TimeSpan? timeout = null)
{
if (timeout == null)
{
timeout = DefaultTimeOut;
}
Task.Run(async () =>
{
await Task.Delay(timeout.Value);
_completionSource.TrySetCanceled();
});
return _completionSource.Task;
}
/// <summary>
/// Configures the current <see cref="LoopbackHttpListener"/>.
/// </summary>
/// <param name="app">
/// The <see cref="IApplicationBuilder"/>.
/// </param>
private void Configure(IApplicationBuilder app)
{
app.Run(async ctx =>
{
var syncIoFeature = ctx.Features.Get<IHttpBodyControlFeature>();
if (syncIoFeature != null)
{
syncIoFeature.AllowSynchronousIO = true;
}
switch (ctx.Request.Method)
{
case "GET":
await SetResult(ctx.Request.QueryString.Value, ctx);
break;
case "POST" when !ctx.Request.ContentType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase):
ctx.Response.StatusCode = 415;
break;
case "POST":
{
using var sr = new StreamReader(ctx.Request.Body, Encoding.UTF8);
var body = await sr.ReadToEndAsync();
await SetResult(body, ctx);
break;
}
default:
ctx.Response.StatusCode = 405;
break;
}
});
}
/// <summary>
/// Disposes the current <see cref="LoopbackHttpListener"/> instance.
/// </summary>
public void Dispose()
{
Task.Run(async () =>
{
await Task.Delay(500);
_host.Dispose();
});
}
/// <summary>
/// Sets the result to be returned by the <see cref="WaitForCallbackAsync"/> method.
/// </summary>
/// <param name="value">
/// The value to set.
/// </param>
/// <param name="ctx">
/// The <see cref="HttpContext"/>.
/// </param>
private async Task SetResult(string value, HttpContext ctx)
{
// Todo: Custom HTML page? Maybe make a request to the main site for a page to render? Or redirect if possible?
try
{
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "text/html";
await ctx.Response.WriteAsync("<h1>You can now return to the application.</h1>", Encoding.UTF8);
await ctx.Response.Body.FlushAsync();
_completionSource.TrySetResult(value);
}
catch
{
ctx.Response.StatusCode = 400;
ctx.Response.ContentType = "text/html";
await ctx.Response.WriteAsync("<h1>Invalid request.</h1>", Encoding.UTF8);
await ctx.Response.Body.FlushAsync();
}
}
}
@kfrancis
Copy link
Author

Usage looks like this:

using var listener = new LoopbackHttpListener("127.0.0.1", Port);
OpenBrowser(options.StartUrl); // kicks off oAuth

try
{
    var result = await listener.WaitForCallbackAsync();
    if (string.IsNullOrWhiteSpace(result))
    {
        return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = "Empty response." };
    }

    return new BrowserResult { Response = result, ResultType = BrowserResultType.Success };
}
catch (TaskCanceledException ex)
{
    return new BrowserResult { ResultType = BrowserResultType.Timeout, Error = ex.Message };
}
catch (Exception ex)
{
    return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = ex.Message };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment