import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; /// Test doubles speaking the two protocols, so handler- and controller-level /// tests can exercise the real fetch path against an in-process server. /// A Gemini server answering every request from [respond], so a test can say /// exactly what the client must cope with. [respond] gets the request line /// and writes the raw response, status line and body together. class FakeGeminiServer { FakeGeminiServer(this.certName, {this.clientCertName}); final String certName; /// The certificate this server asks the client for, the way a Gemini site /// that wants an identity does. Dart's TLS verifies a client certificate /// against the server's trust, so the test fixture is trusted by name. final String? clientCertName; late final int port; late final SecureServerSocket server; Future start() async { final context = SecurityContext() ..useCertificateChain('test/fixtures/$certName-cert.pem') ..usePrivateKey('test/fixtures/$certName-key.pem'); if (clientCertName != null) { context.setTrustedCertificates('test/fixtures/$clientCertName-cert.pem'); } server = await SecureServerSocket.bind('localhost', 0, context, requestClientCertificate: clientCertName != null); port = server.port; } StreamSubscription answer(FutureOr Function(String, SecureSocket) respond) { return server.listen((socket) async { final line = await socket .map(utf8.decode) .transform(const LineSplitter()) .first .timeout(const Duration(seconds: 5)); await respond(line, socket); socket.destroy(); }); } Future close() => server.close(); } /// A Spartan server answering every request from [respond], which gets the /// request line and any uploaded bytes, and writes the raw response. class FakeSpartanServer { late final int port; late final ServerSocket server; Future start() async { server = await ServerSocket.bind('localhost', 0); port = server.port; } StreamSubscription answer(FutureOr Function(String, List, Socket) respond) { return server.listen((socket) async { final buffer = BytesBuilder(copy: false); var lineEnd = -1; await for (final chunk in socket) { buffer.add(chunk); final bytes = buffer.toBytes(); if (lineEnd < 0) lineEnd = _indexOfCrlf(bytes); if (lineEnd < 0) continue; final line = utf8.decode(bytes.sublist(0, lineEnd)); final length = int.parse(line.split(' ').last); // Answer as soon as the line and any upload it announces are here; // the client keeps the connection open waiting for the response. if (bytes.length - lineEnd - 2 >= length) { final upload = bytes.sublist(lineEnd + 2, lineEnd + 2 + length); await respond(line, upload, socket); socket.destroy(); return; } } }); } Future close() => server.close(); } int _indexOfCrlf(List bytes) { for (var i = 0; i + 1 < bytes.length; i++) { if (bytes[i] == 0x0d && bytes[i + 1] == 0x0a) return i; } return -1; }