/// /// The HTTP Loopback Listener used to receive commands via HTTP. /// internal class LoopbackHttpListener : IDisposable { /// /// The . /// private readonly TaskCompletionSource _completionSource = new TaskCompletionSource(); /// /// The . /// private readonly IWebHost _host; /// /// Gets the default to wait before timing out. /// public static TimeSpan DefaultTimeOut => TimeSpan.FromMinutes(5); /// /// Gets the URL which the current is listening on. /// public string Url { get; } /// /// Initializes a new instance of the class. /// /// /// The hostname or IP address to use. /// /// /// The port to use. /// /// /// The URL path after the address and port (http://127.0.0.1:42069/{PATH}). /// 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(); } /// /// Waits until a callback has been received, then returns the result as an asynchronous operation. /// /// /// The to wait before timing out. /// /// /// The representing the asynchronous operation. /// The contains the result. /// public Task WaitForCallbackAsync(TimeSpan? timeout = null) { if (timeout == null) { timeout = DefaultTimeOut; } Task.Run(async () => { await Task.Delay(timeout.Value); _completionSource.TrySetCanceled(); }); return _completionSource.Task; } /// /// Configures the current . /// /// /// The . /// private void Configure(IApplicationBuilder app) { app.Run(async ctx => { var syncIoFeature = ctx.Features.Get(); 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; } }); } /// /// Disposes the current instance. /// public void Dispose() { Task.Run(async () => { await Task.Delay(500); _host.Dispose(); }); } /// /// Sets the result to be returned by the method. /// /// /// The value to set. /// /// /// The . /// 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("

You can now return to the application.

", Encoding.UTF8); await ctx.Response.Body.FlushAsync(); _completionSource.TrySetResult(value); } catch { ctx.Response.StatusCode = 400; ctx.Response.ContentType = "text/html"; await ctx.Response.WriteAsync("

Invalid request.

", Encoding.UTF8); await ctx.Response.Body.FlushAsync(); } } }