fetchOhttpKeys function
Fetches the OHTTP keys from a payjoin directory through an OHTTP relay proxy so the directory never observes the client IP address.
ohttpRelayUrl is the HTTP(S) CONNECT proxy that tunnels the request.
directoryUrl is the payjoin directory whose /.well-known/ohttp-gateway
endpoint is queried. certificate is the DER-encoded certificate the
directory is expected to present, intended for local test setups that use a
self-signed directory certificate; leave unset in production so normal
system trust-root validation applies. relayCertificate serves the same
purpose for an HTTPS relay.
Implementation
Future<OhttpKeys> fetchOhttpKeys({
required String ohttpRelayUrl,
required String directoryUrl,
Uint8List? certificate,
Uint8List? relayCertificate,
Duration timeout = const Duration(seconds: 10),
}) async {
final relayUri = Uri.parse(ohttpRelayUrl);
_validateUrl(relayUri, 'ohttpRelayUrl', ohttpRelayUrl);
final keysUrl = Uri.parse(directoryUrl).resolve('/.well-known/ohttp-gateway');
_validateUrl(keysUrl, 'directoryUrl', directoryUrl);
final client = HttpClient()..connectionTimeout = timeout;
client.findProxy = (_) => 'PROXY ${_proxyHost(relayUri)}:${_port(relayUri)}';
final directoryCertificateCallback = _httpCertificateCallback(certificate);
if (directoryCertificateCallback != null) {
client.badCertificateCallback = directoryCertificateCallback;
}
if (relayUri.scheme == 'https') {
// Dart's proxy grammar only accepts DIRECT and PROXY. Advertising an HTTPS
// relay as PROXY still lets HttpClient do CONNECT, while connectionFactory
// opens the underlying TLS connection to the relay.
client.connectionFactory = (_, proxyHost, proxyPort) {
if (proxyHost == null || proxyPort == null) {
throw StateError('fetchOhttpKeys expected a proxy connection');
}
return SecureSocket.startConnect(
proxyHost,
proxyPort,
onBadCertificate: _secureSocketCertificateCallback(relayCertificate),
);
};
}
try {
final request = await client
.getUrl(keysUrl)
.timeout(
timeout,
onTimeout: () => throw HttpException(
'fetchOhttpKeys connection timed out',
uri: keysUrl,
),
);
request.headers.set(HttpHeaders.acceptHeader, 'application/ohttp-keys');
final response = await request.close().timeout(
timeout,
onTimeout: () =>
throw HttpException('fetchOhttpKeys request timed out', uri: keysUrl),
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw HttpException(
'fetchOhttpKeys failed: HTTP ${response.statusCode}',
uri: keysUrl,
);
}
final bodyBytes = await _collectBytes(response).timeout(
timeout,
onTimeout: () => throw HttpException(
'fetchOhttpKeys response timed out',
uri: keysUrl,
),
);
return OhttpKeys.decode(bytes: bodyBytes);
} finally {
client.close(force: true);
}
}