|
| 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 | +} |
0 commit comments