diff --git a/bruig/flutterui/bruig/lib/config.dart b/bruig/flutterui/bruig/lib/config.dart index 0b256512..012c425f 100644 --- a/bruig/flutterui/bruig/lib/config.dart +++ b/bruig/flutterui/bruig/lib/config.dart @@ -239,7 +239,7 @@ class Config { } // replaceConfig replaces the settings that can be modified by the GUI, while -// preserving manual chages made to the config file. +// preserving manual changes made to the config file. Future replaceConfig( String filepath, { String? debugLevel, @@ -250,6 +250,16 @@ Future replaceConfig( String? proxyPassword, int? torCircuitLimit, bool? torIsolation, + String? jsonRPCListen, + String? rpcCertPath, + String? rpcKeyPath, + String? rpcClientCApath, + String? rpcUser, + String? rpcPass, + String? rpcAuthMode, + bool? rpcIssueClientCert, + bool? rpcAllowRemoteSendTip, + double? rpcMaxRemoteSendTipAmt, }) async { var f = ini.Config.fromStrings(File(filepath).readAsLinesSync()); @@ -271,6 +281,11 @@ Future replaceConfig( set(section, opt, "$val"); } + void setDouble(String section, String opt, double? val) { + if (val == null) return; + set(section, opt, "$val"); + } + set("log", "debuglevel", debugLevel); setBool("log", "pings", logPings); set("payment", "lndebuglevel", lnDebugLevel); @@ -281,6 +296,18 @@ Future replaceConfig( setInt("default", "circuitlimit", torCircuitLimit); setBool("default", "torisolation", torIsolation); + // RPC settings + set("clientrpc", "jsonrpclisten", jsonRPCListen); + set("clientrpc", "rpccertpath", rpcCertPath); + set("clientrpc", "rpckeypath", rpcKeyPath); + set("clientrpc", "rpcclientcapath", rpcClientCApath); + set("clientrpc", "rpcuser", rpcUser); + set("clientrpc", "rpcpass", rpcPass); + set("clientrpc", "rpcauthmode", rpcAuthMode); + setBool("clientrpc", "rpcissueclientcert", rpcIssueClientCert); + setBool("clientrpc", "rpcallowremotesendtip", rpcAllowRemoteSendTip); + setDouble("clientrpc", "rpcmaxremotesendtipamt", rpcMaxRemoteSendTipAmt); + await File(filepath).writeAsString(f.toString()); } @@ -427,7 +454,7 @@ Future loadConfig(String filepath) async { c.jsonRPCListen = getCommaList("clientrpc", "jsonrpclisten") ?? [""]; // default to [""] c.rpcCertPath = f.get("clientrpc", "rpccertpath") ?? ""; c.rpcKeyPath = f.get("clientrpc", "rpckeypath") ?? ""; - c.rpcIssueClientCert = f.get("clientrpc", "rpcissueclientcert") == "true"; + c.rpcIssueClientCert = getBool("clientrpc", "rpcissueclientcert"); c.rpcClientCApath = f.get("clientrpc", "rpcclientcapath") ?? ""; c.rpcUser = f.get("clientrpc", "rpcuser") ?? ""; c.rpcPass = f.get("clientrpc", "rpcpass") ?? ""; diff --git a/bruig/flutterui/bruig/lib/models/newconfig.dart b/bruig/flutterui/bruig/lib/models/newconfig.dart index bedd9e23..aeabd1c0 100644 --- a/bruig/flutterui/bruig/lib/models/newconfig.dart +++ b/bruig/flutterui/bruig/lib/models/newconfig.dart @@ -49,22 +49,38 @@ class NewConfigModel extends ChangeNotifier { LNNodeType nodeType = LNNodeType.internal; NetworkType netType = NetworkType.mainnet; - String rpcHost = ""; - String tlsCertPath = ""; - String macaroonPath = ""; + // default properties String serverAddr = ""; String newWalletSeed = ""; bool advancedSetup = false; + + // LN configuration properties + String rpcHost = ""; + String tlsCertPath = ""; + String macaroonPath = ""; List seedToRestore = []; Uint8List? multichanBackupRestore; List confirmSeedWords = []; + // Network configuration properties String proxyAddr = ""; String proxyUser = ""; String proxyPassword = ""; int torCircuitLimit = 32; bool torIsolation = false; + // RPC configuration properties + List jsonRPCListen = [""]; + String rpcCertPath = ""; + String rpcKeyPath = ""; + String rpcClientCApath = ""; + String rpcUser = ""; + String rpcPass = ""; + String rpcAuthMode = ""; + bool rpcIssueClientCert = false; + bool rpcAllowRemoteSendTip = false; + double rpcMaxRemoteSendTipAmt = 0.0; + Future tryExternalDcrlnd( String host, String tlsPath, String macaroonPath) async { var res = await Golib.lnTryExternalDcrlnd(host, tlsPath, macaroonPath); @@ -90,6 +106,18 @@ class NewConfigModel extends ChangeNotifier { proxyPassword: proxyPassword, circuitLimit: torCircuitLimit, torIsolation: torIsolation, + + // RPC configuration settings + jsonRPCListen: jsonRPCListen, + rpcCertPath: rpcCertPath, + rpcKeyPath: rpcKeyPath, + rpcClientCApath: rpcClientCApath, + rpcUser: rpcUser, + rpcPass: rpcPass, + rpcAuthMode: rpcAuthMode, + rpcIssueClientCert: rpcIssueClientCert, + rpcAllowRemoteSendTip: rpcAllowRemoteSendTip, + rpcMaxRemoteSendTipAmt: rpcMaxRemoteSendTipAmt, ); await cfg.saveNewConfig(await configFileName(appArgs)); cfg = await configFromArgs(appArgs); // Reload to fill defaults. @@ -107,7 +135,7 @@ class NewConfigModel extends ChangeNotifier { return cfg; } - List createConfirmSeedWords(String seed) { + List createConfirmSeedWords(String seed) { List confirmSeedWords = []; var seedWords = seed.trim().split(' '); var numWords = 5; diff --git a/bruig/flutterui/bruig/lib/screens/config_rpc.dart b/bruig/flutterui/bruig/lib/screens/config_rpc.dart new file mode 100644 index 00000000..a3aa399d --- /dev/null +++ b/bruig/flutterui/bruig/lib/screens/config_rpc.dart @@ -0,0 +1,202 @@ +import 'package:bruig/components/buttons.dart'; +import 'package:bruig/components/confirmation_dialog.dart'; +import 'package:bruig/components/text.dart'; +import 'package:bruig/config.dart'; +import 'package:bruig/models/newconfig.dart'; +import 'package:bruig/screens/shutdown.dart'; +import 'package:bruig/screens/startupscreen.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class RpcConfigScreen extends StatefulWidget { + static const String routeName = "/rpcConfig"; + final NewConfigModel? newConf; + const RpcConfigScreen({this.newConf, super.key}); + + @override + State createState() => _RpcConfigScreenState(); +} + +class _RpcConfigScreenState extends State { + NewConfigModel? get newConfigModel => widget.newConf; + TextEditingController rpcListenCtrl = TextEditingController(); + TextEditingController rpcCertPathCtrl = TextEditingController(); + TextEditingController rpcKeyPathCtrl = TextEditingController(); + TextEditingController rpcClientCACtrl = TextEditingController(); + TextEditingController rpcUserCtrl = TextEditingController(); + TextEditingController rpcPassCtrl = TextEditingController(); + TextEditingController rpcAuthModeCtrl = TextEditingController(); + TextEditingController rpcMaxRemoteSendTipAmtCtrl = TextEditingController(); + bool rpcIssueClientCert = false; + bool rpcAllowRemoteSendTip = false; + + void doRestart() { + ShutdownScreen.startShutdown(context, restart: true); + Navigator.pop(context); + } + + void changeConfig() async { + await replaceConfig( + mainConfigFilename, + jsonRPCListen: rpcListenCtrl.text, + rpcCertPath: rpcCertPathCtrl.text, + rpcKeyPath: rpcKeyPathCtrl.text, + rpcClientCApath: rpcClientCACtrl.text, + rpcUser: rpcUserCtrl.text, + rpcPass: rpcPassCtrl.text, + rpcAuthMode: rpcAuthModeCtrl.text, + rpcIssueClientCert: rpcIssueClientCert, + rpcAllowRemoteSendTip: rpcAllowRemoteSendTip, + rpcMaxRemoteSendTipAmt: + double.tryParse(rpcMaxRemoteSendTipAmtCtrl.text) ?? 0, + ); + if (!mounted) return; + confirmationDialog( + context, + doRestart, + "Restart App?", + "App restart is required to apply RPC settings changes.", + "Restart", + "Cancel", + onCancel: () { + Navigator.of(context).pop(); + }, + ); + } + + void confirmAcceptChanges() { + if (newConfigModel != null) { + var newConfigModel = Provider.of(context, listen: false); + newConfigModel.jsonRPCListen = + rpcListenCtrl.text.split(',').map((addr) => addr.trim()).toList(); + newConfigModel.rpcCertPath = rpcCertPathCtrl.text; + newConfigModel.rpcKeyPath = rpcKeyPathCtrl.text; + newConfigModel.rpcClientCApath = rpcClientCACtrl.text; + newConfigModel.rpcUser = rpcUserCtrl.text; + newConfigModel.rpcPass = rpcPassCtrl.text; + newConfigModel.rpcAuthMode = rpcAuthModeCtrl.text; + newConfigModel.rpcIssueClientCert = rpcIssueClientCert; + newConfigModel.rpcAllowRemoteSendTip = rpcAllowRemoteSendTip; + newConfigModel.rpcMaxRemoteSendTipAmt = + double.tryParse(rpcMaxRemoteSendTipAmtCtrl.text) ?? 0; + Navigator.of(context).pop(); + return; + } + + confirmationDialog( + context, + changeConfig, + onCancel: () => Navigator.of(context).pop(), + "Change Config?", + "Change RPC config? To apply the changes, the app will require a restart.", + "Accept", + "Cancel"); + } + + void readConfig() async { + if (newConfigModel != null) { + setState(() { + rpcListenCtrl.text = newConfigModel!.jsonRPCListen.join(", "); + rpcCertPathCtrl.text = newConfigModel!.rpcCertPath; + rpcKeyPathCtrl.text = newConfigModel!.rpcKeyPath; + rpcClientCACtrl.text = newConfigModel!.rpcClientCApath; + rpcUserCtrl.text = newConfigModel!.rpcUser; + rpcPassCtrl.text = newConfigModel!.rpcPass; + rpcAuthModeCtrl.text = newConfigModel!.rpcAuthMode; + rpcIssueClientCert = newConfigModel!.rpcIssueClientCert; + rpcAllowRemoteSendTip = newConfigModel!.rpcAllowRemoteSendTip; + rpcMaxRemoteSendTipAmtCtrl.text = + newConfigModel!.rpcMaxRemoteSendTipAmt.toString(); + }); + return; + } + + var cfg = await loadConfig(mainConfigFilename); + setState(() { + rpcListenCtrl.text = cfg.jsonRPCListen.join(", "); + rpcCertPathCtrl.text = cfg.rpcCertPath; + rpcKeyPathCtrl.text = cfg.rpcKeyPath; + rpcClientCACtrl.text = cfg.rpcClientCApath; + rpcUserCtrl.text = cfg.rpcUser; + rpcPassCtrl.text = cfg.rpcPass; + rpcAuthModeCtrl.text = cfg.rpcAuthMode; + rpcIssueClientCert = cfg.rpcIssueClientCert; + rpcAllowRemoteSendTip = cfg.rpcAllowRemoteSendTip; + rpcMaxRemoteSendTipAmtCtrl.text = cfg.rpcMaxRemoteSendTipAmt.toString(); + }); + } + + @override + void initState() { + super.initState(); + readConfig(); + } + + @override + Widget build(BuildContext context) { + return StartupScreen(childrenWidth: 400, [ + const Txt.H("Configure RPC Options"), + const SizedBox(height: 20), + TextField( + controller: rpcListenCtrl, + decoration: const InputDecoration( + labelText: "JSON-RPC Listen Address", + hintText: "127.0.0.1:7676")), + const SizedBox(height: 10), + TextField( + controller: rpcCertPathCtrl, + decoration: const InputDecoration( + labelText: "RPC Certificate Path", hintText: "/path/to/cert")), + const SizedBox(height: 10), + TextField( + controller: rpcKeyPathCtrl, + decoration: const InputDecoration( + labelText: "RPC Key Path", hintText: "/path/to/key")), + const SizedBox(height: 10), + TextField( + controller: rpcClientCACtrl, + decoration: const InputDecoration( + labelText: "RPC Client CA Path", hintText: "/path/to/ca")), + const SizedBox(height: 10), + TextField( + controller: rpcUserCtrl, + decoration: const InputDecoration( + labelText: "RPC Username", hintText: "rpcuser")), + const SizedBox(height: 10), + TextField( + controller: rpcPassCtrl, + decoration: const InputDecoration( + labelText: "RPC Password", hintText: "rpcpass")), + const SizedBox(height: 10), + TextField( + controller: rpcAuthModeCtrl, + decoration: const InputDecoration( + labelText: "RPC Auth Mode", hintText: "authmode")), + const SizedBox(height: 10), + TextField( + keyboardType: TextInputType.number, + controller: rpcMaxRemoteSendTipAmtCtrl, + decoration: const InputDecoration( + labelText: "Max Remote Send Tip Amount", hintText: "0.0")), + const SizedBox(height: 20), + SwitchListTile( + title: const Text("Issue Client Certificate"), + value: rpcIssueClientCert, + onChanged: (value) => setState(() => rpcIssueClientCert = value), + ), + const SizedBox(height: 10), + SwitchListTile( + title: const Text("Allow Remote Send Tip"), + value: rpcAllowRemoteSendTip, + onChanged: (value) => setState(() => rpcAllowRemoteSendTip = value), + ), + const SizedBox(height: 30), + Wrap(runSpacing: 10, children: [ + OutlinedButton( + onPressed: confirmAcceptChanges, child: const Text("Accept")), + const SizedBox(width: 50), + CancelButton(onPressed: () => Navigator.pop(context)), + ]), + ]); + } +} diff --git a/bruig/flutterui/bruig/lib/screens/settings.dart b/bruig/flutterui/bruig/lib/screens/settings.dart index 5f4832aa..3e93b530 100644 --- a/bruig/flutterui/bruig/lib/screens/settings.dart +++ b/bruig/flutterui/bruig/lib/screens/settings.dart @@ -12,6 +12,7 @@ import 'package:bruig/models/uistate.dart'; import 'package:bruig/notification_service.dart'; import 'package:bruig/screens/config_network.dart'; import 'package:bruig/screens/list_kxs.dart'; +import 'package:bruig/screens/config_rpc.dart'; import 'package:bruig/screens/ln_management.dart'; import 'package:bruig/screens/log.dart'; import 'package:bruig/screens/manage_content/manage_content.dart'; @@ -60,6 +61,16 @@ class _SettingsScreenState extends State { ClientModel get client => widget.client; bool loading = false; String settingsPage = "main"; + bool showRPCWarning = true; + + void loadSettings() async { + var showWarning = await StorageManager.readBool( + StorageManager.showRPCWarningKey, + defaultVal: true); + setState(() { + showRPCWarning = showWarning; + }); + } void connStateChanged() async { setState(() {}); @@ -148,6 +159,7 @@ class _SettingsScreenState extends State { @override void initState() { super.initState(); + loadSettings(); client.connState.addListener(connStateChanged); } @@ -168,6 +180,65 @@ class _SettingsScreenState extends State { super.dispose(); } + void showRpcWarningDialog() { + if (!showRPCWarning) { + changePage("RPC"); + return; + } + + bool turnOffAlert = false; + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: const Text("Allow JSON RPC Access"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Enabling JSON RPC allows connections from third-party applications. Are you sure you want to proceed?", + ), + Row( + children: [ + Checkbox( + value: turnOffAlert, + onChanged: (bool? value) { + setDialogState(() => turnOffAlert = value ?? false); + }, + ), + const Text("Don’t show this message again"), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + // Update the setting if the user chose to disable future alerts + if (turnOffAlert) { + StorageManager.saveBool( + StorageManager.showRPCWarningKey, false); + setState(() => showRPCWarning = false); + } + setState(() => settingsPage = "RPC"); + }, + child: const Text("Continue"), + ), + ], + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { bool isScreenSmall = checkIsScreenSmall(context); @@ -190,6 +261,9 @@ class _SettingsScreenState extends State { case "Network": settingsView = NetworkSettingsScreen(client); break; + case "RPC": + settingsView = const RpcConfigScreen(); + break; case "About": settingsView = const AboutScreen(settings: true); break; @@ -236,6 +310,11 @@ class _SettingsScreenState extends State { title: const Txt.S("Audio"), onTap: () => changePage("Audio"), ), + ListTile( + selected: settingsPage == "RPC", + title: const Txt.S("RPC"), + onTap: () => showRpcWarningDialog(), + ), ]), Expanded(child: settingsView), ]); diff --git a/bruig/flutterui/bruig/lib/storage_manager.dart b/bruig/flutterui/bruig/lib/storage_manager.dart index f928b6b3..a0149790 100644 --- a/bruig/flutterui/bruig/lib/storage_manager.dart +++ b/bruig/flutterui/bruig/lib/storage_manager.dart @@ -16,6 +16,7 @@ class StorageManager { static const String notifiedGCUnkxdMembers = "notifiedGCUnkdMembers"; static const String audioCaptureDeviceIdKey = "audioCaptureDeviceId"; static const String audioPlaybackDeviceIdKey = "audioPlaybackDeviceId"; + static const String showRPCWarningKey = "showRPCWarning"; static Future saveData(String key, dynamic value) async { final prefs = await SharedPreferences.getInstance(); @@ -71,5 +72,10 @@ class StorageManager { null) { await StorageManager.saveData(StorageManager.notificationsKey, true); } + if ((await StorageManager.readData(StorageManager.showRPCWarningKey) + as bool?) == + null) { + await StorageManager.saveData(StorageManager.showRPCWarningKey, true); + } } }