Skip to content

Instantly share code, notes, and snippets.

@isaacrlevin
Last active November 23, 2021 19:48
Show Gist options
  • Save isaacrlevin/7400d4078a479f6d5d6bba17d5ed6067 to your computer and use it in GitHub Desktop.
Save isaacrlevin/7400d4078a479f6d5d6bba17d5ed6067 to your computer and use it in GitHub Desktop.
Sample showing issue with Listener
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Sample
{
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 = $"https://{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();
}
}
}
}
var browser = new SystemBrowser(7777);
string redirectUri = string.Format($"https://127.0.0.1:{browser.Port}");
var authResponse = browser.GetAuthCode($"https://github.com/login/oauth/authorize?client_id=Iv1.754b0923165bdbc1&redirect_uri={HttpUtility.UrlEncode(redirectUri)}", new System.Threading.CancellationToken()).Result;
var code = authResponse.Response.Replace("?code=", "");
using IdentityModel.OidcClient.Browser;
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Sample
{
public class SystemBrowser
{
public int Port { get; }
private readonly string _path;
public SystemBrowser(int? port = null, string path = null)
{
_path = path;
if (!port.HasValue)
{
Port = GetRandomUnusedPort();
}
else
{
Port = port.Value;
}
}
private int GetRandomUnusedPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
public async Task<BrowserResult> GetAuthCode(string url, CancellationToken cancellationToken)
{
using (var listener = new LoopbackHttpListener("127.0.0.1", Port, _path))
{
OpenBrowser(url);
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 };
}
}
}
public static void OpenBrowser(string url)
{
try
{
Process.Start(url);
}
catch
{
// hack because of this: https://github.com/dotnet/corefx/issues/10361
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = url.Replace("&", "^&");
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", url);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url);
}
else
{
throw;
}
}
}
}
}
@davidfowl
Copy link

Something like this (untested).

using System.Diagnostics;
using System.Text;
using System.Web;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

app.Run(async ctx =>
{
    Task WriteResponse(HttpContext ctx)
    {
        ctx.Response.StatusCode = 200;
        ctx.Response.ContentType = "text/html";
        return ctx.Response.WriteAsync("<h1>You can now return to the application.</h1>", Encoding.UTF8);
    }

    switch (ctx.Request.Method)
    {
        case "GET":
            await WriteResponse(ctx);

            tcs.TrySetResult(ctx.Request.QueryString.Value);
            break;

        case "POST" when !ctx.Request.HasFormContentType:
            ctx.Response.StatusCode = 415;
            break;

        case "POST":
            {
                using var sr = new StreamReader(ctx.Request.Body, Encoding.UTF8);
                var body = await sr.ReadToEndAsync();

                await WriteResponse(ctx);

                tcs.TrySetResult(body);
                break;
            }

        default:
            ctx.Response.StatusCode = 405;
            break;
    }
});

var browserPort = 7777;

app.Urls.Add($"http://127.0.0.1:{browserPort}");

app.Start();

var timeout = TimeSpan.FromMinutes(5);

string redirectUri = string.Format($"https://127.0.0.1:{browserPort}");

OpenBrowser($"https://github.com/login/oauth/authorize?client_id=Iv1.754b0923165bdbc1&redirect_uri={HttpUtility.UrlEncode(redirectUri)}");

var code = await tcs.Task.WaitAsync(timeout);

await app.DisposeAsync();


static void OpenBrowser(string url)
{
    try
    {
        Process.Start(url);
    }
    catch
    {
        // hack because of this: https://github.com/dotnet/corefx/issues/10361
        if (OperatingSystem.IsWindows())
        {
            url = url.Replace("&", "^&");
            Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
        }
        else if (OperatingSystem.IsLinux())
        {
            Process.Start("xdg-open", url);
        }
        else if (OperatingSystem.IsMacOS())
        {
            Process.Start("open", url);
        }
        else
        {
            throw;
        }
    }
}

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