Files

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());