arblant
firefox-vpn-client / app / lib / core / tun_service.dart
tun_service.dart83 lines · 2.7 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
import 'dart:convert';
import 'dart:io';

/// What installing or removing the TUN service came to.
sealed class TunServiceResult {
  const TunServiceResult();
}

class TunServiceDone extends TunServiceResult {
  const TunServiceDone();
}

/// The user declined the administrator prompt.
class TunServiceCancelled extends TunServiceResult {
  const TunServiceCancelled();
}

class TunServiceFailed extends TunServiceResult {
  const TunServiceFailed(this.error);

  /// Why, in the program's English.
  final String error;
}

/// The TUN mode's service (core/cmd/firefox-vpn-tun), beside the core: its
/// install command asks for administrator rights itself (one prompt:
/// Windows', polkit's on Linux, the administrator password on macOS),
/// installs the service, and starts it; its uninstall command removes it,
/// through the running service without a prompt. The core then uses the
/// service; the app only installs it.
class TunService {
  TunService(this.executable);

  /// From the core's executable: the service is in the same folder.
  factory TunService.besideCore(String coreExecutable) => TunService(
    '${File(coreExecutable).parent.path}${Platform.pathSeparator}'
    'firefox-vpn-tun${Platform.isWindows ? '.exe' : ''}',
  );

  final String executable;

  /// [prompt] is the reason macOS' administrator prompt shows, in the
  /// user's language; Windows and polkit name the program themselves.
  Future<TunServiceResult> install({String? prompt}) => _run('install', prompt);

  Future<TunServiceResult> uninstall({String? prompt}) =>
      _run('uninstall', prompt);

  /// Exit status 0 done, 3 the prompt declined, anything else failed with
  /// the reason on the standard error (core/cmd/firefox-vpn-tun).
  Future<TunServiceResult> _run(String command, String? prompt) async {
    if (!File(executable).existsSync()) {
      return TunServiceFailed('$executable is missing');
    }
    try {
      final r = await Process.run(
        executable,
        [
          command,
          if (Platform.isMacOS && prompt != null) ...['-prompt', prompt],
        ],
        stderrEncoding: utf8,
        stdoutEncoding: utf8,
      );
      return switch (r.exitCode) {
        0 => const TunServiceDone(),
        3 => const TunServiceCancelled(),
        _ => TunServiceFailed(
          _firstLine(r.stderr as String? ?? '', 'exit code ${r.exitCode}'),
        ),
      };
    } on ProcessException catch (e) {
      return TunServiceFailed(e.message);
    }
  }

  static String _firstLine(String text, String fallback) {
    final line = text.trim().split('\n').first.trim();
    return line.replaceFirst(RegExp(r'^firefox-vpn-tun:\s*'), '').isEmpty
        ? fallback
        : line.replaceFirst(RegExp(r'^firefox-vpn-tun:\s*'), '');
  }
}