Last active
November 23, 2021 19:48
-
-
Save isaacrlevin/7400d4078a479f6d5d6bba17d5ed6067 to your computer and use it in GitHub Desktop.
Sample showing issue with Listener
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=", ""); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Something like this (untested).