///
/// 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();
}
}
}