mobile_nebula/lib/screens/MainScreen.dart
Ian VanSchooten 64d45f66c7
Update Flutter, target android SDK 34 (#160)
This updates flutter to 3.24.1, the latest stable version, and also updates our flutter dependencies to latest.

It targets the latest android sdk, 34, which is required if we want to publish a new version to the Google Play store.

I also needed to make a few adjustments to handle deprecations. The biggest change is that I needed to wrap the main widget in MaterialApp to avoid problems with AdaptiveSwitch in iOS.
2024-09-20 14:19:23 -04:00

345 lines
10 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
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';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/components/SiteItem.dart';
import 'package:mobile_nebula/models/Certificate.dart';
import 'package:mobile_nebula/models/IPAndPort.dart';
import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/models/StaticHosts.dart';
import 'package:mobile_nebula/models/UnsafeRoute.dart';
import 'package:mobile_nebula/screens/SettingsScreen.dart';
import 'package:mobile_nebula/screens/SiteDetailScreen.dart';
import 'package:mobile_nebula/screens/siteConfig/SiteConfigScreen.dart';
import 'package:mobile_nebula/services/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:uuid/uuid.dart';
/// Contains an expired CA and certificate
const badDebugSave = {
'name': 'Bad Site',
'cert': '''-----BEGIN NEBULA CERTIFICATE-----
CmIKBHRlc3QSCoKUoIUMgP7//w8ourrS+QUwjre3iAY6IDbmIX5cwd+UYVhLADLa
A5PwucZPVrNtP0P9NJE0boM2SiBSGzy8bcuFWWK5aVArJGA9VDtLg1HuujBu8lOp
VTgklxJAgbI1Xb1C9JC3a1Cnc6NPqWhnw+3VLoDXE9poBav09+zhw5DPDtgvQmxU
Sbw6cAF4gPS4e/tZ5Kjc8QEvjk3HDQ==
-----END NEBULA CERTIFICATE-----''',
'key': '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
rmXnR1yvDZi1VPVmnNVY8NMsQpEpbbYlq7rul+ByQvg=
-----END NEBULA X25519 PRIVATE KEY-----''',
'ca': '''-----BEGIN NEBULA CERTIFICATE-----
CjkKB3Rlc3QgY2EopYyK9wUwpfOOhgY6IHj4yrtHbq+rt4hXTYGrxuQOS0412uKT
4wi5wL503+SAQAESQPhWXuVGjauHS1Qqd3aNA3DY+X8CnAweXNEoJKAN/kjH+BBv
mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
-----END NEBULA CERTIFICATE-----''',
};
/// Contains an expired CA and certificate
const goodDebugSave = {
'name': 'Good Site',
'cert': '''-----BEGIN NEBULA CERTIFICATE-----
CmcKCmRlYnVnIGhvc3QSCYKAhFCA/v//DyiX0ZaaBjDjjPf5ETogyYzKdlRh7pW6
yOd8+aMQAFPha2wuYixuq53ru9+qXC9KIJd3ow6qIiaHInT1dgJvy+122WK7g86+
Z8qYtTZnox1cEkBYpC0SySrCp6jd/zeAFEJM6naPYgc6rmy/H/qveyQ6WAtbgLpK
tM3EXbbOE9+fV/Ma6Oilf1SixO3ZBo30nRYL
-----END NEBULA CERTIFICATE-----''',
'key': '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
vu9t0mNy8cD5x3CMVpQ/cdKpjdz46NBlcRqvJAQpO44=
-----END NEBULA X25519 PRIVATE KEY-----''',
'ca': '''-----BEGIN NEBULA CERTIFICATE-----
CjcKBWRlYnVnKOTQlpoGMOSM9/kROiCWNJUs7c4ZRzUn2LbeAEQrz2PVswnu9dcL
Sn/2VNNu30ABEkCQtWxmCJqBr5Yd9vtDWCPo/T1JQmD3stBozcM6aUl1hP3zjURv
MAIH7gzreMGgrH/yR6rZpIHR3DxJ3E0aHtEI
-----END NEBULA CERTIFICATE-----''',
};
class MainScreen extends StatefulWidget {
const MainScreen(this.dnEnrollStream, {Key? key}) : super(key: key);
final StreamController dnEnrollStream;
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
List<Site>? sites;
// A set of widgets to display in a column that represents an error blocking us from moving forward entirely
List<Widget>? error;
bool supportsQRScanning = false;
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
RefreshController refreshController = RefreshController();
ScrollController scrollController = ScrollController();
@override
void initState() {
_loadSites();
widget.dnEnrollStream.stream.listen((_) {
_loadSites();
});
platform.setMethodCallHandler(handleMethodCall);
super.initState();
}
@override
void dispose() {
scrollController.dispose();
refreshController.dispose();
super.dispose();
}
Future<dynamic> handleMethodCall(MethodCall call) async {
switch (call.method) {
case "refreshSites":
_loadSites();
break;
default:
print("ERR: Unexpected method call ${call.method}");
}
}
@override
Widget build(BuildContext context) {
Widget? debugSite;
if (kDebugMode) {
debugSite = Row(
children: [
_debugSave(badDebugSave),
_debugSave(goodDebugSave),
_debugClearKeys(),
],
mainAxisAlignment: MainAxisAlignment.center,
);
}
// Determine whether the device supports QR scanning. For example, some
// Chromebooks do not have camera support.
if (Platform.isAndroid) {
platform
.invokeMethod("android.deviceHasCamera")
.then((hasCamera) => setState(() => supportsQRScanning = hasCamera));
} else {
supportsQRScanning = true;
}
return SimplePage(
title: Text('Nebula'),
scrollable: SimpleScrollable.vertical,
scrollController: scrollController,
leadingAction: PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.add, size: 28.0),
onPressed: () => Utils.openPage(context, (context) {
return SiteConfigScreen(
onSave: (_) {
_loadSites();
},
supportsQRScanning: supportsQRScanning);
}),
),
refreshController: refreshController,
onRefresh: () {
_loadSites();
refreshController.refreshCompleted();
},
trailingActions: <Widget>[
PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.menu, size: 28.0),
onPressed: () => Utils.openPage(context, (_) => SettingsScreen(widget.dnEnrollStream)),
),
],
bottomBar: debugSite,
child: _buildBody(),
);
}
Widget _buildBody() {
if (error != null) {
return Center(
child: Padding(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: error!,
),
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10)));
}
return _buildSites();
}
Widget _buildNoSites() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
child: Text('Welcome to Nebula!', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
),
Text('You don\'t have any site configurations installed yet. Hit the plus button above to get started.',
textAlign: TextAlign.center),
],
),
));
}
Widget _buildSites() {
if (sites == null || sites!.length == 0) {
return _buildNoSites();
}
List<Widget> items = [];
sites!.forEach((site) {
items.add(SiteItem(
key: Key(site.id),
site: site,
onPressed: () {
Utils.openPage(context, (context) {
return SiteDetailScreen(
site: site,
onChanged: () => _loadSites(),
supportsQRScanning: supportsQRScanning,
);
});
}));
});
Widget child = ReorderableListView(
shrinkWrap: true,
scrollController: scrollController,
padding: EdgeInsets.symmetric(vertical: 5),
children: items,
onReorder: (oldI, newI) async {
if (oldI < newI) {
// removing the item at oldIndex will shorten the list by 1.
newI -= 1;
}
setState(() {
final Site moved = sites!.removeAt(oldI);
sites!.insert(newI, moved);
});
for (var i = 0; i < sites!.length; i++) {
if (sites![i].sortKey == i) {
continue;
}
sites![i].sortKey = i;
try {
await sites![i].save();
} catch (err) {
//TODO: display error at the end
print('ERR ${sites![i].name} - $err');
}
}
_loadSites();
});
if (Platform.isIOS) {
child = CupertinoTheme(child: child, data: CupertinoTheme.of(context));
}
// The theme here is to remove the hardcoded canvas border reordering forces on us
return Theme(data: Theme.of(context).copyWith(canvasColor: Colors.transparent), child: child);
}
Widget _debugSave(Map<String, String> siteConfig) {
return CupertinoButton(
child: Text(siteConfig['name']!),
onPressed: () async {
var uuid = Uuid();
var s = Site(
name: siteConfig['name']!,
id: uuid.v4(),
staticHostmap: {
"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: siteConfig['ca'])],
certInfo: CertificateInfo.debug(rawCert: siteConfig['cert']),
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]);
s.key = siteConfig['key'];
var err = await s.save();
if (err != null) {
Utils.popError(context, "Failed to save the site", err);
} else {
_loadSites();
}
},
);
}
Widget _debugClearKeys() {
return CupertinoButton(
child: Text("Clear Keys"),
onPressed: () async {
await platform.invokeMethod("debug.clearKeys", null);
},
);
}
_loadSites() async {
//TODO: This can throw, we need to show an error dialog
Map<String, dynamic> rawSites = jsonDecode(await platform.invokeMethod('listSites'));
sites = [];
rawSites.forEach((id, rawSite) {
try {
var site = Site.fromJson(rawSite);
//TODO: we need to cancel change listeners when we rebuild
site.onChange().listen((_) {
setState(() {});
}, onError: (err) {
setState(() {});
if (ModalRoute.of(context)!.isCurrent) {
Utils.popError(context, "${site.name} Error", err);
}
});
sites!.add(site);
} catch (err) {
//TODO: handle error
print("$err site config: $rawSite");
// Sometimes it is helpful to just nuke these is dev
// platform.invokeMethod('deleteSite', id);
}
});
if (Platform.isAndroid) {
// Android suffers from a race to discover the active site and attach site specific event listeners
platform.invokeMethod("android.registerActiveSite");
}
sites!.sort((a, b) {
if (a.sortKey == b.sortKey) {
return a.name.compareTo(b.name);
}
return a.sortKey - b.sortKey;
});
setState(() {});
}
}