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: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, this.onSave}) : super(key: key); final Site site; // This is called after the target OS has saved the configuration final ValueChanged onSave; @override _SiteConfigScreenState createState() => _SiteConfigScreenState(); } class _SiteConfigScreenState extends State { bool changed = false; bool newSite = false; bool debug = false; 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) { 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); if (widget.onSave != null) { widget.onSave(site); } }, child: Column( children: [ _main(), _keys(), _hosts(), _advanced(), 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 _keys() { final certError = site.certInfo == null || !site.certInfo.validity.valid; var caError = site.ca.length == 0; if (!caError) { site.ca.forEach((ca) { if (!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) ]), onPressed: () { Utils.openPage(context, (context) { if (site.certInfo != null) { return CertificateDetailsScreen( certInfo: site.certInfo, pubKey: pubKey, privKey: privKey, onReplace: (result) { setState(() { changed = true; site.certInfo = result.certInfo; site.key = result.key; }); }); } return AddCertificateScreen(pubKey: pubKey, privKey: privKey, onSave: (result) { setState(() { changed = true; site.certInfo = result.certInfo; site.key = result.key; }); }); }); }, ), 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: (ca) { setState(() { changed = true; site.ca = ca; }); }); }); }) ], ); } Widget _hosts() { return ConfigSection( label: "Set up static hosts and lighthouses", 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: (map) { setState(() { changed = true; site.staticHostmap = map; }); }); }); }, ), ], ); } Widget _advanced() { return ConfigSection( 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.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); } } }