import 'dart:async'; import 'dart:io'; import 'package:computer/computer.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:dartssh2/dartssh2.dart'; typedef PveCtrlFunc = Future Function(String node, String id); final class PveProvider extends ChangeNotifier { final ServerPrivateInfo spi; late String addr; late final SSHClient _client; late final ServerSocket _serverSocket; final List _forwards = []; int _localPort = 0; PveProvider({required this.spi}) { final client = spi.server?.client; if (client == null) { throw Exception('Server client is null'); } _client = client; final addr = spi.custom?.pveAddr; if (addr == null) { err.value = 'PVE address is null'; return; } this.addr = addr; _init(); } final err = ValueNotifier(null); final connected = Completer(); late final _ignoreCert = spi.custom?.pveIgnoreCert ?? false; late final session = Dio() ..httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final client = HttpClient(); client.connectionFactory = cf; if (_ignoreCert) { client.badCertificateCallback = (_, __, ___) => true; } return client; }, validateCertificate: _ignoreCert ? (_, __, ___) => true : null, ); final data = ValueNotifier(null); bool get onlyOneNode => data.value?.nodes.length == 1; String? release; bool isBusy = false; Future _init() async { try { await _forward(); await _login(); await _getRelease(); } on PveErr { err.value = l10n.pveLoginFailed; } catch (e, s) { Loggers.app.warning('PVE init failed', e, s); err.value = e.toString(); } finally { connected.complete(); } } Future _forward() async { final url = Uri.parse(addr); if (_localPort == 0) { _serverSocket = await ServerSocket.bind('localhost', 0); _localPort = _serverSocket.port; _serverSocket.listen((socket) async { final forward = await _client.forwardLocal(url.host, url.port); _forwards.add(forward); forward.stream.cast>().pipe(socket); socket.cast>().pipe(forward.sink); }); final newUrl = Uri.parse(addr) .replace(host: 'localhost', port: _localPort) .toString(); debugPrint('Forwarding $newUrl to $addr'); } } Future> cf( Uri url, String? proxyHost, int? proxyPort) async { /* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0); final _localPort = serverSocket.port; serverSocket.listen((socket) async { final forward = await _client.forwardLocal(url.host, url.port); forwards.add(forward); forward.stream.cast>().pipe(socket); socket.cast>().pipe(forward.sink); });*/ if (url.isScheme('https')) { return SecureSocket.startConnect('localhost', _localPort, onBadCertificate: (_) => true); } else { return Socket.startConnect('localhost', _localPort); } } Future _login() async { final resp = await session.post( '$addr/api2/extjs/access/ticket', data: { 'username': spi.user, 'password': spi.pwd, 'realm': 'pam', 'new-format': '1' }, options: Options( headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType}, ), ); try { final ticket = resp.data['data']['ticket']; session.options.headers['CSRFPreventionToken'] = resp.data['data']['CSRFPreventionToken']; session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket'; } catch (e) { throw PveErr(type: PveErrType.loginFailed, message: e.toString()); } } /// Returns true if the PVE version is 8.0 or later Future _getRelease() async { final resp = await session.get('$addr/api2/extjs/version'); final version = resp.data['data']['release'] as String?; if (version != null) { release = version; } } Future list() async { await connected.future; if (isBusy) return; isBusy = true; try { final resp = await session.get('$addr/api2/json/cluster/resources'); final res = resp.data['data'] as List; final result = await Computer.shared.start(PveRes.parse, (res, data.value)); data.value = result; } catch (e) { Loggers.app.warning('PVE list failed', e); err.value = e.toString(); } finally { isBusy = false; } } Future reboot(String node, String id) async { await connected.future; final resp = await session.post('$addr/api2/json/nodes/$node/$id/status/reboot'); return _isCtrlSuc(resp); } Future start(String node, String id) async { await connected.future; final resp = await session.post('$addr/api2/json/nodes/$node/$id/status/start'); return _isCtrlSuc(resp); } Future stop(String node, String id) async { await connected.future; final resp = await session.post('$addr/api2/json/nodes/$node/$id/status/stop'); return _isCtrlSuc(resp); } Future shutdown(String node, String id) async { await connected.future; final resp = await session.post('$addr/api2/json/nodes/$node/$id/status/shutdown'); return _isCtrlSuc(resp); } bool _isCtrlSuc(Response resp) { return resp.statusCode == 200; } @override Future dispose() async { super.dispose(); await _serverSocket.close(); for (final forward in _forwards) { forward.close(); } } }