Added authentication
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace EonaCat.DoxaApi.Middleware
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
/// <summary>
|
||||
/// Backs the DoxaApi "Console" feature: lets the UI send an arbitrary HTTP request
|
||||
/// (method, absolute URL, headers, body) through the server rather than the browser.
|
||||
/// This sidesteps CORS restrictions when documenting third-party or differently-hosted
|
||||
/// APIs, and keeps credentials out of client-side network logs when desired.
|
||||
///
|
||||
/// Only absolute http(s) URLs are allowed, and a small set of hop-by-hop / sensitive
|
||||
/// headers are stripped from both the outgoing request and the proxied response to
|
||||
/// avoid surprising behavior.
|
||||
/// </summary>
|
||||
internal static class ConsoleProxy
|
||||
{
|
||||
private static readonly HttpClient _client = new(new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
})
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _blockedRequestHeaders = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"host", "content-length", "connection", "transfer-encoding"
|
||||
};
|
||||
|
||||
public static async Task HandleAsync(HttpContext context, JsonSerializerOptions writeOptions)
|
||||
{
|
||||
ConsoleRequest? req;
|
||||
try
|
||||
{
|
||||
req = await JsonSerializer.DeserializeAsync<ConsoleRequest>(context.Request.Body, ReadOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
await context.Response.WriteAsync($"Invalid console request body: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (req is null || string.IsNullOrWhiteSpace(req.Url))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
await context.Response.WriteAsync("A 'url' field is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(req.Url, UriKind.Absolute, out var uri) ||
|
||||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
await context.Response.WriteAsync("Only absolute http:// or https:// URLs are supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
var method = string.IsNullOrWhiteSpace(req.Method) ? "GET" : req.Method.ToUpperInvariant();
|
||||
|
||||
using var requestMessage = new HttpRequestMessage(new HttpMethod(method), uri);
|
||||
|
||||
if (req.Headers is not null)
|
||||
{
|
||||
foreach (var (key, value) in req.Headers)
|
||||
{
|
||||
if (_blockedRequestHeaders.Contains(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
requestMessage.Headers.TryAddWithoutValidation(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(req.Body) && method is not ("GET" or "HEAD"))
|
||||
{
|
||||
var contentType = req.Headers?.FirstOrDefault(h =>
|
||||
string.Equals(h.Key, "content-type", StringComparison.OrdinalIgnoreCase)).Value
|
||||
?? "application/json";
|
||||
|
||||
requestMessage.Content = new StringContent(req.Body, Encoding.UTF8);
|
||||
requestMessage.Content.Headers.Remove("Content-Type");
|
||||
requestMessage.Content.Headers.TryAddWithoutValidation("Content-Type", contentType);
|
||||
}
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
using var responseMessage = await _client.SendAsync(requestMessage, context.RequestAborted);
|
||||
sw.Stop();
|
||||
|
||||
var responseBody = await responseMessage.Content.ReadAsStringAsync(context.RequestAborted);
|
||||
|
||||
var responseHeaders = new Dictionary<string, string>();
|
||||
foreach (var h in responseMessage.Headers)
|
||||
{
|
||||
responseHeaders[h.Key] = string.Join(", ", h.Value);
|
||||
}
|
||||
foreach (var h in responseMessage.Content.Headers)
|
||||
{
|
||||
responseHeaders[h.Key] = string.Join(", ", h.Value);
|
||||
}
|
||||
|
||||
var result = new ConsoleResponse
|
||||
{
|
||||
StatusCode = (int)responseMessage.StatusCode,
|
||||
ElapsedMs = sw.ElapsedMilliseconds,
|
||||
Headers = responseHeaders,
|
||||
Body = responseBody
|
||||
};
|
||||
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, result, writeOptions);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
context.Response.StatusCode = 504;
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, new ConsoleResponse
|
||||
{
|
||||
StatusCode = 0,
|
||||
ElapsedMs = sw.ElapsedMilliseconds,
|
||||
NetworkError = true,
|
||||
Body = "Request timed out after 30 seconds."
|
||||
}, writeOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Response.StatusCode = 502;
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, new ConsoleResponse
|
||||
{
|
||||
StatusCode = 0,
|
||||
ElapsedMs = sw.ElapsedMilliseconds,
|
||||
NetworkError = true,
|
||||
Body = ex.Message
|
||||
}, writeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions ReadOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user