254 lines
7.5 KiB
Dart
254 lines
7.5 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.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:uuid/uuid.dart';
|
|
|
|
//TODO: add refresh
|
|
|
|
class MainScreen extends StatefulWidget {
|
|
const MainScreen({Key key}) : super(key: key);
|
|
|
|
@override
|
|
_MainScreenState createState() => _MainScreenState();
|
|
}
|
|
|
|
class _MainScreenState extends State<MainScreen> {
|
|
bool ready = false;
|
|
List<Site> sites;
|
|
|
|
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
|
|
|
@override
|
|
void initState() {
|
|
_loadSites();
|
|
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SimplePage(
|
|
title: 'Nebula',
|
|
scrollable: SimpleScrollable.none,
|
|
leadingAction: PlatformIconButton(
|
|
padding: EdgeInsets.zero,
|
|
icon: Icon(Icons.add, size: 28.0),
|
|
onPressed: () => Utils.openPage(context, (context) {
|
|
return SiteConfigScreen(onSave: (_) {
|
|
_loadSites();
|
|
});
|
|
}),
|
|
),
|
|
trailingActions: <Widget>[
|
|
PlatformIconButton(
|
|
padding: EdgeInsets.zero,
|
|
icon: Icon(Icons.menu, size: 28.0),
|
|
onPressed: () => Utils.openPage(context, (_) => SettingsScreen()),
|
|
),
|
|
],
|
|
bottomBar: kDebugMode ? _debugSave() : null,
|
|
child: _buildBody(),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
if (!ready) {
|
|
return Center(
|
|
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
|
|
return CupertinoProgressIndicatorData(radius: 50);
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (sites == null || sites.length == 0) {
|
|
return _buildNoSites();
|
|
}
|
|
|
|
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() {
|
|
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());
|
|
});
|
|
}));
|
|
});
|
|
|
|
Widget child = ReorderableListView(
|
|
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 = min(oldI, newI); i <= max(oldI, newI); i++) {
|
|
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() {
|
|
return CupertinoButton(
|
|
key: Key('debug-save'),
|
|
child: Text("DEBUG SAVE"),
|
|
onPressed: () async {
|
|
var uuid = Uuid();
|
|
|
|
var cert = '''-----BEGIN NEBULA CERTIFICATE-----
|
|
CmIKBHRlc3QSCoKUoIUMgP7//w8ourrS+QUwjre3iAY6IDbmIX5cwd+UYVhLADLa
|
|
A5PwucZPVrNtP0P9NJE0boM2SiBSGzy8bcuFWWK5aVArJGA9VDtLg1HuujBu8lOp
|
|
VTgklxJAgbI1Xb1C9JC3a1Cnc6NPqWhnw+3VLoDXE9poBav09+zhw5DPDtgvQmxU
|
|
Sbw6cAF4gPS4e/tZ5Kjc8QEvjk3HDQ==
|
|
-----END NEBULA CERTIFICATE-----''';
|
|
|
|
var ca = '''-----BEGIN NEBULA CERTIFICATE-----
|
|
CjkKB3Rlc3QgY2EopYyK9wUwpfOOhgY6IHj4yrtHbq+rt4hXTYGrxuQOS0412uKT
|
|
4wi5wL503+SAQAESQPhWXuVGjauHS1Qqd3aNA3DY+X8CnAweXNEoJKAN/kjH+BBv
|
|
mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
|
|
-----END NEBULA CERTIFICATE-----''';
|
|
|
|
var s = Site(
|
|
name: "DEBUG TEST",
|
|
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: ca)],
|
|
certInfo: CertificateInfo.debug(rawCert: cert),
|
|
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
|
|
);
|
|
|
|
s.key = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
|
|
rmXnR1yvDZi1VPVmnNVY8NMsQpEpbbYlq7rul+ByQvg=
|
|
-----END NEBULA X25519 PRIVATE KEY-----''';
|
|
|
|
var err = await s.save();
|
|
if (err != null) {
|
|
Utils.popError(context, "Failed to save the site", err);
|
|
} else {
|
|
_loadSites();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
_loadSites() async {
|
|
if (Platform.isAndroid) {
|
|
await platform.invokeMethod("android.requestPermissions");
|
|
}
|
|
|
|
//TODO: This can throw, we need to show an error dialog
|
|
Map<String, dynamic> rawSites = jsonDecode(await platform.invokeMethod('listSites'));
|
|
bool hasErrors = false;
|
|
|
|
sites = [];
|
|
rawSites.forEach((id, rawSite) {
|
|
try {
|
|
var site = Site.fromJson(rawSite);
|
|
if (site.errors.length > 0) {
|
|
hasErrors = true;
|
|
}
|
|
|
|
//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");
|
|
}
|
|
|
|
if (hasErrors) {
|
|
Utils.popError(context, "Site Error(s)", "1 or more sites have errors and need your attention, problem sites have a red border.");
|
|
}
|
|
|
|
sites.sort((a, b) {
|
|
return a.sortKey - b.sortKey;
|
|
});
|
|
|
|
setState(() {
|
|
ready = true;
|
|
});
|
|
}
|
|
}
|