fix: harden controlled Amap probe
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m46s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m46s
This commit is contained in:
@@ -32,6 +32,7 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
var security = await TestCredentialBoundaryAsync();
|
var security = await TestCredentialBoundaryAsync();
|
||||||
var supervisor = await TestSupervisorLifecycleAsync();
|
var supervisor = await TestSupervisorLifecycleAsync();
|
||||||
|
await TestAmapProbeSecurityAsync();
|
||||||
TestSecureConfigurationPersistence();
|
TestSecureConfigurationPersistence();
|
||||||
TestStructuredLogging();
|
TestStructuredLogging();
|
||||||
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
|
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
|
||||||
@@ -46,6 +47,20 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task TestAmapProbeSecurityAsync()
|
||||||
|
{
|
||||||
|
using var handler = AmapProbe.CreateHandler();
|
||||||
|
False(handler.AllowAutoRedirect, "Amap probe redirects disabled");
|
||||||
|
Equal(1, handler.MaxConnectionsPerServer, "Amap probe per-server connection cap");
|
||||||
|
True(AmapProbe.IsAllowedEndpoint(new Uri("https://restapi.amap.com/v3/geocode/regeo")), "Amap fixed HTTPS endpoint accepted");
|
||||||
|
False(AmapProbe.IsAllowedEndpoint(new Uri("http://restapi.amap.com/v3/geocode/regeo")), "Amap HTTP endpoint rejected");
|
||||||
|
False(AmapProbe.IsAllowedEndpoint(new Uri("https://example.invalid/v3/geocode/regeo")), "Amap alternate host rejected");
|
||||||
|
using var oversized = new ByteArrayContent(new byte[AmapProbe.MaximumResponseBytes + 1]);
|
||||||
|
await ThrowsAsync<InvalidDataException>(
|
||||||
|
() => AmapProbe.ReadBoundedJsonAsync(oversized),
|
||||||
|
"Amap oversized response must be rejected before parsing");
|
||||||
|
}
|
||||||
|
|
||||||
private static void TestStructuredLogging()
|
private static void TestStructuredLogging()
|
||||||
{
|
{
|
||||||
Equal(10 * 1024 * 1024L, StructuredJsonlLogger.SizeLimitBytes, "Supervisor log byte limit");
|
Equal(10 * 1024 * 1024L, StructuredJsonlLogger.SizeLimitBytes, "Supervisor log byte limit");
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Buffers;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -5,7 +6,24 @@ namespace Dada.Supervisor;
|
|||||||
|
|
||||||
internal static class AmapProbe
|
internal static class AmapProbe
|
||||||
{
|
{
|
||||||
private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(15) };
|
internal const int MaximumResponseBytes = 65_536;
|
||||||
|
private const string ProviderHostname = "restapi.amap.com";
|
||||||
|
private static readonly HttpClient Client = new(CreateHandler()) { Timeout = TimeSpan.FromSeconds(15) };
|
||||||
|
|
||||||
|
internal static SocketsHttpHandler CreateHandler() => new()
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = false,
|
||||||
|
AutomaticDecompression = DecompressionMethods.None,
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(10),
|
||||||
|
MaxConnectionsPerServer = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static bool IsAllowedEndpoint(Uri endpoint) =>
|
||||||
|
endpoint.Scheme == Uri.UriSchemeHttps
|
||||||
|
&& endpoint.Host.Equals(ProviderHostname, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (endpoint.IsDefaultPort || endpoint.Port == 443)
|
||||||
|
&& string.IsNullOrEmpty(endpoint.UserInfo)
|
||||||
|
&& endpoint.AbsolutePath is "/v3/geocode/regeo" or "/v3/geocode/geo";
|
||||||
|
|
||||||
internal static int Run(string? key)
|
internal static int Run(string? key)
|
||||||
{
|
{
|
||||||
@@ -15,41 +33,73 @@ internal static class AmapProbe
|
|||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var realCalls = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
realCalls += 1;
|
||||||
var reverse = Call("https://restapi.amap.com/v3/geocode/regeo?location=120.6994,27.9943&extensions=base", key).GetAwaiter().GetResult();
|
var reverse = Call("https://restapi.amap.com/v3/geocode/regeo?location=120.6994,27.9943&extensions=base", key).GetAwaiter().GetResult();
|
||||||
|
realCalls += 1;
|
||||||
var geocode = Call($"https://restapi.amap.com/v3/geocode/geo?address={Uri.EscapeDataString("北京市天安门")}", key).GetAwaiter().GetResult();
|
var geocode = Call($"https://restapi.amap.com/v3/geocode/geo?address={Uri.EscapeDataString("北京市天安门")}", key).GetAwaiter().GetResult();
|
||||||
var success = reverse.Status == "1" && geocode.Status == "1";
|
var success = reverse.Status == "1" && geocode.Status == "1";
|
||||||
Write(success ? "amap_probe_passed" : "amap_provider_rejected", success, 2, reverse.Status, geocode.Status);
|
Write(success ? "amap_probe_passed" : "amap_provider_rejected", success, realCalls, reverse.Status, geocode.Status);
|
||||||
return success ? 0 : 3;
|
return success ? 0 : 3;
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
Write("amap_probe_timeout", false, 0, null, null);
|
Write("amap_probe_timeout", false, realCalls, null, null);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
catch (HttpRequestException exception)
|
catch (HttpRequestException exception)
|
||||||
{
|
{
|
||||||
Write($"amap_probe_network_{exception.StatusCode?.ToString() ?? "error"}", false, 0, null, null);
|
Write($"amap_probe_network_{exception.StatusCode?.ToString() ?? "error"}", false, realCalls, null, null);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (Exception exception) when (exception is JsonException or InvalidDataException)
|
||||||
{
|
{
|
||||||
Write("amap_probe_invalid_response", false, 0, null, null);
|
Write("amap_probe_invalid_response", false, realCalls, null, null);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<ProbeResponse> Call(string endpoint, string key)
|
private static async Task<ProbeResponse> Call(string endpoint, string key)
|
||||||
{
|
{
|
||||||
using var response = await Client.GetAsync($"{endpoint}&key={Uri.EscapeDataString(key)}");
|
var requestUri = new Uri($"{endpoint}&key={Uri.EscapeDataString(key)}", UriKind.Absolute);
|
||||||
|
if (!IsAllowedEndpoint(requestUri)) throw new InvalidDataException("amap_endpoint_rejected");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||||
|
using var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
using var document = await ReadBoundedJsonAsync(response.Content);
|
||||||
using var document = await JsonDocument.ParseAsync(stream);
|
|
||||||
var root = document.RootElement;
|
var root = document.RootElement;
|
||||||
return new ProbeResponse(root.GetProperty("status").GetString() ?? "", root.TryGetProperty("infocode", out var code) ? code.GetString() : null);
|
return new ProbeResponse(root.GetProperty("status").GetString() ?? "", root.TryGetProperty("infocode", out var code) ? code.GetString() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static async Task<JsonDocument> ReadBoundedJsonAsync(HttpContent content)
|
||||||
|
{
|
||||||
|
if (content.Headers.ContentLength is > MaximumResponseBytes) throw new InvalidDataException("amap_response_too_large");
|
||||||
|
await using var stream = await content.ReadAsStreamAsync();
|
||||||
|
using var buffered = new MemoryStream();
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(4_096);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(buffer);
|
||||||
|
if (read == 0) break;
|
||||||
|
total += read;
|
||||||
|
if (total > MaximumResponseBytes) throw new InvalidDataException("amap_response_too_large");
|
||||||
|
await buffered.WriteAsync(buffer.AsMemory(0, read));
|
||||||
|
}
|
||||||
|
buffered.Position = 0;
|
||||||
|
return await JsonDocument.ParseAsync(buffered);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Array.Clear(buffer);
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void Write(string code, bool success, int realCalls, string? reverseStatus, string? geocodeStatus) =>
|
private static void Write(string code, bool success, int realCalls, string? reverseStatus, string? geocodeStatus) =>
|
||||||
Console.WriteLine(JsonSerializer.Serialize(new { code, success, real_calls = realCalls, reverse_status = reverseStatus, geocode_status = geocodeStatus }));
|
Console.WriteLine(JsonSerializer.Serialize(new { code, success, real_calls = realCalls, reverse_status = reverseStatus, geocode_status = geocodeStatus }));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user