yubioath-flutter/lib/desktop/init.dart

412 lines
13 KiB
Dart
Raw Normal View History

2022-10-04 13:12:54 +03:00
/*
2023-03-02 19:11:41 +03:00
* Copyright (C) 2022-2023 Yubico.
2022-10-04 13:12:54 +03:00
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
2023-11-07 12:49:54 +03:00
import 'package:args/args.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
2022-12-20 16:14:14 +03:00
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
2023-02-24 16:17:14 +03:00
import 'package:local_notifier/local_notifier.dart';
import 'package:logging/logging.dart';
2023-03-02 13:10:11 +03:00
import 'package:screen_retriever/screen_retriever.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart';
import '../app/app.dart';
2022-12-20 16:14:14 +03:00
import '../app/logging.dart';
import '../app/message.dart';
2023-03-02 17:42:26 +03:00
import '../app/state.dart';
2022-12-20 16:14:14 +03:00
import '../app/views/app_failure_page.dart';
import '../app/views/main_page.dart';
2022-12-20 16:14:14 +03:00
import '../app/views/message_page.dart';
import '../core/state.dart';
2022-03-15 19:16:14 +03:00
import '../fido/state.dart';
import '../management/state.dart';
2023-03-02 17:42:26 +03:00
import '../oath/state.dart';
2023-11-09 16:09:59 +03:00
import '../otp/state.dart';
2023-12-13 21:35:17 +03:00
import '../piv/state.dart';
2022-12-20 16:14:14 +03:00
import '../version.dart';
2023-03-02 17:42:26 +03:00
import 'devices.dart';
2022-03-15 19:16:14 +03:00
import 'fido/state.dart';
2022-03-04 15:42:10 +03:00
import 'management/state.dart';
import 'oath/state.dart';
2023-11-09 16:09:59 +03:00
import 'otp/state.dart';
2023-12-13 21:35:17 +03:00
import 'piv/state.dart';
import 'qr_scanner.dart';
2023-03-02 17:42:26 +03:00
import 'rpc.dart';
import 'state.dart';
2023-02-24 16:22:36 +03:00
import 'systray.dart';
2023-03-02 17:42:26 +03:00
import 'window_manager_helper/defaults.dart';
import 'window_manager_helper/window_manager_helper.dart';
final _log = Logger('desktop.init');
2023-03-02 13:10:11 +03:00
const String _keyLeft = 'DESKTOP_WINDOW_LEFT';
const String _keyTop = 'DESKTOP_WINDOW_TOP';
const String _keyWidth = 'DESKTOP_WINDOW_WIDTH';
const String _keyHeight = 'DESKTOP_WINDOW_HEIGHT';
2023-11-08 12:34:16 +03:00
const String _logLevel = 'log-level';
const String _logFile = 'log-file';
const String _hidden = 'hidden';
const String _shown = 'shown';
2023-03-02 13:10:11 +03:00
void _saveWindowBounds(WindowManagerHelper helper) async {
final bounds = await helper.getBounds();
await helper.sharedPreferences.setDouble(_keyWidth, bounds.width);
await helper.sharedPreferences.setDouble(_keyHeight, bounds.height);
await helper.sharedPreferences.setDouble(_keyLeft, bounds.left);
await helper.sharedPreferences.setDouble(_keyTop, bounds.top);
2023-03-02 19:18:51 +03:00
_log.debug('Saving window bounds: $bounds');
2023-03-02 13:10:11 +03:00
}
class _ScreenRetrieverListener extends ScreenListener {
final WindowManagerHelper _helper;
2023-03-02 17:42:26 +03:00
2023-03-02 13:10:11 +03:00
_ScreenRetrieverListener(this._helper);
@override
void onScreenEvent(String eventName) async {
_log.debug('Screen event: $eventName');
_saveWindowBounds(_helper);
}
}
2023-02-24 16:10:39 +03:00
class _WindowEventListener extends WindowListener {
2023-03-02 13:10:11 +03:00
final WindowManagerHelper _helper;
2023-03-02 17:42:26 +03:00
2023-03-02 13:10:11 +03:00
_WindowEventListener(this._helper);
@override
void onWindowResize() async {
2023-03-02 13:10:11 +03:00
_log.debug('Window event: onWindowResize');
_saveWindowBounds(_helper);
}
@override
void onWindowMoved() async {
_log.debug('Window event: onWindowMoved');
_saveWindowBounds(_helper);
}
2023-02-24 16:10:39 +03:00
@override
void onWindowClose() async {
if (Platform.isMacOS) {
await windowManager.destroy();
}
}
}
Future<Widget> initialize(List<String> argv) async {
2023-11-08 12:34:16 +03:00
final parser = ArgParser();
parser.addOption(_logFile);
parser.addOption(_logLevel);
parser.addFlag(_hidden);
parser.addFlag(_shown);
final args = parser.parse(argv);
2023-11-07 12:49:54 +03:00
_initLogging(args);
await windowManager.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
2023-03-02 13:10:11 +03:00
final windowManagerHelper = WindowManagerHelper.withPreferences(prefs);
2023-11-07 12:49:54 +03:00
final isHidden = _getIsHidden(args, prefs);
_log.info('Window hidden on startup: $isHidden');
2023-03-02 13:10:11 +03:00
final bounds = Rect.fromLTWH(
2023-03-02 17:42:26 +03:00
prefs.getDouble(_keyLeft) ?? WindowDefaults.bounds.left,
prefs.getDouble(_keyTop) ?? WindowDefaults.bounds.top,
prefs.getDouble(_keyWidth) ?? WindowDefaults.bounds.width,
prefs.getDouble(_keyHeight) ?? WindowDefaults.bounds.height,
2023-03-02 13:10:11 +03:00
);
2023-03-02 19:18:51 +03:00
_log.debug('Using saved window bounds (or defaults): $bounds');
2023-03-02 13:10:11 +03:00
2023-02-24 16:10:39 +03:00
unawaited(windowManager
2024-01-24 15:09:33 +03:00
.waitUntilReadyToShow(
const WindowOptions(minimumSize: WindowDefaults.minSize))
2023-02-24 16:10:39 +03:00
.then((_) async {
2023-03-02 17:42:26 +03:00
await windowManagerHelper.setBounds(bounds);
2023-03-02 13:10:11 +03:00
2023-02-24 16:10:39 +03:00
if (isHidden) {
2024-01-24 15:09:33 +03:00
await windowManager.setSkipTaskbar(true);
2023-02-24 16:10:39 +03:00
} else {
await windowManager.show();
}
2023-03-02 13:10:11 +03:00
windowManager.addListener(_WindowEventListener(windowManagerHelper));
screenRetriever.addListener(_ScreenRetrieverListener(windowManagerHelper));
}));
// Either use the _HELPER_PATH environment variable, or look relative to executable.
var exe = Platform.environment['_HELPER_PATH'];
if (exe?.isEmpty ?? true) {
var relativePath = 'helper/authenticator-helper';
if (Platform.isMacOS) {
2022-05-12 09:34:51 +03:00
relativePath = '../Resources/$relativePath';
} else if (Platform.isWindows) {
relativePath += '.exe';
}
exe = Uri.file(Platform.resolvedExecutable)
.resolve(relativePath)
.toFilePath();
}
2023-10-04 12:08:02 +03:00
// Locate feature flags file
final featureFile = File(Uri.file(Platform.resolvedExecutable)
.resolve('features.json')
.toFilePath());
2022-12-20 16:14:14 +03:00
final rpcFuture = _initHelper(exe!);
_initLicenses();
2023-02-24 16:17:14 +03:00
await localNotifier.setup(
appName: 'Yubico Authenticator',
shortcutPolicy: ShortcutPolicy.ignore,
);
return ProviderScope(
overrides: [
prefProvider.overrideWithValue(prefs),
2022-12-20 16:14:14 +03:00
rpcProvider.overrideWith((_) => rpcFuture),
windowStateProvider.overrideWith(
2022-12-20 16:14:14 +03:00
(ref) => ref.watch(desktopWindowStateProvider),
),
2023-04-27 10:13:38 +03:00
clipboardProvider.overrideWith(
(ref) => ref.watch(desktopClipboardProvider),
),
supportedThemesProvider.overrideWith(
(ref) => ref.watch(desktopSupportedThemesProvider),
),
attachedDevicesProvider.overrideWith(
2022-12-20 16:14:14 +03:00
() => DesktopDevicesNotifier(),
),
currentDeviceProvider.overrideWith(
2022-12-20 16:14:14 +03:00
() => DesktopCurrentDeviceNotifier(),
),
currentDeviceDataProvider.overrideWith(
2022-12-20 16:14:14 +03:00
(ref) => ref.watch(desktopDeviceDataProvider),
),
// OATH
oathStateProvider.overrideWithProvider(desktopOathState.call),
credentialListProvider
.overrideWithProvider(desktopOathCredentialListProvider.call),
qrScannerProvider.overrideWith(
2022-12-20 16:14:14 +03:00
(ref) => ref.watch(desktopQrScannerProvider),
),
// Management
managementStateProvider.overrideWithProvider(desktopManagementState.call),
// FIDO
fidoStateProvider.overrideWithProvider(desktopFidoState.call),
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider.call),
credentialProvider.overrideWithProvider(desktopCredentialProvider.call),
2023-04-27 10:13:38 +03:00
// PIV
pivStateProvider.overrideWithProvider(desktopPivState.call),
pivSlotsProvider.overrideWithProvider(desktopPivSlots.call),
2023-11-09 16:09:59 +03:00
// OTP
otpStateProvider.overrideWithProvider(desktopOtpState.call)
],
2022-05-25 17:10:26 +03:00
child: YubicoAuthenticatorApp(
page: Consumer(
2022-12-20 16:14:14 +03:00
builder: ((context, ref, child) {
2022-05-25 17:10:26 +03:00
// keep RPC log level in sync with app
ref.listen<Level>(logLevelProvider, (_, level) {
2022-12-20 16:14:14 +03:00
ref.read(rpcProvider).valueOrNull?.setLogLevel(level);
2022-05-25 17:10:26 +03:00
});
2023-10-04 12:08:02 +03:00
// Load feature flags, if they exist
featureFile.exists().then(
(exists) async {
if (exists) {
try {
final featureConfig =
jsonDecode(await featureFile.readAsString());
ref
.read(featureFlagProvider.notifier)
.loadConfig(featureConfig);
} catch (error) {
_log.error('Failed to parse feature flags', error);
}
}
},
);
2023-02-24 16:22:36 +03:00
// Initialize systray
ref.watch(systrayProvider);
2022-12-20 16:14:14 +03:00
// Show a loading or error page while the Helper isn't ready
2023-10-04 12:08:02 +03:00
return Consumer(
builder: (context, ref, child) => ref.watch(rpcProvider).when(
data: (data) => const MainPage(),
error: (error, stackTrace) => AppFailurePage(cause: error),
loading: () => _HelperWaiter(),
));
2022-05-25 17:10:26 +03:00
}),
),
),
);
}
2022-12-20 16:14:14 +03:00
Future<RpcSession> _initHelper(String exe) async {
_log.info('Starting Helper subprocess: $exe');
final rpc = RpcSession(exe);
await rpc.initialize();
_log.info('Helper process started');
await rpc.setLogLevel(Logger.root.level);
_log.info('Helper log level set');
return rpc;
}
2023-11-07 12:49:54 +03:00
void _initLogging(ArgResults args) {
2023-11-08 12:34:16 +03:00
final path = args[_logFile];
final levelName = args[_logLevel];
2023-10-18 10:30:31 +03:00
File? file;
2023-11-08 12:34:16 +03:00
if (path != null) {
2023-10-18 10:30:31 +03:00
file = File(path);
}
2023-11-07 12:49:54 +03:00
Logger.root.onRecord.listen((record) {
2023-11-07 12:49:54 +03:00
if (file != null) {
file.writeAsStringSync(
'${record.time.logFormat} [${record.loggerName}] ${record.level}: ${record.message}${Platform.lineTerminator}',
mode: FileMode.append);
if (record.error != null) {
file.writeAsStringSync('${record.error}${Platform.lineTerminator}',
2023-10-18 10:30:31 +03:00
mode: FileMode.append);
}
}
2022-05-30 17:04:50 +03:00
stderr.writeln(
'${record.time.logFormat} [${record.loggerName}] ${record.level}: ${record.message}');
if (record.error != null) {
stderr.writeln(record.error);
}
});
2023-11-08 12:34:16 +03:00
if (levelName != null) {
try {
2022-05-03 12:24:25 +03:00
Level level = Levels.LEVELS
.firstWhere((level) => level.name == levelName.toUpperCase());
Logger.root.level = level;
_log.info('Log level initialized from command line argument');
} catch (error) {
2022-05-03 12:24:25 +03:00
_log.error('Failed to set log level', error);
}
}
_log.info('Logging initialized, outputting to stderr');
}
void _initLicenses() async {
LicenseRegistry.addLicense(() async* {
final python =
await rootBundle.loadString('assets/licenses/raw/python.txt');
yield LicenseEntryWithLineBreaks(['Python'], python);
final zxingcpp =
await rootBundle.loadString('assets/licenses/raw/apache-2.0.txt');
yield LicenseEntryWithLineBreaks(['zxing-cpp'], zxingcpp);
final helper = await rootBundle.loadStructuredData<List>(
'assets/licenses/helper.json',
(value) async => jsonDecode(value),
);
for (final e in helper) {
yield LicenseEntryWithLineBreaks([e['Name']], e['LicenseText']);
}
});
}
2022-12-20 16:14:14 +03:00
2023-11-07 12:49:54 +03:00
bool _getIsHidden(ArgResults args, SharedPreferences prefs) {
bool isHidden = false;
2023-11-08 12:34:16 +03:00
if (args[_hidden] || args[_shown]) {
isHidden = args[_hidden] && !args[_shown];
2023-10-02 11:07:58 +03:00
}
prefs.setBool(windowHidden, isHidden);
return isHidden;
2023-10-02 11:07:58 +03:00
}
2022-12-20 16:14:14 +03:00
class _HelperWaiter extends ConsumerStatefulWidget {
@override
ConsumerState<_HelperWaiter> createState() => _HelperWaiterState();
}
class _HelperWaiterState extends ConsumerState<_HelperWaiter> {
bool slow = false;
late final Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer(const Duration(seconds: 10), () {
setState(() {
slow = true;
});
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (slow) {
2023-02-28 17:02:12 +03:00
final l10n = AppLocalizations.of(context)!;
2022-12-20 16:14:14 +03:00
return MessagePage(
centered: true,
2022-12-20 16:14:14 +03:00
graphic: const CircularProgressIndicator(),
2023-02-28 17:02:12 +03:00
message: l10n.l_helper_not_responding,
actionsBuilder: (context, expanded) => [
2022-12-20 16:14:14 +03:00
ActionChip(
avatar: const Icon(Icons.copy),
2023-12-20 17:09:31 +03:00
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
label: Text(l10n.s_copy_log),
2022-12-20 16:14:14 +03:00
onPressed: () async {
_log.info('Copying log to clipboard ($version)...');
final logs = await ref.read(logLevelProvider.notifier).getLogs();
var clipboard = ref.read(clipboardProvider);
await clipboard.setText(logs.join('\n'));
if (!clipboard.platformGivesFeedback()) {
await ref.read(withContextProvider)(
(context) async {
showMessage(
context,
2023-02-28 17:02:12 +03:00
l10n.l_log_copied,
2022-12-20 16:14:14 +03:00
);
},
);
}
},
),
],
);
} else {
return const MessagePage(
centered: true,
delayedContent: true,
graphic: CircularProgressIndicator(),
2022-12-20 16:14:14 +03:00
);
}
}
}