import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:mobile_nebula/models/HostInfo.dart'; import 'package:mobile_nebula/models/UnsafeRoute.dart'; import 'package:mobile_nebula/models/IPAndPort.dart'; import 'package:uuid/uuid.dart'; import 'Certificate.dart'; import 'StaticHosts.dart'; var uuid = Uuid(); class Site { static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService'); late EventChannel _updates; /// Signals that something about this site has changed. onError is called with an error string if there was an error StreamController _change = StreamController.broadcast(); // Identifiers late String name; late String id; // static_host_map late Map staticHostmap; late List unsafeRoutes; late List dnsResolvers; // pki fields late List ca; String? key; late CertificateInfo? certInfo; // lighthouse options late int lhDuration; // in seconds // listen settings late int port; late int mtu; late String cipher; late int sortKey; late bool connected; late String status; late String logFile; late String logVerbosity; late bool managed; // The following fields are present when managed = true late String? rawConfig; late DateTime? lastManagedUpdate; // A list of errors encountered while loading the site late List errors; Site({ String name = '', String? id, Map? staticHostmap, List? ca, CertificateInfo? certInfo, int lhDuration = 0, int port = 0, String cipher = "aes", int sortKey = 0, int mtu = 1300, bool connected = false, String status = '', String logFile = '', String logVerbosity = 'info', List? errors, List? unsafeRoutes, List? dnsResolvers, bool managed = false, String? rawConfig, DateTime? lastManagedUpdate, }) { this.name = name; this.id = id ?? uuid.v4(); this.staticHostmap = staticHostmap ?? {}; this.ca = ca ?? []; this.certInfo = certInfo; this.lhDuration = lhDuration; this.port = port; this.cipher = cipher; this.sortKey = sortKey; this.mtu = mtu; this.connected = connected; this.status = status; this.logFile = logFile; this.logVerbosity = logVerbosity; this.errors = errors ?? []; this.unsafeRoutes = unsafeRoutes ?? []; this.dnsResolvers = dnsResolvers ?? []; this.managed = managed; this.rawConfig = rawConfig; this.lastManagedUpdate = lastManagedUpdate; _updates = EventChannel('net.defined.nebula/$id'); _updates.receiveBroadcastStream().listen((d) { try { _updateFromJson(d); _change.add(null); } catch (err) { //TODO: handle the error print(err); } }, onError: (err) { _updateFromJson(err.details); var error = err as PlatformException; _change.addError(error.message ?? 'An unexpected error occurred'); }); } factory Site.fromJson(Map json) { var decoded = Site._fromJson(json); return Site( name: decoded["name"], id: decoded['id'], staticHostmap: decoded['staticHostmap'], ca: decoded['ca'], certInfo: decoded['certInfo'], lhDuration: decoded['lhDuration'], port: decoded['port'], cipher: decoded['cipher'], sortKey: decoded['sortKey'], mtu: decoded['mtu'], connected: decoded['connected'], status: decoded['status'], logFile: decoded['logFile'], logVerbosity: decoded['logVerbosity'], errors: decoded['errors'], unsafeRoutes: decoded['unsafeRoutes'], dnsResolvers: decoded['dnsResolvers'], managed: decoded['managed'], rawConfig: decoded['rawConfig'], lastManagedUpdate: decoded['lastManagedUpdate'], ); } _updateFromJson(String json) { var decoded = Site._fromJson(jsonDecode(json)); this.name = decoded["name"]; this.id = decoded['id']; // TODO update EventChannel this.staticHostmap = decoded['staticHostmap']; this.ca = decoded['ca']; this.certInfo = decoded['certInfo']; this.lhDuration = decoded['lhDuration']; this.port = decoded['port']; this.cipher = decoded['cipher']; this.sortKey = decoded['sortKey']; this.mtu = decoded['mtu']; this.connected = decoded['connected']; this.status = decoded['status']; this.logFile = decoded['logFile']; this.logVerbosity = decoded['logVerbosity']; this.errors = decoded['errors']; this.unsafeRoutes = decoded['unsafeRoutes']; this.dnsResolvers = decoded['dnsResolvers']; this.managed = decoded['managed']; this.rawConfig = decoded['rawConfig']; this.lastManagedUpdate = decoded['lastManagedUpdate']; } static _fromJson(Map json) { Map rawHostmap = json['staticHostmap']; Map staticHostmap = {}; rawHostmap.forEach((key, val) { staticHostmap[key] = StaticHost.fromJson(val); }); List rawUnsafeRoutes = json['unsafeRoutes']; List unsafeRoutes = []; rawUnsafeRoutes.forEach((val) { unsafeRoutes.add(UnsafeRoute.fromJson(val)); }); List rawDNSResolvers = json['dnsResolvers']; List dnsResolvers = []; rawDNSResolvers.forEach((val) { dnsResolvers.add(val); }); List rawCA = json['ca']; List ca = []; rawCA.forEach((val) { ca.add(CertificateInfo.fromJson(val)); }); CertificateInfo? certInfo; if (json['cert'] != null) { certInfo = CertificateInfo.fromJson(json['cert']); } List rawErrors = json["errors"]; List errors = []; rawErrors.forEach((error) { errors.add(error); }); return { "name": json["name"], "id": json['id'], "staticHostmap": staticHostmap, "ca": ca, "certInfo": certInfo, "lhDuration": json['lhDuration'], "port": json['port'], "cipher": json['cipher'], "sortKey": json['sortKey'], "mtu": json['mtu'], "connected": json['connected'] ?? false, "status": json['status'] ?? "", "logFile": json['logFile'], "logVerbosity": json['logVerbosity'], "errors": errors, "unsafeRoutes": unsafeRoutes, "dnsResolvers": dnsResolvers, "managed": json['managed'] ?? false, "rawConfig": json['rawConfig'], "lastManagedUpdate": json["lastManagedUpdate"] == null ? null : DateTime.parse(json["lastManagedUpdate"]), }; } Stream onChange() { return _change.stream; } Map toJson() { return { 'name': name, 'id': id, 'staticHostmap': staticHostmap, 'unsafeRoutes': unsafeRoutes, 'dnsResolvers': dnsResolvers, 'ca': ca.map((cert) { return cert.rawCert; }).join('\n'), 'cert': certInfo?.rawCert, 'key': key, 'lhDuration': lhDuration, 'port': port, 'mtu': mtu, 'cipher': cipher, 'sortKey': sortKey, 'logVerbosity': logVerbosity, 'managed': managed, 'rawConfig': rawConfig, }; } save() async { try { var raw = jsonEncode(this); await platform.invokeMethod("saveSite", raw); } on PlatformException catch (err) { //TODO: fix this message throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } Future renderConfig() async { try { var raw = jsonEncode(this); return await platform.invokeMethod("nebula.renderConfig", raw); } on PlatformException catch (err) { //TODO: fix this message throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } start() async { try { await platform.invokeMethod("startSite", {"id": id}); } on PlatformException catch (err) { throw err.message ?? err.toString(); } catch (err) { throw err.toString(); } } stop() async { try { await platform.invokeMethod("stopSite", {"id": id}); } on PlatformException catch (err) { //TODO: fix this message throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } Future> listHostmap() async { try { var ret = await platform.invokeMethod("active.listHostmap", {"id": id}); if (ret == null || ret == "null") { return []; } List f = jsonDecode(ret); List hosts = []; f.forEach((v) { hosts.add(HostInfo.fromJson(v)); }); return hosts; } on PlatformException catch (err) { //TODO: fix this message throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } Future> listPendingHostmap() async { try { var ret = await platform.invokeMethod("active.listPendingHostmap", {"id": id}); if (ret == null || ret == "null") { return []; } List f = jsonDecode(ret); List hosts = []; f.forEach((v) { hosts.add(HostInfo.fromJson(v)); }); return hosts; } on PlatformException catch (err) { throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } Future>> listAllHostmaps() async { try { var res = await Future.wait([this.listHostmap(), this.listPendingHostmap()]); return {"active": res[0], "pending": res[1]}; } on PlatformException catch (err) { throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } void dispose() { _change.close(); } Future getHostInfo(String vpnIp, bool pending) async { try { var ret = await platform .invokeMethod("active.getHostInfo", {"id": id, "vpnIp": vpnIp, "pending": pending}); final h = jsonDecode(ret); if (h == null) { return null; } return HostInfo.fromJson(h); } on PlatformException catch (err) { throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } Future setRemoteForTunnel(String vpnIp, String addr) async { try { var ret = await platform .invokeMethod("active.setRemoteForTunnel", {"id": id, "vpnIp": vpnIp, "addr": addr}); final h = jsonDecode(ret); if (h == null) { return null; } return HostInfo.fromJson(h); } on PlatformException catch (err) { throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } Future closeTunnel(String vpnIp) async { try { return await platform.invokeMethod("active.closeTunnel", {"id": id, "vpnIp": vpnIp}); } on PlatformException catch (err) { throw err.details ?? err.message ?? err.toString(); } catch (err) { throw err.toString(); } } }