187 lines
5.3 KiB
Dart
187 lines
5.3 KiB
Dart
import 'dart:convert';
|
|
|
|
enum ConnectionMode { automatic, direct, relay }
|
|
|
|
enum HostPlatform { linux, windows }
|
|
|
|
class RemoteHost {
|
|
const RemoteHost({
|
|
required this.id,
|
|
required this.name,
|
|
required this.address,
|
|
required this.user,
|
|
required this.certificateSha256,
|
|
this.platform = HostPlatform.linux,
|
|
this.connectionMode = ConnectionMode.direct,
|
|
this.edgeApiUrl,
|
|
this.agentPublicKey,
|
|
});
|
|
|
|
final String id;
|
|
final String name;
|
|
final String address;
|
|
final String user;
|
|
final String certificateSha256;
|
|
final HostPlatform platform;
|
|
final ConnectionMode connectionMode;
|
|
final String? edgeApiUrl;
|
|
final String? agentPublicKey;
|
|
|
|
Uri get uri => normalizeAgentUri(address);
|
|
Uri get windowsUri => normalizeWindowsAgentUri(address);
|
|
|
|
RemoteHost copyWith({
|
|
String? id,
|
|
String? name,
|
|
String? address,
|
|
String? user,
|
|
String? certificateSha256,
|
|
HostPlatform? platform,
|
|
ConnectionMode? connectionMode,
|
|
String? edgeApiUrl,
|
|
String? agentPublicKey,
|
|
}) => RemoteHost(
|
|
id: id ?? this.id,
|
|
name: name ?? this.name,
|
|
address: address ?? this.address,
|
|
user: user ?? this.user,
|
|
certificateSha256: certificateSha256 ?? this.certificateSha256,
|
|
platform: platform ?? this.platform,
|
|
connectionMode: connectionMode ?? this.connectionMode,
|
|
edgeApiUrl: edgeApiUrl ?? this.edgeApiUrl,
|
|
agentPublicKey: agentPublicKey ?? this.agentPublicKey,
|
|
);
|
|
|
|
Map<String, Object?> toJson() => {
|
|
'id': id,
|
|
'name': name,
|
|
'address': address,
|
|
'user': user,
|
|
'certificate_sha256': certificateSha256,
|
|
'platform': platform.name,
|
|
'connection_mode': connectionMode.name,
|
|
'use_relay': connectionMode != ConnectionMode.direct,
|
|
'edge_api_url': edgeApiUrl,
|
|
'agent_public_key': agentPublicKey,
|
|
};
|
|
|
|
factory RemoteHost.fromJson(Map<String, dynamic> json) => RemoteHost(
|
|
id: json['id'] as String,
|
|
name: json['name'] as String,
|
|
address: json['address'] as String,
|
|
user: json['user'] as String? ?? '',
|
|
certificateSha256: json['certificate_sha256'] as String? ?? '',
|
|
platform: _hostPlatformFromJson(json['platform']),
|
|
connectionMode: _connectionModeFromJson(json),
|
|
edgeApiUrl: json['edge_api_url'] as String?,
|
|
agentPublicKey: json['agent_public_key'] as String?,
|
|
);
|
|
|
|
static List<RemoteHost> decodeList(String value) {
|
|
final decoded = jsonDecode(value) as List<dynamic>;
|
|
return decoded
|
|
.map((item) => RemoteHost.fromJson(item as Map<String, dynamic>))
|
|
.toList(growable: false);
|
|
}
|
|
|
|
static String encodeList(List<RemoteHost> hosts) =>
|
|
jsonEncode(hosts.map((host) => host.toJson()).toList());
|
|
}
|
|
|
|
HostPlatform _hostPlatformFromJson(Object? value) {
|
|
if (value is String) {
|
|
for (final platform in HostPlatform.values) {
|
|
if (platform.name == value) return platform;
|
|
}
|
|
}
|
|
return HostPlatform.linux;
|
|
}
|
|
|
|
ConnectionMode _connectionModeFromJson(Map<String, dynamic> json) {
|
|
final value = json['connection_mode'];
|
|
if (value is String) {
|
|
for (final mode in ConnectionMode.values) {
|
|
if (mode.name == value) return mode;
|
|
}
|
|
}
|
|
return json['use_relay'] == true
|
|
? ConnectionMode.relay
|
|
: ConnectionMode.direct;
|
|
}
|
|
|
|
class AppSettings {
|
|
const AppSettings({
|
|
this.languageCode,
|
|
this.maxWidth = 1280,
|
|
this.maxHeight = 720,
|
|
this.framesPerSecond = 15,
|
|
});
|
|
|
|
final String? languageCode;
|
|
final int maxWidth;
|
|
final int maxHeight;
|
|
final int framesPerSecond;
|
|
|
|
AppSettings copyWith({
|
|
String? languageCode,
|
|
int? maxWidth,
|
|
int? maxHeight,
|
|
int? framesPerSecond,
|
|
}) => AppSettings(
|
|
languageCode: languageCode ?? this.languageCode,
|
|
maxWidth: maxWidth ?? this.maxWidth,
|
|
maxHeight: maxHeight ?? this.maxHeight,
|
|
framesPerSecond: framesPerSecond ?? this.framesPerSecond,
|
|
);
|
|
}
|
|
|
|
Uri normalizeAgentUri(String input) {
|
|
final value = input.trim();
|
|
if (value.isEmpty) {
|
|
throw const FormatException('empty address');
|
|
}
|
|
final withScheme = value.contains('://') ? value : 'wss://$value';
|
|
final parsed = Uri.parse(withScheme);
|
|
if (parsed.scheme != 'wss' || parsed.host.isEmpty) {
|
|
throw const FormatException('invalid WSS address');
|
|
}
|
|
if (parsed.path.isNotEmpty && parsed.path != '/') {
|
|
throw const FormatException('agent URL must not contain a path');
|
|
}
|
|
return Uri(
|
|
scheme: 'wss',
|
|
host: parsed.host,
|
|
port: parsed.hasPort ? parsed.port : 39500,
|
|
path: '/',
|
|
);
|
|
}
|
|
|
|
Uri normalizeWindowsAgentUri(String input) {
|
|
final value = input.trim();
|
|
if (value.isEmpty) throw const FormatException('empty address');
|
|
final withScheme = value.contains('://') ? value : 'tcp://$value';
|
|
final parsed = Uri.parse(withScheme);
|
|
if (parsed.scheme != 'tcp' ||
|
|
parsed.host.isEmpty ||
|
|
parsed.userInfo.isNotEmpty ||
|
|
parsed.query.isNotEmpty ||
|
|
parsed.fragment.isNotEmpty ||
|
|
(parsed.path.isNotEmpty && parsed.path != '/')) {
|
|
throw const FormatException('invalid Windows Agent address');
|
|
}
|
|
return Uri(
|
|
scheme: 'tcp',
|
|
host: parsed.host,
|
|
port: parsed.hasPort ? parsed.port : 39501,
|
|
);
|
|
}
|
|
|
|
String normalizeFingerprint(String input) =>
|
|
input.trim().replaceAll(':', '').toLowerCase();
|
|
|
|
bool isValidFingerprint(String input) =>
|
|
RegExp(r'^[0-9a-f]{64}$').hasMatch(normalizeFingerprint(input));
|
|
|
|
bool isValidAgentPublicKey(String input) =>
|
|
RegExp(r'^[A-Za-z0-9+/]{43}$').hasMatch(input.trim());
|