feat: add Gemini and Spartan protocol support
This commit is contained in:
parent
a4a49f41d7
commit
fa7aa5557c
48 changed files with 4992 additions and 161 deletions
93
app/test/fake_protocol_server.dart
Normal file
93
app/test/fake_protocol_server.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue