port_forward_page.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import 'dart:convert';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_hbb/common.dart';
  5. import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
  6. import 'package:flutter_hbb/models/model.dart';
  7. import 'package:flutter_hbb/models/platform_model.dart';
  8. import 'package:get/get.dart';
  9. const double _kColumn1Width = 30;
  10. const double _kColumn4Width = 100;
  11. const double _kRowHeight = 60;
  12. const double _kTextLeftMargin = 20;
  13. class _PortForward {
  14. int localPort;
  15. String remoteHost;
  16. int remotePort;
  17. _PortForward.fromJson(List<dynamic> json)
  18. : localPort = json[0] as int,
  19. remoteHost = json[1] as String,
  20. remotePort = json[2] as int;
  21. }
  22. class PortForwardPage extends StatefulWidget {
  23. const PortForwardPage({
  24. Key? key,
  25. required this.id,
  26. required this.password,
  27. required this.tabController,
  28. required this.isRDP,
  29. required this.isSharedPassword,
  30. this.forceRelay,
  31. this.connToken,
  32. }) : super(key: key);
  33. final String id;
  34. final String? password;
  35. final DesktopTabController tabController;
  36. final bool isRDP;
  37. final bool? forceRelay;
  38. final bool? isSharedPassword;
  39. final String? connToken;
  40. @override
  41. State<PortForwardPage> createState() => _PortForwardPageState();
  42. }
  43. class _PortForwardPageState extends State<PortForwardPage>
  44. with AutomaticKeepAliveClientMixin {
  45. final TextEditingController localPortController = TextEditingController();
  46. final TextEditingController remoteHostController = TextEditingController();
  47. final TextEditingController remotePortController = TextEditingController();
  48. RxList<_PortForward> pfs = RxList.empty(growable: true);
  49. late FFI _ffi;
  50. @override
  51. void initState() {
  52. super.initState();
  53. _ffi = FFI(null);
  54. _ffi.start(widget.id,
  55. isPortForward: true,
  56. password: widget.password,
  57. isSharedPassword: widget.isSharedPassword,
  58. forceRelay: widget.forceRelay,
  59. connToken: widget.connToken,
  60. isRdp: widget.isRDP);
  61. Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
  62. debugPrint("Port forward page init success with id ${widget.id}");
  63. // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
  64. WidgetsBinding.instance.addPostFrameCallback((_) {
  65. widget.tabController.onSelected?.call(widget.id);
  66. });
  67. }
  68. @override
  69. void dispose() {
  70. _ffi.close();
  71. _ffi.dialogManager.dismissAll();
  72. Get.delete<FFI>(tag: 'pf_${widget.id}');
  73. super.dispose();
  74. }
  75. @override
  76. Widget build(BuildContext context) {
  77. super.build(context);
  78. return Scaffold(
  79. backgroundColor: Theme.of(context).scaffoldBackgroundColor,
  80. body: FutureBuilder(future: () async {
  81. if (!widget.isRDP) {
  82. refreshTunnelConfig();
  83. }
  84. }(), builder: (context, snapshot) {
  85. if (snapshot.connectionState == ConnectionState.done) {
  86. return Container(
  87. decoration: BoxDecoration(
  88. border: Border.all(
  89. width: 20,
  90. color: Theme.of(context).scaffoldBackgroundColor)),
  91. child: Column(
  92. crossAxisAlignment: CrossAxisAlignment.stretch,
  93. children: [
  94. buildPrompt(context),
  95. Flexible(
  96. child: Container(
  97. decoration: BoxDecoration(
  98. color: Theme.of(context).colorScheme.background,
  99. border: Border.all(width: 1, color: MyTheme.border)),
  100. child:
  101. widget.isRDP ? buildRdp(context) : buildTunnel(context),
  102. ),
  103. ),
  104. ],
  105. ),
  106. );
  107. }
  108. return const Offstage();
  109. }),
  110. );
  111. }
  112. buildPrompt(BuildContext context) {
  113. return Obx(() => Offstage(
  114. offstage: pfs.isEmpty && !widget.isRDP,
  115. child: Container(
  116. height: 45,
  117. color: const Color(0xFF007F00),
  118. child: Column(
  119. mainAxisAlignment: MainAxisAlignment.center,
  120. children: [
  121. Text(
  122. translate('Listening ...'),
  123. style: const TextStyle(fontSize: 16, color: Colors.white),
  124. ),
  125. Text(
  126. translate('not_close_tcp_tip'),
  127. style: const TextStyle(
  128. fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2),
  129. )
  130. ])).marginOnly(bottom: 8),
  131. ));
  132. }
  133. buildTunnel(BuildContext context) {
  134. text(String label) => Expanded(
  135. child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
  136. return Theme(
  137. data: Theme.of(context).copyWith(
  138. colorScheme: Theme.of(context).colorScheme,
  139. ),
  140. child: Obx(() => ListView.builder(
  141. controller: ScrollController(),
  142. itemCount: pfs.length + 2,
  143. itemBuilder: ((context, index) {
  144. if (index == 0) {
  145. return Container(
  146. height: 25,
  147. color: Theme.of(context).scaffoldBackgroundColor,
  148. child: Row(children: [
  149. text('Local Port'),
  150. const SizedBox(width: _kColumn1Width),
  151. text('Remote Host'),
  152. text('Remote Port'),
  153. SizedBox(
  154. width: _kColumn4Width, child: Text(translate('Action')))
  155. ]),
  156. );
  157. } else if (index == 1) {
  158. return buildTunnelAddRow(context);
  159. } else {
  160. return buildTunnelDataRow(context, pfs[index - 2], index - 2);
  161. }
  162. }))),
  163. );
  164. }
  165. buildTunnelAddRow(BuildContext context) {
  166. var portInputFormatter = [
  167. FilteringTextInputFormatter.allow(RegExp(
  168. r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
  169. ];
  170. return Container(
  171. height: _kRowHeight,
  172. decoration:
  173. BoxDecoration(color: Theme.of(context).colorScheme.background),
  174. child: Row(children: [
  175. buildTunnelInputCell(context,
  176. controller: localPortController,
  177. inputFormatters: portInputFormatter),
  178. const SizedBox(
  179. width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)),
  180. buildTunnelInputCell(context,
  181. controller: remoteHostController, hint: 'localhost'),
  182. buildTunnelInputCell(context,
  183. controller: remotePortController,
  184. inputFormatters: portInputFormatter),
  185. ElevatedButton(
  186. onPressed: () async {
  187. int? localPort = int.tryParse(localPortController.text);
  188. int? remotePort = int.tryParse(remotePortController.text);
  189. if (localPort != null &&
  190. remotePort != null &&
  191. (remoteHostController.text.isEmpty ||
  192. remoteHostController.text.trim().isNotEmpty)) {
  193. await bind.sessionAddPortForward(
  194. sessionId: _ffi.sessionId,
  195. localPort: localPort,
  196. remoteHost: remoteHostController.text.trim().isEmpty
  197. ? 'localhost'
  198. : remoteHostController.text.trim(),
  199. remotePort: remotePort);
  200. localPortController.clear();
  201. remoteHostController.clear();
  202. remotePortController.clear();
  203. refreshTunnelConfig();
  204. }
  205. },
  206. child: Text(
  207. translate('Add'),
  208. ),
  209. ).marginSymmetric(horizontal: 10),
  210. ]),
  211. );
  212. }
  213. buildTunnelInputCell(BuildContext context,
  214. {required TextEditingController controller,
  215. List<TextInputFormatter>? inputFormatters,
  216. String? hint}) {
  217. return Expanded(
  218. child: Padding(
  219. padding: const EdgeInsets.all(10.0),
  220. child: TextField(
  221. controller: controller,
  222. inputFormatters: inputFormatters,
  223. decoration: InputDecoration(
  224. hintText: hint,
  225. ))),
  226. );
  227. }
  228. Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
  229. text(String label) => Expanded(
  230. child: Text(label, style: const TextStyle(fontSize: 20))
  231. .marginOnly(left: _kTextLeftMargin));
  232. return Container(
  233. height: _kRowHeight,
  234. decoration: BoxDecoration(
  235. color: index % 2 == 0
  236. ? MyTheme.currentThemeMode() == ThemeMode.dark
  237. ? const Color(0xFF202020)
  238. : const Color(0xFFF4F5F6)
  239. : Theme.of(context).colorScheme.background),
  240. child: Row(children: [
  241. text(pf.localPort.toString()),
  242. const SizedBox(width: _kColumn1Width),
  243. text(pf.remoteHost),
  244. text(pf.remotePort.toString()),
  245. SizedBox(
  246. width: _kColumn4Width,
  247. child: IconButton(
  248. icon: const Icon(Icons.close),
  249. onPressed: () async {
  250. await bind.sessionRemovePortForward(
  251. sessionId: _ffi.sessionId, localPort: pf.localPort);
  252. refreshTunnelConfig();
  253. },
  254. ),
  255. ),
  256. ]),
  257. );
  258. }
  259. void refreshTunnelConfig() async {
  260. String peer = bind.mainGetPeerSync(id: widget.id);
  261. Map<String, dynamic> config = jsonDecode(peer);
  262. List<dynamic> infos = config['port_forwards'] as List;
  263. List<_PortForward> result = List.empty(growable: true);
  264. for (var e in infos) {
  265. result.add(_PortForward.fromJson(e));
  266. }
  267. pfs.value = result;
  268. }
  269. buildRdp(BuildContext context) {
  270. text1(String label) => Expanded(
  271. child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
  272. text2(String label) => Expanded(
  273. child: Text(
  274. label,
  275. style: const TextStyle(fontSize: 20),
  276. ).marginOnly(left: _kTextLeftMargin));
  277. return Theme(
  278. data: Theme.of(context)
  279. .copyWith(colorScheme: Theme.of(context).colorScheme),
  280. child: ListView.builder(
  281. controller: ScrollController(),
  282. itemCount: 2,
  283. itemBuilder: ((context, index) {
  284. if (index == 0) {
  285. return Container(
  286. height: 25,
  287. color: Theme.of(context).scaffoldBackgroundColor,
  288. child: Row(children: [
  289. text1('Local Port'),
  290. const SizedBox(width: _kColumn1Width),
  291. text1('Remote Host'),
  292. text1('Remote Port'),
  293. ]),
  294. );
  295. } else {
  296. return Container(
  297. height: _kRowHeight,
  298. decoration: BoxDecoration(
  299. color: Theme.of(context).colorScheme.background),
  300. child: Row(children: [
  301. Expanded(
  302. child: Align(
  303. alignment: Alignment.centerLeft,
  304. child: SizedBox(
  305. width: 120,
  306. child: ElevatedButton(
  307. onPressed: () =>
  308. bind.sessionNewRdp(sessionId: _ffi.sessionId),
  309. child: Text(
  310. translate('New RDP'),
  311. ),
  312. ).marginSymmetric(vertical: 10),
  313. ).marginOnly(left: 20),
  314. ),
  315. ),
  316. const SizedBox(
  317. width: _kColumn1Width,
  318. child: Icon(Icons.arrow_forward_sharp)),
  319. text2('localhost'),
  320. text2('RDP'),
  321. ]),
  322. );
  323. }
  324. })),
  325. );
  326. }
  327. @override
  328. bool get wantKeepAlive => true;
  329. }