BTCMaps v2: enforce https on BtcMap import endpoint before bearer auth
Per CodeRabbit review on PR #226. A misconfigured BTCMAPS:BtcMapImportEndpoint over http:// would silently leak the scoped token to anyone on the network path between plugin-builder and btcmap. Parse the configured value as an absolute https URI before building the request, throwing InvalidOperationException with the offending value if the parse / scheme check fails. The exception fires before SendAsync, so the token never reaches a HttpRequestMessage header. Adds 3 tests: http-rejected, non-absolute-rejected, token-missing maps to BtcMapTokenMissingException (controller-ladder regression guard). 40/40 BtcMapsServiceTests passing.
This commit is contained in:
parent
3836fe6b38
commit
717f55d2a6
@ -285,6 +285,51 @@ public class BtcMapsServiceTests
|
||||
Assert.Contains(MakeService().Validate(req), e => e.Path == nameof(BtcMapsSubmitRequest.ExternalId));
|
||||
}
|
||||
|
||||
private static BtcMapsService MakeServiceWithConfig(IDictionary<string, string?> config) =>
|
||||
new BtcMapsService(
|
||||
configuration: new ConfigurationBuilder().AddInMemoryCollection(config).Build(),
|
||||
httpClientFactory: new StubHttpClientFactory(),
|
||||
logger: NullLogger<BtcMapsService>.Instance);
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task SubmitToBtcMapAsync_RejectsHttpEndpoint()
|
||||
{
|
||||
// Bearer token must never cross the wire over plaintext http://. Misconfigured
|
||||
// endpoint surfaces as InvalidOperationException before HttpClient.SendAsync,
|
||||
// so the token is never built into a request header.
|
||||
var service = MakeServiceWithConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["BTCMAPS:BtcMapImportToken"] = "test-token",
|
||||
["BTCMAPS:BtcMapImportEndpoint"] = "http://api.btcmap.org/rpc"
|
||||
});
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => service.SubmitToBtcMapAsync(MakeValidBtcMap()));
|
||||
Assert.Contains("https", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task SubmitToBtcMapAsync_RejectsNonAbsoluteEndpoint()
|
||||
{
|
||||
var service = MakeServiceWithConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["BTCMAPS:BtcMapImportToken"] = "test-token",
|
||||
["BTCMAPS:BtcMapImportEndpoint"] = "/rpc"
|
||||
});
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => service.SubmitToBtcMapAsync(MakeValidBtcMap()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task SubmitToBtcMapAsync_ThrowsTokenMissingWhenUnset()
|
||||
{
|
||||
// Token unset is the ops-deployment-pending shape; controller maps this to
|
||||
// 503 btcmap-not-configured. Test the underlying exception type so the
|
||||
// controller exception ladder stays wired.
|
||||
var service = MakeServiceWithConfig(new Dictionary<string, string?>());
|
||||
await Assert.ThrowsAsync<BtcMapsService.BtcMapTokenMissingException>(
|
||||
() => service.SubmitToBtcMapAsync(MakeValidBtcMap()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeUrl_LowercasesSchemeAndHostOnly()
|
||||
{
|
||||
|
||||
@ -177,7 +177,19 @@ public sealed class BtcMapsService
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw new BtcMapTokenMissingException();
|
||||
|
||||
var endpoint = _configuration["BTCMAPS:BtcMapImportEndpoint"] ?? DefaultBtcMapImportEndpoint;
|
||||
// Bearer tokens MUST NOT cross the wire over http://; an operator-
|
||||
// misconfigured endpoint would silently leak the scoped token to
|
||||
// anyone on the network path. Parse the configured value into an
|
||||
// absolute https URI before we even create the request, so a bad
|
||||
// BTCMAPS:BtcMapImportEndpoint fails loudly with the offending
|
||||
// value in the message instead of producing a quiet credential leak.
|
||||
var endpoint = (_configuration["BTCMAPS:BtcMapImportEndpoint"] ?? DefaultBtcMapImportEndpoint).Trim();
|
||||
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var endpointUri) ||
|
||||
endpointUri.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"BTCMAPS:BtcMapImportEndpoint must be an absolute https:// URL (got '{endpoint}').");
|
||||
}
|
||||
|
||||
var client = _httpClientFactory.CreateClient(HttpClientNames.BtcMap);
|
||||
|
||||
@ -222,7 +234,7 @@ public sealed class BtcMapsService
|
||||
["id"] = 1
|
||||
};
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, endpointUri);
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
req.Content = new StringContent(JsonSerializer.Serialize(envelope), Encoding.UTF8, "application/json");
|
||||
|
||||
@ -230,7 +242,7 @@ public sealed class BtcMapsService
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException($"BtcMap RPC {(int)response.StatusCode} {endpoint}: {body}");
|
||||
throw new HttpRequestException($"BtcMap RPC {(int)response.StatusCode} {endpointUri}: {body}");
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user