arblant
Railynx / test / live / mock_vless_server.dart
mock_vless_server.dart99 lines · 2.8 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
// Local TLS + WebSocket mock, the Dart twin of tests/mock_server.py.
// It answers the probe's upgrade with 101, reads the masked VLESS frame, then
// answers according to [MockMode]. Test-only: bound to 127.0.0.1, with the
// self-signed certificate in test/fixtures/.

import 'dart:async';
import 'dart:io';

enum MockMode {
  /// 101, then "HTTP/1.1 204 No Content": a clean IP.
  success204,

  /// 101, then "HTTP/1.1 200 OK": reachable but not clean.
  wrong200,

  /// "HTTP/1.1 400": the upgrade is refused.
  upgradeRefused,

  /// Closes right after the TLS handshake, before any reply.
  closeBeforeUpgrade,
}

class MockVlessServer {
  MockVlessServer(this.mode);

  final MockMode mode;
  SecureServerSocket? _server;
  StreamSubscription<SecureSocket>? _accepting;

  /// Number of probes that were answered with a 204.
  int accepted = 0;

  int get port => _server!.port;

  static SecurityContext _context() {
    return SecurityContext()
      ..useCertificateChain('test/fixtures/mock_cert.pem')
      ..usePrivateKey('test/fixtures/mock_key.pem');
  }

  Future<void> start() async {
    _server = await SecureServerSocket.bind(InternetAddress.loopbackIPv4, 0, _context());
    _accepting = _server!.listen(_handle);
  }

  Future<void> close() async {
    await _accepting?.cancel();
    await _server?.close();
  }

  void _handle(SecureSocket client) {
    final pending = BytesBuilder(copy: false);
    var upgraded = false;
    StreamSubscription<List<int>>? sub;
    sub = client.listen(
      (List<int> data) async {
        if (!upgraded) {
          pending.add(data);
          final text = String.fromCharCodes(pending.toBytes());
          if (!text.contains('\r\n\r\n')) {
            return;
          }
          upgraded = true;
          if (mode == MockMode.closeBeforeUpgrade) {
            client.destroy();
            return;
          }
          if (mode == MockMode.upgradeRefused) {
            client.add('HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n'.codeUnits);
            await client.flush();
            client.destroy();
            return;
          }
          client.add(
            'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n'
                'Connection: Upgrade\r\n\r\n'
                .codeUnits,
          );
          await client.flush();
          return;
        }
        // The masked VLESS frame arrived: answer, then close.
        final success = mode == MockMode.success204;
        final reply = success
            ? 'HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n'
            : 'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n';
        client.add(reply.codeUnits);
        await client.flush();
        if (success) {
          accepted++;
        }
        await sub?.cancel();
        client.destroy();
      },
      onError: (Object _) {},
      cancelOnError: true,
    );
  }
}