using System.Buffers; using System.Net; using System.Text.Json; namespace Dada.Supervisor; internal static class AmapProbe { 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) { if (string.IsNullOrWhiteSpace(key)) { Write("amap_credentials_missing", false, 0, null, null); return 3; } var realCalls = 0; try { realCalls += 1; 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 success = reverse.Status == "1" && geocode.Status == "1"; Write(success ? "amap_probe_passed" : "amap_provider_rejected", success, realCalls, reverse.Status, geocode.Status); return success ? 0 : 3; } catch (TaskCanceledException) { Write("amap_probe_timeout", false, realCalls, null, null); return 3; } catch (HttpRequestException exception) { Write($"amap_probe_network_{exception.StatusCode?.ToString() ?? "error"}", false, realCalls, null, null); return 3; } catch (Exception exception) when (exception is JsonException or InvalidDataException) { Write("amap_probe_invalid_response", false, realCalls, null, null); return 3; } } private static async Task Call(string endpoint, string 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(); using var document = await ReadBoundedJsonAsync(response.Content); var root = document.RootElement; return new ProbeResponse(root.GetProperty("status").GetString() ?? "", root.TryGetProperty("infocode", out var code) ? code.GetString() : null); } internal static async Task 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.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.Shared.Return(buffer); } } 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 })); private sealed record ProbeResponse(string Status, string? InfoCode); }