158 lines
6.2 KiB
C#
158 lines
6.2 KiB
C#
using System.Text;
|
|
using Microsoft.AspNetCore.Http;
|
|
using EonaCat.Json;
|
|
|
|
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"
|
|
};
|
|
|
|
private static readonly JsonSerializerSettings _settings = new()
|
|
{
|
|
NullValueHandling = NullValueHandling.Ignore,
|
|
Formatting = Formatting.Indented
|
|
};
|
|
|
|
public static async Task HandleAsync(HttpContext context, JsonSerializerSettings writeOptions)
|
|
{
|
|
ConsoleRequest? req;
|
|
try
|
|
{
|
|
using var reader = new StreamReader(context.Request.Body);
|
|
var json = await reader.ReadToEndAsync();
|
|
req = JsonHelper.ToObject<ConsoleRequest>(json);
|
|
}
|
|
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";
|
|
var json = JsonHelper.ToJson(result, writeOptions);
|
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
context.Response.StatusCode = 504;
|
|
var errorResponse = new ConsoleResponse
|
|
{
|
|
StatusCode = 0,
|
|
ElapsedMs = sw.ElapsedMilliseconds,
|
|
NetworkError = true,
|
|
Body = "Request timed out after 30 seconds."
|
|
};
|
|
var json = JsonHelper.ToJson(errorResponse, writeOptions);
|
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
context.Response.StatusCode = 502;
|
|
var errorResponse = new ConsoleResponse
|
|
{
|
|
StatusCode = 0,
|
|
ElapsedMs = sw.ElapsedMilliseconds,
|
|
NetworkError = true,
|
|
Body = ex.Message
|
|
};
|
|
var json = JsonHelper.ToJson(errorResponse, writeOptions);
|
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
|
}
|
|
}
|
|
}
|
|
} |