import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' as fpw; import 'package:intl/intl.dart'; import 'package:mobile_nebula/components/FormPage.dart'; import 'package:mobile_nebula/components/PlatformTextFormField.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/models/Site.dart'; import 'package:mobile_nebula/screens/siteConfig/AdvancedScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/CAListScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/AddCertificateScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/StaticHostsScreen.dart'; import 'package:mobile_nebula/services/utils.dart'; //TODO: Add a config test mechanism //TODO: Enforce a name class SiteConfigScreen extends StatefulWidget { const SiteConfigScreen({ Key? key, this.site, required this.onSave, required this.supportsQRScanning, }) : super(key: key); final Site? site; // This is called after the target OS has saved the configuration final ValueChanged onSave; final bool supportsQRScanning; @override _SiteConfigScreenState createState() => _SiteConfigScreenState(); } class _SiteConfigScreenState extends State { bool changed = false; bool newSite = false; bool debug = false; late Site site; String? pubKey; String? privKey; static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService'); final nameController = TextEditingController(); @override void initState() { //NOTE: this is slightly wasteful since a keypair will be generated every time this page is opened _generateKeys(); if (widget.site == null) { newSite = true; site = Site(); } else { site = widget.site!; nameController.text = site.name; } super.initState(); } @override Widget build(BuildContext context) { if (pubKey == null || privKey == null) { return Center( child: fpw.PlatformCircularProgressIndicator(cupertino: (_, __) { return fpw.CupertinoProgressIndicatorData(radius: 50); }), ); } return FormPage( title: newSite ? 'New Site' : 'Edit Site', changed: changed, onSave: () async { site.name = nameController.text; try { await site.save(); } catch (error) { return Utils.popError(context, 'Failed to save the site configuration', error.toString()); } Navigator.pop(context); widget.onSave(site); }, child: Column( children: [ _main(), _keys(), _hosts(), _advanced(), _managed(), kDebugMode ? _debugConfig() : Container(height: 0), ], )); } Widget _debugConfig() { var data = ""; try { final encoder = new JsonEncoder.withIndent(' '); data = encoder.convert(site); } catch (err) { data = err.toString(); } return ConfigSection(label: 'DEBUG', children: [ConfigItem(labelWidth: 0, content: SelectableText(data))]); } Widget _main() { return ConfigSection(children: [ ConfigItem( label: Text("Name"), content: PlatformTextFormField( placeholder: 'Required', controller: nameController, validator: (name) { if (name == null || name == "") { return "A name is required"; } return null; }, )) ]); } Widget _managed() { final formatter = DateFormat.yMMMMd('en_US').add_jm(); var lastUpdate = "Unknown"; if (site.lastManagedUpdate != null) { lastUpdate = formatter.format(site.lastManagedUpdate!.toLocal()); } return site.managed ? ConfigSection( label: "MANAGED CONFIG", children: [ ConfigItem( label: Text("Last Update"), content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text(lastUpdate), ]), ) ] ) : Container(); } Widget _keys() { final certError = site.certInfo == null || site.certInfo!.validity == null || !site.certInfo!.validity!.valid; var caError = false; if (!site.managed) { var caError = site.ca.length == 0; if (!caError) { site.ca.forEach((ca) { if (ca.validity == null || !ca.validity!.valid) { caError = true; } }); } } return ConfigSection( label: "IDENTITY", children: [ ConfigPageItem( label: Text('Certificate'), content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: [ certError ? Padding( child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20), padding: EdgeInsets.only(right: 5)) : Container(), certError ? Text('Needs attention') : Text(site.certInfo?.cert.details.name ?? 'Unknown certificate') ]), onPressed: () { Utils.openPage(context, (context) { if (site.certInfo != null) { return CertificateDetailsScreen( certInfo: site.certInfo!, pubKey: pubKey, privKey: privKey, onReplace: site.managed ? null : (result) { setState(() { changed = true; site.certInfo = result.certInfo; site.key = result.key; }); }, supportsQRScanning: widget.supportsQRScanning, ); } return AddCertificateScreen( pubKey: pubKey!, privKey: privKey!, onSave: (result) { setState(() { changed = true; site.certInfo = result.certInfo; site.key = result.key; }); }, supportsQRScanning: widget.supportsQRScanning, ); }); }, ), ConfigPageItem( label: Text("CA"), content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: [ caError ? Padding( child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20), padding: EdgeInsets.only(right: 5)) : Container(), caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.ca.length)) ]), onPressed: () { Utils.openPage(context, (context) { return CAListScreen( cas: site.ca, onSave: site.managed ? null : (ca) { setState(() { changed = true; site.ca = ca; }); }, supportsQRScanning: widget.supportsQRScanning, ); }); }) ], ); } Widget _hosts() { return ConfigSection( label: "LIGHTHOUSES / STATIC HOSTS", children: [ ConfigPageItem( label: Text('Hosts'), content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: [ site.staticHostmap.length == 0 ? Padding( child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20), padding: EdgeInsets.only(right: 5)) : Container(), site.staticHostmap.length == 0 ? Text('Needs attention') : Text(Utils.itemCountFormat(site.staticHostmap.length)) ]), onPressed: () { Utils.openPage(context, (context) { return StaticHostsScreen( hostmap: site.staticHostmap, onSave: site.managed ? null : (map) { setState(() { changed = true; site.staticHostmap = map; }); }); }); }, ), ], ); } Widget _advanced() { return ConfigSection( label: "ADVANCED", children: [ ConfigPageItem( label: Text('Advanced'), onPressed: () { Utils.openPage(context, (context) { return AdvancedScreen( site: site, onSave: (settings) { setState(() { changed = true; site.cipher = settings.cipher; site.lhDuration = settings.lhDuration; site.port = settings.port; site.logVerbosity = settings.verbosity; site.unsafeRoutes = settings.unsafeRoutes; site.dnsResolvers = settings.dnsResolvers; site.mtu = settings.mtu; }); }); }); }) ], ); } _generateKeys() async { try { var kp = await platform.invokeMethod("nebula.generateKeyPair"); Map keyPair = jsonDecode(kp); setState(() { pubKey = keyPair['PublicKey']; privKey = keyPair['PrivateKey']; }); } on PlatformException catch (err) { Utils.popError(context, 'Failed to generate key pair', err.details ?? err.message); } } }