Support replacing certs (#33)

This commit is contained in:
Nate Brown 2021-05-03 15:16:00 -05:00 committed by GitHub
parent a283bf8010
commit 0bb2a30829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 150 deletions

View File

@ -263,9 +263,11 @@ class IncomingSite(
} }
if (key != null) { if (key != null) {
val f = EncFile(context).openWrite(siteDir.resolve("key")) val keyFile = siteDir.resolve("key")
f.use { it.write(key) } keyFile.delete()
f.close() val encFile = EncFile(context).openWrite(keyFile)
encFile.use { it.write(key) }
encFile.close()
} }
key = null key = null

View File

@ -8,12 +8,13 @@ import 'package:mobile_nebula/services/utils.dart';
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made /// SimplePage with a form and built in validation and confirmation to discard changes if any are made
class FormPage extends StatefulWidget { class FormPage extends StatefulWidget {
const FormPage( const FormPage(
{Key key, this.title, @required this.child, @required this.onSave, @required this.changed, this.hideSave = false}) {Key key, this.title, @required this.child, @required this.onSave, @required this.changed, this.hideSave = false, this.scrollController})
: super(key: key); : super(key: key);
final String title; final String title;
final Function onSave; final Function onSave;
final Widget child; final Widget child;
final ScrollController scrollController;
/// If you need the page to progress to a certain point before saving, control it here /// If you need the page to progress to a certain point before saving, control it here
final bool hideSave; final bool hideSave;
@ -50,6 +51,7 @@ class _FormPageState extends State<FormPage> {
child: SimplePage( child: SimplePage(
leadingAction: _buildLeader(context), leadingAction: _buildLeader(context),
trailingActions: _buildTrailer(context), trailingActions: _buildTrailer(context),
scrollController: widget.scrollController,
title: widget.title, title: widget.title,
child: Form( child: Form(
key: _formKey, key: _formKey,

View File

@ -28,8 +28,8 @@ class SiteItem extends StatelessWidget {
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final border = BorderSide(color: Utils.configSectionBorder(context)); final border = BorderSide(color: Utils.configSectionBorder(context));
var ip = "Error"; var ip = "Error";
if (site.cert != null && site.cert.cert.details.ips.length > 0) { if (site.certInfo != null && site.certInfo.cert.details.ips.length > 0) {
ip = site.cert.cert.details.ips[0]; ip = site.certInfo.cert.details.ips[0];
} }
return SpecialButton( return SpecialButton(

View File

@ -1,5 +1,3 @@
import 'dart:io';
class IPAndPort { class IPAndPort {
String ip; String ip;
int port; int port;

View File

@ -27,7 +27,7 @@ class Site {
// pki fields // pki fields
List<CertificateInfo> ca; List<CertificateInfo> ca;
CertificateInfo cert; CertificateInfo certInfo;
String key; String key;
// lighthouse options // lighthouse options
@ -52,7 +52,7 @@ class Site {
id, id,
staticHostmap, staticHostmap,
ca, ca,
this.cert, this.certInfo,
this.lhDuration = 0, this.lhDuration = 0,
this.port = 0, this.port = 0,
this.cipher = "aes", this.cipher = "aes",
@ -95,7 +95,7 @@ class Site {
}); });
if (json['cert'] != null) { if (json['cert'] != null) {
cert = CertificateInfo.fromJson(json['cert']); certInfo = CertificateInfo.fromJson(json['cert']);
} }
lhDuration = json['lhDuration']; lhDuration = json['lhDuration'];
@ -146,7 +146,7 @@ class Site {
return cert.rawCert; return cert.rawCert;
})?.join('\n') ?? })?.join('\n') ??
"", "",
'cert': cert?.rawCert, 'cert': certInfo?.rawCert,
'key': key, 'key': key,
'lhDuration': lhDuration, 'lhDuration': lhDuration,
'port': port, 'port': port,

View File

@ -68,7 +68,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
labelWidth: 150, labelWidth: 150,
content: Text(hostInfo.cert.details.name), content: Text(hostInfo.cert.details.name),
onPressed: () => Utils.openPage( onPressed: () => Utils.openPage(
context, (context) => CertificateDetailsScreen(certificate: CertificateInfo(cert: hostInfo.cert)))) context, (context) => CertificateDetailsScreen(certInfo: CertificateInfo(cert: hostInfo.cert))))
: Container(), : Container(),
]); ]);
} }

View File

@ -179,7 +179,7 @@ mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
"10.1.0.1": StaticHost(lighthouse: true, destinations: [IPAndPort(ip: '10.1.1.53', port: 4242), IPAndPort(ip: '1::1', port: 4242)]) "10.1.0.1": StaticHost(lighthouse: true, destinations: [IPAndPort(ip: '10.1.1.53', port: 4242), IPAndPort(ip: '1::1', port: 4242)])
}, },
ca: [CertificateInfo.debug(rawCert: ca)], ca: [CertificateInfo.debug(rawCert: ca)],
cert: CertificateInfo.debug(rawCert: cert), certInfo: CertificateInfo.debug(rawCert: cert),
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')] unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
); );

View File

@ -1,56 +1,54 @@
import 'dart:convert'; import 'dart:convert';
import 'package:barcode_scan/barcode_scan.dart'; import 'package:barcode_scan/barcode_scan.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:mobile_nebula/components/FormPage.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/SpecialSelectableText.dart';
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart'; import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/components/config/ConfigTextItem.dart'; import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
import 'package:mobile_nebula/models/Certificate.dart'; import 'package:mobile_nebula/models/Certificate.dart';
import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart';
import 'package:mobile_nebula/services/share.dart'; import 'package:mobile_nebula/services/share.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
import 'CertificateDetailsScreen.dart';
class CertificateResult { class CertificateResult {
CertificateInfo cert; CertificateInfo certInfo;
String key; String key;
CertificateResult({this.cert, this.key}); CertificateResult({this.certInfo, this.key});
} }
class CertificateScreen extends StatefulWidget { class AddCertificateScreen extends StatefulWidget {
const CertificateScreen({Key key, this.cert, this.onSave}) : super(key: key); const AddCertificateScreen({Key key, this.onSave, this.onReplace}) : super(key: key);
final CertificateInfo cert; // onSave will pop a new CertificateDetailsScreen
final ValueChanged<CertificateResult> onSave; final ValueChanged<CertificateResult> onSave;
// onReplace will return the CertificateResult, assuming the previous screen is a CertificateDetailsScreen
final ValueChanged<CertificateResult> onReplace;
@override @override
_CertificateScreenState createState() => _CertificateScreenState(); _AddCertificateScreenState createState() => _AddCertificateScreenState();
} }
class _CertificateScreenState extends State<CertificateScreen> { class _AddCertificateScreenState extends State<AddCertificateScreen> {
String pubKey; String pubKey;
String privKey; String privKey;
bool changed = false;
CertificateInfo cert;
String inputType = 'paste'; String inputType = 'paste';
bool shared = false;
final pasteController = TextEditingController(); final pasteController = TextEditingController();
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService'); static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
@override @override
void initState() { void initState() {
cert = widget.cert; _generateKeys();
super.initState(); super.initState();
} }
@ -62,71 +60,29 @@ class _CertificateScreenState extends State<CertificateScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> items = []; if (pubKey == null) {
bool hideSave = true; return Center(
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
if (cert == null) { return CupertinoProgressIndicatorData(radius: 500);
if (pubKey == null) { }),
items = _buildGenerate(); );
} else {
items.addAll(_buildShare());
items.addAll(_buildLoadCert());
}
} else {
items.addAll(_buildCertList());
hideSave = false;
} }
return FormPage( List<Widget> items = [];
items.addAll(_buildShare());
items.addAll(_buildLoadCert());
return SimplePage(
title: 'Certificate', title: 'Certificate',
changed: changed,
hideSave: hideSave,
onSave: () {
Navigator.pop(context);
if (widget.onSave != null) {
widget.onSave(CertificateResult(cert: cert, key: privKey));
}
},
child: Column(children: items)); child: Column(children: items));
} }
_buildCertList() {
//TODO: generate a full list
return [
ConfigSection(
children: [
ConfigPageItem(
content: Text(cert.cert.details.name),
onPressed: () {
Utils.openPage(context, (context) {
//TODO: wire on delete
return CertificateDetailsScreen(certificate: cert);
});
},
)
],
)
];
}
List<Widget> _buildGenerate() {
return [
ConfigSection(label: 'Please generate a new public and private key', children: [
ConfigButtonItem(
content: Text('Generate Keys'),
onPressed: () => _generateKeys(),
)
])
];
}
_generateKeys() async { _generateKeys() async {
try { try {
var kp = await platform.invokeMethod("nebula.generateKeyPair"); var kp = await platform.invokeMethod("nebula.generateKeyPair");
Map<String, dynamic> keyPair = jsonDecode(kp); Map<String, dynamic> keyPair = jsonDecode(kp);
setState(() { setState(() {
changed = true;
pubKey = keyPair['PublicKey']; pubKey = keyPair['PublicKey'];
privKey = keyPair['PrivateKey']; privKey = keyPair['PrivateKey'];
}); });
@ -148,9 +104,6 @@ class _CertificateScreenState extends State<CertificateScreen> {
content: Text('Share Public Key'), content: Text('Share Public Key'),
onPressed: () async { onPressed: () async {
await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub'); await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
setState(() {
shared = true;
});
}, },
), ),
]) ])
@ -198,14 +151,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
ConfigButtonItem( ConfigButtonItem(
content: Center(child: Text('Load Certificate')), content: Center(child: Text('Load Certificate')),
onPressed: () { onPressed: () {
_addCertEntry(pasteController.text, (err) { _addCertEntry(pasteController.text);
if (err != null) {
return Utils.popError(context, 'Failed to parse certificate content', err);
}
pasteController.text = '';
setState(() {});
});
}), }),
], ],
) )
@ -225,13 +171,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
return; return;
} }
_addCertEntry(content, (err) { _addCertEntry(content);
if (err != null) {
Utils.popError(context, 'Error loading certificate file', err);
} else {
setState(() {});
}
});
} catch (err) { } catch (err) {
return Utils.popError(context, 'Failed to load certificate file', err.toString()); return Utils.popError(context, 'Failed to load certificate file', err.toString());
} }
@ -254,13 +194,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
var result = await BarcodeScanner.scan(options: options); var result = await BarcodeScanner.scan(options: options);
if (result.rawContent != "") { if (result.rawContent != "") {
_addCertEntry(result.rawContent, (err) { _addCertEntry(result.rawContent);
if (err != null) {
Utils.popError(context, 'Error loading certificate content', err);
} else {
setState(() {});
}
});
} }
}), }),
], ],
@ -268,9 +202,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
]; ];
} }
_addCertEntry(String rawCert, ValueChanged<String> callback) async { _addCertEntry(String rawCert) async {
String error;
// Allow for app store review testing cert to override the generated key // Allow for app store review testing cert to override the generated key
if (rawCert.trim() == _testCert) { if (rawCert.trim() == _testCert) {
privKey = _testKey; privKey = _testKey;
@ -278,21 +210,36 @@ class _CertificateScreenState extends State<CertificateScreen> {
try { try {
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert}); var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
List<dynamic> certs = jsonDecode(rawCerts); List<dynamic> certs = jsonDecode(rawCerts);
if (certs.length > 0) { if (certs.length > 0) {
var tryCert = CertificateInfo.fromJson(certs.first); var tryCertInfo = CertificateInfo.fromJson(certs.first);
if (tryCert.cert.details.isCa) { if (tryCertInfo.cert.details.isCa) {
return callback('A certificate authority is not appropriate for a client certificate.'); return Utils.popError(context, 'Error loading certificate content', 'A certificate authority is not appropriate for a client certificate.');
} else if (!tryCertInfo.validity.valid) {
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity.reason);
} }
//TODO: test that the pubkey matches the privkey
cert = tryCert; //TODO: test that the pubkey we generated equals the pub key in the cert
// If we are replacing we just return the results now
if (widget.onReplace != null) {
Navigator.pop(context);
widget.onReplace(CertificateResult(certInfo: tryCertInfo, key: privKey));
return;
}
// We have a cert, pop the details screen where they can hit save
Utils.openPage(context, (context) {
return CertificateDetailsScreen(certInfo: tryCertInfo, onSave: () {
Navigator.pop(context);
widget.onSave(CertificateResult(certInfo: tryCertInfo, key: privKey));
});
});
} }
} on PlatformException catch (err) { } on PlatformException catch (err) {
error = err.details ?? err.message; return Utils.popError(context, 'Error loading certificate content', err.details ?? err.message);
}
if (callback != null) {
callback(error);
} }
} }
} }

View File

@ -77,7 +77,7 @@ class _CAListScreenState extends State<CAListScreen> {
onPressed: () { onPressed: () {
Utils.openPage(context, (context) { Utils.openPage(context, (context) {
return CertificateDetailsScreen( return CertificateDetailsScreen(
certificate: ca, certInfo: ca,
onDelete: () { onDelete: () {
setState(() { setState(() {
changed = true; changed = true;

View File

@ -2,34 +2,73 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart'; import 'package:mobile_nebula/components/FormPage.dart';
import 'package:mobile_nebula/components/SpecialSelectableText.dart'; import 'package:mobile_nebula/components/SpecialSelectableText.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Certificate.dart'; import 'package:mobile_nebula/models/Certificate.dart';
import 'package:mobile_nebula/screens/siteConfig/AddCertificateScreen.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
/// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert) /// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert)
class CertificateDetailsScreen extends StatefulWidget { class CertificateDetailsScreen extends StatefulWidget {
const CertificateDetailsScreen({Key key, this.certificate, this.onDelete}) : super(key: key); const CertificateDetailsScreen({Key key, this.certInfo, this.onDelete, this.onSave, this.onReplace}) : super(key: key);
final CertificateInfo certificate; final CertificateInfo certInfo;
// onDelete is used to remove a CA cert
final Function onDelete; final Function onDelete;
// onSave is used to install a new certificate
final Function onSave;
// onReplace is used to install a new certificate over top of the old one
final ValueChanged<CertificateResult> onReplace;
@override @override
_CertificateDetailsScreenState createState() => _CertificateDetailsScreenState(); _CertificateDetailsScreenState createState() => _CertificateDetailsScreenState();
} }
class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> { class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
bool changed = false;
CertificateResult certResult;
CertificateInfo certInfo;
ScrollController controller = ScrollController();
@override
void initState() {
certInfo = widget.certInfo;
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SimplePage( return FormPage(
title: 'Certificate Details', title: 'Certificate Details',
scrollController: controller,
changed: widget.onSave != null || changed,
onSave: () {
if (widget.onSave != null) {
Navigator.pop(context);
widget.onSave();
} else if (widget.onReplace != null) {
Navigator.pop(context);
widget.onReplace(certResult);
}
},
hideSave: widget.onSave == null && widget.onReplace == null,
child: Column(children: [ child: Column(children: [
_buildID(), _buildID(),
_buildFilters(), _buildFilters(),
_buildValid(), _buildValid(),
_buildAdvanced(), _buildAdvanced(),
_buildReplace(),
_buildDelete(), _buildDelete(),
]), ]),
); );
@ -37,17 +76,17 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
Widget _buildID() { Widget _buildID() {
return ConfigSection(children: <Widget>[ return ConfigSection(children: <Widget>[
ConfigItem(label: Text('Name'), content: SpecialSelectableText(widget.certificate.cert.details.name)), ConfigItem(label: Text('Name'), content: SpecialSelectableText(certInfo.cert.details.name)),
ConfigItem( ConfigItem(
label: Text('Type'), label: Text('Type'),
content: Text(widget.certificate.cert.details.isCa ? 'CA certificate' : 'Client certificate')), content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
]); ]);
} }
Widget _buildValid() { Widget _buildValid() {
var valid = Text('yes'); var valid = Text('yes');
if (widget.certificate.validity != null && !widget.certificate.validity.valid) { if (certInfo.validity != null && !certInfo.validity.valid) {
valid = Text(widget.certificate.validity.valid ? 'yes' : widget.certificate.validity.reason, valid = Text(certInfo.validity.valid ? 'yes' : certInfo.validity.reason,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context))); style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
} }
return ConfigSection( return ConfigSection(
@ -56,33 +95,33 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
ConfigItem(label: Text('Valid?'), content: valid), ConfigItem(label: Text('Valid?'), content: valid),
ConfigItem( ConfigItem(
label: Text('Created'), label: Text('Created'),
content: SpecialSelectableText(widget.certificate.cert.details.notBefore.toLocal().toString())), content: SpecialSelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
ConfigItem( ConfigItem(
label: Text('Expires'), label: Text('Expires'),
content: SpecialSelectableText(widget.certificate.cert.details.notAfter.toLocal().toString())), content: SpecialSelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
], ],
); );
} }
Widget _buildFilters() { Widget _buildFilters() {
List<Widget> items = []; List<Widget> items = [];
if (widget.certificate.cert.details.groups.length > 0) { if (certInfo.cert.details.groups.length > 0) {
items.add(ConfigItem( items.add(ConfigItem(
label: Text('Groups'), content: SpecialSelectableText(widget.certificate.cert.details.groups.join(', ')))); label: Text('Groups'), content: SpecialSelectableText(certInfo.cert.details.groups.join(', '))));
} }
if (widget.certificate.cert.details.ips.length > 0) { if (certInfo.cert.details.ips.length > 0) {
items items
.add(ConfigItem(label: Text('IPs'), content: SpecialSelectableText(widget.certificate.cert.details.ips.join(', ')))); .add(ConfigItem(label: Text('IPs'), content: SpecialSelectableText(certInfo.cert.details.ips.join(', '))));
} }
if (widget.certificate.cert.details.subnets.length > 0) { if (certInfo.cert.details.subnets.length > 0) {
items.add(ConfigItem( items.add(ConfigItem(
label: Text('Subnets'), content: SpecialSelectableText(widget.certificate.cert.details.subnets.join(', ')))); label: Text('Subnets'), content: SpecialSelectableText(certInfo.cert.details.subnets.join(', '))));
} }
return items.length > 0 return items.length > 0
? ConfigSection(label: widget.certificate.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items) ? ConfigSection(label: certInfo.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
: Container(); : Container();
} }
@ -91,18 +130,18 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
children: <Widget>[ children: <Widget>[
ConfigItem( ConfigItem(
label: Text('Fingerprint'), label: Text('Fingerprint'),
content: SpecialSelectableText(widget.certificate.cert.fingerprint, content: SpecialSelectableText(certInfo.cert.fingerprint,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start), crossAxisAlignment: CrossAxisAlignment.start),
ConfigItem( ConfigItem(
label: Text('Public Key'), label: Text('Public Key'),
content: SpecialSelectableText(widget.certificate.cert.details.publicKey, content: SpecialSelectableText(certInfo.cert.details.publicKey,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start), crossAxisAlignment: CrossAxisAlignment.start),
widget.certificate.rawCert != null certInfo.rawCert != null
? ConfigItem( ? ConfigItem(
label: Text('PEM Format'), label: Text('PEM Format'),
content: SpecialSelectableText(widget.certificate.rawCert, content: SpecialSelectableText(certInfo.rawCert,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start) crossAxisAlignment: CrossAxisAlignment.start)
: Container(), : Container(),
@ -110,12 +149,40 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
); );
} }
Widget _buildReplace() {
if (widget.onReplace == null) {
return Container();
}
return Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox(
width: double.infinity,
child: PlatformButton(
child: Text('Replace certificate'),
color: CupertinoColors.systemRed.resolveFrom(context),
onPressed: () {
Utils.openPage(context, (context) {
return AddCertificateScreen(
onReplace: (result) {
setState(() {
changed = true;
certResult = result;
certInfo = certResult.certInfo;
});
// Slam the page back to the top
controller.animateTo(0, duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
});
});
})));
}
Widget _buildDelete() { Widget _buildDelete() {
if (widget.onDelete == null) { if (widget.onDelete == null) {
return Container(); return Container();
} }
var title = widget.certificate.cert.details.isCa ? 'Delete CA?' : 'Delete cert?'; var title = certInfo.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
return Padding( return Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),

View File

@ -13,11 +13,13 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Site.dart'; import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/screens/siteConfig/AdvancedScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/AdvancedScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/CAListScreen.dart'; import 'package:mobile_nebula/screens/siteConfig/CAListScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/CertificateScreen.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/screens/siteConfig/StaticHostsScreen.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
//TODO: Add a config test mechanism //TODO: Add a config test mechanism
//TODO: Enforce a name
class SiteConfigScreen extends StatefulWidget { class SiteConfigScreen extends StatefulWidget {
const SiteConfigScreen({Key key, this.site, this.onSave}) : super(key: key); const SiteConfigScreen({Key key, this.site, this.onSave}) : super(key: key);
@ -100,12 +102,18 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
content: PlatformTextFormField( content: PlatformTextFormField(
placeholder: 'Required', placeholder: 'Required',
controller: nameController, controller: nameController,
validator: (name) {
if (name == null || name == "") {
return "A name is required";
}
return null;
},
)) ))
]); ]);
} }
Widget _keys() { Widget _keys() {
final certError = site.cert == null || !site.cert.validity.valid; final certError = site.certInfo == null || !site.certInfo.validity.valid;
var caError = site.ca.length == 0; var caError = site.ca.length == 0;
if (!caError) { if (!caError) {
site.ca.forEach((ca) { site.ca.forEach((ca) {
@ -126,16 +134,28 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20), child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5)) padding: EdgeInsets.only(right: 5))
: Container(), : Container(),
certError ? Text('Needs attention') : Text(site.cert.cert.details.name) certError ? Text('Needs attention') : Text(site.certInfo.cert.details.name)
]), ]),
onPressed: () { onPressed: () {
Utils.openPage(context, (context) { Utils.openPage(context, (context) {
return CertificateScreen( if (site.certInfo != null) {
cert: site.cert, return CertificateDetailsScreen(
certInfo: site.certInfo,
onReplace: (result) {
setState(() {
changed = true;
site.certInfo = result.certInfo;
site.key = result.key;
});
}
);
}
return AddCertificateScreen(
onSave: (result) { onSave: (result) {
setState(() { setState(() {
changed = true; changed = true;
site.cert = result.cert; site.certInfo = result.certInfo;
site.key = result.key; site.key = result.key;
}); });
}); });