93 lines
3.2 KiB
Dart
93 lines
3.2 KiB
Dart
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<void> 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<SecureSocket> answer(FutureOr<void> 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<void> 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<void> start() async {
|
|
server = await ServerSocket.bind('localhost', 0);
|
|
port = server.port;
|
|
}
|
|
|
|
StreamSubscription<Socket> answer(FutureOr<void> Function(String, List<int>, 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<void> close() => server.close();
|
|
}
|
|
|
|
int _indexOfCrlf(List<int> bytes) {
|
|
for (var i = 0; i + 1 < bytes.length; i++) {
|
|
if (bytes[i] == 0x0d && bytes[i + 1] == 0x0a) return i;
|
|
}
|
|
return -1;
|
|
}
|