Skip to content

Commit edda76b

Browse files
committed
Implement native http.fetchOhttpKeys in dart
1 parent f93247e commit edda76b

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

payjoin-ffi/dart/lib/http.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
library http;
2+
3+
import 'dart:async';
4+
import 'dart:io';
5+
import 'dart:typed_data';
6+
7+
import 'payjoin.dart' show OhttpKeys;
8+
9+
/// Fetches the OHTTP keys from a payjoin directory through an OHTTP relay
10+
/// proxy so the directory never observes the client IP address.
11+
///
12+
/// [ohttpRelayUrl] is the HTTP CONNECT proxy that tunnels the request.
13+
/// [directoryUrl] is the payjoin directory whose `/.well-known/ohttp-gateway`
14+
/// endpoint is queried. [certificate] is the DER-encoded
15+
/// certificate the directory is expected to present, intended for
16+
/// local test setups that use a self-signed directory certificate; leave
17+
/// unset in production so normal system trust-root validation applies.
18+
Future<OhttpKeys> fetchOhttpKeys({
19+
required String ohttpRelayUrl,
20+
required String directoryUrl,
21+
Uint8List? certificate,
22+
}) async {
23+
final relayUri = Uri.parse(ohttpRelayUrl);
24+
final keysUrl = Uri.parse(directoryUrl).resolve('/.well-known/ohttp-gateway');
25+
26+
final client = HttpClient();
27+
client.findProxy = (_) => 'PROXY ${relayUri.host}:${relayUri.port}';
28+
if (certificate != null && certificate.isNotEmpty) {
29+
client.badCertificateCallback = (cert, _, _) =>
30+
_bytesEqual(cert.der, certificate);
31+
}
32+
33+
try {
34+
final request = await client.getUrl(keysUrl);
35+
request.headers.set(HttpHeaders.acceptHeader, 'application/ohttp-keys');
36+
final response = await request.close();
37+
final bodyBytes = await _collectBytes(response);
38+
if (response.statusCode < 200 || response.statusCode >= 300) {
39+
throw HttpException(
40+
'fetchOhttpKeys failed: HTTP ${response.statusCode}',
41+
uri: keysUrl,
42+
);
43+
}
44+
return OhttpKeys.decode(bytes: bodyBytes);
45+
} finally {
46+
client.close(force: true);
47+
}
48+
}
49+
50+
bool _bytesEqual(Uint8List a, Uint8List b) {
51+
if (a.length != b.length) return false;
52+
for (var i = 0; i < a.length; i++) {
53+
if (a[i] != b[i]) return false;
54+
}
55+
return true;
56+
}
57+
58+
Future<Uint8List> _collectBytes(Stream<List<int>> stream) async {
59+
final builder = BytesBuilder(copy: false);
60+
await for (final chunk in stream) {
61+
builder.add(chunk);
62+
}
63+
return builder.toBytes();
64+
}

payjoin-ffi/dart/test/test_payjoin_integration_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "package:http/http.dart" as http;
55
import 'package:test/test.dart';
66
import "package:convert/convert.dart";
77

8+
import "package:payjoin/http.dart" as payjoin_http;
89
import "package:payjoin/payjoin.dart" as payjoin;
910
import "package:payjoin/test_utils.dart" as test_utils;
1011

@@ -400,6 +401,31 @@ Future<payjoin.ReceiveSession?> process_receiver_proposal(
400401
}
401402

402403
void main() {
404+
group('fetchOhttpKeys', () {
405+
test('fetches and decodes keys via relay proxy', () async {
406+
final services = test_utils.TestServices.initialize();
407+
services.waitForServicesReady();
408+
final keys = await payjoin_http.fetchOhttpKeys(
409+
ohttpRelayUrl: services.ohttpRelayUrl(),
410+
directoryUrl: services.directoryUrl(),
411+
certificate: services.cert(),
412+
);
413+
expect(keys, isA<payjoin.OhttpKeys>());
414+
}, timeout: const Timeout(Duration(minutes: 2)));
415+
416+
test('without trusted certificate throws', () async {
417+
final services = test_utils.TestServices.initialize();
418+
services.waitForServicesReady();
419+
await expectLater(
420+
payjoin_http.fetchOhttpKeys(
421+
ohttpRelayUrl: services.ohttpRelayUrl(),
422+
directoryUrl: services.directoryUrl(),
423+
),
424+
throwsA(isA<Exception>()),
425+
);
426+
}, timeout: const Timeout(Duration(minutes: 2)));
427+
});
428+
403429
group('Test integration', () {
404430
test('FFI validation', () async {
405431
final tooLargeAmount = 21000000 * 100000000 + 1;

0 commit comments

Comments
 (0)