diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 37b2be6..dc7bccf 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -204,6 +204,10 @@ class ChannelContext { set currentRequest(HttpRequest? request) => putAttribute(AttributeKeys.request, request); + set processInfo(ProcessInfo? processInfo) => putAttribute(AttributeKeys.processInfo, processInfo); + + ProcessInfo? get processInfo => getAttribute(AttributeKeys.processInfo); + StreamSetting? setting; HttpRequest? putStreamRequest(int streamId, HttpRequest request) { diff --git a/lib/network/network.dart b/lib/network/network.dart index 16edb9f..a8b42d3 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -24,6 +24,7 @@ import 'package:network_proxy/network/components/host_filter.dart'; import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/util/attribute_keys.dart'; import 'package:network_proxy/network/util/crts.dart'; +import 'package:network_proxy/network/util/process_info.dart'; import 'package:network_proxy/network/util/tls.dart'; import 'host_port.dart'; @@ -131,10 +132,11 @@ class Server extends Network { void ssl(ChannelContext channelContext, Channel channel, Uint8List data) async { var hostAndPort = channelContext.host; try { - hostAndPort?.scheme = HostAndPort.httpsScheme; if (hostAndPort == null && TLS.getDomain(data) != null) { hostAndPort = HostAndPort.host(TLS.getDomain(data)!, 443); } + + hostAndPort?.scheme = HostAndPort.httpsScheme; channelContext.putAttribute(AttributeKeys.domain, hostAndPort?.host); Channel? remoteChannel = channelContext.serverChannel; @@ -162,6 +164,13 @@ class Server extends Network { var secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data); channel.secureSocket(secureSocket, channelContext); } catch (error, trace) { + try { + channelContext.processInfo = + await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, hostAndPort?.domain ?? 'unknown'); + } catch (ignore) { + /*ignore*/ + } + if (error is HandshakeException) { channelContext.host = hostAndPort; } diff --git a/lib/network/proxy_helper.dart b/lib/network/proxy_helper.dart index b248921..86dc530 100644 --- a/lib/network/proxy_helper.dart +++ b/lib/network/proxy_helper.dart @@ -67,7 +67,8 @@ class ProxyHelper { static exceptionHandler( ChannelContext channelContext, Channel channel, EventListener? listener, HttpRequest? request, error) async { HostAndPort? hostAndPort = channelContext.host; - hostAndPort ??= HostAndPort.host(scheme: HostAndPort.httpScheme, channel.remoteSocketAddress.host, channel.remoteSocketAddress.port); + hostAndPort ??= HostAndPort.host( + scheme: HostAndPort.httpScheme, channel.remoteSocketAddress.host, channel.remoteSocketAddress.port); String message = error.toString(); HttpStatus status = HttpStatus(-1, message); if (error is HandshakeException) { @@ -84,13 +85,7 @@ class ProxyHelper { ..body = message.codeUnits ..headers.contentLength = message.codeUnits.length ..hostAndPort = hostAndPort; - - try { - request.processInfo ??= - await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, request.remoteDomain()!); - } catch (ignore) { - /*ignore*/ - } + request.processInfo ??= channelContext.processInfo; request.response ??= HttpResponse(status) ..headers.contentType = 'text/plain' diff --git a/lib/network/util/attribute_keys.dart b/lib/network/util/attribute_keys.dart index 769ee42..2b473ce 100644 --- a/lib/network/util/attribute_keys.dart +++ b/lib/network/util/attribute_keys.dart @@ -7,4 +7,5 @@ interface class AttributeKeys { static const String request = "REQUEST"; static const String remote = "REMOTE"; static const String proxyInfo = "PROXY_INFO"; + static const String processInfo = "PROCESS_INFO"; } diff --git a/lib/network/util/process_info.dart b/lib/network/util/process_info.dart index c5bda6b..9c91201 100644 --- a/lib/network/util/process_info.dart +++ b/lib/network/util/process_info.dart @@ -5,15 +5,15 @@ import 'dart:typed_data'; import 'package:network_proxy/native/installed_apps.dart'; import 'package:network_proxy/native/process_info.dart'; import 'package:network_proxy/network/util/socket_address.dart'; +import 'package:win32audio/win32audio.dart'; import 'cache.dart'; void main() async { var processInfo = await ProcessInfoUtils.getProcess(512); - print(await processInfo!._getIconPath()); // await ProcessInfoUtils.getMacIcon(processInfo!.path); // print(await ProcessInfoUtils.getProcessByPort(63194)); - print((await ProcessInfoUtils.getProcess(30025))?._getIconPath()); + print(processInfo); } class ProcessInfoUtils { @@ -29,29 +29,49 @@ class ProcessInfoUtils { return null; } + var pid = await _getPid(socketAddress); + if (pid == null) return null; + + String cacheKey = "$cacheKeyPre:$pid"; + var processInfo = processInfoCache.get(cacheKey); + if (processInfo != null) return processInfo; + + processInfo = await getProcess(pid); + processInfoCache.set(cacheKey, processInfo!); + return processInfo; + } + + // 获取进程 ID + static Future _getPid(InetSocketAddress socketAddress) async { + if (Platform.isWindows) { + var result = await Process.run('cmd', ['/c', 'netstat -ano | findstr :${socketAddress.port}']); + var lines = LineSplitter.split(result.stdout); + for (var line in lines) { + var parts = line.trim().split(RegExp(r'\s+')); + if (parts.length < 5) { + continue; + } + if (parts[1].trim().contains("${socketAddress.host}:${socketAddress.port}")) { + return int.tryParse(parts[4]); + } + } + return null; + } + if (Platform.isMacOS) { - var results = await Process.run('bash', [ - '-c', - _concatCommands(['lsof -nP -iTCP:${socketAddress.port} |grep "${socketAddress.port}->"']) - ]); + var results = + await Process.run('bash', ['-c', 'lsof -nP -iTCP:${socketAddress.port} |grep "${socketAddress.port}->"']); - if (results.exitCode == 0) { - var lines = LineSplitter.split(results.stdout); + if (results.exitCode != 0) { + return null; + } - for (var line in lines) { - var parts = line.trim().split(RegExp(r'\s+')); - if (parts.length >= 9) { - var pid = int.tryParse(parts[1]); - if (pid != null) { - String cacheKey = "$cacheKeyPre:$pid"; - var processInfo = processInfoCache.get(cacheKey); - if (processInfo != null) return processInfo; + var lines = LineSplitter.split(results.stdout); - processInfo = await getProcess(pid); - processInfoCache.set(cacheKey, processInfo!); - return processInfo; - } - } + for (var line in lines) { + var parts = line.trim().split(RegExp(r'\s+')); + if (parts.length >= 9) { + return int.tryParse(parts[1]); } } } @@ -59,11 +79,18 @@ class ProcessInfoUtils { } static Future getProcess(int pid) async { + if (Platform.isWindows) { + // 获取应用路径 + var result = await Process.run('cmd', ['/c', 'wmic process where processid=$pid get ExecutablePath']); + var output = result.stdout.toString(); + var path = output.split('\n')[1].trim(); + print(output); + String name = path.substring(path.lastIndexOf('\\') + 1); + return ProcessInfo(name, name.split(".")[0], path); + } + if (Platform.isMacOS) { - var results = await Process.run('bash', [ - '-c', - _concatCommands(['ps -p $pid -o pid= -o comm=']) - ]); + var results = await Process.run('bash', ['-c', 'ps -p $pid -o pid= -o comm=']); if (results.exitCode == 0) { var lines = LineSplitter.split(results.stdout); for (var line in lines) { @@ -80,10 +107,6 @@ class ProcessInfoUtils { return null; } - - static _concatCommands(List commands) { - return commands.where((element) => element.isNotEmpty).join(' && '); - } } class ProcessInfo { @@ -101,10 +124,6 @@ class ProcessInfo { return ProcessInfo(json['id'], json['name'], json['path']); } - Future _getIconPath() async { - return _getMacIcon(path); - } - Future getIcon() async { if (icon != null) return icon!; if (_iconCache.get(id) != null) return _iconCache.get(id)!; @@ -114,10 +133,15 @@ class ProcessInfo { icon = (await InstalledApps.getAppInfo(id)).icon; } + if (Platform.isWindows) { + icon = await _getWindowsIcon(path); + } + if (Platform.isMacOS) { - var macIcon = await _getIconPath(); + var macIcon = await _getMacIcon(path); icon = await File(macIcon).readAsBytes(); } + icon = icon ?? Uint8List(0); _iconCache.set(id, icon); } catch (e) { @@ -126,6 +150,10 @@ class ProcessInfo { return icon!; } + Future _getWindowsIcon(String path) async { + return await WinIcons().extractFileIcon(path); + } + static Future _getMacIcon(String path) async { var xml = await File('$path/Contents/Info.plist').readAsString(); var key = "CFBundleIconFile"; diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index 7c29dea..2b994de 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -197,7 +197,7 @@ class NetworkTabState extends State with SingleTickerProvi const SizedBox(height: 20), rowWidget("Remote Address", response?.remoteAddress), const SizedBox(height: 20), - rowWidget("Request Time", request.requestTime.toString()), + rowWidget("Request Time", request.requestTime.format()), const SizedBox(height: 20), rowWidget("Duration", response?.costTime()), const SizedBox(height: 20), @@ -209,6 +209,10 @@ class NetworkTabState extends State with SingleTickerProvi const SizedBox(height: 20), rowWidget("Response Package", getPackage(response)), ]; + if (request.processInfo != null) { + content.add(const SizedBox(height: 20)); + content.add(rowWidget("App", request.processInfo!.name)); + } return ListView(children: [expansionTile("General", content)]); } diff --git a/lib/ui/desktop/left/list.dart b/lib/ui/desktop/left/list.dart index 6dd8bdc..7bed6b6 100644 --- a/lib/ui/desktop/left/list.dart +++ b/lib/ui/desktop/left/list.dart @@ -126,6 +126,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM //按照域名分类 DomainRequests domainRequests = getDomainRequests(request); var isNew = domainRequests.body.isEmpty; + domainRequests.addRequest(request.requestId, request); //搜索视图 if (searchModel?.isNotEmpty == true && searchModel?.filter(request, null) == true) { @@ -142,14 +143,10 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM DomainRequests getDomainRequests(HttpRequest request) { var host = request.remoteDomain()!; DomainRequests? domainRequests = containerMap[host]; - var processInfo = request.processInfo; if (domainRequests == null) { domainRequests = DomainRequests(host, proxyServer: widget.panel.proxyServer, - trailing: processInfo == null - ? null - : futureWidget(processInfo.getIcon(), - (data) => SizedBox(width: 25, child: data.isEmpty ? null : Image.memory(data))), + trailing: appIcon(request), onDelete: deleteHost, onRequestRemove: (req) => container.remove(req)); containerMap[host] = domainRequests; @@ -158,6 +155,18 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM return domainRequests; } + Widget? appIcon(HttpRequest request) { + var processInfo = request.processInfo; + if (processInfo == null) { + return null; + } + + return futureWidget( + processInfo.getIcon(), + (data) => + data.isEmpty ? const SizedBox() : Image.memory(data, width: 23, height: Platform.isWindows ? 16 : null)); + } + ///移除域名 deleteHost(String host) { setState(() { @@ -250,6 +259,11 @@ class DomainRequests extends StatefulWidget { return requestMap[response.request?.requestId ?? response.requestId]; } + setTrailing(Widget? trailing) { + var state = key as GlobalKey<_DomainRequestsState>; + state.currentState?.trailing = trailing; + } + _remove(RequestWidget requestWidget) { if (body.remove(requestWidget)) { onRequestRemove?.call(requestWidget.request); @@ -298,6 +312,7 @@ class _DomainRequestsState extends State { final GlobalKey transitionState = GlobalKey(); late Configuration configuration; late bool selected; + Widget? trailing; AppLocalizations get localizations => AppLocalizations.of(context)!; @@ -306,6 +321,7 @@ class _DomainRequestsState extends State { super.initState(); configuration = widget.proxyServer.configuration; selected = widget.selected; + trailing = widget.trailing; } changeState() { @@ -327,7 +343,7 @@ class _DomainRequestsState extends State { child: ListTile( minLeadingWidth: 25, leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18), - trailing: widget.trailing, + trailing: trailing, dense: true, horizontalTitleGap: 0, contentPadding: const EdgeInsets.only(left: 3, right: 8), diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart index b89fbbd..1376138 100644 --- a/lib/ui/desktop/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -69,7 +69,7 @@ class _ToolbarState extends State { return Row( children: [ Padding(padding: EdgeInsets.only(left: Platform.isMacOS ? 80 : 30)), - SocketLaunch(proxyServer: widget.proxyServer), + SocketLaunch(proxyServer: widget.proxyServer, startup: widget.proxyServer.configuration.startup), const Padding(padding: EdgeInsets.only(left: 20)), IconButton( tooltip: localizations.clear, diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index b4c91f6..4e6d7c5 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -233,7 +233,7 @@ class MobileHomeState extends State implements EventListener, Li '4. The script supports log viewing, output through console.log();\n' '5. Setting Auto Start Recording Traffic;\n' '6. Android certificate download optimization; \n'; - showAlertDialog(isCN ? '更新内容V1.0.9-Beta' : "Update content V1.0.9-Beta", content, () { + showAlertDialog(isCN ? '更新内容V1.0.9' : "Update content V1.0.9", content, () { widget.appConfiguration.upgradeNoticeV9 = false; widget.appConfiguration.flushConfig(); }); diff --git a/pubspec.lock b/pubspec.lock index 4438d7e..71f89d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -811,6 +811,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.3.0" + win32audio: + dependency: "direct main" + description: + name: win32audio + sha256: "30d663f393c41c0dc6de956f56f4b98c3942b8b1002943c75fac1a6861a6d368" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index eeadb3b..3679f7c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_code_editor: file_picker: ^8.0.0 flutter_desktop_context_menu: ^0.2.0 + win32audio: ^1.3.1 # video_player: # git: # url: https://github.com/icapps/plugins.git diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5101675..471e914 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + Win32audioPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Win32audioPluginCApi")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b78b3a9..ac28324 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever share_plus url_launcher_windows + win32audio window_manager )