mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
246 lines
7.3 KiB
Dart
246 lines
7.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:computer/computer.dart';
|
|
import 'package:dartssh2/dartssh2.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:freezed_annotation/freezed_annotation.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.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:server_box/data/provider/server.dart';
|
|
|
|
part 'pve.freezed.dart';
|
|
part 'pve.g.dart';
|
|
|
|
typedef PveCtrlFunc = Future<bool> Function(String node, String id);
|
|
|
|
@freezed
|
|
abstract class PveState with _$PveState {
|
|
const factory PveState({
|
|
@Default(null) PveErr? error,
|
|
@Default(null) PveRes? data,
|
|
@Default(null) String? release,
|
|
@Default(false) bool isBusy,
|
|
@Default(false) bool isConnected,
|
|
}) = _PveState;
|
|
}
|
|
|
|
@riverpod
|
|
class PveNotifier extends _$PveNotifier {
|
|
late final Spi spi;
|
|
late String addr;
|
|
late final SSHClient _client;
|
|
late final ServerSocket _serverSocket;
|
|
final List<SSHForwardChannel> _forwards = [];
|
|
int _localPort = 0;
|
|
late final Dio session;
|
|
late final bool _ignoreCert;
|
|
|
|
@override
|
|
PveState build(Spi spiParam) {
|
|
spi = spiParam;
|
|
final serverState = ref.watch(individualServerNotifierProvider(spi.id));
|
|
final client = serverState.client;
|
|
if (client == null) {
|
|
return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null'));
|
|
}
|
|
_client = client;
|
|
final addr = spi.custom?.pveAddr;
|
|
if (addr == null) {
|
|
return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null'));
|
|
}
|
|
this.addr = addr;
|
|
_ignoreCert = spi.custom?.pveIgnoreCert ?? false;
|
|
_initSession();
|
|
// Async initialization
|
|
Future.microtask(() => _init());
|
|
return const PveState();
|
|
}
|
|
|
|
void _initSession() {
|
|
session = Dio()
|
|
..httpClientAdapter = IOHttpClientAdapter(
|
|
createHttpClient: () {
|
|
final client = HttpClient();
|
|
client.connectionFactory = cf;
|
|
if (_ignoreCert) {
|
|
client.badCertificateCallback = (_, _, _) => true;
|
|
}
|
|
return client;
|
|
},
|
|
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
|
|
);
|
|
}
|
|
|
|
bool get onlyOneNode => state.data?.nodes.length == 1;
|
|
|
|
Future<void> _init() async {
|
|
try {
|
|
await _forward();
|
|
await _login();
|
|
await _getRelease();
|
|
state = state.copyWith(isConnected: true);
|
|
} on PveErr {
|
|
state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed));
|
|
} catch (e, s) {
|
|
Loggers.app.warning('PVE init failed', e, s);
|
|
state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));
|
|
}
|
|
}
|
|
|
|
Future<void> _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<List<int>>().pipe(socket);
|
|
socket.cast<List<int>>().pipe(forward.sink);
|
|
});
|
|
final newUrl = Uri.parse(
|
|
addr,
|
|
).replace(host: 'localhost', port: _localPort).toString();
|
|
debugPrint('Forwarding $newUrl to $addr');
|
|
}
|
|
}
|
|
|
|
Future<ConnectionTask<Socket>> 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<List<int>>().pipe(socket);
|
|
socket.cast<List<int>>().pipe(forward.sink);
|
|
});*/
|
|
|
|
if (url.isScheme('https')) {
|
|
return SecureSocket.startConnect(
|
|
'localhost',
|
|
_localPort,
|
|
onBadCertificate: (_) => true,
|
|
);
|
|
} else {
|
|
return Socket.startConnect('localhost', _localPort);
|
|
}
|
|
}
|
|
|
|
Future<void> _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<void> _getRelease() async {
|
|
final resp = await session.get('$addr/api2/extjs/version');
|
|
final version = resp.data['data']['release'] as String?;
|
|
if (version != null) {
|
|
state = state.copyWith(release: version);
|
|
}
|
|
}
|
|
|
|
Future<void> list() async {
|
|
if (!state.isConnected || state.isBusy) return;
|
|
state = state.copyWith(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,
|
|
state.data,
|
|
));
|
|
state = state.copyWith(data: result, error: null);
|
|
} catch (e) {
|
|
Loggers.app.warning('PVE list failed', e);
|
|
state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));
|
|
} finally {
|
|
state = state.copyWith(isBusy: false);
|
|
}
|
|
}
|
|
|
|
Future<bool> reboot(String node, String id) async {
|
|
if (!state.isConnected) return false;
|
|
final resp = await session.post(
|
|
'$addr/api2/json/nodes/$node/$id/status/reboot',
|
|
);
|
|
final success = _isCtrlSuc(resp);
|
|
if (success) await list(); // Refresh data
|
|
return success;
|
|
}
|
|
|
|
Future<bool> start(String node, String id) async {
|
|
if (!state.isConnected) return false;
|
|
final resp = await session.post(
|
|
'$addr/api2/json/nodes/$node/$id/status/start',
|
|
);
|
|
final success = _isCtrlSuc(resp);
|
|
if (success) await list(); // Refresh data
|
|
return success;
|
|
}
|
|
|
|
Future<bool> stop(String node, String id) async {
|
|
if (!state.isConnected) return false;
|
|
final resp = await session.post(
|
|
'$addr/api2/json/nodes/$node/$id/status/stop',
|
|
);
|
|
final success = _isCtrlSuc(resp);
|
|
if (success) await list(); // Refresh data
|
|
return success;
|
|
}
|
|
|
|
Future<bool> shutdown(String node, String id) async {
|
|
if (!state.isConnected) return false;
|
|
final resp = await session.post(
|
|
'$addr/api2/json/nodes/$node/$id/status/shutdown',
|
|
);
|
|
final success = _isCtrlSuc(resp);
|
|
if (success) await list(); // Refresh data
|
|
return success;
|
|
}
|
|
|
|
bool _isCtrlSuc(Response resp) {
|
|
return resp.statusCode == 200;
|
|
}
|
|
|
|
Future<void> dispose() async {
|
|
try {
|
|
await _serverSocket.close();
|
|
} catch (_) {}
|
|
for (final forward in _forwards) {
|
|
try {
|
|
forward.close();
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
}
|