Repository: wanghongenpin/proxypin Branch: main Commit: 8ab517bb3835 Files: 435 Total size: 2.5 MB Directory structure: gitextract_xlm06j4i/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.yml │ ├── feature_request.yml │ └── 功能请求.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── README_CN.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── network/ │ │ │ │ └── proxy/ │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── ProxyVpnService.kt │ │ │ │ ├── VpnAlertDialog.kt │ │ │ │ ├── plugin/ │ │ │ │ │ ├── AndroidFlutterPlugin.kt │ │ │ │ │ ├── AppInfo.kt │ │ │ │ │ ├── AppLifecyclePlugin.kt │ │ │ │ │ ├── InstalledAppsPlugin.kt │ │ │ │ │ ├── PictureInPicturePlugin.kt │ │ │ │ │ ├── ProcessInfoPlugin.kt │ │ │ │ │ └── VpnServicePlugin.kt │ │ │ │ └── vpn/ │ │ │ │ ├── Connection.kt │ │ │ │ ├── ConnectionHandler.kt │ │ │ │ ├── ConnectionManager.kt │ │ │ │ ├── Protocol.java │ │ │ │ ├── ProxyVpnThread.kt │ │ │ │ ├── Tag.kt │ │ │ │ ├── socket/ │ │ │ │ │ ├── ClientPacketWriter.kt │ │ │ │ │ ├── CloseableConnection.kt │ │ │ │ │ ├── Constant.kt │ │ │ │ │ ├── ProtectSocket.kt │ │ │ │ │ ├── ProtectSocketHolder.kt │ │ │ │ │ ├── SocketChannelReader.java │ │ │ │ │ ├── SocketChannelWriter.java │ │ │ │ │ └── SocketNIODataService.java │ │ │ │ ├── transport/ │ │ │ │ │ ├── Packet.kt │ │ │ │ │ ├── icmp/ │ │ │ │ │ │ ├── ICMPPacket.java │ │ │ │ │ │ └── ICMPPacketFactory.java │ │ │ │ │ └── protocol/ │ │ │ │ │ ├── IP4Header.kt │ │ │ │ │ ├── TCPHeader.kt │ │ │ │ │ ├── TCPPacketFactory.kt │ │ │ │ │ ├── TransportHeader.kt │ │ │ │ │ └── UDPHeader.kt │ │ │ │ └── util/ │ │ │ │ ├── PacketUtil.kt │ │ │ │ ├── ProcessInfoManager.kt │ │ │ │ ├── SimpleCache.kt │ │ │ │ └── TLS.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ ├── values/ │ │ │ │ └── styles.xml │ │ │ └── values-night/ │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── assets/ │ ├── certs/ │ │ ├── ca.crt │ │ ├── ca_key.pem │ │ └── ca_private.der │ └── js/ │ └── fetch.js ├── devtools_options.yaml ├── distribute_options.yaml ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── ProxyPin/ │ │ ├── Info.plist │ │ ├── PacketTunnelProvider.swift │ │ ├── ProxyPin-Bridging-Header.h │ │ ├── ProxyPin.entitlements │ │ └── vpn/ │ │ ├── Connection.swift │ │ ├── ConnectionHandler.swift │ │ ├── ConnectionManager.swift │ │ ├── NWProtocol.swift │ │ ├── ProxyVpnService.swift │ │ ├── QueueFactory.swift │ │ ├── ping/ │ │ │ ├── GBPing.h │ │ │ ├── GBPing.m │ │ │ ├── GBPingHelper.swift │ │ │ ├── GBPingSummary.h │ │ │ ├── GBPingSummary.m │ │ │ └── ICMPHeader.h │ │ ├── socket/ │ │ │ ├── ClientPacketWriter.swift │ │ │ ├── CloseableConnection.swift │ │ │ └── SocketIOService.swift │ │ ├── transport/ │ │ │ ├── Packet.swift │ │ │ └── protocol/ │ │ │ ├── ICMPPacket.swift │ │ │ ├── IP4Header.swift │ │ │ ├── TCPHeader.swift │ │ │ ├── TCPPacketFactory.swift │ │ │ ├── TransportHeader.swift │ │ │ └── UDPHeader.swift │ │ └── utils/ │ │ ├── PacketUtil.swift │ │ └── TLS.swift │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── AudioManager.swift │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Handlers/ │ │ │ └── MethodHandler.swift │ │ ├── Info.plist │ │ ├── Runner-Bridging-Header.h │ │ ├── Runner.entitlements │ │ ├── VpnManager.swift │ │ ├── en.lproj/ │ │ │ └── InfoPlist.strings │ │ ├── pip/ │ │ │ ├── PictureInPictureManager.swift │ │ │ └── PictureInPictureView.swift │ │ └── zh-Hans.lproj/ │ │ ├── InfoPlist.strings │ │ ├── LaunchScreen.strings │ │ └── Main.strings │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── RunnerTests/ │ └── RunnerTests.swift ├── l10n.yaml ├── lib/ │ ├── l10n/ │ │ ├── app_en.arb │ │ ├── app_localizations.dart │ │ ├── app_localizations_en.dart │ │ ├── app_localizations_zh.dart │ │ ├── app_zh.arb │ │ └── app_zh_Hant.arb │ ├── main.dart │ ├── native/ │ │ ├── app_lifecycle.dart │ │ ├── installed_apps.dart │ │ ├── native_method.dart │ │ ├── pip.dart │ │ ├── process_info.dart │ │ └── vpn.dart │ ├── network/ │ │ ├── bin/ │ │ │ ├── configuration.dart │ │ │ ├── listener.dart │ │ │ └── server.dart │ │ ├── channel/ │ │ │ ├── channel.dart │ │ │ ├── channel_context.dart │ │ │ ├── channel_dispatcher.dart │ │ │ ├── host_port.dart │ │ │ └── network.dart │ │ ├── components/ │ │ │ ├── host_filter.dart │ │ │ ├── hosts.dart │ │ │ ├── interceptor.dart │ │ │ ├── js/ │ │ │ │ ├── file.dart │ │ │ │ ├── md5.dart │ │ │ │ ├── script_engine.dart │ │ │ │ └── xhr.dart │ │ │ ├── manager/ │ │ │ │ ├── hosts_manager.dart │ │ │ │ ├── report_server_manager.dart │ │ │ │ ├── request_block_manager.dart │ │ │ │ ├── request_breakpoint_manager.dart │ │ │ │ ├── request_crypto_manager.dart │ │ │ │ ├── request_map_manager.dart │ │ │ │ ├── request_rewrite_manager.dart │ │ │ │ ├── rewrite_rule.dart │ │ │ │ └── script_manager.dart │ │ │ ├── report_server_interceptor.dart │ │ │ ├── request_block.dart │ │ │ ├── request_breakpoint.dart │ │ │ ├── request_map.dart │ │ │ ├── request_rewrite.dart │ │ │ └── script.dart │ │ ├── handle/ │ │ │ ├── http_proxy_handle.dart │ │ │ ├── relay_handle.dart │ │ │ ├── sse_handle.dart │ │ │ └── websocket_handle.dart │ │ ├── http/ │ │ │ ├── codec.dart │ │ │ ├── constants.dart │ │ │ ├── content_type.dart │ │ │ ├── h2/ │ │ │ │ ├── frame.dart │ │ │ │ ├── h2_codec.dart │ │ │ │ ├── hpack/ │ │ │ │ │ ├── hpack.dart │ │ │ │ │ ├── huffman.dart │ │ │ │ │ └── huffman_table.dart │ │ │ │ └── setting.dart │ │ │ ├── http.dart │ │ │ ├── http_client.dart │ │ │ ├── http_headers.dart │ │ │ ├── parse/ │ │ │ │ ├── body_reader.dart │ │ │ │ └── http_parser.dart │ │ │ ├── sse.dart │ │ │ └── websocket.dart │ │ ├── socks/ │ │ │ └── socks5.dart │ │ └── util/ │ │ ├── attribute_keys.dart │ │ ├── byte_buf.dart │ │ ├── byte_utils.dart │ │ ├── cache.dart │ │ ├── cert/ │ │ │ ├── basic_constraints.dart │ │ │ ├── cert_data.dart │ │ │ ├── der.dart │ │ │ ├── extension.dart │ │ │ ├── key_usage.dart │ │ │ ├── pkcs12.dart │ │ │ └── x509.dart │ │ ├── compress.dart │ │ ├── crts.dart │ │ ├── crypto.dart │ │ ├── file_read.dart │ │ ├── lang.dart │ │ ├── localizations.dart │ │ ├── logger.dart │ │ ├── process_info.dart │ │ ├── proxy_helper.dart │ │ ├── random.dart │ │ ├── socket_address.dart │ │ ├── system_proxy.dart │ │ ├── task_queue.dart │ │ ├── tls.dart │ │ └── uri.dart │ ├── storage/ │ │ ├── favorites.dart │ │ ├── histories.dart │ │ ├── local_storage.dart │ │ ├── path.dart │ │ └── shared_preference_keys.dart │ ├── ui/ │ │ ├── app_update/ │ │ │ ├── app_update_repository.dart │ │ │ ├── constants.dart │ │ │ ├── new_version_dialog.dart │ │ │ └── remote_version_entity.dart │ │ ├── component/ │ │ │ ├── app_dialog.dart │ │ │ ├── buttons.dart │ │ │ ├── chinese_font.dart │ │ │ ├── context_menu_region.dart │ │ │ ├── device.dart │ │ │ ├── history_cache_time.dart │ │ │ ├── http_method_popup.dart │ │ │ ├── json/ │ │ │ │ ├── json_text.dart │ │ │ │ ├── json_viewer.dart │ │ │ │ ├── theme.dart │ │ │ │ └── toast.dart │ │ │ ├── memory_cleanup.dart │ │ │ ├── model/ │ │ │ │ └── search_model.dart │ │ │ ├── multi_window.dart │ │ │ ├── proxy_port_setting.dart │ │ │ ├── qrcode/ │ │ │ │ └── qr_scan_view.dart │ │ │ ├── search/ │ │ │ │ ├── highlight_text.dart │ │ │ │ ├── search_controller.dart │ │ │ │ └── search_field.dart │ │ │ ├── search_condition.dart │ │ │ ├── split_view.dart │ │ │ ├── state_component.dart │ │ │ ├── text_field.dart │ │ │ ├── transition.dart │ │ │ ├── utils.dart │ │ │ └── widgets.dart │ │ ├── configuration.dart │ │ ├── content/ │ │ │ ├── body.dart │ │ │ ├── headers.dart │ │ │ ├── menu.dart │ │ │ ├── panel.dart │ │ │ └── web_socket.dart │ │ ├── desktop/ │ │ │ ├── common.dart │ │ │ ├── debug/ │ │ │ │ └── breakpoint_executor.dart │ │ │ ├── desktop.dart │ │ │ ├── left_menus/ │ │ │ │ ├── favorite.dart │ │ │ │ ├── history.dart │ │ │ │ └── navigation.dart │ │ │ ├── preference.dart │ │ │ ├── request/ │ │ │ │ ├── domians.dart │ │ │ │ ├── list.dart │ │ │ │ ├── repeat.dart │ │ │ │ ├── report_servers.dart │ │ │ │ ├── request.dart │ │ │ │ ├── request_editor.dart │ │ │ │ ├── request_sequence.dart │ │ │ │ └── search.dart │ │ │ ├── setting/ │ │ │ │ ├── about.dart │ │ │ │ ├── external_proxy.dart │ │ │ │ ├── filter.dart │ │ │ │ ├── hosts.dart │ │ │ │ ├── request_block.dart │ │ │ │ ├── request_breakpoint.dart │ │ │ │ ├── request_crypto.dart │ │ │ │ ├── request_map/ │ │ │ │ │ ├── map_local.dart │ │ │ │ │ └── map_scipt.dart │ │ │ │ ├── request_map.dart │ │ │ │ ├── request_rewrite.dart │ │ │ │ ├── rewrite/ │ │ │ │ │ ├── rewrite_replace.dart │ │ │ │ │ └── rewrite_update.dart │ │ │ │ ├── script.dart │ │ │ │ └── setting.dart │ │ │ ├── ssl/ │ │ │ │ ├── cert_installer.dart │ │ │ │ ├── pc_cert.dart │ │ │ │ └── ssl.dart │ │ │ ├── toolbar/ │ │ │ │ ├── phone_connect.dart │ │ │ │ └── toolbar.dart │ │ │ ├── widgets/ │ │ │ │ ├── highlight.dart │ │ │ │ └── windows_toolbar.dart │ │ │ └── window_listener.dart │ │ ├── launch/ │ │ │ └── launch.dart │ │ ├── mobile/ │ │ │ ├── debug/ │ │ │ │ └── breakpoint_executor.dart │ │ │ ├── menu/ │ │ │ │ ├── bottom_navigation.dart │ │ │ │ ├── drawer.dart │ │ │ │ └── menu.dart │ │ │ ├── mobile.dart │ │ │ ├── request/ │ │ │ │ ├── domians.dart │ │ │ │ ├── favorite.dart │ │ │ │ ├── history.dart │ │ │ │ ├── list.dart │ │ │ │ ├── repeat.dart │ │ │ │ ├── request.dart │ │ │ │ ├── request_editor.dart │ │ │ │ ├── request_editor_source.dart │ │ │ │ ├── request_sequence.dart │ │ │ │ └── search.dart │ │ │ ├── setting/ │ │ │ │ ├── app_filter.dart │ │ │ │ ├── filter.dart │ │ │ │ ├── hosts.dart │ │ │ │ ├── preference.dart │ │ │ │ ├── proxy.dart │ │ │ │ ├── report_servers.dart │ │ │ │ ├── request_block.dart │ │ │ │ ├── request_breakpoint.dart │ │ │ │ ├── request_crypto.dart │ │ │ │ ├── request_map/ │ │ │ │ │ ├── map_local.dart │ │ │ │ │ └── map_scipt.dart │ │ │ │ ├── request_map.dart │ │ │ │ ├── request_rewrite.dart │ │ │ │ ├── rewrite/ │ │ │ │ │ ├── rewrite_replace.dart │ │ │ │ │ └── rewrite_update.dart │ │ │ │ ├── script.dart │ │ │ │ ├── ssl.dart │ │ │ │ ├── theme.dart │ │ │ │ └── video_player.dart │ │ │ └── widgets/ │ │ │ ├── about.dart │ │ │ ├── floating_window.dart │ │ │ ├── highlight.dart │ │ │ ├── pip.dart │ │ │ └── remote_device.dart │ │ └── toolbox/ │ │ ├── aes_page.dart │ │ ├── cert_hash.dart │ │ ├── encoder.dart │ │ ├── js_run.dart │ │ ├── qr_code_page.dart │ │ ├── regexp.dart │ │ ├── timestamp.dart │ │ ├── toolbox.dart │ │ └── websocket_request.dart │ └── utils/ │ ├── aes.dart │ ├── crypto_body_decoder.dart │ ├── curl.dart │ ├── desktop_support.dart │ ├── export_request.dart │ ├── files.dart │ ├── font.dart │ ├── har.dart │ ├── ip.dart │ ├── keyword_highlight.dart │ ├── lang.dart │ ├── listenable_list.dart │ ├── navigator.dart │ ├── num.dart │ ├── platform.dart │ ├── python.dart │ └── task.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── build.sh │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ ├── my_application.h │ └── proxy-pin.desktop ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── AppLifecycleChannel.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ ├── Release.entitlements │ │ └── RunnerProfile.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ ├── RunnerTests/ │ │ └── RunnerTests.swift │ └── packaging/ │ └── dmg/ │ └── make_config.yaml ├── pubspec.yaml ├── test/ │ ├── base64_test.dart │ ├── cert_test.dart │ ├── favorites_trim_test.dart │ ├── http_test.dart │ ├── js.js │ ├── js_test.dart │ ├── pk12_test.dart │ ├── requests_test.py │ ├── temp_ipv6_test.dart │ ├── tests.dart │ ├── url_test.dart │ ├── web_test.dart │ ├── websocket.dart │ ├── websocket_persistence_test.dart │ ├── widget_test.dart │ └── x509_test.dart └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── packaging/ │ ├── exe/ │ │ ├── inno_setup.sas │ │ └── make_config.yaml │ └── msix/ │ └── make_config.yaml └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Create reports to help us improve labels: bug body: - type: dropdown attributes: label: Operating system description: Operating system type options: - iOS - macOS - Android - Windows - Linux - All validations: required: true - type: textarea attributes: label: 描述错误(Describe the bug) description: Please provide a detailed description of the error. validations: required: true - type: textarea attributes: label: To Reproduce description: "重现行为的步骤: 如具体应用抓包失败,请说明软件名称以及具体操作页面." ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Suggest an idea for this project labels: enhancement body: - type: dropdown attributes: label: Operating system description: Operating system type options: - iOS - macOS - Android - Windows - Linux - All validations: required: true - type: textarea attributes: label: Is your feature request related to a problem? Please describe. description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] validations: required: true - type: textarea attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. - type: checkboxes id: supporter attributes: label: Supporter options: - label: I am a [sponsor](https://buymeacoffee.com/proxypin) ================================================ FILE: .github/ISSUE_TEMPLATE/功能请求.yml ================================================ name: 功能请求 description: 为这个项目提出一个想法 labels: enhancement body: - type: dropdown attributes: label: Operating system description: Operating system type options: - iOS - macOS - Android - Windows - Linux - All validations: required: true - type: textarea attributes: label: 您的功能请求是否与某个问题相关?请描述. description: 对问题所在的清晰简洁的描述. validations: required: true - type: textarea attributes: label: 描述您想要的解决方案 description: 对您想要的解决方案的清晰简洁的描述. - type: checkboxes id: supporter attributes: label: 支持我们 options: - label: 我已经 [赞助](https://afdian.com/a/proxypin) ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ Podfile.lock # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release l10n_errors.txt pubspec.lock /dist/ ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: 796c8ef79279f9c774545b3771238c3098dbefab channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: ios create_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: 796c8ef79279f9c774545b3771238c3098dbefab # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: README.md ================================================ # ProxyPin English | [中文](README_CN.md) ## Open source free traffic capture HTTP(S),Support Windows、Mac、Android、IOS、Linux Full platform system You can use it to intercept, inspect & rewrite HTTP(S) traffic, Support capturing Flutter app traffic, ProxyPin is based on Flutter develop, and the UI is beautiful and easy to use. ## Features * Mobile scan code connection: no need to manually configure WiFi proxy, including configuration synchronization. All terminals can scan codes to connect and forward traffic to each other. * Domain name filtering: Only intercept the traffic you need, and do not intercept other traffic to avoid interference with other applications. * Search: Search requests according to keywords, response types and other conditions * Script: Support writing JavaScript scripts to process requests or responses. * Request Rewrite: Support redirection, support replacement of request or response message, and can also modify request or response according to the increase. * Request Mapping: Do not request remote services, use local configuration or scripts for response * Request Decryption: Configure AES decryption key to automatically decrypt HTTP message body * Request Blocking: Support blocking requests according to URL, and do not send requests to the server. * History: Automatically save the captured traffic data for easy backtracking and viewing. Support HAR format export and import. * Others: Favorites, toolbox, common encoding tools, as well as QR codes, regular expressions, etc. **Mac will prompt untrusted developers when first opened, you need to go to System Preferences-Security & Privacy-Allow any source.** ## Sponsors If ProxyPin is helpful to you, you are welcome to support us in the following ways to help the project develop in the long term: * [Buy Me A Coffee](https://buymeacoffee.com/proxypin) * [AFDIAN](https://afdian.com/a/proxypin) * Submit feedback and suggestions to help us improve * Contribute code or documentation to the project **Your support will be used for project maintenance, feature development, and user experience optimization. Thank you very much!** ## Downloads Github Releases: https://github.com/wanghongenpin/proxypin/releases iOS App Store:https://apps.apple.com/app/proxypin/id6450932949 Android Google Play:https://play.google.com/store/apps/details?id=com.network.proxy TG: https://t.me/proxypin_en **We will continue to improve the features and experience, as well as optimize the UI.** image.image ================================================ FILE: README_CN.md ================================================ # ProxyPin [English](README.md) | 中文 ## 开源免费抓包工具,支持Windows、Mac、Android、IOS、Linux 全平台系统 您可以使用它来拦截、检查和重写HTTP(S)流量,支持Flutter应用抓包,ProxyPin基于Flutter开发,UI美观易用。 ## 核心特性 * 手机扫码连接: 不用手动配置Wifi代理,包括配置同步。所有终端都可以互相扫码连接转发流量。 * 域名过滤: 只拦截您所需要的流量,不拦截其他流量,避免干扰其他应用。 * 搜索:根据关键词响应类型多种条件搜索请求 * 脚本: 支持编写JavaScript脚本来处理请求或响应。 * 请求重写: 支持重定向,支持替换请求或响应报文,也可以根据增则修改请求或或响应。 * 请求映射: 不请求远程服务,使用本地配置或脚本进行响应 * 请求解密: 配置AES解密密钥,自动解密HTTP消息体 * 请求屏蔽: 支持根据URL屏蔽请求,不让请求发送到服务器。 * 历史记录:自动保存抓包的流量数据,方便回溯查看。支持HAR格式导出与导入。 * 其他:收藏、工具箱、常用编码工具、以及二维码、正则等 **Mac首次打开会提示不受信任开发者,需要到系统偏好设置-安全性与隐私-允许任何来源。** ## 赞助 如果您觉得ProxyPin对您有帮助,欢迎通过以下方式支持我们,帮助项目长期发展: * [爱发电赞助](https://afdian.com/a/proxypin) * [Buy Me A Coffee](https://buymeacoffee.com/proxypin) * 提交反馈和建议,帮助我们改进 * 为项目贡献代码或文档 **您的支持将用于项目的维护、功能开发和用户体验优化,非常感谢!** ## 下载地址 国内下载: https://gitee.com/wanghongenpin/proxypin/releases iOS App Store: https://apps.apple.com/app/proxypin/id6450932949 Android Google Play:https://play.google.com/store/apps/details?id=com.network.proxy TG: https://t.me/proxypin_tg **接下来会持续完善功能和体验,UI优化。** image.image ================================================ FILE: analysis_options.yaml ================================================ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml formatter: page_width: 120 linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: avoid_print: false # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks /app/.cxx/ ================================================ FILE: android/app/build.gradle ================================================ import java.util.Properties import java.io.FileInputStream import java.io.File plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { namespace "com.network.proxy" compileSdk flutter.compileSdkVersion ndkVersion flutter.ndkVersion // ndkVersion "26.1.10909125" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } packagingOptions { dex { useLegacyPackaging true } jniLibs { useLegacyPackaging true } } defaultConfig { applicationId "com.network.proxy" ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' } // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName } signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } flutter { source '../..' } // Set a fixed APK name for release builds // Use the older ApplicationVariants API which is compatible across AGP versions android.applicationVariants.all { variant -> if (variant.buildType.name == 'release') { variant.outputs.all { output -> def apkName = "proxypin-android.apk" try { // Newer output API output.outputFileName = apkName } catch (Exception e) { // Fallback for older API output.outputFile = new File(output.outputFile.parent, apkName) } } } } dependencies { } ================================================ FILE: android/app/proguard-rules.pro ================================================ #Flutter Wrapper -keep class io.flutter.app.** { *; } -keep class io.flutter.plugin.** { *; } -keep class io.flutter.util.** { *; } -keep class io.flutter.view.** { *; } -keep class io.flutter.** { *; } -keep class io.flutter.plugins.** { *; } -keep class de.prosiebensat1digital.** { *; } -dontwarn com.google.android.play.core.** ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/MainActivity.kt ================================================ package com.network.proxy import android.content.Intent import android.content.res.Configuration import com.network.proxy.plugin.AppLifecyclePlugin import com.network.proxy.plugin.InstalledAppsPlugin import com.network.proxy.plugin.PictureInPicturePlugin import com.network.proxy.plugin.ProcessInfoPlugin import com.network.proxy.plugin.VpnServicePlugin import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine class MainActivity : FlutterActivity() { private val lifecycleChannel: AppLifecyclePlugin = AppLifecyclePlugin() override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) pluginRegister(flutterEngine) } override fun onUserLeaveHint() { super.onUserLeaveHint() lifecycleChannel.onUserLeaveHint() } override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration? ) { lifecycleChannel.onPictureInPictureModeChanged(isInPictureInPictureMode) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) } /** * 注册插件 */ private fun pluginRegister(flutterEngine: FlutterEngine) { flutterEngine.plugins.add(VpnServicePlugin()) flutterEngine.plugins.add(PictureInPicturePlugin()) flutterEngine.plugins.add(lifecycleChannel) flutterEngine.plugins.add(InstalledAppsPlugin()) flutterEngine.plugins.add(ProcessInfoPlugin()) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == VpnServicePlugin.REQUEST_CODE) { if (resultCode == RESULT_OK) { activity.startService(ProxyVpnService.startVpnIntent(activity)) return } val alertDialog = Intent(applicationContext, VpnAlertDialog::class.java) .setAction("com.network.proxy.ProxyVpnService") alertDialog.flags = Intent.FLAG_ACTIVITY_NEW_TASK startActivity(alertDialog) return } super.onActivityResult(requestCode, resultCode, data) } override fun onDestroy() { // activity.startService(ProxyVpnService.stopVpnIntent(activity)) super.onDestroy() } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt ================================================ package com.network.proxy import android.app.Activity import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.net.IpPrefix import android.net.ProxyInfo import android.net.VpnService import android.os.Build import android.os.ParcelFileDescriptor import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import com.network.proxy.plugin.VpnServicePlugin.Companion.REQUEST_CODE import com.network.proxy.vpn.ProxyVpnThread import com.network.proxy.vpn.socket.ProtectSocket import com.network.proxy.vpn.socket.ProtectSocketHolder import java.net.InetAddress /** * VPN服务 * @author wanghongen */ class ProxyVpnService : VpnService(), ProtectSocket { private var vpnInterface: ParcelFileDescriptor? = null private var vpnThread: ProxyVpnThread? = null companion object { const val MAX_PACKET_LEN = 1500 const val VIRTUAL_HOST = "10.0.0.2" const val PROXY_HOST_KEY = "ProxyHost" const val PROXY_PORT_KEY = "ProxyPort" const val ALLOW_APPS_KEY = "AllowApps" //允许的名单 const val DISALLOW_APPS_KEY = "DisallowApps" //禁止的名单 const val SET_SYSTEM_PROXY_KEY = "SetSystemProxy" const val PROXY_PASS_DOMAINS_KEY = "ProxyPassDomains" /** * 动作:断开连接 */ const val ACTION_DISCONNECT = "DISCONNECT" /** * 通知配置 */ private const val NOTIFICATION_ID = 9527 const val VPN_NOTIFICATION_CHANNEL_ID = "vpn-notifications" var isRunning = false var host: String? = null var port: Int = 9099 var allowApps: ArrayList? = null var disallowApps: ArrayList? = null var setSystemProxy: Boolean = true var proxyPassDomains: ArrayList? = null fun stopVpnIntent(context: Context): Intent { return Intent(context, ProxyVpnService::class.java).also { it.action = ACTION_DISCONNECT } } fun startVpnIntent( context: Context, proxyHost: String? = host, proxyPort: Int? = port, allowApps: ArrayList? = this.allowApps, disallowApps: ArrayList? = this.disallowApps, setSystemProxy: Boolean = true, proxyPassDomains: ArrayList? = null ): Intent { return Intent(context, ProxyVpnService::class.java).also { it.putExtra(PROXY_HOST_KEY, proxyHost) it.putExtra(PROXY_PORT_KEY, proxyPort) it.putStringArrayListExtra(ALLOW_APPS_KEY, allowApps) it.putStringArrayListExtra(DISALLOW_APPS_KEY, disallowApps) it.putExtra(SET_SYSTEM_PROXY_KEY, setSystemProxy) it.putStringArrayListExtra(PROXY_PASS_DOMAINS_KEY, proxyPassDomains) } } /** * 准备vpn
* 设备可能弹出连接vpn提示 */ fun prepareVpn( activity: Activity, host: String, port: Int, allowApps: ArrayList?, disallowApps: ArrayList?, setSystemProxy: Boolean = true, proxyPassDomains: ArrayList? = null ): Boolean { val intent = prepare(activity) if (intent != null) { ProxyVpnService.host = host ProxyVpnService.port = port ProxyVpnService.allowApps = allowApps ProxyVpnService.disallowApps = disallowApps ProxyVpnService.setSystemProxy = setSystemProxy ProxyVpnService.proxyPassDomains = proxyPassDomains activity.startActivityForResult(intent, REQUEST_CODE) return false } return true } } override fun onDestroy() { super.onDestroy() disconnect() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { return START_NOT_STICKY } return if (intent.action == ACTION_DISCONNECT) { disconnect() START_NOT_STICKY } else { val proxyHost = intent.getStringExtra(PROXY_HOST_KEY) ?: (host ?: "127.0.0.1") val proxyPort = intent.getIntExtra(PROXY_PORT_KEY, port) val allowPackages = intent.getStringArrayListExtra(ALLOW_APPS_KEY) ?: allowApps ?: ArrayList() val disallowPackages = intent.getStringArrayListExtra(DISALLOW_APPS_KEY) ?: disallowApps ?: ArrayList() val setSystemProxy = intent.getBooleanExtra(SET_SYSTEM_PROXY_KEY, setSystemProxy) val proxyPassDomains = intent.getStringArrayListExtra(PROXY_PASS_DOMAINS_KEY) connect( proxyHost, proxyPort, allowPackages, disallowPackages, setSystemProxy, proxyPassDomains ) START_STICKY } } private fun disconnect() { vpnThread?.run { stopThread() } vpnInterface?.close() stopForeground(STOP_FOREGROUND_REMOVE) vpnInterface = null isRunning = false } private fun connect( proxyHost: String, proxyPort: Int, allowPackages: ArrayList?, disallowPackages: ArrayList?, setSystemProxy: Boolean = true, proxyPassDomains: ArrayList? = null ) { Log.i( "ProxyVpnService", "startVpn $proxyHost:$proxyPort systemProxy: $setSystemProxy allowPackages: $allowPackages proxyPassDomains: $proxyPassDomains" ) host = proxyHost port = proxyPort allowApps = allowPackages disallowApps = disallowPackages ProxyVpnService.proxyPassDomains = proxyPassDomains vpnInterface = createVpnInterface( proxyHost, proxyPort, allowPackages, disallowPackages, setSystemProxy, proxyPassDomains ) if (vpnInterface == null) { val alertDialog = Intent(applicationContext, VpnAlertDialog::class.java) .setAction("com.network.proxy.ProxyVpnService") alertDialog.flags = Intent.FLAG_ACTIVITY_NEW_TASK startActivity(alertDialog) return } ProtectSocketHolder.setProtectSocket(this) showServiceNotification() vpnThread = ProxyVpnThread( vpnInterface!!, proxyHost, proxyPort, proxyPassDomains ) vpnThread!!.start() isRunning = true } private fun showServiceNotification() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val notificationChannel = NotificationChannel( VPN_NOTIFICATION_CHANNEL_ID, "VPN Status", NotificationManager.IMPORTANCE_LOW ) notificationManager.createNotificationChannel(notificationChannel) } val pendingActivityIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } val notification: Notification = NotificationCompat.Builder(this, VPN_NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingActivityIntent) .setContentTitle(getString(R.string.vpn_active_notification_title)) .setContentText(getString(R.string.vpn_active_notification_content)) .setOngoing(true) .build() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startForeground(NOTIFICATION_ID, notification) } } private fun createVpnInterface( proxyHost: String, proxyPort: Int, allowPackages: List?, disallowApps: ArrayList?, setSystemProxy: Boolean = true, proxyPassDomains: ArrayList? = null ): ParcelFileDescriptor? { val build = Builder() .setMtu(MAX_PACKET_LEN) .addAddress(VIRTUAL_HOST, 32) .addRoute("0.0.0.0", 0) .setSession(baseContext.applicationInfo.name) .setBlocking(true) // 处理 proxyPassDomains 中的 CIDR 格式,添加到 excludeRoute if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && proxyPassDomains != null) { applyExcludeRoutes(build, proxyPassDomains) } val packages = allowPackages?.filter { it != baseContext.packageName } if (packages?.isNotEmpty() == true) { packages.forEach { build.addAllowedApplication(it) } } else { build.addDisallowedApplication(baseContext.packageName) } disallowApps?.forEach { if (packages?.contains(it) == true) return@forEach build.addDisallowedApplication(it) } build.setConfigureIntent( PendingIntent.getActivity( this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE ) ) return build.apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { setMetered(false) } if (setSystemProxy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Log.d("ProxyVpnService", "set system proxy $proxyHost:$proxyPort") val buildProxy = ProxyInfo.buildDirectProxy(proxyHost, proxyPort) setHttpProxy(buildProxy) } }.establish() } /** * 应用排除路由规则 * 根据 proxyPassDomains 列表配置 VPN 的 excludeRoute * * @param builder VPN Builder 实例 * @param proxyPassDomains 需要排除的域名/IP列表 */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun applyExcludeRoutes(builder: Builder, proxyPassDomains: ArrayList) { proxyPassDomains.forEach { domain -> try { val trimmedDomain = domain.trim() when { // 2. localhost 或 127.0.0.1 trimmedDomain == "localhost" || trimmedDomain == "127.0.0.1" -> { Log.d("ProxyVpnService", "Skipped excludeRoute for localhost: $trimmedDomain") } // 1. CIDR 格式:192.168.0.0/16 trimmedDomain.contains("/") -> { addCidrExcludeRoute(builder, trimmedDomain) } // 3. 单个 IP 地址(不含通配符) !trimmedDomain.contains("*") && isValidIpAddress(trimmedDomain) -> { addSingleIpExcludeRoute(builder, trimmedDomain) } // 4. 域名和通配符域名会被跳过(不能用于 excludeRoute) } } catch (e: Exception) { Log.w("ProxyVpnService", "Error processing proxyPassDomain: $domain", e) } } } /** * 添加 CIDR 格式的排除路由 * @param builder VPN Builder 实例 * @param cidr CIDR 格式的地址,如 "192.168.0.0/16" */ private fun addCidrExcludeRoute(builder: Builder, cidr: String) { try { val parts = cidr.split("/") if (parts.size != 2) { Log.w("ProxyVpnService", "Invalid CIDR format: $cidr") return } val ipAddress = parts[0] val prefixLength = parts[1].toIntOrNull() if (prefixLength == null || prefixLength !in 0..32) { Log.w("ProxyVpnService", "Invalid prefix length in CIDR: $cidr") return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val inetAddress = InetAddress.getByName(ipAddress) builder.excludeRoute(IpPrefix(inetAddress, prefixLength)) Log.d("ProxyVpnService", "Added excludeRoute: $cidr") } } catch (e: Exception) { Log.w("ProxyVpnService", "Failed to add CIDR excludeRoute: $cidr", e) } } /** * 添加单个 IP 地址的排除路由 * @param builder VPN Builder 实例 * @param ipAddress IP 地址字符串 */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun addSingleIpExcludeRoute(builder: Builder, ipAddress: String) { val inetAddress = InetAddress.getByName(ipAddress) builder.excludeRoute(IpPrefix(inetAddress, 32)) Log.d("ProxyVpnService", "Added excludeRoute for single IP: $ipAddress/32") } /** * 检查字符串是否是有效的 IPv4 地址格式 * @param ip IP 地址字符串 * @return 是否是有效的 IPv4 地址 */ private fun isValidIpAddress(ip: String): Boolean { return ip.matches(Regex("\\d+\\.\\d+\\.\\d+\\.\\d+")) } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/VpnAlertDialog.kt ================================================ package com.network.proxy import android.app.Activity import android.app.AlertDialog import android.os.Bundle import kotlin.system.exitProcess /** * @author wanghongen */ class VpnAlertDialog : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val dialog: AlertDialog = AlertDialog.Builder(this) .setTitle("提示") .setMessage("必须添加VPN才能使用") .setPositiveButton("确认") { _, _ -> exitProcess(0) } .setCancelable(false) .create() dialog.show() } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/AndroidFlutterPlugin.kt ================================================ package com.network.proxy.plugin import android.app.Activity import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding abstract class AndroidFlutterPlugin : FlutterPlugin, ActivityAware { protected lateinit var activity: Activity override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity } override fun onDetachedFromActivityForConfigChanges() { } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { } override fun onDetachedFromActivity() { } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/AppInfo.kt ================================================ package com.network.proxy.plugin import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import java.io.ByteArrayOutputStream import androidx.core.graphics.createBitmap class ProcessInfo(name: CharSequence, packageName: String, icon: ByteArray?, versionName: String?) : HashMap() { init { put("name", name) put("packageName", packageName) put("icon", icon) put("versionName", versionName) } fun copy(): ProcessInfo { val name = this["name"] as? CharSequence ?: "" val packageName = this["packageName"] as? String ?: "" val icon = this["icon"] as? ByteArray val versionName = this["versionName"] as? String val newInfo = ProcessInfo(name, packageName, icon, versionName) newInfo.putAll(this) return newInfo } companion object { fun create( packageManager: PackageManager, app: ApplicationInfo, withIcon: Boolean = true ): ProcessInfo { val name = packageManager.getApplicationLabel(app) val packageName = app.packageName val icon = if (withIcon) drawableToByteArray(app.loadIcon(packageManager)) else ByteArray(0) val packageInfo = packageManager.getPackageInfo(app.packageName, 0) // 部分应用可能没有设置versionName,将导致获取列表操作失败 val versionName = packageInfo.versionName ?: "" return ProcessInfo(name, packageName, icon, versionName) } private fun drawableToByteArray(drawable: Drawable): ByteArray { val bitmap = drawableToBitmap(drawable) val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) return stream.toByteArray() } private fun drawableToBitmap(drawable: Drawable): Bitmap { if (drawable is BitmapDrawable) { return drawable.bitmap } // 获取宽度和高度,如果无效则使用默认值 96dp var width = drawable.intrinsicWidth var height = drawable.intrinsicHeight // 如果宽度或高度无效(≤ 0),使用默认的 96 作为大小 if (width <= 0) width = 96 if (height <= 0) height = 96 val bitmap = createBitmap(width, height) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bitmap } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/AppLifecyclePlugin.kt ================================================ package com.network.proxy.plugin import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodChannel class AppLifecyclePlugin : AndroidFlutterPlugin() { var channel: MethodChannel? = null companion object { const val CHANNEL = "com.proxy/appLifecycle" } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(binding.binaryMessenger, CHANNEL) } fun onUserLeaveHint() { channel?.invokeMethod("onUserLeaveHint", null) } fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { channel?.invokeMethod("onPictureInPictureModeChanged", isInPictureInPictureMode) } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/InstalledAppsPlugin.kt ================================================ package com.network.proxy.plugin import android.content.pm.ApplicationInfo import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodChannel import java.util.Locale import java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.TimeUnit /** * 已经安装应用列表 * * @author wanghongen */ class InstalledAppsPlugin : AndroidFlutterPlugin() { var channel: MethodChannel? = null companion object { const val CHANNEL = "com.proxy/installedApps" } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(binding.binaryMessenger, CHANNEL) channel!!.setMethodCallHandler { call, result -> when (call.method) { "getInstalledApps" -> { val withIcon = call.argument("withIcon") ?: false val packageNamePrefix = call.argument("packageNamePrefix") ?: "" val includeSystemApps = call.argument("includeSystemApps") ?: false Thread { result.success( getInstalledApps( withIcon, packageNamePrefix, includeSystemApps ) ) }.start() } "getAppInfo" -> { val packageName = call.argument("packageName") ?: "" result.success(getAppInfo(packageName)) } else -> result.notImplemented() } } } private fun getAppInfo(packageName: String): ProcessInfo { val packageManager = activity.packageManager packageManager.getApplicationInfo(packageName, 0).let { app -> return ProcessInfo.create(packageManager, app, true) } } private fun isSystemApp(applicationInfo: ApplicationInfo?): Boolean { if (applicationInfo == null) return false return (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0 } private fun getInstalledApps( withIcon: Boolean, packageNamePrefix: String, includeSystemApps: Boolean ): List { val packageManager = activity.packageManager var installedApps = packageManager.getInstalledApplications(0) if (!includeSystemApps) { installedApps = installedApps.filter { app -> !isSystemApp(app) } } if (packageNamePrefix.isNotEmpty()) { installedApps = installedApps.filter { app -> app.packageName.startsWith( packageNamePrefix.lowercase(Locale.ENGLISH) ) } } val threadPoolExecutor = Executors.newFixedThreadPool(4) installedApps.map { app -> val task: Callable = Callable { ProcessInfo.create(packageManager, app, withIcon) } threadPoolExecutor.submit(task) }.map { future -> future.get() }.let { threadPoolExecutor.shutdown() threadPoolExecutor.awaitTermination(3, TimeUnit.SECONDS) return it } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt ================================================ package com.network.proxy.plugin import android.app.PendingIntent import android.app.PictureInPictureParams import android.app.RemoteAction import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.drawable.Icon import android.os.Build import android.util.Rational import com.network.proxy.ProxyVpnService import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodChannel import android.util.Log import androidx.core.content.ContextCompat /** * 画中画插件 */ class PictureInPicturePlugin : AndroidFlutterPlugin() { private var registerBroadcast = false var channel: MethodChannel? = null var proxyHost: String? = null var proxyPort: Int? = null var allowApps: ArrayList? = null var disallowApps: ArrayList? = null ///广播事件接受者 private val vpnBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { Log.d("com.network.proxy", "onReceive ${intent?.action}") if (context == null || (intent?.action != VPN_ACTION && intent?.action != CLEAN_ACTION)) { return } if (intent.action == CLEAN_ACTION) { channel?.invokeMethod("cleanSession", null) return } val isRunning = ProxyVpnService.isRunning if (isRunning) { activity.startService(ProxyVpnService.stopVpnIntent(activity)) } else { val prepareVpn = ProxyVpnService.prepareVpn(activity, proxyHost!!, proxyPort!!, allowApps, disallowApps) if (prepareVpn) { activity.startService( ProxyVpnService.startVpnIntent( activity, proxyHost, proxyPort, allowApps, disallowApps ) ) } } //设置画中画参数 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { updatePictureInPictureParams(!isRunning) } } } companion object { const val CHANNEL = "com.proxy/pictureInPicture" const val VPN_ACTION = "VPN_ACTION" const val CLEAN_ACTION = "CLEAN_ACTION" } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(binding.binaryMessenger, CHANNEL) channel!!.setMethodCallHandler { call, result -> when (call.method) { "enterPictureInPictureMode" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { proxyHost = call.argument("proxyHost") proxyPort = call.argument("proxyPort") allowApps = call.argument>("allowApps") disallowApps = call.argument>("disallowApps") val param = updatePictureInPictureParams(ProxyVpnService.isRunning) if (!registerBroadcast) { registerBroadcast = true ContextCompat.registerReceiver( activity, vpnBroadcastReceiver, IntentFilter().apply { addAction(VPN_ACTION) addAction(CLEAN_ACTION) }, ContextCompat.RECEIVER_NOT_EXPORTED ) } result.success(activity.enterPictureInPictureMode(param)) } } else -> { result.notImplemented() } } } } // 画中画参数 private fun updatePictureInPictureParams(isRunning: Boolean): PictureInPictureParams { val params = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PictureInPictureParams.Builder() .setAspectRatio(Rational(9, 19)) .apply { setActions(actions(isRunning)) //vpn服务运行中,显示停止按钮 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setSeamlessResizeEnabled(false) } } .build() } else { throw RuntimeException("getPictureInPictureParams error") } activity.setPictureInPictureParams(params) return params } //停止vpn服务 RemoteAction private fun actions(isRunning: Boolean): List { val pIntent: PendingIntent = PendingIntent.getBroadcast( activity, if (isRunning) 0 else 1, Intent(VPN_ACTION).apply { setPackage(activity.packageName) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT ) val cleanIntent: PendingIntent = PendingIntent.getBroadcast( activity, 2, Intent(CLEAN_ACTION).apply { setPackage(activity.packageName) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT ) //vpn服务运行中,显示停止按钮 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return listOf( RemoteAction( Icon.createWithResource( this@PictureInPicturePlugin.activity, if (isRunning) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ), "Proxy", "Proxy", pIntent ), RemoteAction( Icon.createWithResource( this@PictureInPicturePlugin.activity, android.R.drawable.ic_menu_delete ), "Clean", "Clean", cleanIntent ) ) } else { throw RuntimeException("action error") } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/ProcessInfoPlugin.kt ================================================ package com.network.proxy.plugin import com.network.proxy.vpn.util.ProcessInfoManager import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * 进程信息插件 * * @author wanghongen */ class ProcessInfoPlugin : AndroidFlutterPlugin() { private val processInfoManager = ProcessInfoManager.instance companion object { const val CHANNEL = "com.proxy/processInfo" } override fun onAttachedToActivity(binding: ActivityPluginBinding) { super.onAttachedToActivity(binding) processInfoManager.activity = binding.activity } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { val channel = MethodChannel(binding.binaryMessenger, CHANNEL) channel.setMethodCallHandler { call, result -> when (call.method) { "getProcessByPort" -> { val host = call.argument("host") val port = call.argument("port") if (port != null) { CoroutineScope(Dispatchers.IO).launch { val appInfo = processInfoManager.getProcessInfoByPort(host, port) withContext(Dispatchers.Main) { result.success(appInfo) } } } else { result.error("INVALID_ARGUMENT", "Port is null", null) } } "getRemoteAddressByPort" -> { val port = call.argument("port") result.success(processInfoManager.getRemoteAddressByPort(port!!)) } else -> { result.notImplemented() } } } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/plugin/VpnServicePlugin.kt ================================================ package com.network.proxy.plugin import com.network.proxy.ProxyVpnService import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodChannel class VpnServicePlugin : AndroidFlutterPlugin() { companion object { const val CHANNEL = "com.proxy/proxyVpn" const val REQUEST_CODE: Int = 24 } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { val channel = MethodChannel(binding.binaryMessenger, CHANNEL) channel.setMethodCallHandler { call, result -> when (call.method) { "isRunning" -> { result.success(ProxyVpnService.isRunning) } "startVpn" -> { val host = call.argument("proxyHost") val port = call.argument("proxyPort") val allowApps = call.argument>("allowApps") val disallowApps = call.argument>("disallowApps") val setSystemProxy = call.argument("setSystemProxy") ?: true val proxyPassDomains = call.argument>("proxyPassDomains") val prepareVpn = ProxyVpnService.prepareVpn( activity, host!!, port!!, allowApps, disallowApps, setSystemProxy, proxyPassDomains ) if (prepareVpn) { startVpn(host, port, allowApps, disallowApps, setSystemProxy, proxyPassDomains) } result.success(prepareVpn) } "stopVpn" -> { stopVpn() result.success(null) } "restartVpn" -> { val host = call.argument("proxyHost") val port = call.argument("proxyPort") val allowApps = call.argument>("allowApps") val disallowApps = call.argument>("disallowApps") val setSystemProxy = call.argument("setSystemProxy") ?: true val proxyPassDomains = call.argument>("proxyPassDomains") stopVpn() startVpn(host!!, port!!, allowApps, disallowApps, setSystemProxy, proxyPassDomains) result.success(null) } else -> { result.notImplemented() } } } } /** * 启动vpn服务 */ private fun startVpn( host: String, port: Int, allowApps: ArrayList? = arrayListOf(), disallowApps: ArrayList? = arrayListOf(), setSystemProxy: Boolean = true, proxyPassDomains: ArrayList? = null ) { val intent = ProxyVpnService.startVpnIntent( activity, host, port, allowApps, disallowApps, setSystemProxy, proxyPassDomains ) activity.startService(intent) } /** * 停止vpn服务 */ private fun stopVpn() { activity.startService(ProxyVpnService.stopVpnIntent(activity)) } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/Connection.kt ================================================ package com.network.proxy.vpn import android.util.Log import com.network.proxy.vpn.socket.CloseableConnection import com.network.proxy.vpn.transport.protocol.IP4Header import com.network.proxy.vpn.transport.protocol.TCPHeader import com.network.proxy.vpn.transport.protocol.UDPHeader import com.network.proxy.vpn.util.PacketUtil import java.io.ByteArrayOutputStream import java.io.IOException import java.nio.ByteBuffer import java.nio.channels.SelectionKey import java.nio.channels.spi.AbstractSelectableChannel import kotlin.concurrent.Volatile class Connection( val protocol: Protocol, val sourceIp: Int, val sourcePort: Int, val destinationIp: Int, val destinationPort: Int, private val connectionCloser: CloseableConnection ) { var channel: AbstractSelectableChannel? = null var selectionKey: SelectionKey? = null //接收用于存储来自远程主机的数据的缓冲器 private val receivingStream: ByteArrayOutputStream = ByteArrayOutputStream() //发送缓冲区,用于存储要从vpn客户端发送到目标主机的数据 private val sendingStream: ByteArrayOutputStream = ByteArrayOutputStream() var hasReceivedLastSegment = false /** * 是否初始化链接 针对代理判断协议延迟初始化 */ var isInitConnect = false //指示三向握手是否已完成 var isConnected = false //从客户端接收的最后一个数据包 var lastIpHeader: IP4Header? = null var lastTcpHeader: TCPHeader? = null var lastUdpHeader: UDPHeader? = null var timestampSender = 0 var timestampReplyTo = 0 //从客户端接收的序列 var recSequence: Long = 0 //在tcp选项内的SYN期间由客户端发送 var maxSegmentSize = 0 //跟踪我们发送给客户端的ack,并等待客户端返回ack var sendUnAck: Long = 0 //发送到客户端的下一个ack var sendNext: Long = 0 //true when connection is about to be close var isClosingConnection = false //指示客户端的数据已准备好发送到目标 @Volatile var isDataForSendingReady = false //closing session and aborting connection, will be done by background task @Volatile var isAbortingConnection = false //indicate that vpn client has sent FIN flag and it has been acked var isAckedToFin = false companion object { fun getConnectionKey( protocol: Protocol, destIp: Int, destPort: Int, sourceIp: Int, sourcePort: Int ): String { return protocol.name + "|" + PacketUtil.intToIPAddress(sourceIp) + ":" + sourcePort + "->" + PacketUtil.intToIPAddress(destIp) + ":" + destPort } } // fun getConnectionKey(): String { // return getConnectionKey(protocol, destinationIp, destinationIp, sourceIp, sourcePort) // } fun closeConnection() { connectionCloser.closeConnection(this) } /** * 设置要发送到目标服务器的数据 */ @Synchronized fun setSendingData(data: ByteBuffer): Int { val remaining = data.remaining() sendingStream.write(data.array(), data.position(), data.remaining()) return remaining } @Synchronized fun addReceivedData(data: ByteArray?) { try { receivingStream.write(data) } catch (e: IOException) { Log.e(TAG, e.toString()) } } /** * 获取缓冲区中接收到的所有数据并清空它。 */ @Synchronized fun getReceivedData(maxSize: Int): ByteArray? { var data = receivingStream.toByteArray() receivingStream.reset() if (data.size > maxSize) { val small = ByteArray(maxSize) System.arraycopy(data, 0, small, 0, maxSize) val len = data.size - maxSize receivingStream.write(data, maxSize, len) data = small } return data } /** * buffer has more data for vpn client */ fun hasReceivedData(): Boolean { return receivingStream.size() > 0 } fun hasDataToSend(): Boolean { return sendingStream.size() > 0 } /** * 出列数据以发送到服务器 */ @Synchronized fun getSendingData(): ByteArray? { val data = sendingStream.toByteArray() sendingStream.reset() return data } fun cancelKey() { selectionKey?.let { synchronized(it) { if (!it.isValid) return it.cancel() } } } fun subscribeKey(op: Int) { selectionKey?.let { synchronized(it) { if (!it.isValid) return it.interestOps(it.interestOps() or op) } } } fun unsubscribeKey(op: Int) { selectionKey?.let { synchronized(it) { if (!it.isValid) return it.interestOps(it.interestOps() and op.inv()) } } } override fun toString(): String { return "Connection{" + "protocol=" + protocol + ", sourceIp=" + PacketUtil.intToIPAddress(sourceIp) + ", sourcePort=" + sourcePort + ", destinationIp=" + PacketUtil.intToIPAddress(destinationIp) + ", destinationPort=" + destinationPort + '}' } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/ConnectionHandler.kt ================================================ package com.network.proxy.vpn import android.util.Log import com.network.proxy.vpn.Connection.Companion.getConnectionKey import com.network.proxy.vpn.socket.ClientPacketWriter import com.network.proxy.vpn.socket.SocketNIODataService import com.network.proxy.vpn.transport.icmp.ICMPPacket import com.network.proxy.vpn.transport.icmp.ICMPPacketFactory import com.network.proxy.vpn.transport.protocol.IP4Header import com.network.proxy.vpn.transport.protocol.IPPacketFactory import com.network.proxy.vpn.transport.protocol.TCPHeader import com.network.proxy.vpn.transport.protocol.TCPPacketFactory import com.network.proxy.vpn.transport.protocol.UDPPacketFactory import com.network.proxy.vpn.util.PacketUtil.getOutput import com.network.proxy.vpn.util.PacketUtil.intToIPAddress import com.network.proxy.vpn.util.PacketUtil.isPacketCorrupted import com.network.proxy.vpn.util.ProcessInfoManager import com.network.proxy.vpn.util.TLS.isTLSClientHello import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.SelectionKey import java.nio.channels.SocketChannel import java.util.concurrent.ExecutorService import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit class ConnectionHandler( private val manager: ConnectionManager, private val nioService: SocketNIODataService, private val writer: ClientPacketWriter ) { private val pingThreadPool: ExecutorService = ThreadPoolExecutor( 1, 20, // 1 - 20 parallel pings max 60L, TimeUnit.SECONDS, SynchronousQueue(), ThreadPoolExecutor.DiscardPolicy() // Replace running pings if there's too many ) /** * Handle unknown raw IP packet data * * @param stream ByteBuffer to be read */ @Throws(IOException::class) fun handlePacket(stream: ByteBuffer) { stream.rewind() val ipHeader = IPPacketFactory.createIP4Header(stream) if (ipHeader == null) { stream.rewind() Log.w(TAG, "Malformed IP packet ") return } if (ipHeader.protocol.toInt() == 6) { handleTCPPacket(stream, ipHeader) } else if (ipHeader.protocol.toInt() == 17) { handleUDPPacket(stream, ipHeader) } else if (ipHeader.protocol.toInt() == 1) { handleICMPPacket(stream, ipHeader) } else { Log.w(TAG, "Unsupported IP protocol: " + ipHeader.protocol) } } @Throws(IOException::class) private fun handleUDPPacket(clientPacketData: ByteBuffer, ipHeader: IP4Header) { val udpHeader = UDPPacketFactory.createUDPHeader(clientPacketData) var connection = manager.getConnection( Protocol.UDP, ipHeader.destinationIP, udpHeader.destinationPort, ipHeader.sourceIP, udpHeader.sourcePort ) val newSession = connection == null if (connection == null) { connection = manager.createUDPConnection( ipHeader.destinationIP, udpHeader.destinationPort, ipHeader.sourceIP, udpHeader.sourcePort ) } synchronized(connection) { connection.lastIpHeader = ipHeader connection.lastUdpHeader = udpHeader manager.addClientData(clientPacketData, connection) connection.isDataForSendingReady = true // We don't register the session until it's fully populated (as above) if (newSession) nioService.registerSession(connection) // Ping the NIO thread to write this, when the session is next writable connection.subscribeKey(SelectionKey.OP_WRITE) nioService.refreshSelect(connection) } manager.keepSessionAlive(connection) } /** * 是否支持协议 */ private val methods: List = mutableListOf("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT", "PROPFIND", "REPORT") private fun supperProtocol(packetData: ByteBuffer): Boolean { val position = packetData.position() //判断是否是ssl握手 if (isTLSClientHello(packetData)) { packetData.position(position) return true } packetData.position(position) for (method in methods) { if (packetData.remaining() < method.length) { continue } val bytes = ByteArray(method.length) for (i in bytes.indices) { bytes[i] = packetData[position + i] } if (method.equals(String(bytes), ignoreCase = true)) { return true } } return false } /** * 获取代理地址 */ private fun getProxyAddress( packetData: ByteBuffer, destinationIP: Int, destinationPort: Int ): InetSocketAddress { val ips = intToIPAddress(destinationIP) // 检查是否在代理过滤列表中 if (shouldBypassProxy(ips)) { Log.d(TAG, "Bypassing proxy for $ips (in proxyPassDomains)") return InetSocketAddress(ips, destinationPort) } val supperProtocol = supperProtocol(packetData) var socketAddress: InetSocketAddress? = null if (supperProtocol) { socketAddress = manager.proxyAddress } if (socketAddress == null) { socketAddress = InetSocketAddress(ips, destinationPort) } return socketAddress } /** * 检查是否应该绕过代理 * 支持 CIDR 格式(如 192.168.0.0/16)、IP地址、localhost 和域名(带通配符)匹配 */ private fun shouldBypassProxy(destinationIP: String): Boolean { val proxyPassDomains = manager.proxyPassDomains ?: return false for (domain in proxyPassDomains) { try { val trimmedDomain = domain.trim() // 处理 localhost if (trimmedDomain == "localhost" && (destinationIP == "127.0.0.1" || destinationIP == "localhost")) { return true } // 处理 CIDR 格式,如 192.168.0.0/16 if (trimmedDomain.contains("/")) { if (matchesCIDR(destinationIP, trimmedDomain)) { return true } } else if (trimmedDomain.startsWith("*.")) { // 支持通配符匹配,如 *.example.com val suffix = trimmedDomain.substring(1) // 去掉 * if (destinationIP.endsWith(suffix)) { return true } } else if (trimmedDomain.contains("*")) { // 支持其他通配符模式 val pattern = trimmedDomain.replace(".", "\\.").replace("*", ".*") if (destinationIP.matches(Regex(pattern))) { return true } } else { // 精确匹配 IP 或域名 if (destinationIP == trimmedDomain) { return true } // 尝试解析域名为IP地址进行比较 try { val address = InetAddress.getByName(trimmedDomain) if (address.hostAddress == destinationIP) { return true } } catch (e: Exception) { Log.w(TAG, "Error resolving domain $trimmedDomain: ${e.message}") } } } catch (e: Exception) { Log.w(TAG, "Error matching domain $domain: ${e.message}") } } return false } /** * 检查 IP 地址是否匹配 CIDR 格式 * @param ip 目标 IP 地址,如 "192.168.1.1" * @param cidr CIDR 格式,如 "192.168.0.0/16" * @return 是否匹配 */ private fun matchesCIDR(ip: String, cidr: String): Boolean { try { val parts = cidr.split("/") if (parts.size != 2) return false val networkAddress = parts[0] val prefixLength = parts[1].toIntOrNull() ?: return false val ipBytes = ipToBytes(ip) val networkBytes = ipToBytes(networkAddress) if (ipBytes == null || networkBytes == null) return false // 计算掩码 val mask = (-1L shl (32 - prefixLength)).toInt() // 将 IP 地址转换为整数进行比较 val ipInt = bytesToInt(ipBytes) val networkInt = bytesToInt(networkBytes) return (ipInt and mask) == (networkInt and mask) } catch (e: Exception) { Log.w(TAG, "Error matching CIDR $cidr for IP $ip: ${e.message}") return false } } /** * 将 IP 地址字符串转换为字节数组 */ private fun ipToBytes(ip: String): ByteArray? { try { val parts = ip.split(".") if (parts.size != 4) return null return ByteArray(4) { i -> parts[i].toInt().toByte() } } catch (_: Exception) { return null } } /** * 将字节数组转换为整数 */ private fun bytesToInt(bytes: ByteArray): Int { return ((bytes[0].toInt() and 0xFF) shl 24) or ((bytes[1].toInt() and 0xFF) shl 16) or ((bytes[2].toInt() and 0xFF) shl 8) or (bytes[3].toInt() and 0xFF) } @Throws(IOException::class) private fun handleTCPPacket(clientPacketData: ByteBuffer, ip4Header: IP4Header) { val tcpHeader = TCPPacketFactory.createTCPHeader(clientPacketData) val dataLength = clientPacketData.limit() - clientPacketData.position() val sourceIP = ip4Header.sourceIP val destinationIP = ip4Header.destinationIP val sourcePort = tcpHeader.getSourcePort() val destinationPort = tcpHeader.getDestinationPort() if (tcpHeader.isSYN()) { // 3-way handshake + create new session replySynAck(ip4Header, tcpHeader) } else if (tcpHeader.isACK()) { val key = getConnectionKey(Protocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort) val connection = manager.getConnectionByKey(key) if (connection == null) { Log.w(TAG, "Ack for unknown session: $key") if (tcpHeader.isFIN()) { sendLastAck(ip4Header, tcpHeader) } else if (!tcpHeader.isRST()) { sendRstPacket(ip4Header, tcpHeader, dataLength) } return } synchronized(connection) { connection.lastIpHeader = ip4Header connection.lastTcpHeader = tcpHeader //any data from client? if (dataLength > 0) { //init proxy initProxyConnect(clientPacketData, destinationIP, destinationPort, connection) //accumulate data from client if (connection.recSequence == 0L || tcpHeader.sequenceNumber >= connection.recSequence) { val addedLength = manager.addClientData(clientPacketData, connection) //send ack to client only if new data was added sendAck(ip4Header, tcpHeader, addedLength, connection) } else { sendAckForDisorder(ip4Header, tcpHeader, dataLength) } } else { //an ack from client for previously sent data acceptAck(tcpHeader, connection) if (connection.isClosingConnection) { sendFinAck(ip4Header, tcpHeader, connection) } else if (connection.isAckedToFin && !tcpHeader.isFIN()) { //the last ACK from client after FIN-ACK flag was sent manager.closeConnection( Protocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort ) // Log.d(TAG, "got last ACK after FIN, session is now closed."); } } //received the last segment of data from vpn client if (tcpHeader.isPSH()) { // Tell the NIO thread to immediately send data to the destination pushDataToDestination(connection, tcpHeader) } else if (tcpHeader.isFIN()) { //fin from vpn client is the last packet //ack it // Log.d(TAG, "FIN from vpn client, will ack it."); ackFinAck(ip4Header, tcpHeader, connection) } else if (tcpHeader.isRST()) { resetTCPConnection(ip4Header, tcpHeader) } if (!connection.isAbortingConnection) { manager.keepSessionAlive(connection) } } } else if (tcpHeader.isFIN()) { //case client sent FIN without ACK val connection = manager.getConnection( Protocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort ) if (connection == null) ackFinAck( ip4Header, tcpHeader, null ) else manager.keepSessionAlive(connection) } else if (tcpHeader.isRST()) { resetTCPConnection(ip4Header, tcpHeader) } else { Log.d(TAG, "unknown TCP flag") val str1 = getOutput(ip4Header, tcpHeader, clientPacketData.array()) Log.d(TAG, ">>>>>>>> Received from client <<<<<<<<<<") Log.d(TAG, str1) Log.d(TAG, ">>>>>>>>>>>>>>>>>>>end receiving from client>>>>>>>>>>>>>>>>>>>>>") } } private fun initProxyConnect( clientPacketData: ByteBuffer, destinationIP: Int, destinationPort: Int, connection: Connection ) { if (connection.isInitConnect) { return } connection.isInitConnect = true val proxyAddress = getProxyAddress(clientPacketData, destinationIP, destinationPort) try { val channel = connection.channel as SocketChannel? val connected = channel!!.connect(proxyAddress) connection.isConnected = connected nioService.registerSession(connection) if (proxyAddress == manager.proxyAddress) { //获取进程信息 ProcessInfoManager.instance.setConnectionOwnerUid(connection) Log.d( TAG, "Proxy Initiate connecting key:" + connection.toString() + " " + channel.localAddress + " to remote tcp server: " + channel.remoteAddress ) } } catch (e: Exception) { val ips = intToIPAddress(destinationIP) Log.w(TAG, "Failed to reconnect to $ips:$destinationPort", e) } } private fun sendRstPacket(ip: IP4Header, tcp: TCPHeader, dataLength: Int) { val data = TCPPacketFactory.createRstData(ip, tcp, dataLength) writer.write(data) Log.d( TAG, "Sent RST Packet to client with dest => " + intToIPAddress(ip.destinationIP) + ":" + tcp.getDestinationPort() ) } private fun sendLastAck(ip: IP4Header, tcp: TCPHeader) { val data = TCPPacketFactory.createResponseAckData(ip, tcp, tcp.sequenceNumber + 1) writer.write(data) // Log.d(TAG,"Sent last ACK Packet to client with dest => " + // PacketUtil.intToIPAddress(ip.getDestinationIP()) + ":" + // tcp.getDestinationPort()); } private fun ackFinAck(ip: IP4Header, tcp: TCPHeader, connection: Connection?) { val ack = tcp.sequenceNumber + 1 val seq = tcp.ackNumber val data = TCPPacketFactory.createFinAckData(ip, tcp, ack, seq, isFin = true, isAck = true) writer.write(data) if (connection != null) { connection.cancelKey() manager.closeConnection(connection) // Log.d(TAG,"ACK to client's FIN and close session => "+PacketUtil.intToIPAddress(ip.getDestinationIP())+":"+tcp.getDestinationPort() // +"-"+PacketUtil.intToIPAddress(ip.getSourceIP())+":"+tcp.getSourcePort()); } } private fun sendFinAck(ip: IP4Header, tcp: TCPHeader, connection: Connection) { val ack = tcp.sequenceNumber val seq = tcp.ackNumber val data = TCPPacketFactory.createFinAckData(ip, tcp, ack, seq, isFin = true, isAck = false) val stream = ByteBuffer.wrap(data) writer.write(data) // Log.d(TAG, "00000000000 FIN-ACK packet data to vpn client 000000000000") var vpnIp: IP4Header? = null try { vpnIp = IPPacketFactory.createIP4Header(stream) } catch (e: Exception) { e.printStackTrace() } var vpnTcp: TCPHeader? = null try { if (vpnIp != null) vpnTcp = TCPPacketFactory.createTCPHeader(stream) } catch (e: Exception) { e.printStackTrace() } if (vpnIp != null && vpnTcp != null) { val logOut = getOutput(vpnIp, vpnTcp, data) Log.d(TAG, logOut) } // Log.d(TAG, "0000000000000 finished sending FIN-ACK packet to vpn client 000000000000") connection.sendNext = seq + 1 //avoid re-sending it, from here client should take care the rest connection.isClosingConnection = false } private fun pushDataToDestination(connection: Connection, tcp: TCPHeader) { connection.isDataForSendingReady = true connection.timestampReplyTo = tcp.timeStampSender connection.timestampSender = System.currentTimeMillis().toInt() // Ping the NIO thread to write this, when the session is next writable connection.subscribeKey(SelectionKey.OP_WRITE) nioService.refreshSelect(connection) } /** * send acknowledgment packet to VPN client * * @param acceptedDataLength Data Length */ private fun sendAck( ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int, connection: Connection ) { val ackNumber = connection.recSequence + acceptedDataLength connection.recSequence = ackNumber val ackData = TCPPacketFactory.createResponseAckData(ipHeader, tcpHeader, ackNumber) writer.write(ackData) } /** * resend the last acknowledgment packet to VPN client, e.g. when an unexpected out of order * packet arrives. */ private fun resendAck(connection: Connection) { val data = TCPPacketFactory.createResponseAckData( connection.lastIpHeader!!, connection.lastTcpHeader!!, connection.recSequence ) writer.write(data) } private fun sendAckForDisorder( ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int ) { val ackNumber = tcpHeader.sequenceNumber + acceptedDataLength Log.e( TAG, "sent disorder ack, ack# " + tcpHeader.sequenceNumber + " + " + acceptedDataLength + " = " + ackNumber ) val data = TCPPacketFactory.createResponseAckData(ipHeader, tcpHeader, ackNumber) writer.write(data) } /** * acknowledge a packet. * * @param tcpHeader TCP Header */ private fun acceptAck(tcpHeader: TCPHeader, connection: Connection) { val isCorrupted = isPacketCorrupted(tcpHeader) // connection.setPacketCorrupted(isCorrupted); if (isCorrupted) { Log.e(TAG, "prev packet was corrupted, last ack# " + tcpHeader.ackNumber) } if (tcpHeader.ackNumber > connection.sendUnAck || tcpHeader.ackNumber == connection.sendNext ) { // connection.setAcked(true); connection.sendUnAck = tcpHeader.ackNumber connection.recSequence = tcpHeader.sequenceNumber connection.timestampReplyTo = tcpHeader.timeStampSender connection.timestampSender = System.currentTimeMillis().toInt() } else { Log.d( TAG, "Not Accepting ack# " + tcpHeader.ackNumber + " , it should be: " + connection.sendNext ) Log.d(TAG, "Prev sendUnAck: " + connection.sendUnAck) // connection.setAcked(false); } } /** * set connection as aborting so that background worker will close it. * * @param ip IP * @param tcp TCP */ private fun resetTCPConnection(ip: IP4Header, tcp: TCPHeader) { val session = manager.getConnection( Protocol.TCP, ip.destinationIP, tcp.getDestinationPort(), ip.sourceIP, tcp.getSourcePort() ) if (session != null) { synchronized(session) { session.isAbortingConnection = true } } } /** * create a new client's session and SYN-ACK packet data to respond to client */ @Throws(IOException::class) private fun replySynAck(ipHeader: IP4Header, tcpHeader: TCPHeader) { ipHeader.identification = 0 val packet = TCPPacketFactory.createSynAckPacketData(ipHeader, tcpHeader) val tcpTransport = packet.transportHeader as TCPHeader val connection = manager.createTCPConnection( ipHeader.destinationIP, tcpHeader.getDestinationPort(), ipHeader.sourceIP, tcpHeader.getSourcePort() ) if (connection.lastIpHeader != null) { // We have an existing session for this connection! We've somehow received a SYN // for an existing socket (or some kind of other race). We resend the last ACK // for this session, rejecting this SYN. Not clear why this happens, but it can. resendAck(connection) return } synchronized(connection) { connection.maxSegmentSize = tcpTransport.maxSegmentSize.toInt() connection.sendUnAck = tcpTransport.sequenceNumber connection.sendNext = tcpTransport.sequenceNumber + 1 //client initial sequence has been incremented by 1 and set to ack connection.recSequence = tcpTransport.ackNumber connection.lastIpHeader = ipHeader connection.lastTcpHeader = tcpHeader if (connection.isInitConnect) { nioService.registerSession(connection) } writer.write(packet.buffer) } } private fun handleICMPPacket(clientPacketData: ByteBuffer, ipHeader: IP4Header) { val requestPacket = ICMPPacketFactory.parseICMPPacket(clientPacketData) // Log.d(TAG, "Got an ICMP ping packet, type $requestPacket") if (requestPacket.type == ICMPPacket.DESTINATION_UNREACHABLE_TYPE) { // This is a packet from the phone, telling somebody that a destination is unreachable. // Might be caused by issues on our end, but it's unclear what kind of issues. Regardless, // we can't send ICMP messages ourselves or react usefully, so we drop these silently. return } else require(requestPacket.type == ICMPPacket.ECHO_REQUEST_TYPE) { // We only actually support outgoing ping packets. Loudly drop anything else: "Unknown ICMP type (" + requestPacket.type + "). Only echo requests are supported" } pingThreadPool.execute(object : Runnable { override fun run() { try { if (!isReachable(intToIPAddress(ipHeader.destinationIP))) { Log.d(TAG, "Failed ping, ignoring") return } val response = ICMPPacketFactory.buildSuccessPacket(requestPacket) // Flip the address val destination = ipHeader.destinationIP val source = ipHeader.sourceIP ipHeader.sourceIP = destination ipHeader.destinationIP = source val responseData = ICMPPacketFactory.packetToBuffer(ipHeader, response) Log.d(TAG, "Successful ping response") writer.write(responseData) } catch (e: Exception) { Log.w(TAG, "Handling ICMP failed with " + e.message) return } } private fun isReachable(ipAddress: String): Boolean { return try { InetAddress.getByName(ipAddress).isReachable(10000) } catch (_: IOException) { false } } }) } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/ConnectionManager.kt ================================================ package com.network.proxy.vpn import android.os.Build import android.util.Log import com.network.proxy.vpn.socket.CloseableConnection import com.network.proxy.vpn.socket.Constant import com.network.proxy.vpn.socket.ProtectSocketHolder.Companion.protect import com.network.proxy.vpn.util.PacketUtil import com.network.proxy.vpn.util.ProcessInfoManager import java.io.IOException import java.net.InetSocketAddress import java.net.SocketAddress import java.nio.ByteBuffer import java.nio.channels.DatagramChannel import java.nio.channels.SocketChannel import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap /** * 管理VPN客户端的连接 */ class ConnectionManager private constructor() : CloseableConnection { //单例 companion object { private const val TAG = "ConnectionManager" val instance = ConnectionManager() } private val table: ConcurrentMap = ConcurrentHashMap() var proxyAddress: InetSocketAddress? = null var proxyPassDomains: ArrayList? = null private val DEFAULT_PORTS: List = listOf( 80, // HTTP 443, // HTTPS 8080, // Common local dev ports 8000, 8080, 8888, 9000 // Common local dev ports ) override fun closeConnection(connection: Connection) { closeConnection( connection.protocol, connection.destinationIp, connection.destinationPort, connection.sourceIp, connection.sourcePort ) } /** * 从内存中删除连接,然后关闭套接字。 * */ fun closeConnection(protocol: Protocol, ip: Int, port: Int, srcIp: Int, srcPort: Int) { val key = Connection.getConnectionKey(protocol, ip, port, srcIp, srcPort) val connection: Connection? = table.remove(key) Log.d(TAG, "close connection $key") connection?.let { val channel = connection.channel try { channel?.close() } catch (e: IOException) { e.printStackTrace() } } } fun getConnection( protocol: Protocol, ip: Int, port: Int, srcIp: Int, srcPort: Int ): Connection? { val key = Connection.getConnectionKey(protocol, ip, port, srcIp, srcPort) return getConnectionByKey(key) } fun getConnectionByKey(key: String?): Connection? { return table[key] } /** * 创建tcp连接 */ fun createTCPConnection(ip: Int, port: Int, srcIp: Int, srcPort: Int): Connection { val key = Connection.getConnectionKey(Protocol.TCP, ip, port, srcIp, srcPort) val existingConnection: Connection? = table[key] if (existingConnection != null) { return existingConnection } val connection = Connection(Protocol.TCP, srcIp, srcPort, ip, port, this) val channel: SocketChannel = SocketChannel.open() channel.socket().keepAlive = true channel.socket().tcpNoDelay = true channel.socket().soTimeout = 0 channel.socket().receiveBufferSize = Constant.MAX_RECEIVE_BUFFER_SIZE channel.configureBlocking(false) Log.d(TAG, "created new SocketChannel for $key") protect(channel.socket()) connection.channel = channel var socketAddress: SocketAddress? = null // if (DEFAULT_PORTS.contains(port)) { // socketAddress = proxyAddress // } connection.isInitConnect = socketAddress != null if (socketAddress != null) { val connected = channel.connect(socketAddress) connection.isConnected = connected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //获取进程信息 ProcessInfoManager.instance.setConnectionOwnerUid(connection) Log.d( TAG, "Initiate connecting " + channel.localAddress + " to remote tcp server: " + channel.remoteAddress ) } } table[key] = connection return connection } @Throws(IOException::class) fun createUDPConnection(ip: Int, port: Int, srcIp: Int, srcPort: Int): Connection { val keys = Connection.getConnectionKey(Protocol.UDP, ip, port, srcIp, srcPort) val existingConnection: Connection? = table[keys] if (existingConnection != null) return existingConnection val connection = Connection(Protocol.UDP, srcIp, srcPort, ip, port, this) val channel: DatagramChannel = DatagramChannel.open() channel.socket().soTimeout = 0 channel.configureBlocking(false) protect(channel.socket()) connection.channel = channel // Initiate connection early to reduce latency val ips = PacketUtil.intToIPAddress(ip) val socketAddress: SocketAddress = InetSocketAddress(ips, port) channel.connect(socketAddress) connection.isConnected = channel.isConnected table[keys] = connection return connection } /** * 添加来自客户端的数据,该数据稍后将在接收到PSH标志时发送到目的服务器。 */ fun addClientData(buffer: ByteBuffer, session: Connection): Int { return if (buffer.limit() <= buffer.position()) 0 else session.setSendingData(buffer) } /** * 阻止java垃圾收集器收集会话 */ fun keepSessionAlive(connection: Connection) { val key = Connection.getConnectionKey( connection.protocol, connection.destinationIp, connection.destinationPort, connection.sourceIp, connection.sourcePort ) table[key] = connection } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/Protocol.java ================================================ package com.network.proxy.vpn; public enum Protocol { TCP, UDP } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/ProxyVpnThread.kt ================================================ package com.network.proxy.vpn import android.os.ParcelFileDescriptor import android.util.Log import com.network.proxy.ProxyVpnService.Companion.MAX_PACKET_LEN import com.network.proxy.vpn.socket.ClientPacketWriter import com.network.proxy.vpn.socket.SocketNIODataService import java.io.FileInputStream import java.io.FileOutputStream import java.io.InterruptedIOException import java.net.InetSocketAddress import java.nio.ByteBuffer /** * VPN线程,负责处理VPN接收到的数据包 * @author wanghongen */ class ProxyVpnThread( vpnInterface: ParcelFileDescriptor, proxyHost: String, proxyPort: Int, proxyPassDomains: ArrayList? = null, ) : Thread("Vpn thread") { companion object { const val TAG = "ProxyVpnThread" } @Volatile private var running = false private val vpnReadChannel = FileInputStream(vpnInterface.fileDescriptor).channel // 此VPN接收的来自上游服务器的数据包 private val vpnWriteStream = FileOutputStream(vpnInterface.fileDescriptor) private val vpnPacketWriter = ClientPacketWriter(vpnWriteStream) private val vpnPacketWriterThread = Thread(vpnPacketWriter) // Background service & task for non-blocking socket private val nioService = SocketNIODataService(vpnPacketWriter) private val dataServiceThread = Thread(nioService, "Socket NIO thread") private val manager = ConnectionManager.instance.apply { //流量转发到代理地址 this.proxyAddress = InetSocketAddress(proxyHost, proxyPort) this.proxyPassDomains = proxyPassDomains } private val handler = ConnectionHandler(manager, nioService, vpnPacketWriter) private var currentThread: Thread? = null override fun run() { Log.i(TAG, "Vpn thread starting") currentThread = currentThread() dataServiceThread.start() vpnPacketWriterThread.start() val readBuffer = ByteBuffer.allocate(MAX_PACKET_LEN) running = true while (running) { try { val length = vpnReadChannel.read(readBuffer) if (length > 0) { try { readBuffer.flip() handler.handlePacket(readBuffer) } catch (e: Exception) { val errorMessage = (e.message ?: e.toString()) Log.e(TAG, errorMessage, e) } readBuffer.clear() } else { sleep(50) } } catch (e: InterruptedException) { Log.i(TAG, "Sleep interrupted: " + e.message) } catch (e: InterruptedIOException) { Log.i(TAG, "Read interrupted: " + e.message) } catch (e: Exception) { val errorMessage = (e.message ?: e.toString()) Log.e(TAG, errorMessage, e) if (!vpnReadChannel.isOpen) { Log.i(TAG, "VPN read channel closed") running = false } } } Log.i(TAG, "Vpn thread stop") } @Synchronized fun stopThread() { if (running) { running = false nioService.shutdown() dataServiceThread.interrupt() vpnPacketWriter.shutdown() vpnPacketWriterThread.interrupt() currentThread?.interrupt() } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/Tag.kt ================================================ package com.network.proxy.vpn fun formatTag(tag: String): String { return tag } val Any.TAG: String get() { return javaClass.name } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/ClientPacketWriter.kt ================================================ package com.network.proxy.vpn.socket import android.util.Log import java.io.FileOutputStream import java.io.IOException import java.util.concurrent.BlockingDeque import java.util.concurrent.LinkedBlockingDeque import kotlin.concurrent.Volatile class ClientPacketWriter(private val clientWriter: FileOutputStream) : Runnable { companion object { private const val TAG: String = "ClientPacketWriter" private const val MAX_PACKET_LEN = 32767 } @Volatile private var shutdown = false private val packetQueue: BlockingDeque = LinkedBlockingDeque() fun write(data: ByteArray) { if (data.size > MAX_PACKET_LEN) throw Error("Packet too large") packetQueue.addLast(data) } fun shutdown() { this.shutdown = true } override fun run() { while (!this.shutdown && clientWriter.channel.isOpen) { try { val data: ByteArray = this.packetQueue.take() try { this.clientWriter.write(data) } catch (e: IOException) { Log.e(TAG, "Error writing $shutdown data.length bytes to the VPN") e.printStackTrace() // this.packetQueue.addFirst(data) // Put the data back, so it's resent Thread.sleep(10) // Add an arbitrary tiny pause, in case that helps } } catch (ignored: InterruptedException) { } } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/CloseableConnection.kt ================================================ package com.network.proxy.vpn.socket import com.network.proxy.vpn.Connection interface CloseableConnection { /** * 关闭连接 */ fun closeConnection(connection: Connection) } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/Constant.kt ================================================ package com.network.proxy.vpn.socket object Constant { const val MAX_RECEIVE_BUFFER_SIZE = 65535 } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/ProtectSocket.kt ================================================ package com.network.proxy.vpn.socket import java.net.DatagramSocket import java.net.Socket interface ProtectSocket { /** * 保护Socket不受VPN连接的影响。保护后,通过该套接字发送的数据将直接进入底层网络,因此其流量不会通过VPN转发。 */ fun protect(socket: Socket): Boolean fun protect(socket: DatagramSocket): Boolean } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/ProtectSocketHolder.kt ================================================ package com.network.proxy.vpn.socket import java.net.DatagramSocket import java.net.Socket /** * ProtectSocket的持有者,用于在VPNService中获取ProtectSocket的实例 */ class ProtectSocketHolder { companion object { private var protectSocket: ProtectSocket? = null fun setProtectSocket(protectSocket: ProtectSocket) { this.protectSocket = protectSocket } fun getProtectSocket(): ProtectSocket? { return protectSocket } fun protect(socket: Socket): Boolean { return protectSocket?.protect(socket) ?: false } fun protect(socket: DatagramSocket): Boolean { return protectSocket?.protect(socket) ?: false } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketChannelReader.java ================================================ package com.network.proxy.vpn.socket; import androidx.annotation.NonNull; import android.util.Log; import com.network.proxy.vpn.Connection; import com.network.proxy.vpn.TagKt; import com.network.proxy.vpn.transport.protocol.IP4Header; import com.network.proxy.vpn.transport.protocol.TCPHeader; import com.network.proxy.vpn.transport.protocol.TCPPacketFactory; import com.network.proxy.vpn.transport.protocol.UDPPacketFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.channels.spi.AbstractSelectableChannel; /** * Takes a session, and reads all available upstream data back into it. * Used by the NIO thread, and run synchronously as part of that non-blocking loop. */ class SocketChannelReader { private final String TAG = TagKt.getTAG(this); private final ClientPacketWriter writer; public SocketChannelReader(ClientPacketWriter writer) { this.writer = writer; } public void read(Connection connection) { AbstractSelectableChannel channel = connection.getChannel(); if (channel instanceof SocketChannel) { readTCP(connection); } else if (channel instanceof DatagramChannel) { readUDP(connection); } else { return; } // Resubscribe to reads, so that we're triggered again if more data arrives later. connection.subscribeKey(SelectionKey.OP_READ); if (connection.isAbortingConnection()) { Log.d(TAG, "removing aborted connection -> " + connection); connection.cancelKey(); if (channel instanceof SocketChannel) { try { SocketChannel socketChannel = (SocketChannel) channel; if (socketChannel.isConnected()) { socketChannel.close(); } } catch (IOException e) { Log.e(TAG, e.toString()); } } else { try { DatagramChannel datagramChannel = (DatagramChannel) channel; if (datagramChannel.isConnected()) { datagramChannel.close(); } } catch (IOException e) { e.printStackTrace(); } } connection.closeConnection(); } } private void readTCP(@NonNull Connection connection) { if (connection.isAbortingConnection()) { return; } SocketChannel channel = (SocketChannel) connection.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(Constant.MAX_RECEIVE_BUFFER_SIZE); int len; try { do { len = channel.read(buffer); if (len > 0) { //-1 mean it reach the end of stream sendToRequester(buffer, len, connection); buffer.clear(); } else if (len == -1) { // Log.d(TAG,"End of data from remote server, will send FIN to client"); Log.d(TAG, "send FIN to: " + connection); sendFin(connection); connection.setAbortingConnection(true); } } while (len > 0); } catch (NotYetConnectedException e) { Log.e(TAG, "socket not connected"); } catch (ClosedByInterruptException e) { Log.e(TAG, "ClosedByInterruptException reading SocketChannel: " + e.getMessage()); } catch (ClosedChannelException e) { Log.e(TAG, "ClosedChannelException reading SocketChannel: " + e.getMessage()); } catch (IOException e) { Log.e(TAG, "Error reading data from SocketChannel: " + e.getMessage()); connection.setAbortingConnection(true); } } private void sendToRequester(ByteBuffer buffer, int dataSize, @NonNull Connection connection) { // Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a // trigger to set PSH on the resulting TCP packet that goes to the VPN. connection.setHasReceivedLastSegment(dataSize < Constant.MAX_RECEIVE_BUFFER_SIZE); buffer.limit(dataSize); buffer.flip(); // TODO should allocate new byte array? byte[] data = new byte[dataSize]; System.arraycopy(buffer.array(), 0, data, 0, dataSize); connection.addReceivedData(data); //pushing all data to vpn client while (connection.hasReceivedData()) { pushDataToClient(connection); } } /** * create packet data and send it to VPN client */ private void pushDataToClient(@NonNull Connection connection) { if (!connection.hasReceivedData()) { //no data to send Log.d(TAG, "no data for vpn client"); } IP4Header ipHeader = connection.getLastIpHeader(); TCPHeader tcpheader = connection.getLastTcpHeader(); // TODO What does 60 mean? int max = connection.getMaxSegmentSize() - 60; if (max < 1) { max = 1024; } byte[] packetBody = connection.getReceivedData(max); if (packetBody != null && packetBody.length > 0) { long unAck = connection.getSendNext(); long nextUnAck = connection.getSendNext() + packetBody.length; connection.setSendNext((int) nextUnAck); //we need this data later on for retransmission // connection.setUnackData(packetBody); // connection.setResendPacketCounter(0); byte[] data = TCPPacketFactory.createResponsePacketData(ipHeader, tcpheader, packetBody, connection.getHasReceivedLastSegment(), connection.getRecSequence(), (int) unAck, connection.getTimestampSender(), connection.getTimestampReplyTo()); writer.write(data); } } private void sendFin(Connection connection) { final IP4Header ipHeader = connection.getLastIpHeader(); final TCPHeader tcpheader = connection.getLastTcpHeader(); final byte[] data = TCPPacketFactory.INSTANCE.createFinData(ipHeader, tcpheader, connection.getRecSequence(), connection.getSendNext(), connection.getTimestampSender(), connection.getTimestampReplyTo()); writer.write(data); } private void readUDP(Connection connection) { DatagramChannel channel = (DatagramChannel) connection.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(Constant.MAX_RECEIVE_BUFFER_SIZE); int len; try { do { if (connection.isAbortingConnection()) { break; } len = channel.read(buffer); if (len > 0) { buffer.limit(len); buffer.flip(); //create UDP packet byte[] data = new byte[len]; System.arraycopy(buffer.array(), 0, data, 0, len); byte[] packetData = UDPPacketFactory.createResponsePacket( connection.getLastIpHeader(), connection.getLastUdpHeader(), data); //write to client writer.write(packetData); buffer.clear(); } } while (len > 0); } catch (NotYetConnectedException ex) { Log.e(TAG, "failed to read from unconnected UDP socket"); } catch (IOException e) { Log.e(TAG, "Failed to read from UDP socket, aborting connection"); connection.setAbortingConnection(true); } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketChannelWriter.java ================================================ package com.network.proxy.vpn.socket; import androidx.annotation.NonNull; import android.util.Log; import com.network.proxy.vpn.Connection; import com.network.proxy.vpn.TagKt; import com.network.proxy.vpn.transport.protocol.TCPPacketFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.channels.spi.AbstractSelectableChannel; /** * Takes a VPN session, and writes all received data from it to the upstream channel. *

* If any writes fail, it resubscribes to OP_WRITE, and tries again next time * that fires (as soon as the channel is ready for more data). *

* Used by the NIO thread, and run synchronously as part of that non-blocking loop. */ public class SocketChannelWriter { private final String TAG = TagKt.getTAG(this); private final ClientPacketWriter writer; SocketChannelWriter(ClientPacketWriter writer) { this.writer = writer; } public void write(@NonNull Connection connection) { AbstractSelectableChannel channel = connection.getChannel(); if (channel instanceof SocketChannel) { writeTCP(connection); } else if(channel instanceof DatagramChannel) { writeUDP(connection); } else { // We only ever create TCP & UDP channels, so this should never happen throw new IllegalArgumentException("Unexpected channel type: " + channel); } if (connection.isAbortingConnection()) { Log.d(TAG,"removing aborted connection -> " + connection); connection.cancelKey(); if (channel instanceof SocketChannel) { try { SocketChannel socketChannel = (SocketChannel) channel; if (socketChannel.isConnected()) { socketChannel.close(); } } catch (IOException e) { e.printStackTrace(); } } else { try { DatagramChannel datagramChannel = (DatagramChannel) channel; if (datagramChannel.isConnected()) { datagramChannel.close(); } } catch (IOException e) { e.printStackTrace(); } } connection.closeConnection(); } } private void writeUDP(Connection connection) { try { writePendingData(connection); // Date dt = new Date(); // connection.connectionStartTime = dt.getTime(); }catch(NotYetConnectedException ex2){ connection.setAbortingConnection(true); Log.e(TAG,"Error writing to unconnected-UDP server, will abort current connection: "+ex2.getMessage()); } catch (IOException e) { connection.setAbortingConnection(true); e.printStackTrace(); Log.e(TAG,"Error writing to UDP server, will abort connection: "+e.getMessage()); } } private void writeTCP(Connection connection) { try { writePendingData(connection); } catch (NotYetConnectedException ex) { Log.e(TAG,"failed to write to unconnected socket: " + ex.getMessage()); } catch (IOException e) { Log.e(TAG,"Error writing to server: " + e); //close connection with vpn client byte[] rstData = TCPPacketFactory.INSTANCE.createRstData( connection.getLastIpHeader(), connection.getLastTcpHeader(), 0); writer.write(rstData); //remove session Log.e(TAG,"failed to write to remote socket, aborting connection"); connection.setAbortingConnection(true); } } private void writePendingData(Connection connection) throws IOException { if (!connection.hasDataToSend()) return; AbstractSelectableChannel channel = connection.getChannel(); byte[] data = connection.getSendingData(); ByteBuffer buffer = ByteBuffer.allocate(data.length); buffer.put(data); buffer.flip(); while (buffer.hasRemaining()) { int bytesWritten = channel instanceof SocketChannel ? ((SocketChannel) channel).write(buffer) : ((DatagramChannel) channel).write(buffer); if (bytesWritten == 0) { break; } } if (buffer.hasRemaining()) { // The channel's own buffer is full, so we have to save this for later. Log.i(TAG, buffer.remaining() + " bytes unwritten for " + channel); // Put the remaining data from the buffer back into the session connection.setSendingData(buffer.compact()); // Subscribe to WRITE events, so we know when this is ready to resume. connection.subscribeKey(SelectionKey.OP_WRITE); } else { // All done, all good -> wait until the next TCP PSH / UDP packet connection.setDataForSendingReady(false); // We don't need to know about WRITE events any more, we've written all our data. // This is safe from races with new data, due to the session lock in NIO. connection.unsubscribeKey(SelectionKey.OP_WRITE); } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketNIODataService.java ================================================ package com.network.proxy.vpn.socket; import android.util.Log; import com.network.proxy.vpn.Connection; import com.network.proxy.vpn.TagKt; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.spi.AbstractSelectableChannel; import java.util.Iterator; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * A service that single-threadedly processes the events around our session connections, * entirely via non-blocking NIO. *

* It uses a Selector that fires on outgoing socket events (connected, readable, writable), * handles the resulting operations, and keeps those subscriptions up to date. */ public class SocketNIODataService implements Runnable { private final String TAG = TagKt.getTAG(this); private final ReentrantLock nioSelectionLock = new ReentrantLock(); private final ReentrantLock nioHandlingLock = new ReentrantLock(); private final Selector selector = Selector.open(); private final SocketChannelReader reader; private final SocketChannelWriter writer; private volatile boolean shutdown = false; public SocketNIODataService(ClientPacketWriter clientPacketWriter) throws IOException { reader = new SocketChannelReader(clientPacketWriter); writer = new SocketChannelWriter(clientPacketWriter); } @Override public void run() { Log.d(TAG,"SocketNIODataService starting in background..."); runTask(); } public void registerSession(Connection connection) throws ClosedChannelException { AbstractSelectableChannel channel = connection.getChannel(); boolean isConnected = channel instanceof DatagramChannel ? ((DatagramChannel) channel).isConnected() : ((SocketChannel) channel).isConnected(); // Log.i(TAG, "Registering new session: " + session); Lock selectorLock = lockSelector(selector); try { SelectionKey selectionKey = channel.register(selector, isConnected ? SelectionKey.OP_READ : SelectionKey.OP_CONNECT ); connection.setSelectionKey(selectionKey); selectionKey.attach(connection); // Log.d(TAG, "Registered selector successfully"); } finally { selectorLock.unlock(); } } private Lock lockSelector(Selector selector) { boolean gotSelectionLock = nioSelectionLock.tryLock(); if (gotSelectionLock) return nioSelectionLock; nioHandlingLock.lock(); // Ensure the NIO thread can't do anything on wakeup selector.wakeup(); nioSelectionLock.lock(); // Actually get the lock we want nioHandlingLock.unlock(); // Release the handling lock, which we no longer care about return nioSelectionLock; } /** * If the selector is currently select()ing, wake it up (e.g. to register changes to * interestOps). If it's not (and so it probably will select() very soon anyway) do nothing. * This is designed to be run after changing readyOps, to ensure the new ops get monitored * immediately (and fire immediately, if already ready). Without this, that blocks. */ public void refreshSelect(Connection connection) { boolean gotLock = nioSelectionLock.tryLock(); if (!gotLock) { connection.getSelectionKey().selector().wakeup(); } else { nioSelectionLock.unlock(); } } /** * Shut down the NIO thread */ public void shutdown(){ this.shutdown = true; selector.wakeup(); } private void runTask(){ Log.i(TAG, "NIO selector is running..."); while(!shutdown){ try { nioSelectionLock.lockInterruptibly(); selector.select(); } catch (IOException e) { Log.e(TAG,"Error in Selector.select(): " + e.getMessage()); try { Thread.sleep(100); } catch (InterruptedException ex) { Log.e(TAG, e.toString()); } continue; } catch (InterruptedException ex) { Log.i(TAG, "Select() interrupted"); } finally { if (nioSelectionLock.isHeldByCurrentThread()) { nioSelectionLock.unlock(); } } if (shutdown) { break; } // A lock here makes it possible to reliably grab the selection lock above nioHandlingLock.lock(); try { Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); Connection connection = ((Connection) key.attachment()); synchronized (connection) { // Sessions are locked during processing (no VPN data races) try { processSelectionKey(key); } catch (IOException e) { synchronized (key) { key.cancel(); } } } iterator.remove(); if (shutdown) { break; } } } finally { nioHandlingLock.unlock(); } } Log.i(TAG, "NIO selector shutdown"); } private void processSelectionKey(SelectionKey key) throws IOException { if (!key.isValid()) { Log.d(TAG,"Invalid SelectionKey"); return; } SelectableChannel channel = key.channel(); Connection connection = ((Connection) key.attachment()); if (connection == null) { Log.w(TAG, "Key fired with no session attached"); return; } if (channel instanceof SocketChannel && !connection.isConnected() && key.isConnectable()) { SocketChannel socketChannel = (SocketChannel) channel; if (socketChannel.isConnectionPending()) { boolean connected = socketChannel.finishConnect(); connection.setConnected(connected); } else { throw new IllegalStateException("TCP channels must either be connected or pending connection"); } } if (isConnected(channel)) { processConnectedSelection(key, connection); } } private boolean isConnected(SelectableChannel channel) { if (channel instanceof DatagramChannel) { return ((DatagramChannel) channel).isConnected(); } else if (channel instanceof SocketChannel) { return ((SocketChannel) channel).isConnected(); } else { throw new IllegalArgumentException("isConnected on unexpected channel type: " + channel); } } private void processConnectedSelection(SelectionKey key, Connection connection) { // Whilst connected, we always want READ and not CONNECT events connection.unsubscribeKey(SelectionKey.OP_CONNECT); connection.subscribeKey(SelectionKey.OP_READ); processSelectorRead(key, connection); processPendingWrite(key, connection); } private void processSelectorRead(SelectionKey selectionKey, Connection connection) { boolean canRead; synchronized (selectionKey) { // There's a race here that requires a lock, as isReadable requires isValid canRead = selectionKey.isValid() && selectionKey.isReadable(); } if (canRead) reader.read(connection); } private void processPendingWrite(SelectionKey selectionKey, Connection connection) { // Nothing to write? Skip this entirely, and make sure we're not subscribed if (!connection.hasDataToSend() || !connection.isDataForSendingReady()) { connection.unsubscribeKey(SelectionKey.OP_WRITE); return; } boolean canWrite; synchronized (selectionKey) { // There's a race here that requires a lock, as isReadable requires isValid canWrite = selectionKey.isValid() && selectionKey.isWritable(); } if (canWrite) { connection.unsubscribeKey(SelectionKey.OP_WRITE); writer.write(connection); // This will resubscribe to OP_WRITE if it can't complete } } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/Packet.kt ================================================ package com.network.proxy.vpn.transport import com.network.proxy.vpn.transport.protocol.IP4Header import com.network.proxy.vpn.transport.protocol.TransportHeader class Packet(var ipHeader: IP4Header, var transportHeader: TransportHeader, var buffer: ByteArray) { } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/icmp/ICMPPacket.java ================================================ package com.network.proxy.vpn.transport.icmp; import androidx.annotation.NonNull; public class ICMPPacket { // Two ICMP packets we can handle: simple ping & pong public static final byte ECHO_REQUEST_TYPE = 8; public static final byte ECHO_SUCCESS_TYPE = 0; // One very common packet we ignore: connection rejection. Unclear why this happens, // random incoming connections that the phone tries to reply to? Nothing we can do though, // as we can't forward ICMP onwards, and we can't usefully respond or react. public static final byte DESTINATION_UNREACHABLE_TYPE = 3; public final byte type; final byte code; // 0 for request, 0 for success, 0 - 15 for error subtypes final int checksum; final int identifier; final int sequenceNumber; final byte[] data; ICMPPacket( int type, int code, int checksum, int identifier, int sequenceNumber, byte[] data ) { this.type = (byte) type; this.code = (byte) code; this.checksum = checksum; this.identifier = identifier; this.sequenceNumber = sequenceNumber; this.data = data; } @NonNull public String toString() { return "ICMP packet type " + type + "/" + code + " id:" + identifier + " seq:" + sequenceNumber + " and " + data.length + " bytes of data"; } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/icmp/ICMPPacketFactory.java ================================================ package com.network.proxy.vpn.transport.icmp; import androidx.annotation.NonNull; import com.network.proxy.vpn.transport.protocol.IP4Header; import com.network.proxy.vpn.util.PacketUtil; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; public class ICMPPacketFactory { public static ICMPPacket parseICMPPacket(@NonNull ByteBuffer stream) { final byte type = stream.get(); final byte code = stream.get(); final int checksum = stream.getShort(); final int identifier = stream.getShort(); final int sequenceNumber = stream.getShort(); final byte[] data = new byte[stream.remaining()]; stream.get(data); return new ICMPPacket(type, code, checksum, identifier, sequenceNumber, data); } public static ICMPPacket buildSuccessPacket(ICMPPacket requestPacket) { return new ICMPPacket( 0, 0, 0, requestPacket.identifier, requestPacket.sequenceNumber, requestPacket.data ); } public static byte[] packetToBuffer(IP4Header ipHeader, ICMPPacket packet) { byte[] ipData = ipHeader.toBytes(); ByteArrayOutputStream icmpDataBuffer = new ByteArrayOutputStream(); icmpDataBuffer.write(packet.type); icmpDataBuffer.write(packet.code); icmpDataBuffer.write(asShortBytes(0 /* checksum placeholder */), 0, 2); if (packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE) { icmpDataBuffer.write(asShortBytes(packet.identifier), 0, 2); icmpDataBuffer.write(asShortBytes(packet.sequenceNumber), 0, 2); byte[] extraData = packet.data; icmpDataBuffer.write(extraData, 0, extraData.length); } else { throw new IllegalArgumentException("Can't serialize unrecognized ICMP packet type"); } byte[] icmpPacketData = icmpDataBuffer.toByteArray(); byte[] checksum = PacketUtil.INSTANCE.calculateChecksum(icmpPacketData, 0, icmpPacketData.length); ByteBuffer resultBuffer = ByteBuffer.allocate(ipData.length + icmpPacketData.length); resultBuffer.put(ipData); resultBuffer.put(icmpPacketData); // Replace the checksum placeholder resultBuffer.position(ipData.length + 2); resultBuffer.put(checksum); resultBuffer.position(0); byte[] result = new byte[resultBuffer.remaining()]; resultBuffer.get(result); return result; } private static byte[] asShortBytes(int value) { return ByteBuffer.allocate(2).putShort((short) value).array(); } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt ================================================ package com.network.proxy.vpn.transport.protocol import android.util.Log import java.nio.ByteBuffer import java.nio.ByteOrder /** * IPv4报头的数据结构。 */ data class IP4Header( var ipVersion: Byte = 0, //对于IPv4,其值为4(因此命名为IPv4)。 4bit private var internetHeaderLength: Byte = 0, //头部长度 4bit private var diffTypeOfService: Byte, //差分服务代码点 =>6位 private var ecn: Byte = 0, //显式拥塞通知(ECN) var totalLength: Int = 0, //此IP数据包的总长度 16bit var identification: Int = 0, //主要用于唯一标识单个IP数据报的片段组。 16bit private var mayFragment: Boolean, // 1bit 用于指示数据报是否可以分段。 private var lastFragment: Boolean, // 1bit 用于指示数据报是否是片段中的最后一个。 var fragmentOffset: Short = 0, //13bit,指定特定片段相对于原始未分段的IP数据报的开始的偏移量。 private var timeToLive: Byte = 0, //用于防止数据报持续存在。8bit var protocol: Byte = 0, //定义IP数据报的数据部分中使用的协议。 8bit var headerChecksum: Int = 0, //用于对头部进行错误检查的16位字段。 16bit var sourceIP: Int = 0, //发送者的IPv4地址。 32bit var destinationIP: Int = 0 //接收者的IPv4地址。 32bit ) { //用于控制或识别片段的3比特字段。 //bit 0: 保留;必须为零 //bit 1: Don't Fragment (DF) //bit 2: More Fragments (MF) private var flag: Byte = initFlag() private fun initFlag(): Byte { var initFlag = 0 if (mayFragment) { initFlag = 0x40 } if (lastFragment) { initFlag = (initFlag or 0x20) } return initFlag.toByte() } fun setMayFragment(mayFragment: Boolean) { this.mayFragment = mayFragment flag = if (mayFragment) { (flag.toInt() or 0x40).toByte() } else { (flag.toInt() and 0xBF).toByte() } } fun getIPHeaderLength(): Int { return internetHeaderLength * 4 } fun copy(): IP4Header { return IP4Header( ipVersion, internetHeaderLength, diffTypeOfService, ecn, totalLength, identification, mayFragment, lastFragment, fragmentOffset, timeToLive, protocol, headerChecksum, sourceIP, destinationIP ) } fun toBytes(): ByteArray { val buffer = ByteBuffer.allocate(getIPHeaderLength()) buffer.order(ByteOrder.BIG_ENDIAN) val versionAndHeaderLength = (ipVersion.toInt() shl 4) + internetHeaderLength buffer.put(versionAndHeaderLength.toByte()) val typeOfService: Byte = (diffTypeOfService.toInt() shl 2 and (ecn .toInt() and 0xFF)).toByte() buffer.put(typeOfService) buffer.putShort(totalLength.toShort()) buffer.putShort(identification.toShort()) //组合标志和部分片段偏移 buffer.put((fragmentOffset.toInt() shr 8 and 0x1F or flag.toInt()).toByte()) buffer.put(fragmentOffset.toByte()) buffer.put(timeToLive) buffer.put(protocol) buffer.putShort(headerChecksum.toShort()) buffer.putInt(sourceIP) buffer.putInt(destinationIP) return buffer.array() } } object IPPacketFactory { private const val IP4_HEADER_SIZE = 20 private const val IP4_VERSION = 0x04 /** * 从给定的ByteBuffer流创建IPv4标头 */ fun createIP4Header(buffer: ByteBuffer): IP4Header? { if (buffer.remaining() < IP4_HEADER_SIZE) { throw IllegalArgumentException("IP header byte array must have at least $IP4_HEADER_SIZE bytes") } val versionAndHeaderLength: Byte = buffer.get() val ipVersion = (versionAndHeaderLength.toInt() shr 4).toByte() if (ipVersion.toInt() != IP4_VERSION) { Log.e("IPPacketFactory", "Invalid IP version $ipVersion") return null } val internetHeaderLength = (versionAndHeaderLength.toInt() and 0x0F).toByte() val typeOfService = buffer.get().toInt() val diffTypeOfService: Byte = (typeOfService shr 2).toByte(); val ecn: Byte = (typeOfService and 0x03).toByte() val totalLength: Int = buffer.getShort().toInt() val identification: Int = buffer.getShort().toInt() val flagsAndFragmentOffset: Short = buffer.getShort() val mayFragment = flagsAndFragmentOffset.toInt() and 0x4000 != 0 val lastFragment = flagsAndFragmentOffset.toInt() and 0x2000 != 0 val fragmentOffset = (flagsAndFragmentOffset.toInt() and 0x1FFF).toShort() val timeToLive: Byte = buffer.get() val protocol: Byte = buffer.get() val checksum: Int = buffer.getShort().toInt() val sourceIp: Int = buffer.getInt() val desIp: Int = buffer.getInt() if (internetHeaderLength > 5) { // drop the IP option for (i in 0 until (internetHeaderLength - 5)) { buffer.getInt() } } return IP4Header( ipVersion, internetHeaderLength, diffTypeOfService, ecn, totalLength, identification, mayFragment, lastFragment, fragmentOffset, timeToLive, protocol, checksum, sourceIp, desIp ) } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPHeader.kt ================================================ package com.network.proxy.vpn.transport.protocol import java.nio.ByteBuffer import java.nio.ByteOrder /** * TCP报头的数据结构。 */ class TCPHeader( private var sourcePort: Int = 0, //源端口号 16bit private var destinationPort: Int = 0, //目的端口号 16bit var sequenceNumber: Long = 0, //序列号 32bit var ackNumber: Long = 0, //确认号 32bit var dataOffset: Int = 0, //数据偏移4bit var isNS: Boolean = false, //ECN-nonce concealment protection (experimental: see RFC 3540) var flags: Int = 0, //标志位 9bit var windowSize: Int = 0, //窗口大小 16bit var checksum: Int = 0, //校验和 16bit private var urgentPointer: Int = 0, //紧急指针 16bit var options: ByteArray? = null //选项 ) : TransportHeader { //options var maxSegmentSize: Short = 0 private var windowScale: Byte = 0 private var isSelectiveAckPermitted = false var timeStampSender = 0 var timeStampReplyTo = 0 companion object { private const val END_OF_OPTIONS_LIST: Byte = 0 private const val NO_OPERATION: Byte = 1 private const val MAX_SEGMENT_SIZE: Byte = 2 private const val WINDOW_SCALE: Byte = 3 private const val SELECTIVE_ACK_PERMITTED: Byte = 4 private const val TIME_STAMP: Byte = 8 } fun isSYN(): Boolean { return flags and 0x02 != 0 } fun isFIN(): Boolean { return flags and 0x01 != 0 } fun isRST(): Boolean { return flags and 0x04 != 0 } fun isPSH(): Boolean { return flags and 0x08 != 0 } fun isACK(): Boolean { return flags and 0x10 != 0 } fun isURG(): Boolean { return flags and 0x20 != 0 } fun isECE(): Boolean { return flags and 0x40 != 0 } fun isCWR(): Boolean { return flags and 0x80 != 0 } fun setIsRST(isRST: Boolean) { flags = if (isRST) { (flags or 0x04) } else { (flags and 0xFB) } } fun setIsSYN(isSYN: Boolean) { flags = if (isSYN) { (flags or 0x02) } else { (flags and 0xFD) } } fun setIsFIN(isFIN: Boolean) { flags = if (isFIN) { (flags or 0x01) } else { (flags and 0xFE) } } fun setIsPSH(isPSH: Boolean) { flags = if (isPSH) { (flags or 0x08) } else { (flags and 0xF7) } } fun setIsACK(isACK: Boolean) { flags = if (isACK) { (flags or 0x10) } else { (flags and 0xEF) } } fun getTCPHeaderLength(): Int { return dataOffset * 4 } fun toBytes(): ByteArray { val tcpHeaderLength = getTCPHeaderLength() val tcpHeader = ByteArray(tcpHeaderLength) val byteBuffer = ByteBuffer.wrap(tcpHeader) byteBuffer.order(ByteOrder.BIG_ENDIAN) byteBuffer.putShort(sourcePort.toShort()) byteBuffer.putShort(destinationPort.toShort()) byteBuffer.putInt(sequenceNumber.toInt()) byteBuffer.putInt(ackNumber.toInt()) //is ns and data offset byteBuffer.put(((dataOffset shl 4) and 0xF0 or (if (isNS) 0x1 else 0x0)).toByte()) byteBuffer.put(flags.toByte()) byteBuffer.putShort(windowSize.toShort()) byteBuffer.putShort(checksum.toShort()) byteBuffer.putShort(urgentPointer.toShort()) // encodeTcpOptions()?.let { // byteBuffer.put(it) // } return tcpHeader } fun copy(): TCPHeader { return TCPHeader( sourcePort, destinationPort, sequenceNumber, ackNumber, dataOffset, isNS, flags, windowSize, checksum, urgentPointer, options ) } private fun handleTcpOptions() { if (options == null) { return } var index = 0 val packet = ByteBuffer.wrap(options!!) val optionsSize = options!!.size while (index < optionsSize) { val optionKind = packet.get() index++ if (optionKind == END_OF_OPTIONS_LIST || optionKind == NO_OPERATION) { continue } val size = packet.get() index++ when (optionKind) { MAX_SEGMENT_SIZE -> { maxSegmentSize = packet.getShort() index += 2 } WINDOW_SCALE -> { windowScale = packet.get() index++ } SELECTIVE_ACK_PERMITTED -> isSelectiveAckPermitted = true TIME_STAMP -> { timeStampSender = packet.getInt() timeStampReplyTo = packet.getInt() index += 8 } else -> { skipRemainingOptions(packet, size.toInt()) index = index + size - 2 } } } } private fun skipRemainingOptions(packet: ByteBuffer, size: Int) { for (i in 2 until size) { packet.get() } } override fun getSourcePort(): Int { return sourcePort } override fun getDestinationPort(): Int { return destinationPort } fun setSourcePort(sourcePort: Int) { this.sourcePort = sourcePort } fun setDestinationPort(destinationPort: Int) { this.destinationPort = destinationPort } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt ================================================ package com.network.proxy.vpn.transport.protocol import com.network.proxy.vpn.transport.Packet import com.network.proxy.vpn.util.PacketUtil import java.nio.ByteBuffer import java.util.concurrent.ThreadLocalRandom object TCPPacketFactory { private const val TCP_HEADER_LENGTH = 20 /** * 从tcp报文创建tcpHeader */ @JvmStatic fun createTCPHeader(byteBuffer: ByteBuffer): TCPHeader { if (byteBuffer.remaining() < TCP_HEADER_LENGTH) { throw IllegalArgumentException("Invalid TCP Header Length") } val sourcePort: Int = byteBuffer.getShort().toInt() and 0xFFFF val destinationPort: Int = byteBuffer.getShort().toInt() and 0xFFFF val sequenceNumber: Long = byteBuffer.getInt().toLong() val ackNumber: Long = byteBuffer.getInt().toLong() val dataOffsetAndReserved = byteBuffer.get() val dataOffset = (dataOffsetAndReserved.toInt() and 0xF0) shr 4 val isNs: Boolean = dataOffsetAndReserved.toInt() and 0x1 > 0x0 val flags = byteBuffer.get().toInt() val window = byteBuffer.short.toInt() val checksum = byteBuffer.short.toInt() val urgentPointer = byteBuffer.short.toInt() var optionsAndPadding: ByteArray? = null val optionsSize = dataOffset - 5 if (optionsSize > 0) { optionsAndPadding = ByteArray(optionsSize * 4) byteBuffer.get(optionsAndPadding, 0, optionsSize * 4) } return TCPHeader( sourcePort, destinationPort, sequenceNumber, ackNumber, dataOffset, isNs, flags, window, checksum, urgentPointer, optionsAndPadding ) } /** * 创建带有RST标志的数据包,以便在需要重置时发送到客户端。 */ fun createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int): ByteArray { val ip = ipHeader.copy() val tcp = tcpHeader.copy() var ackNumber: Long = 0 var seqNumber: Long = 0 if (tcp.ackNumber > 0) { seqNumber = tcp.ackNumber } else { ackNumber = tcp.sequenceNumber + dataLength } tcp.ackNumber = ackNumber tcp.sequenceNumber = seqNumber //将IP从源翻转到目标 flipIp(ip, tcp) ip.identification = 0 tcp.flags = 0 tcp.isNS = false tcp.setIsRST(true) tcp.dataOffset = 5 tcp.options = null tcp.windowSize = 0 //重新计算IP长度 val totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() ip.totalLength = totalLength return createPacketData(ip, tcp, null) } /** * 创建数据包数据以发送回客户端 */ @JvmStatic fun createResponsePacketData( ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: ByteArray?, isPsh: Boolean, ackNumber: Long, seqNumber: Long, timeSender: Int, timeReplyTo: Int ): ByteArray { val ip = ipHeader.copy() val tcp = tcpHeader.copy() flipIp(ip, tcp) tcp.ackNumber = ackNumber tcp.sequenceNumber = seqNumber ip.identification = PacketUtil.getPacketId() //总是发送ACK //ACK is always sent tcp.setIsACK(true) tcp.setIsSYN(false) tcp.setIsPSH(isPsh) tcp.setIsFIN(false) tcp.timeStampSender = timeSender tcp.timeStampReplyTo = timeReplyTo tcp.dataOffset = 5 tcp.options = null var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() if (packetData != null) { totalLength += packetData.size } ip.totalLength = totalLength return createPacketData(ip, tcp, packetData) } /** * 向客户端确认服务器已收到请求。 */ @JvmStatic fun createResponseAckData( ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: Long ): ByteArray { val ip = ipHeader.copy() val tcp = tcpHeader.copy() flipIp(ip, tcp) val seqNumber = tcp.ackNumber tcp.ackNumber = ackToClient tcp.sequenceNumber = seqNumber ip.identification = PacketUtil.getPacketId() //ACK tcp.setIsACK(true) tcp.setIsSYN(false) tcp.setIsPSH(false) tcp.setIsFIN(false) tcp.dataOffset = 5 tcp.options = null ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() return createPacketData(ip, tcp, null) } //将IP从源翻转到目标 private fun flipIp(ip: IP4Header, tcp: TCPHeader) { val sourceIp = ip.destinationIP val destIp = ip.sourceIP val sourcePort = tcp.getDestinationPort() val destPort = tcp.getSourcePort() ip.destinationIP = destIp ip.sourceIP = sourceIp tcp.setDestinationPort(destPort) tcp.setSourcePort(sourcePort) } /** * 通过写回客户端流创建SYN-ACK数据包数据 */ fun createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader): Packet { val ip = ipHeader.copy() val tcp = tcpHeader.copy() flipIp(ip, tcp) //ack = received sequence + 1 val ackNumber = tcpHeader.sequenceNumber + 1 tcp.ackNumber = ackNumber //服务器生成的初始序列号 val seqNumber = ThreadLocalRandom.current().nextLong(0, 100000) tcp.sequenceNumber = seqNumber //SYN-ACK tcp.setIsACK(true) tcp.setIsSYN(true) tcp.timeStampReplyTo = tcp.timeStampSender tcp.timeStampSender = PacketUtil.currentTime tcp.dataOffset = 5 tcp.options = null ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() return Packet(ip, tcp, createPacketData(ip, tcp, null)) } /** * 创建发送到客户端的FIN-ACK */ fun createFinAckData( ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: Long, seqToClient: Long, isFin: Boolean, isAck: Boolean ): ByteArray { val ip = ipHeader.copy() val tcp = tcpHeader.copy() flipIp(ip, tcp) tcp.ackNumber = ackToClient tcp.sequenceNumber = seqToClient ip.identification = PacketUtil.getPacketId() //ACK tcp.setIsACK(isAck) tcp.setIsSYN(false) tcp.setIsPSH(false) tcp.setIsFIN(isFin) tcp.dataOffset = 5 tcp.options = null ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() return createPacketData(ip, tcp, null) } fun createFinData( ip: IP4Header, tcp: TCPHeader, ackNumber: Long, seqNumber: Long, timeSender: Int, timeReplyTo: Int ): ByteArray { //将IP从源翻转到目标 flipIp(ip, tcp) tcp.ackNumber = ackNumber tcp.sequenceNumber = seqNumber ip.identification = PacketUtil.getPacketId() tcp.timeStampReplyTo = timeReplyTo tcp.timeStampSender = timeSender tcp.flags = 0 tcp.isNS = false tcp.setIsACK(true) tcp.setIsFIN(true) tcp.dataOffset = 5 tcp.options = null //窗口大小应为零 tcp.windowSize = 0 ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() return createPacketData(ip, tcp, null) } /** * 从tcpHeader创建tcp报文 */ private fun createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: ByteArray?): ByteArray { val dataLength = data?.size ?: 0 val buffer = ByteBuffer.allocate(ipHeader.getIPHeaderLength() + tcpHeader.getTCPHeaderLength() + dataLength) val ipBuffer = ipHeader.toBytes() val tcpBuffer = tcpHeader.toBytes() buffer.put(ipBuffer) buffer.put(tcpBuffer) data?.let { buffer.put(it) } val zero = byteArrayOf(0, 0) //计算前先将校验和清零 buffer.position(10) buffer.put(zero) val ipChecksum = PacketUtil.calculateChecksum(buffer.array(), 0, ipBuffer.size) buffer.position(10) buffer.put(ipChecksum) val tcpStart = ipBuffer.size buffer.position(tcpStart + 16) buffer.put(zero) val tcpChecksum = PacketUtil.calculateTCPHeaderChecksum( buffer.array(), tcpStart, tcpBuffer.size + dataLength, ipHeader.destinationIP, ipHeader.sourceIP ) //将新的校验和写回阵列 buffer.position(tcpStart + 16) buffer.put(tcpChecksum) return buffer.array() } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TransportHeader.kt ================================================ package com.network.proxy.vpn.transport.protocol interface TransportHeader { fun getSourcePort(): Int fun getDestinationPort(): Int } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt ================================================ package com.network.proxy.vpn.transport.protocol import com.network.proxy.vpn.util.PacketUtil import java.nio.ByteBuffer /** * UDP报头的数据结构。 */ data class UDPHeader( var sourcePort: Int = 0, //源端口号 16bit var destinationPort: Int = 0, //目的端口号 16bit var length: Int = 0, //UDP数据报长度 16bit var checksum: Int = 0 //校验和 16bit ) object UDPPacketFactory { @JvmStatic fun createUDPHeader(stream: ByteBuffer): UDPHeader { require(stream.remaining() >= 8) { "Minimum UDP header is 8 bytes." } val srcPort = stream.getShort().toInt() and 0xffff val destPort = stream.getShort().toInt() and 0xffff val length = stream.getShort().toInt() and 0xffff val checksum = stream.getShort().toInt() return UDPHeader(srcPort, destPort, length, checksum) } /** * 创建用于响应vpn客户端的数据包 */ @JvmStatic fun createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: ByteArray?): ByteArray { val buffer: ByteArray var udpLen = 8 if (packetData != null) { udpLen += packetData.size } val srcPort = udp.destinationPort val destPort = udp.sourcePort val ipHeader = ip.copy() val srcIp = ip.destinationIP val destIp = ip.sourceIP ipHeader.setMayFragment(false) ipHeader.sourceIP = srcIp ipHeader.destinationIP = destIp ipHeader.identification = PacketUtil.getPacketId() //ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length val totalLength = ipHeader.getIPHeaderLength() + udpLen ipHeader.totalLength = totalLength buffer = ByteArray(totalLength) val ipData = ipHeader.toBytes() // clear IP checksum ipData[11] = 0 ipData[10] = 0 //calculate checksum for IP header val ipChecksum = PacketUtil.calculateChecksum(ipData, 0, ipData.size) //write result of checksum back to buffer System.arraycopy(ipChecksum, 0, ipData, 10, 2) System.arraycopy(ipData, 0, buffer, 0, ipData.size) //copy UDP header to buffer var start = ipData.size val intContainer = ByteArray(4) PacketUtil.writeIntToBytes(srcPort, intContainer, 0) //extract the last two bytes of int value System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 PacketUtil.writeIntToBytes(destPort, intContainer, 0) System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 PacketUtil.writeIntToBytes(udpLen, intContainer, 0) System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 val checksum: Short = 0 PacketUtil.writeIntToBytes(checksum.toInt(), intContainer, 0) System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 //now copy udp data if (packetData != null) System.arraycopy(packetData, 0, buffer, start, packetData.size) return buffer } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/PacketUtil.kt ================================================ package com.network.proxy.vpn.util import android.util.Log import com.network.proxy.vpn.formatTag import com.network.proxy.vpn.transport.protocol.IP4Header import com.network.proxy.vpn.transport.protocol.TCPHeader import java.nio.ByteBuffer import java.nio.ByteOrder /** * Helper class to perform various useful task */ object PacketUtil { @get:Synchronized private var packetId = 0 fun getPacketId() = packetId++ val currentTime: Int get() = (System.currentTimeMillis() / 1000).toInt() /** * convert int to byte array * [...](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) * * @param value int value 32 bits * @param buffer array of byte to write to * @param offset position to write to */ fun writeIntToBytes(value: Int, buffer: ByteArray, offset: Int) { if (buffer.size - offset < 4) { return } buffer[offset] = (value ushr 24 and 0x000000FF).toByte() buffer[offset + 1] = (value shr 16 and 0x000000FF).toByte() buffer[offset + 2] = (value shr 8 and 0x000000FF).toByte() buffer[offset + 3] = (value and 0x000000FF).toByte() } /** * convert array of max 4 bytes to int * * @param buffer byte array * @param start Starting point to be read in byte array * @param length Length to be read * @return value of int */ fun getNetworkInt(buffer: ByteArray, start: Int, length: Int): Int { var value = 0 var end = start + Math.min(length, 4) if (end > buffer.size) end = buffer.size for (i in start until end) { value = value or (buffer[i].toInt() and 0xFF) if (i < end - 1) value = value shl 8 } return value } /** * validate TCP header checksum * * @param source Source Port * @param destination Destination Port * @param data Payload * @param tcpLength TCP Header length * @return boolean */ fun isValidTCPChecksum( source: Int, destination: Int, data: ByteArray, tcpLength: Short, tcpOffset: Int ): Boolean { var buffersize = tcpLength + 12 var isodd = false if (buffersize % 2 != 0) { buffersize++ isodd = true } val buffer = ByteBuffer.allocate(buffersize) buffer.putInt(source) buffer.putInt(destination) buffer.put(0.toByte()) //reserved => 0 buffer.put(6.toByte()) //TCP protocol => 6 buffer.putShort(tcpLength) buffer.put(data, tcpOffset, tcpLength.toInt()) if (isodd) { buffer.put(0.toByte()) } return isValidIPChecksum(buffer.array(), buffersize) } /** * validate IP Header checksum * * @param data byte stream * @return boolean */ private fun isValidIPChecksum(data: ByteArray, length: Int): Boolean { var start = 0 var sum = 0 while (start < length) { sum += getNetworkInt(data, start, 2) start = start + 2 } //carry over one's complement while (sum shr 16 > 0) sum = (sum and 0xffff) + (sum shr 16) //flip the bit to get one' complement sum = sum.inv() val buffer = ByteBuffer.allocate(4) buffer.putInt(sum) return buffer.getShort(2).toInt() == 0 } fun calculateChecksum(data: ByteArray, offset: Int, length: Int): ByteArray { var start = offset var sum = 0 while (start < length) { sum += getNetworkInt(data, start, 2) start = start + 2 } //carry over one's complement while (sum shr 16 > 0) { sum = (sum and 0xffff) + (sum shr 16) } //flip the bit to get one' complement sum = sum.inv() //extract the last two byte of int val checksum = ByteArray(2) checksum[0] = (sum shr 8).toByte() checksum[1] = sum.toByte() return checksum } fun calculateTCPHeaderChecksum( data: ByteArray, offset: Int, tcplength: Int, destip: Int, sourceip: Int ): ByteArray { var buffersize = tcplength + 12 var odd = false if (buffersize % 2 != 0) { buffersize++ odd = true } val buffer = ByteBuffer.allocate(buffersize) buffer.order(ByteOrder.BIG_ENDIAN) //create virtual header buffer.putInt(sourceip) buffer.putInt(destip) buffer.put(0.toByte()) //reserved => 0 buffer.put(6.toByte()) //tcp protocol => 6 buffer.putShort(tcplength.toShort()) //add actual header + data buffer.put(data, offset, tcplength) //padding last byte to zero if (odd) { buffer.put(0.toByte()) } val tcparray = buffer.array() return calculateChecksum(tcparray, 0, buffersize) } fun intToIPAddress(addressInt: Int): String { return (addressInt ushr 24 and 0x000000FF).toString() + "." + (addressInt ushr 16 and 0x000000FF) + "." + (addressInt ushr 8 and 0x000000FF) + "." + (addressInt and 0x000000FF) } fun getOutput( ipHeader: IP4Header, tcpheader: TCPHeader, packetData: ByteArray ): String { val tcpLength = (packetData.size - ipHeader.getIPHeaderLength()).toShort() val isValidChecksum = isValidTCPChecksum( ipHeader.sourceIP, ipHeader.destinationIP, packetData, tcpLength, ipHeader.getIPHeaderLength() ) val isValidIPChecksum = isValidIPChecksum( packetData, ipHeader.getIPHeaderLength() ) val packetBodyLength = (packetData.size - ipHeader.getIPHeaderLength() - tcpheader.getTCPHeaderLength()) val str = StringBuilder("\r\nIP Version: ") .append(ipHeader.ipVersion.toInt()) .append("\r\nProtocol: ").append(ipHeader.protocol.toInt()) .append("\r\nID# ").append(ipHeader.identification) .append("\r\nTotal Length: ").append(ipHeader.totalLength) .append("\r\nData Length: ").append(packetBodyLength) .append("\r\nDest: ").append(intToIPAddress(ipHeader.destinationIP)) .append(":").append(tcpheader.getDestinationPort()) .append("\r\nSrc: ").append(intToIPAddress(ipHeader.sourceIP)) .append(":").append(tcpheader.getSourcePort()) .append("\r\nACK: ").append(tcpheader.ackNumber) .append("\r\nSeq: ").append(tcpheader.sequenceNumber) .append("\r\nIP Header length: ").append(ipHeader.getIPHeaderLength()) .append("\r\nTCP Header length: ").append(tcpheader.getTCPHeaderLength()) .append("\r\nACK: ").append(tcpheader.isACK()) .append("\r\nSYN: ").append(tcpheader.isSYN()) .append("\r\nCWR: ").append(tcpheader.isCWR()) .append("\r\nECE: ").append(tcpheader.isECE()) .append("\r\nFIN: ").append(tcpheader.isFIN()) .append("\r\nNS: ").append(tcpheader.isNS) .append("\r\nPSH: ").append(tcpheader.isPSH()) .append("\r\nRST: ").append(tcpheader.isRST()) .append("\r\nURG: ").append(tcpheader.isURG()) .append("\r\nIP checksum: ").append(ipHeader.headerChecksum) .append("\r\nIs Valid IP Checksum: ").append(isValidIPChecksum) .append("\r\nTCP Checksum: ").append(tcpheader.checksum) .append("\r\nIs Valid TCP checksum: ").append(isValidChecksum) .append("\r\nFragment Offset: ").append(ipHeader.fragmentOffset.toInt()) .append("\r\nWindow: ").append(tcpheader.windowSize) .append("\r\nData Offset: ").append(tcpheader.dataOffset) return str.toString() } /** * detect packet corruption flag in tcp options sent from client ACK * * @param tcpHeader TCPHeader * @return boolean */ fun isPacketCorrupted(tcpHeader: TCPHeader): Boolean { val options = tcpHeader.options if (options != null) { var i = 0 while (i < options.size) { val kind = options[i] if (kind.toInt() == 0 || kind.toInt() == 1) { } else if (kind.toInt() == 2) { i += 3 } else if (kind.toInt() == 3 || kind.toInt() == 14) { i += 2 } else if (kind.toInt() == 4) { i++ } else if (kind.toInt() == 5 || kind.toInt() == 15) { i = i + options[++i] - 2 } else if (kind.toInt() == 8) { i += 9 } else if (kind.toInt() == 23) { return true } else { Log.e( formatTag(PacketUtil::class.java.name), "unknown option: $kind" ) } i++ } } return false } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt ================================================ package com.network.proxy.vpn.util import android.content.Context import android.net.ConnectivityManager import android.os.Build import android.os.Process import android.system.OsConstants import android.util.Log import androidx.annotation.RequiresApi import com.network.proxy.ProxyVpnService import com.network.proxy.plugin.ProcessInfo import com.network.proxy.vpn.Connection import kotlinx.coroutines.CoroutineScope import java.io.File import java.net.InetSocketAddress import java.nio.channels.SocketChannel import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * 进程信息管理器,用于获取进程信息 * @author wanghongen */ class ProcessInfoManager private constructor() { companion object { @Suppress("all") val instance = ProcessInfoManager() } class NetworkInfo(val uid: Int, val remoteHost: String, val remotePort: Int) private val localPortCache = SimpleCache(10_000, 60, TimeUnit.SECONDS) private val appInfoCache = SimpleCache(10_000, 300, TimeUnit.SECONDS) var activity: Context? = null @RequiresApi(Build.VERSION_CODES.N) fun setConnectionOwnerUid(connection: Connection) { CoroutineScope(Dispatchers.IO).launch { val sourceAddress = InetSocketAddress(PacketUtil.intToIPAddress(connection.sourceIp), connection.sourcePort) val destinationAddress = InetSocketAddress( PacketUtil.intToIPAddress(connection.destinationIp), connection.destinationPort ) val uid = getProcessInfoUid(sourceAddress, destinationAddress) val channel = connection.channel if (uid != null && uid != Process.INVALID_UID && channel is SocketChannel && channel.isOpen) { try { val localAddress = channel.localAddress as InetSocketAddress val networkInfo = NetworkInfo(uid, destinationAddress.hostString, destinationAddress.port) localPortCache.put(localAddress.port, networkInfo) } catch (e: java.nio.channels.ClosedChannelException) { Log.w("ProcessInfoManager", "setConnectionOwnerUid", e) } } } } fun removeConnection(connection: Connection) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return } val channel = connection.channel if (channel is SocketChannel && channel.isOpen) { try { val localAddress = channel.localAddress as InetSocketAddress localPortCache.remove(localAddress.port) } catch (e: java.nio.channels.ClosedChannelException) { Log.w("ProcessInfoManager", "removeConnection", e) } } } @RequiresApi(Build.VERSION_CODES.N) private fun getProcessInfoUid( localAddress: InetSocketAddress, remoteAddress: InetSocketAddress ): Int? { // Log.d(TAG, "getProcessInfo: $localAddress $remoteAddress") if (activity == null) { return null } try { val connectivityManager: ConnectivityManager = activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val uid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { connectivityManager.getConnectionOwnerUid( OsConstants.IPPROTO_TCP, localAddress, remoteAddress ) } else { val method = ConnectivityManager::class.java.getMethod( "getConnectionOwnerUid", Int::class.javaPrimitiveType, InetSocketAddress::class.java, InetSocketAddress::class.java ) method.invoke( connectivityManager, OsConstants.IPPROTO_TCP, localAddress, remoteAddress ) as Int } if (uid != Process.INVALID_UID) { return uid } } catch (e: Exception) { Log.w("ProcessInfoManager", "Exception in getProcessInfoUid", e) return null } Log.w( "ProcessInfoManager", "Failed to get UID for local address $localAddress and remote address $remoteAddress" ) return null } suspend fun getProcessInfoByPort(host: String?, localPort: Int): ProcessInfo? { val networkInfo = localPortCache.get(localPort) if (networkInfo != null) { val processInfo = getProcessInfo(networkInfo.uid) if (processInfo != null) { val result = processInfo.copy() result["remoteHost"] = networkInfo.remoteHost result["remotePort"] = networkInfo.remotePort return result } return null } if (host == null || localPort <= 0 || ProxyVpnService.host == null || ProxyVpnService.port <= 0) { Log.w("ProcessInfoManager", "Invalid host or local port: $host:$localPort or ProxyVpnService not initialized") return null } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return withContext(Dispatchers.IO) { val localAddress = InetSocketAddress(host, localPort) val remoteAddress = InetSocketAddress(ProxyVpnService.host, ProxyVpnService.port) val uid = getProcessInfoUid(localAddress, remoteAddress) if (uid == null || uid == Process.INVALID_UID) { return@withContext null } val processInfo = getProcessInfo(uid) if (processInfo != null) { localPortCache.put( localPort, NetworkInfo(uid, remoteAddress.hostString, remoteAddress.port) ) val result = processInfo.copy() result["remoteHost"] = remoteAddress.hostString result["remotePort"] = remoteAddress.port return@withContext result } else { Log.w("ProcessInfoManager", "No process info found for UID: $uid") null } } } else { Log.w("ProcessInfoManager", "Access to /proc/net/tcp is restricted on non-rooted devices.") } return null } fun getRemoteAddressByPort(localPort: Int): Map? { val networkInfo = localPortCache.get(localPort) if (networkInfo != null) { return mapOf( "remoteHost" to networkInfo.remoteHost, "remotePort" to networkInfo.remotePort ) } return null } private fun getProcessInfo(uid: Int): ProcessInfo? { var appInfo = appInfoCache.get(uid) if (appInfo != null) return appInfo val packageManager = activity?.packageManager ?: return null val pkgNames: Array? = try { packageManager.getPackagesForUid(uid) } catch (e: Exception) { Log.w("ProcessInfoManager", "getPackagesForUid SecurityException: $uid", e) null } if (pkgNames == null) return null for (pkgName in pkgNames) { try { val applicationInfo = packageManager.getApplicationInfo(pkgName, 0) appInfo = ProcessInfo.create(packageManager, applicationInfo) appInfoCache.put(uid, appInfo) return appInfo } catch (e: Exception) { // Ignore packages that can't be found } } return null } } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/SimpleCache.kt ================================================ package com.network.proxy.vpn.util import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.TimeUnit class SimpleCache( private val maxSize: Int, private val expireAfterAccess: Long, private val timeUnit: TimeUnit ) { private val cache = ConcurrentHashMap>() companion object { private val EXECUTOR = Executors.newSingleThreadScheduledExecutor() } init { EXECUTOR.scheduleWithFixedDelay( { cleanUp() }, expireAfterAccess, expireAfterAccess, timeUnit ) } fun put(key: K, value: V) { if (cache.size >= maxSize) { cache.keys.iterator().next()?.let { cache.remove(it) } } cache[key] = CacheEntry(value, System.nanoTime()) } fun get(key: K): V? { val entry = cache[key] ?: return null if (System.nanoTime() - entry.lastAccessTime > timeUnit.toNanos(expireAfterAccess)) { cache.remove(key) return null } entry.lastAccessTime = System.nanoTime() return entry.value } fun remove(key: K) { cache.remove(key) } fun clear() { cache.clear() } private fun cleanUp() { val now = System.nanoTime() val expirationTime = timeUnit.toNanos(expireAfterAccess) val iterator = cache.entries.iterator() while (iterator.hasNext()) { val entry = iterator.next() if (now - entry.value.lastAccessTime > expirationTime) { iterator.remove() } } } private data class CacheEntry(val value: V, var lastAccessTime: Long) } ================================================ FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt ================================================ package com.network.proxy.vpn.util import java.nio.ByteBuffer import kotlin.math.min object TLS { /** * 判断是否是TLS Client Hello */ fun isTLSClientHello(packetData: ByteBuffer): Boolean { if (packetData.remaining() < 43) return false val position = packetData.position() val data = packetData.array() if (data[position].toInt() != 0x16 /* handshake */) return false if (data[1 + position].toInt() != 0x03) return false return if (data[5 + position].toInt() != 0x01) false else data[9 + position].toInt() == 0x03 && data[10 + position] >= 0x00 && data[1 + position] <= 0x03 } /** * 从TLS Client Hello 解析域名 */ fun getDomain(buffer: ByteBuffer): String? { var offset = buffer.position() val limit = buffer.limit() //TLS Client Hello if (buffer[offset].toInt() != 0x16) return null //Skip 43 byte header offset += 43 if (limit < (offset + 1)) return null //read session id val sessionIDLength = buffer[offset++] offset += sessionIDLength //read cipher suites if (offset + 2 > limit) return null val cipherSuitesLength = buffer.getShort(offset) offset += 2 offset += cipherSuitesLength //read Compression method. if (offset + 1 > limit) return null val compressionMethodLength = buffer[offset++].toInt() and 0xFF offset += compressionMethodLength if (offset > limit) return null //read Extensions if (offset + 2 > limit) return null val extensionsLength = buffer.getShort(offset) offset += 2 if (offset + extensionsLength > limit) return null var end: Int = offset + extensionsLength end = min(end, limit) while (offset + 4 <= end) { val extensionType = buffer.getShort(offset) val extensionLength = buffer.getShort(offset + 2) offset += 4 //server_name if (extensionType.toInt() == 0) { if (offset + 5 > limit) return null val serverNameListLength = buffer.getShort(offset) offset += 2 if (offset > limit) return null if (offset + serverNameListLength > limit) return null val serverNameType = buffer[offset++] val serverNameLength = buffer.getShort(offset) offset += 2 if (offset > limit || serverNameType.toInt() != 0) return null if (offset + serverNameLength > limit) return null val serverNameBytes = ByteArray(serverNameLength.toInt()) buffer.get(serverNameBytes) return String(serverNameBytes) } else { offset += extensionLength } } return null } } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ProxyPin Active 抓包正在运行 ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { google() mavenCentral() } subprojects { afterEvaluate { project -> if (project.hasProperty('android')) { project.android { if (namespace == null) { namespace project.group } } } } } } rootProject.buildDir = '../build' subprojects { afterEvaluate { project -> if (project.extensions.findByName("android") != null) { Integer pluginCompileSdk = project.android.compileSdk if (pluginCompileSdk != null && pluginCompileSdk < 31) { project.logger.error( "Warning: Overriding compileSdk version in Flutter plugin: " + project.name + " from " + pluginCompileSdk + " to 31 (to work around https://issuetracker.google.com/issues/199180389)." + "\nIf there is not a new version of " + project.name + ", consider filing an issue against " + project.name + " to increase their compileSdk to the latest (otherwise try updating to the latest version)." ) project.android { compileSdk 31 } } } } } subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Nov 28 00:35:45 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.nonFinalResIds=false ================================================ FILE: android/settings.gradle ================================================ import java.util.Properties pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath } settings.ext.flutterSdkPath = flutterSdkPath() includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.9.1' apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" ================================================ FILE: assets/certs/ca.crt ================================================ -----BEGIN CERTIFICATE----- MIID4jCCAsqgAwIBAgIJAKcH8Dna4mnZMA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV BAYTAkNOMQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzERMA8GA1UECgwI UHJveHlQaW4xETAPBgNVBAsMCFByb3h5UGluMRQwEgYDVQQDDAtQcm94eVBpbiBD QTAeFw0yMzA2MjQxNjA2MDlaFw0zMzA2MjExNjA2MTBaMGgxCzAJBgNVBAYTAkNO MQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzERMA8GA1UECgwIUHJveHlQ aW4xETAPBgNVBAsMCFByb3h5UGluMRQwEgYDVQQDDAtQcm94eVBpbiBDQTCCASIw DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRjfFvFDZS+PsdedUNq0Kn5t7RF NS0iQrZALr4LJm3UwtatHtMEWBb9ptam8pWezxrZPZ81+qnTcaq/To82yus5hJa4 JRk223YWn5JDd4izH4gcnSomhUQ6Ycrc0v+I7UEaHV+bQsleHEfYi2+E1qF+FBhR veLSPmz2QORd/U4+gDlOptgNWMQ9OTRHsMoDzb8J4SlcBu+s0dnq2WHOM9boGnfk 2wIgE+16uB23epPoYjex8zYGUswh8gNrIzXsr7i9IGtGf67FQYCWOXfZLeGgy0Q0 /r1lwSmywUkNaZIsiGZHveZsLtW93MWMFw0uneEvHsuQV+e8sdLI/633TGcCAwEA AaOBjjCBizAdBgNVHQ4EFgQU4YXwKkBDFoZY3D81RM25ECSc2qcwHwYDVR0jBBgw FoAU4YXwKkBDFoZY3D81RM25ECSc2qcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B Af8EBAMCAaYwEwYDVR0RBAwwCoIIUHJveHlQaW4wEwYDVR0lBAwwCgYIKwYBBQUH AwEwDQYJKoZIhvcNAQELBQADggEBAAc2s5TwuOdPdl3gYs121sY+HEMyXfsnVTGV dIlgjf+a0ECir2bcs64udaCIgBjd/vqhShMeqeQ4GJW7Ypb9556L213xjbLj/ZVU rgZda6oVd4der8YEHXKLxWAGlZQeeKHhw1lN4PYwxxGaf7/wsM7Dil0JLyOBtJaJ zNRzVzK9UHASDx0qDQVUBbeYzRviVCjxAGBNM/eNlPDX7m//vgCLxQgcxVdoJvMQ kSVQddo+d8fxnPAVx77dyX0T/ek7PQOsL6d08TVCdvgv50LwE8f9EMhHVv7zjEv2 0ZSaRQ0pvUnc0ClKXIGeMD71eYeeTz7CGjndxy5bdV/wmoo3Yek= -----END CERTIFICATE----- ================================================ FILE: assets/certs/ca_key.pem ================================================ -----BEGIN CERTIFICATE----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEY3xbxQ2Uvj7H XnVDatCp+be0RTUtIkK2QC6+CyZt1MLWrR7TBFgW/abWpvKVns8a2T2fNfqp03Gq v06PNsrrOYSWuCUZNtt2Fp+SQ3eIsx+IHJ0qJoVEOmHK3NL/iO1BGh1fm0LJXhxH 2ItvhNahfhQYUb3i0j5s9kDkXf1OPoA5TqbYDVjEPTk0R7DKA82/CeEpXAbvrNHZ 6tlhzjPW6Bp35NsCIBPtergdt3qT6GI3sfM2BlLMIfIDayM17K+4vSBrRn+uxUGA ljl32S3hoMtENP69ZcEpssFJDWmSLIhmR73mbC7VvdzFjBcNLp3hLx7LkFfnvLHS yP+t90xnAgMBAAECggEAPYnPFhKRRuK2WVLH+/Akop6Vae+l0hbCQMmr2/EygYgB 5bMpzYW29L1W4jw+F5RD4W3hWVpYyY5wN8jqnQXWYA8N9QyO02/VJRPBvNtXQYaf gs80kFixucdxjVfU5i3J6nR8b9D/BIpw4jKAvtkpSEFxmo1CqyimVw5zFxw8m599 gJX/WeA88l9/4/tGQ24TAZV7OaP+jgqb4hOPC6gB5YHYnGFfuAh8q1Gf3wGnU+3/ pdEDq1UPvMwZ3J7ifTjMHYh2gnT/xQSxOddbtNJfaBx8fUFC4GVEEZ1+j0zc2bOp /7q+Ab0QXLjMbe3bftMZZqffp2X6NLJipLQcw/HRQQKBgQDonHdqt5ZcsF7+Vsl4 KwmMNAz+jO6j51LU60F7QDhk7hGkCvUF2zJgYSkjlUNl6VS5aGmWTyUR3T3Eqiqs r81Qao5mxF0MjUU2QKgsw57YG2yASgSPdGqW0PFu1yrxLS51qLIGbp5AuZLULO+M iTvO1SRm29q45F9f/m0NRda0TQKBgQDYIqGVFcyQvQzGPZc5iOI9we526p+MGEsa ysRHs8wXJKCiINH2iw1bJGyRCOIZyFQwMRteC174tRnyZpsgTu6wTgaVnTHS8ZNQ LfjAQsMbs7TItjQF88/thujP15BXzTN7HN1y5kOVCAI7EvLJlZ4jMewfj+aqv2Sb o5ungsWtgwKBgQCgo2WIqk5JpneDt9WcikQmsc+DfzpSsK6wYeMvxbLsaMh//B0o NS8+BftOGoeX+qJLBINejTuxcZN1nHqqFSJ59YxwBg2oXGs+wzog59trrMyqb/Nk SmZNzu/ctvVt5uDd2mlPLddWJZHzuzCXYjKObP2dlxkedIA1H9SZxPA4RQKBgDAS 29/ePmb/NcUuU+GfObtE1HaszxoJGUN3UFsmecG4Cuak6C6vVSQtoNxNnoTfkyI4 +f5cBx7IoWHSQrTX+a1LXZmPolJqGzsdTpPtBZq2yQJPzJh6V4hclpIMP3XYFZhP nk39O5D9fAmJuGjwF4F6jCulBUh7U7RumqOSqcdjAoGBAKxCtQ0XT0Rlc6B37xTK /fVYaVbSDISBSVYJTy5vjQi5z+bqUaQrmfeW1z+WoVTeP0ZUgcxTXJbPBVeAC8Wx oTYfh5yTEu8FCBpBSWWsCteodBBZxXpINLuk9Ex44yxvuFhulugmYzyga+nqufV/ N5e8NEl7aISBW+PK16pnNO0e -----END CERTIFICATE----- ================================================ FILE: assets/js/fetch.js ================================================ function fetch(url, options) { options = options || {}; return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); const keys = []; const all = []; const headers = {}; const response = () => ({ ok: (request.status / 100 | 0) === 2, // 200-299 statusText: request.statusText, status: request.status, url: request.responseURL, body: request.response.body, text: () => Promise.resolve(request.responseText), json: () => { // TODO: review this handle because it may discard \n from json attributes try { // console.log('RESPONSE TEXT IN FETCH: ' + request.responseText); return Promise.resolve(JSON.parse(request.responseText)); } catch (e) { // console.log('ERROR on fetch parsing JSON: ' + e.message); return Promise.resolve(request.responseText); } }, blob: () => Promise.resolve(request.response.body), clone: response, headers: { ...headers, keys: () => keys, entries: () => all, get: n => headers[n.toLowerCase()], has: n => n.toLowerCase() in headers, } }); request.open(options.method || 'get', url, true); request.onload = () => { request.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => { keys.push(key = key.toLowerCase()); all.push([key, value]); headers[key] = headers[key] ? `${headers[key]},${value}` : value; }); resolve(response()); }; request.onerror = reject; request.withCredentials = options.credentials == 'include'; if (options.headers) { if (options.headers.constructor.name == 'Object') { for (const i in options.headers) { request.setRequestHeader(i, options.headers[i]); } } else { // if it is some Headers pollyfill, the way to iterate is through for of for (const header of options.headers) { request.setRequestHeader(header[0], header[1]); } } } request.send(options.body || null); }); } ================================================ FILE: devtools_options.yaml ================================================ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: ================================================ FILE: distribute_options.yaml ================================================ output: dist/ releases: - name: release jobs: - name: macos-dmg package: platform: macos target: dmg build_args: profile: true - name: windows-exe package: platform: windows target: exe build_args: profile: true - name: windows-msix package: platform: windows target: msix - name: windows-zip package: platform: windows target: zip ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! pod 'SnapKit', '~> 5.0.1' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| # You can remove unused permissions here # for more information: https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera 'PERMISSION_CAMERA=1', ## dart: PermissionGroup.photos # 'PERMISSION_PHOTOS=1', ] end end end ================================================ FILE: ios/ProxyPin/Info.plist ================================================ NSExtension NSExtensionPointIdentifier com.apple.networkextension.packet-tunnel NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).PacketTunnelProvider ================================================ FILE: ios/ProxyPin/PacketTunnelProvider.swift ================================================ // // PacketTunnelProvider.swift // ProxyPin // // Created by 汪红恩 on 2023/7/4. // import NetworkExtension import Network import os.log class PacketTunnelProvider: NEPacketTunnelProvider { private var proxyVpnService: ProxyVpnService? override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { NSLog("startTunnel") guard let conf = (protocolConfiguration as! NETunnelProviderProtocol).providerConfiguration else{ NSLog("[ERROR] No ProtocolConfiguration Found") exit(EXIT_FAILURE) } let host = conf["proxyHost"] as! String let proxyPort = conf["proxyPort"] as! Int let ipProxy = conf["ipProxy"] as! Bool? ?? false // parse proxyPassDomains: accept either [String] or comma-separated String var proxyPassDomains: [String]? = nil if let arr = conf["proxyPassDomains"] as? [String] { proxyPassDomains = arr.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } } else if let csv = conf["proxyPassDomains"] as? String { let list = csv.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } proxyPassDomains = list.isEmpty ? nil : list } // let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: host) NSLog(conf.debugDescription) networkSettings.mtu = 1500 let ipv4Settings = NEIPv4Settings(addresses: ["10.0.0.2"], subnetMasks: ["255.255.255.255"]) if (ipProxy){ ipv4Settings.includedRoutes = [NEIPv4Route.default()] ipv4Settings.excludedRoutes = [ NEIPv4Route(destinationAddress: "10.0.0.0", subnetMask: "255.0.0.0"), NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0"), // NEIPv4Route(destinationAddress: "127.0.0.0", subnetMask: "255.0.0.0"), NEIPv4Route(destinationAddress: "169.254.0.0", subnetMask: "255.255.0.0"), NEIPv4Route(destinationAddress: "172.16.0.0", subnetMask: "255.240.0.0"), NEIPv4Route(destinationAddress: "192.168.0.0", subnetMask: "255.255.0.0"), NEIPv4Route(destinationAddress: "17.0.0.0", subnetMask: "255.0.0.0"), ] let dns = "223.5.5.5,8.8.8.8" let dnsSettings = NEDNSSettings(servers: dns.components(separatedBy: ",")) dnsSettings.matchDomains = [""] dnsSettings.matchDomainsNoSearch = true networkSettings.dnsSettings = dnsSettings } //http代理 let proxySettings = NEProxySettings() proxySettings.httpEnabled = true proxySettings.httpServer = NEProxyServer(address: host, port: proxyPort) proxySettings.httpsEnabled = true proxySettings.httpsServer = NEProxyServer(address: host, port: proxyPort) // If a proxyPassDomains list was provided, use it as the exceptionList so these domains bypass the proxy. if let pass = proxyPassDomains { proxySettings.exceptionList = pass } proxySettings.matchDomains = [""] networkSettings.proxySettings = proxySettings networkSettings.ipv4Settings = ipv4Settings setTunnelNetworkSettings(networkSettings) { error in guard error == nil else { NSLog("startTunnel Encountered an error setting up the network: \(error.debugDescription)") completionHandler(error) return } if (ipProxy){ let proxyAddress = Network.NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: UInt16(proxyPort))!) self.proxyVpnService = ProxyVpnService(packetFlow: self.packetFlow, proxyAddress: proxyAddress) self.proxyVpnService!.start() } completionHandler(nil) } } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { proxyVpnService?.stop() completionHandler() } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { // Add code here to handle the message. if let handler = completionHandler { NSLog("handleAppMessage ", messageData.debugDescription) handler(messageData) } } override func sleep(completionHandler: @escaping () -> Void) { // Add code here to get ready to sleep. completionHandler() } override func wake() { // Add code here to wake up. } } ================================================ FILE: ios/ProxyPin/ProxyPin-Bridging-Header.h ================================================ // // ProxyPin-Bridging-Header.h // Runner // // Created by wanghongen on 2025/5/28. // #import "GBPing.h" ================================================ FILE: ios/ProxyPin/ProxyPin.entitlements ================================================ com.apple.security.application-groups group.com.proxy.pin com.apple.developer.networking.networkextension packet-tunnel-provider com.apple.developer.networking.vpn.api allow-vpn ================================================ FILE: ios/ProxyPin/vpn/Connection.swift ================================================ // // Connection.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation import Foundation import Network import os.log class Connection{ var nwProtocol: NWProtocol var sourceIp: UInt32 var sourcePort: UInt16 var destinationIp: UInt32 var destinationPort: UInt16 var channel: NWConnection? var isInitConnect: Bool = false var isConnected: Bool = false var isClosingConnection: Bool = false var isAbortingConnection: Bool = false var isAckedToFin: Bool = false private let connectionCloser: ConnectionManager init(nwProtocol: NWProtocol, sourceIp: UInt32, sourcePort: UInt16, destinationIp: UInt32, destinationPort: UInt16, connectionCloser: ConnectionManager) { self.nwProtocol = nwProtocol self.sourceIp = sourceIp self.sourcePort = sourcePort self.destinationIp = destinationIp self.destinationPort = destinationPort self.connectionCloser = connectionCloser } //发送缓冲区,用于存储要从vpn客户端发送到目标主机的数据 var sendBuffer = Data() var hasReceivedLastSegment = false //从客户端接收的最后一个数据包 var lastIpHeader: IP4Header? var lastTcpHeader: TCPHeader? var lastUdpHeader: UDPHeader? var timestampSender = 0 var timestampReplyTo = 0 //从客户端接收的序列 var recSequence: UInt32 = 0 //在tcp选项内的SYN期间由客户端发送 var maxSegmentSize = 0 //跟踪我们发送给客户端的ack,并等待客户端返回ack var sendUnAck: UInt32 = 0 //发送到客户端的下一个ack var sendNext: UInt32 = 0 static func getConnectionKey(nwProtocol: NWProtocol, destIp: UInt32, destPort: UInt16, sourceIp: UInt32, sourcePort: UInt16) -> String { let destIpString = PacketUtil.intToIPAddress(destIp) let sourceIpString = PacketUtil.intToIPAddress(sourceIp) return "\(nwProtocol)|\(sourceIpString):\(sourcePort)->\(destIpString):\(destPort)" } func closeConnection() { connectionCloser.closeConnection(connection: self) } func addSendData(data: Data) { self.sendBuffer.append(data) if (self.channel?.state != .ready) { os_log("Connection %{public}@ is not ready, cannot send data", log: OSLog.default, type: .debug, self.description) return } self.sendToDestination() } //发送到目标服务器的数据 func sendToDestination() { // os_log("Sending data to destination key %{public}@", log: OSLog.default, type: .debug, self.description) if (self.sendBuffer.count == 0) { return } let data = self.sendBuffer self.sendBuffer.removeAll() self.channel?.send(content: data, completion: .contentProcessed({ error in if let error = error { os_log("Failed to send data to destination key %{public}@ error: %{public}@", log: OSLog.default, type: .error, self.description, error.localizedDescription) self.closeConnection() } })) } var description: String { return Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: destinationIp, destPort: destinationPort, sourceIp: sourceIp, sourcePort: sourcePort) } } ================================================ FILE: ios/ProxyPin/vpn/ConnectionHandler.swift ================================================ // // ConnectionHandler.swift // ProxyPin // // Created by wanghongen on 2024/9/16. // import Foundation import NetworkExtension import Network import os.log enum ProtocolType: UInt8 { case icmp = 1, tcp = 6, udp = 17 } /// Handles incoming packets and routes them to the appropriate connection. class ConnectionHandler { private let manager: ConnectionManager private let writer: NEPacketTunnelFlow private let ioService: SocketIOService init(manager: ConnectionManager, writer: NEPacketTunnelFlow, ioService: SocketIOService) { self.manager = manager self.writer = writer self.ioService = ioService } //Handle unknown raw IP packet data public func handlePacket(packet: Data, version: NSNumber?) { guard let ipHeader = IPPacketFactory.createIP4Header(data: packet) else { os_log("Malformed IP packet", log: OSLog.default, type: .error) return } if ipHeader.ipVersion != 4 { os_log("Unsupported IP version: %d", log: OSLog.default, type: .error, ipHeader.ipVersion) return } // os_log("Handling packet length:%d, protocolNumber: %d", log: OSLog.default, type: .default, packet.count, ipHeader.protocolNumber) var clientPacketData = packet.subdata(in: IPPacketFactory.IP4_HEADER_SIZE.. Void) { // objc_sync_enter(lock) closure() // objc_sync_exit(lock) } private func handleUDPPacket(clientPacketData: Data, ipHeader: IP4Header) { guard let udpHeader = UDPPacketFactory.createUDPHeader(from: clientPacketData) else { os_log("Malformed UDP packet", log: OSLog.default, type: .error) return } var connection = manager.getConnection( nwProtocol: .UDP, ip: ipHeader.destinationIP, port: udpHeader.destinationPort, srcIp: ipHeader.sourceIP, srcPort: udpHeader.sourcePort ) let newSession = connection == nil if connection == nil { connection = manager.createUDPConnection( ip: ipHeader.destinationIP, port: udpHeader.destinationPort, srcIp: ipHeader.sourceIP, srcPort: udpHeader.sourcePort ) } guard let connection = connection else { os_log("Failed to create UDP connection", log: OSLog.default, type: .error) return } synchronized(connection) { os_log("handle UDP Packet %{public}@", log: OSLog.default, type: .default, connection.description) if newSession { ioService.registerSession(connection: connection) } let payload = clientPacketData.subdata(in: UDPPacketFactory.UDP_HEADER_LENGTH.. 0 { // os_log("[ConnectionHandler] Received data packet %{public}@ length:%d seq:%u, ack:%u", log: OSLog.default, type: .default, connection.description, dataLength, tcpHeader.sequenceNumber, tcpHeader.ackNumber) //init proxy self.initProxyConnect(packetData: tcpHeader.payload!, destinationIP: destinationIP, destinationPort: destinationPort, connection: connection) manager.addClientData(data: tcpHeader.payload!, connection: connection) sendAck(ipHeader: ipHeader, tcpHeader: tcpHeader, acceptedDataLength: dataLength, connection: connection) } else { // os_log("[ConnectionHandler] Received ACK packet %{public}@ seq:%u, ack:%u", log: OSLog.default, type: .default, connection.description, tcpHeader.sequenceNumber, tcpHeader.ackNumber) } acceptAck(tcpHeader: tcpHeader, connection: connection) if connection.isClosingConnection { sendFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection) } else if connection.isAckedToFin && !tcpHeader.isFIN() { manager.closeConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort) } //received the last segment of data from vpn client if tcpHeader.isPSH() { // Tell the NIO thread to immediately send data to the destination pushDataToDestination(connection: connection, tcpHeader: tcpHeader) } else if tcpHeader.isFIN() { //fin from vpn client is the last packet //ack it ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection) } else if tcpHeader.isRST() { resetTCPConnection(ip: ipHeader, tcp: tcpHeader) } if !connection.isAbortingConnection { manager.keepSessionAlive(connection: connection) } } } else if tcpHeader.isFIN() { os_log("Received FIN packet %{public}@:%d seq:%u", log: OSLog.default, type: .default, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber) //case client sent FIN without ACK guard let connection = manager.getConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort) else { ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: nil) return } manager.keepSessionAlive(connection: connection) } else if tcpHeader.isRST() { os_log("Received RST packet %{public}@:%d seq:%u", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber) resetTCPConnection(ip: ipHeader, tcp: tcpHeader) } else { os_log("Unknown TCP flag", log: OSLog.default, type: .error) } } private func initProxyConnect( packetData: Data, destinationIP: UInt32, destinationPort: UInt16, connection: Connection ) { guard !connection.isInitConnect else { return } connection.isInitConnect = true let supportsProtocol = supportsProtocol(packetData: packetData) let endpoint: Network.NWEndpoint if (supportsProtocol && manager.proxyAddress != nil) { endpoint = manager.proxyAddress! } else { let ipString = PacketUtil.intToIPAddress(destinationIP) endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: destinationPort)!) } // 使用 TCP 协议 let parameters = NWParameters.tcp let nwConnection = NWConnection(to: endpoint, using: parameters) connection.channel = nwConnection connection.isInitConnect = true self.ioService.registerSession(connection: connection) } private let methods: [String] = [ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT", "PROPFIND", "REPORT" ] private func supportsProtocol(packetData: Data) -> Bool { let position = packetData.startIndex // 判断是否是 SSL 握手 if TLS.isTLSClientHello(packetData: packetData) { return true } // 检查是否包含 HTTP 方法 for method in methods { if packetData.count - position < method.count { continue } let range = position..<(position + method.count) if let substring = String(data: packetData.subdata(in: range), encoding: .utf8), method.caseInsensitiveCompare(substring) == .orderedSame { return true } } return false } //set connection as aborting so that background worker will close it. func resetTCPConnection(ip: IP4Header, tcp: TCPHeader) { let session = manager.getConnection(nwProtocol: .TCP, ip: ip.destinationIP, port: tcp.destinationPort, srcIp: ip.sourceIP, srcPort: tcp.sourcePort) if let session = session { session.isAbortingConnection = true } } func ackFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection?) { let ackNumber = tcpHeader.sequenceNumber + 1 let seqNumber = tcpHeader.ackNumber let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: true) write(data: finAckData) // os_log("Sent FIN-ACK packet ack# %{public}d, seq# %{public}d", log: OSLog.default, type: .default, ackNumber, seqNumber) if let connection = connection { manager.closeConnection(connection: connection) } } func pushDataToDestination(connection: Connection, tcpHeader: TCPHeader) { connection.timestampReplyTo = tcpHeader.timeStampSender connection.timestampSender = Int(Date().timeIntervalSince1970) } func sendFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection) { let ackNumber = tcpHeader.sequenceNumber let seqNumber = tcpHeader.ackNumber let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: false) write(data: finAckData) connection.sendNext = seqNumber + 1 connection.isClosingConnection = false } //acknowledge a packet. func acceptAck(tcpHeader: TCPHeader, connection: Connection) { let isCorrupted = PacketUtil.isPacketCorrupted(tcpHeader: tcpHeader) if isCorrupted { os_log("Packet is corrupted", log: OSLog.default, type: .error) } if (tcpHeader.sequenceNumber > connection.recSequence) { connection.recSequence = tcpHeader.sequenceNumber } if tcpHeader.ackNumber >= connection.sendUnAck - 1 || tcpHeader.ackNumber == connection.sendNext { connection.sendUnAck = tcpHeader.ackNumber connection.timestampReplyTo = tcpHeader.timeStampSender connection.timestampSender = Int(Date().timeIntervalSince1970) } else { os_log("%{public}@ Not accepting ack# %d, it should be: %d", log: OSLog.default, type: .error, connection.description ,tcpHeader.ackNumber, connection.sendNext) os_log("%{public}@ Previous sendUnAck: %d", log: OSLog.default, type: .error, connection.description, connection.sendUnAck) } } func sendAckForDisorder(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int) { let ackNumber = tcpHeader.sequenceNumber + UInt32(acceptedDataLength) // os_log("Sent disorder ack, ack# %{public}d", log: OSLog.default, type: .debug, ackNumber) let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber) write(data: ackData) } func sendAck(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int, connection: Connection) { synchronized(connection) { let ackNumber = (tcpHeader.sequenceNumber + UInt32(acceptedDataLength)) % UInt32.max connection.recSequence = ackNumber let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber) self.write(data: ackData) // os_log("[ConnectionHandler] Sent ACK packet to client %{public}@ ack: %u", log: OSLog.default, type: .default, connection.description, ackNumber) } } private func sendLastAck(ip: IP4Header, tcp: TCPHeader) { let data = TCPPacketFactory.createResponseAckData(ipHeader: ip, tcpHeader: tcp, ackToClient: tcp.sequenceNumber + 1) self.write(data: data) os_log("Sent last ACK Packet to client with dest => %{public}@:%{public}d", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort) } private func sendRstPacket(ip: IP4Header, tcp: TCPHeader, dataLength: Int) { let data = TCPPacketFactory.createRstData(ipHeader: ip, tcpHeader: tcp, dataLength: dataLength) self.write(data: data) os_log("Sent RST Packet to client with dest => %{public}@:%{public}d", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort) } //create a new client's session and SYN-ACK packet data to respond to client private func replySynAck(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Void { ipHeader.identification = 0 let packet = TCPPacketFactory.createSynAckPacketData(ipHeader: ipHeader, tcpHeader: tcpHeader) guard let tcpTransport = packet.transportHeader as? TCPHeader else { os_log("Failed to extract TCP header from packet", log: OSLog.default, type: .error) return } let connection = manager.createTCPConnection( ip: ipHeader.destinationIP, port: tcpHeader.destinationPort, srcIp: ipHeader.sourceIP, srcPort: tcpHeader.sourcePort ) if connection.lastIpHeader != nil { resendAck(connection: connection) return } synchronized(connection) { connection.maxSegmentSize = Int(tcpTransport.maxSegmentSize) connection.sendUnAck = tcpTransport.sequenceNumber connection.sendNext = tcpTransport.sequenceNumber + 1 //client initial sequence has been incremented by 1 and set to ack connection.recSequence = tcpTransport.ackNumber connection.lastIpHeader = ipHeader connection.lastTcpHeader = tcpHeader if connection.isInitConnect { self.ioService.registerSession(connection: connection) } self.write(data: packet.buffer) // os_log("SYN-ACK %{public}@ packet length:%d sent ack:%u", log: OSLog.default, type: .default, connection.description, packet.buffer.count, tcpTransport.ackNumber) } } /** * resend the last acknowledgment packet to VPN client, e.g. when an unexpected out of order * packet arrives. */ private func resendAck(connection: Connection) { let data = TCPPacketFactory.createResponseAckData( ipHeader: connection.lastIpHeader!, tcpHeader: connection.lastTcpHeader!, ackToClient: connection.recSequence ) // os_log("Resending ACK packet %{public}@ ackToClient: %d", log: OSLog.default, type: .default, connection.description, connection.recSequence) self.write(data: data) } private func write(data: Data) { self.writer.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) } private func handleICMPPacket(clientPacketData: inout Data, ipHeader: IP4Header) { guard let requestPacket = ICMPPacketFactory.parseICMPPacket(&clientPacketData) else { os_log("Failed to parse ICMP packet", log: OSLog.default, type: .error) return } // os_log("Handling ICMP packet type: %d", log: OSLog.default, type: .default, requestPacket.type) if requestPacket.type == ICMPPacket.DESTINATION_UNREACHABLE_TYPE { // This is a packet from the phone, telling somebody that a destination is unreachable. // Might be caused by issues on our end, but it's unclear what kind of issues. Regardless, // we can't send ICMP messages ourselves or react usefully, so we drop these silently. return } else if requestPacket.type != ICMPPacket.ECHO_REQUEST_TYPE { // We only actually support outgoing ping packets. Loudly drop anything else: os_log("Unknown ICMP type: %d", log: OSLog.default, type: .error, requestPacket.type) return } QueueFactory.instance.getQueue().async { if !self.isReachable(ipAddress: PacketUtil.intToIPAddress(ipHeader.destinationIP)) { os_log("Failed ping, ignoring", log: OSLog.default, type: .default) return } let response = ICMPPacketFactory.buildSuccessPacket(requestPacket) // Flip the address let destination = ipHeader.destinationIP let source = ipHeader.sourceIP ipHeader.sourceIP = destination ipHeader.destinationIP = source let responseData = ICMPPacketFactory.packetToBuffer(ipHeader: ipHeader, packet: response) os_log("Successful ping response", log: OSLog.default, type: .default) self.write(data: responseData) } } private func isReachable(ipAddress: String) -> Bool { do { return true // return try InetAddress.getByName(ipAddress).isReachable(timeout: 10000) } catch { return false } } } ================================================ FILE: ios/ProxyPin/vpn/ConnectionManager.swift ================================================ // // ConnectionManager.swift // ProxyPin // // Created by wanghongen on 2024/9/16. // import Foundation import Network import os.log //管理VPN客户端的连接 class ConnectionManager : CloseableConnection{ //static let instance = ConnectionManager() private var table: [String: Connection] = [:] public var proxyAddress: NWEndpoint? private let defaultPorts: [UInt16] = [80, 443, 8080, 8088, 8888, 9000] func getConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection? { let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) return getConnectionByKey(key: key) } func getConnectionByKey(key: String) -> Connection? { return table[key] } func createTCPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection { let key = Connection.getConnectionKey(nwProtocol: .TCP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) if let existingConnection = table[key] { return existingConnection } let connection = Connection(nwProtocol: .TCP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self) let ipString = PacketUtil.intToIPAddress(ip) let endpoint: NWEndpoint if (proxyAddress == nil || !defaultPorts.contains(port) || isPrivateIP(ipString)) { endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: port)!) // 使用 TCP 协议 let parameters = NWParameters.tcp let nwConnection = NWConnection(to: endpoint, using: parameters) connection.channel = nwConnection connection.isInitConnect = true } self.table[key] = connection os_log("Created TCP connection %{public}@", log: OSLog.default, type: .default, key) return connection } private func isPrivateIP(_ ip: String) -> Bool { return ip.hasPrefix("10.") || ip.hasPrefix("172.") && (16...31).contains(Int(ip.split(separator: ".")[1]) ?? -1) || ip.hasPrefix("192.168.") } func createUDPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection { let key = Connection.getConnectionKey(nwProtocol: .UDP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) if let existingConnection = table[key] { return existingConnection } let connection = Connection(nwProtocol: .UDP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self) let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host((PacketUtil.intToIPAddress(ip))), port: NWEndpoint.Port(rawValue: port)!) let nwConnection = NWConnection(to: endpoint, using: .udp) connection.channel = nwConnection os_log("Created UDP connection %{public}@", log: OSLog.default, type: .default, key) self.table[key] = connection return connection } func closeConnection(connection: Connection) { closeConnection( nwProtocol: connection.nwProtocol, ip: connection.destinationIp, port: connection.destinationPort, srcIp: connection.sourceIp, srcPort: connection.sourcePort ) } // 从内存中删除连接,然后关闭套接字。 func closeConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) { let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) if let connection = self.table.removeValue(forKey: key) { if connection.channel?.state != .cancelled { connection.channel?.cancel() os_log("Closed connection %{public}@", log: OSLog.default, type: .debug, key) } else { os_log("Connection %{public}@ is already cancelled", log: OSLog.default, type: .debug, key) } } } //添加来自客户端的数据,该数据稍后将在接收到PSH标志时发送到目的服务器。 func addClientData(data: Data, connection: Connection) { guard data.count > 0 else { return } connection.addSendData(data: data) } func keepSessionAlive(connection: Connection) { let key = Connection.getConnectionKey( nwProtocol: connection.nwProtocol, destIp: connection.destinationIp, destPort: connection.destinationPort, sourceIp: connection.sourceIp, sourcePort: connection.sourcePort ) self.table[key] = connection } } ================================================ FILE: ios/ProxyPin/vpn/NWProtocol.swift ================================================ // // NWProtocol.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation public enum NWProtocol { case TCP,UDP } ================================================ FILE: ios/ProxyPin/vpn/ProxyVpnService.swift ================================================ // // ProxyService.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation import NetworkExtension import Network import os.log class ProxyVpnService { private var packetFlow: NEPacketTunnelFlow private var connectionHandler: ConnectionHandler private var socketIOService: SocketIOService private var isRunning = true; init(packetFlow: NEPacketTunnelFlow, proxyAddress: Network.NWEndpoint?) { self.packetFlow = packetFlow self.socketIOService = SocketIOService(clientPacketWriter: packetFlow) let manager = ConnectionManager() manager.proxyAddress = proxyAddress self.connectionHandler = ConnectionHandler(manager: manager, writer: packetFlow, ioService: socketIOService) } /** Start processing packets, this should be called after registering all IP stacks. A stopped interface should never start again. Create a new interface instead. */ func start() { isRunning = true; self.readPackets() } func stop() { isRunning = false; self.socketIOService.stop() } func readPackets() -> Void { if (!isRunning) { return } self.packetFlow.readPackets { (packets, protocols) in // os_log("Read %d packets", packets.count) for (i, packet) in packets.enumerated() { self.connectionHandler.handlePacket(packet: packet, version: protocols[i]) } self.readPackets() } } } ================================================ FILE: ios/ProxyPin/vpn/QueueFactory.swift ================================================ // // QueueFactory.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation class QueueFactory { static let instance = QueueFactory() private let queue: DispatchQueue private init() { queue = DispatchQueue(label: "com.network.ProxyPin.queue") } func getQueue() -> DispatchQueue { return queue } func executeAsync(block: @escaping () -> Void) { queue.async { block() } } } ================================================ FILE: ios/ProxyPin/vpn/ping/GBPing.h ================================================ // // GBPing.h // GBPing // // Created by Luka Mirosevic on 05/11/2012. // Copyright (c) 2012 Goonbee. All rights reserved. // #import #import "GBPingSummary.h" @class GBPingSummary; @protocol GBPingDelegate; NS_ASSUME_NONNULL_BEGIN typedef void(^StartupCallback)(BOOL success, NSError * _Nullable error); @interface GBPing : NSObject @property (weak, nonatomic, nullable) id delegate; @property (copy, nonatomic, nullable) NSString *host; @property (assign, atomic) NSTimeInterval pingPeriod; @property (assign, atomic) NSTimeInterval timeout; @property (assign, atomic) NSUInteger payloadSize; @property (assign, atomic) NSUInteger ttl; @property (assign, atomic) NSUInteger count; @property (assign, atomic, readonly) BOOL isPinging; @property (assign, atomic, readonly) BOOL isReady; @property (assign, atomic) BOOL useIpv4; @property (assign, atomic) BOOL useIpv6; @property (assign, atomic) BOOL debug; -(void)setupWithBlock:(StartupCallback)callback; -(void)startPinging; -(void)stop; @end @protocol GBPingDelegate @optional -(void)ping:(GBPing *)pinger didFinishWithTime:(NSTimeInterval)time; -(void)ping:(GBPing *)pinger didFailWithError:(NSError *)error; -(void)ping:(GBPing *)pinger didSendPingWithSummary:(GBPingSummary *)summary; -(void)ping:(GBPing *)pinger didFailToSendPingWithSummary:(GBPingSummary *)summary error:(NSError *)error; -(void)ping:(GBPing *)pinger didTimeoutWithSummary:(GBPingSummary *)summary; -(void)ping:(GBPing *)pinger didReceiveReplyWithSummary:(GBPingSummary *)summary; -(void)ping:(GBPing *)pinger didReceiveUnexpectedReplyWithSummary:(GBPingSummary *)summary; @end NS_ASSUME_NONNULL_END ================================================ FILE: ios/ProxyPin/vpn/ping/GBPing.m ================================================ // // GBPing.m // GBPing // // Created by Luka Mirosevic on 05/11/2012. // Copyright (c) 2012 Goonbee. All rights reserved. // #import "GBPing.h" #if TARGET_OS_EMBEDDED || TARGET_IPHONE_SIMULATOR #import #else #import #endif #import "ICMPHeader.h" #include #include #include #include #include #include #include #include #include #include static NSTimeInterval const kPendingPingsCleanupGrace = 1.0; static NSUInteger const kDefaultPayloadSize = 56; static NSUInteger const kDefaultTTL = 49; static NSTimeInterval const kDefaultPingPeriod = 1.0; static NSTimeInterval const kDefaultTimeout = 2.0; @interface GBPing () @property (assign, atomic) int socket; @property (strong, nonatomic) NSData *hostAddress; @property (strong, nonatomic) NSString *hostAddressString; @property (assign, nonatomic) uint16_t identifier; @property (assign, nonatomic) NSUInteger counter; @property (assign, atomic, readwrite) BOOL isPinging; @property (assign, atomic, readwrite) BOOL isReady; @property (assign, nonatomic) NSUInteger nextSequenceNumber; @property (strong, atomic) NSMutableDictionary *pendingPings; @property (strong, nonatomic) NSMutableDictionary *timeoutTimers; @property (strong, nonatomic) dispatch_queue_t setupQueue; @property (assign, atomic) BOOL isStopped; @end @implementation GBPing { NSUInteger _payloadSize; NSUInteger _ttl; NSUInteger _count; NSTimeInterval _timeout; NSTimeInterval _pingPeriod; NSTimeInterval _endTime; } #pragma mark - custom acc -(void)setTimeout:(NSTimeInterval)timeout { @synchronized(self) { if (self.isPinging) { if (self.debug) { NSLog(@"GBPing: can't set timeout while pinger is running."); } } else { _timeout = timeout; } } } -(NSTimeInterval)timeout { @synchronized(self) { if (!_timeout) { return kDefaultTimeout; } else { return _timeout; } } } -(void)setTtl:(NSUInteger)ttl { @synchronized(self) { if (self.isPinging) { if (self.debug) { NSLog(@"GBPing: can't set ttl while pinger is running."); } } else { _ttl = ttl; } } } -(NSUInteger)ttl { @synchronized(self) { if (!_ttl) { return kDefaultTTL; } else { return _ttl; } } } -(void)setCount:(NSUInteger)count { @synchronized(self) { if (self.isPinging) { if (self.debug) { NSLog(@"GBPing: can't set count while pinger is running."); } } else { _count = count; } } } -(NSUInteger)count { @synchronized(self) { if (!_count) { return 0; } else { return _count; } } } -(void)setPayloadSize:(NSUInteger)payloadSize { @synchronized(self) { if (self.isPinging) { if (self.debug) { NSLog(@"GBPing: can't set payload size while pinger is running."); } } else { _payloadSize = payloadSize; } } } -(NSUInteger)payloadSize { @synchronized(self) { if (!_payloadSize) { return kDefaultPayloadSize; } else { return _payloadSize; } } } -(void)setPingPeriod:(NSTimeInterval)pingPeriod { @synchronized(self) { if (self.isPinging) { if (self.debug) { NSLog(@"GBPing: can't set pingPeriod while pinger is running."); } } else { _pingPeriod = pingPeriod; } } } -(NSTimeInterval)pingPeriod { @synchronized(self) { if (!_pingPeriod) { return (NSTimeInterval)kDefaultPingPeriod; } else { return _pingPeriod; } } } #pragma mark - core pinging methods -(void)setupWithBlock:(StartupCallback)callback { //error out of its already setup if (self.isReady) { if (self.debug) { NSLog(@"GBPing: Can't setup, already setup."); } //notify about error and return dispatch_async(dispatch_get_main_queue(), ^{ callback(NO, nil); }); return; } //error out if no host is set if (!self.host) { if (self.debug) { NSLog(@"GBPing: set host before attempting to start."); } //notify about error and return dispatch_async(dispatch_get_main_queue(), ^{ callback(NO, nil); }); return; } //set up data structs self.nextSequenceNumber = 0; @synchronized (self) { self.pendingPings = [[NSMutableDictionary alloc] init]; self.timeoutTimers = [[NSMutableDictionary alloc] init]; } dispatch_async(self.setupQueue, ^{ CFStreamError streamError; BOOL success; CFHostRef hostRef = CFHostCreateWithName(NULL, (__bridge CFStringRef)self.host); /* * CFHostCreateWithName will return a null result in certain cases. * CFHostStartInfoResolution will return YES if _hostRef is null. */ if (hostRef) { success = CFHostStartInfoResolution(hostRef, kCFHostAddresses, &streamError); } else { success = NO; } if (!success) { //construct an error NSDictionary *userInfo; NSError *error; if (hostRef && streamError.domain == kCFStreamErrorDomainNetDB) { userInfo = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:streamError.error], kCFGetAddrInfoFailureKey, nil ]; } else { userInfo = nil; } error = [NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorUnknown userInfo:userInfo]; //clean up so far [self stop]; //notify about error and return dispatch_async(dispatch_get_main_queue(), ^{ callback(NO, error); }); //just incase if (hostRef) { CFRelease(hostRef); } return; } //get the first IPv4 or IPv6 address Boolean resolved; NSArray *addresses = (__bridge NSArray *)CFHostGetAddressing(hostRef, &resolved); if (resolved && (addresses != nil)) { resolved = false; for (NSData *address in addresses) { const struct sockaddr *anAddrPtr = (const struct sockaddr *)[address bytes]; if ([address length] >= sizeof(struct sockaddr) && ((self.useIpv4 && anAddrPtr->sa_family == AF_INET) || (self.useIpv6 && anAddrPtr->sa_family == AF_INET6)) ) { resolved = true; self.hostAddress = address; self.hostAddressString = [self ntop:(struct sockaddr *)anAddrPtr len:(socklen_t)address.length]; break; } } } //we can stop host resolution now if (hostRef) { CFRelease(hostRef); } //if an error occurred during resolution if (!resolved) { //stop [self stop]; //notify about error and return dispatch_async(dispatch_get_main_queue(), ^{ callback(NO, [NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorHostNotFound userInfo:nil]); }); return; } //set up socket int err = 0; switch (self.hostAddressFamily) { case AF_INET: { self.socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); if (self.socket < 0) { err = errno; } } break; case AF_INET6: { self.socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); if (self.socket < 0) { err = errno; } } break; default: { err = EPROTONOSUPPORT; } break; } //couldnt setup socket if (err) { //clean up so far [self stop]; //notify about error and close dispatch_async(dispatch_get_main_queue(), ^{ callback(NO, [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]); }); return; } //set ttl on the socket if (self.ttl) { u_char ttlForSockOpt = (u_char)self.ttl; setsockopt(self.socket, IPPROTO_IP, IP_TTL, &ttlForSockOpt, sizeof(NSUInteger)); } //we are ready now self.isReady = YES; //notify that we are ready dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); }); }); self.isStopped = NO; } -(void)startPinging { if (self.isReady && !self.isPinging) { //go into infinite listenloop on a new thread (listenThread) NSThread *listenThread = [[NSThread alloc] initWithTarget:self selector:@selector(listenLoop) object:nil]; listenThread.name = @"listenThread"; //set up loop that sends packets on a new thread (sendThread) NSThread *sendThread = [[NSThread alloc] initWithTarget:self selector:@selector(sendLoop) object:nil]; sendThread.name = @"sendThread"; //we're pinging now self.isPinging = YES; [listenThread start]; [sendThread start]; } } -(void)listenLoop { @autoreleasepool { while (self.isPinging) { [self listenOnce]; } } } -(void)listenOnce { int err; struct sockaddr_storage addr; socklen_t addrLen; ssize_t bytesRead; void * buffer; enum { kBufferSize = 65535 }; buffer = malloc(kBufferSize); if (buffer == nil) { err = errno; return; } //read the data. addrLen = sizeof(addr); bytesRead = recvfrom(self.socket, buffer, kBufferSize, 0, (struct sockaddr *)&addr, &addrLen); err = 0; if (bytesRead < 0) { err = errno; } //process the data we read. if (bytesRead > 0) { _endTime = CFAbsoluteTimeGetCurrent(); struct sockaddr_in *sin = (struct sockaddr_in *)&addr; NSString *host = [self ntop:(struct sockaddr *)&addr len:addrLen]; if([host isEqualToString:self.hostAddressString]) { // only make sense where received packet comes from expected source NSDate *receiveDate = [NSDate date]; NSMutableData *packet; packet = [NSMutableData dataWithBytes:buffer length:(NSUInteger) bytesRead]; if (packet == nil) { err = errno; return; } //complete the ping summary const struct ICMPHeader *headerPointer; if (sin->sin_family == AF_INET) { headerPointer = [[self class] icmp4InPacket:packet]; } else { headerPointer = (const struct ICMPHeader *)[packet bytes]; } NSUInteger seqNo = (NSUInteger)OSSwapBigToHostInt16(headerPointer->sequenceNumber); NSNumber *key = @(seqNo); GBPingSummary *pingSummary; @synchronized (self) { pingSummary = [(GBPingSummary *)self.pendingPings[key] copy]; } if (pingSummary) { if ([self isValidPingResponsePacket:packet]) { pingSummary.receiveDate = receiveDate; if (sin->sin_family == AF_INET) { //set ttl from response (different servers may respond with different ttls) const struct IPHeader *ipPtr; if ([packet length] >= sizeof(IPHeader)) { ipPtr = (const IPHeader *)[packet bytes]; pingSummary.ttl = ipPtr->timeToLive; } } pingSummary.status = GBPingStatusSuccess; //invalidate the timeouttimer @synchronized (self) { NSTimer *timer = self.timeoutTimers[key]; [timer invalidate]; [self.timeoutTimers removeObjectForKey:key]; } dispatch_async(dispatch_get_main_queue(), ^{ //notify delegate if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didReceiveReplyWithSummary:)] ) { [self.delegate ping:self didReceiveReplyWithSummary:[pingSummary copy]]; } }); } else { pingSummary.status = GBPingStatusFail; dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didReceiveUnexpectedReplyWithSummary:)] ) { [self.delegate ping:self didReceiveUnexpectedReplyWithSummary:[pingSummary copy]]; } }); } } } } else { //we failed to read the data, so shut everything down. if (err == 0) { err = EPIPE; } @synchronized(self) { if (!self.isStopped) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFailWithError:)] ) { [self.delegate ping:self didFailWithError:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]]; } }); } } //stop the whole thing [self stop]; } free(buffer); } -(void)sendLoop { @autoreleasepool { self.counter = _count; BOOL stopping = NO; NSTimeInterval startTime = CFAbsoluteTimeGetCurrent(); _endTime = 0; while (self.isPinging) { [self sendPing]; if (_count > 0) { self.counter -= 1; if (self.counter == 0) { stopping = YES; } } NSTimeInterval runUntil = CFAbsoluteTimeGetCurrent() + (stopping ? self.timeout : self.pingPeriod); NSTimeInterval time = 0; while (runUntil > time) { NSDate *runUntilDate = [NSDate dateWithTimeIntervalSinceReferenceDate:runUntil]; [[NSRunLoop currentRunLoop] runUntilDate:runUntilDate]; time = CFAbsoluteTimeGetCurrent(); } if (stopping) { break; } } [self stop]; dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFinishWithTime:)] ) { NSTimeInterval interval = 0; if (self->_endTime > 0) { interval = self->_endTime - startTime; } [self.delegate ping:self didFinishWithTime:interval]; } }); } } -(void)sendPing { if (self.isPinging) { int err; NSData *packet; ssize_t bytesSent; // Construct the ping packet. NSData *payload = [self generateDataWithLength:(self.payloadSize)]; switch (self.hostAddressFamily) { case AF_INET: { packet = [self pingPacketWithType:kICMPv4TypeEchoRequest payload:payload requiresChecksum:YES]; } break; case AF_INET6: { packet = [self pingPacketWithType:kICMPv6TypeEchoRequest payload:payload requiresChecksum:NO]; } break; default: { err = errno; return; } break; } // this is our ping summary GBPingSummary *newPingSummary = [GBPingSummary new]; // Send the packet. if (self.socket == 0) { bytesSent = -1; err = EBADF; } else { //record the send date NSDate *sendDate = [NSDate date]; //construct ping summary, as much as it can newPingSummary.sequenceNumber = self.nextSequenceNumber; newPingSummary.host = self.host; newPingSummary.ip = self.hostAddressString; newPingSummary.sendDate = sendDate; newPingSummary.ttl = self.ttl; newPingSummary.payloadSize = self.payloadSize; newPingSummary.status = GBPingStatusPending; //add it to pending pings NSNumber *key = @(self.nextSequenceNumber); @synchronized (self) { self.pendingPings[key] = newPingSummary; } //increment sequence number self.nextSequenceNumber += 1; //we create a copy, this one will be passed out to other threads GBPingSummary *pingSummaryCopy = [newPingSummary copy]; //we need to clean up our list of pending pings, and we do that after the timeout has elapsed (+ some grace period) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((self.timeout + kPendingPingsCleanupGrace) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //remove the ping from the pending list @synchronized (self) { [self.pendingPings removeObjectForKey:key]; } }); //add a timeout timer //add a timeout timer NSTimer *timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeout target:[NSBlockOperation blockOperationWithBlock:^{ newPingSummary.status = GBPingStatusFail; self->_endTime = CFAbsoluteTimeGetCurrent(); //notify about the failure dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didTimeoutWithSummary:)]) { [self.delegate ping:self didTimeoutWithSummary:pingSummaryCopy]; } }); //remove the timer itself from the timers list //lm make sure that the timer list doesnt grow and these removals actually work... try logging the count of the timeoutTimers when stopping the pinger @synchronized (self) { [self.timeoutTimers removeObjectForKey:key]; } }] selector:@selector(main) userInfo:nil repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:timeoutTimer forMode:NSRunLoopCommonModes]; //keep a local ref to it if (self.timeoutTimers) { @synchronized (self) { self.timeoutTimers[key] = timeoutTimer; } } //notify delegate about this dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didSendPingWithSummary:)]) { [self.delegate ping:self didSendPingWithSummary:pingSummaryCopy]; } }); bytesSent = sendto( self.socket, [packet bytes], [packet length], 0, (struct sockaddr *) [self.hostAddress bytes], (socklen_t) [self.hostAddress length] ); err = 0; if (bytesSent < 0) { err = errno; } } // This is after the sending //successfully sent if ((bytesSent > 0) && (((NSUInteger) bytesSent) == [packet length])) { //noop, we already notified delegate about sending of the ping } //failed to send else { //complete the error if (err == 0) { err = ENOBUFS; // This is not a hugely descriptor error, alas. } //little log if (self.debug) { NSLog(@"GBPing: failed to send packet with error code: %d", err); } //change status newPingSummary.status = GBPingStatusFail; GBPingSummary *pingSummaryCopyAfterFailure = [newPingSummary copy]; //notify delegate dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFailToSendPingWithSummary:error:)]) { [self.delegate ping:self didFailToSendPingWithSummary:pingSummaryCopyAfterFailure error:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]]; } }); } } } -(void)stop { @synchronized(self) { if (!self.isStopped) { self.isPinging = NO; self.isReady = NO; //destroy listenThread by closing socket (listenThread) if (self.socket) { close(self.socket); self.socket = 0; } //destroy host self.hostAddress = nil; //clean up pendingpings [self.pendingPings removeAllObjects]; self.pendingPings = nil; for (NSNumber *key in [self.timeoutTimers copy]) { NSTimer *timer = self.timeoutTimers[key]; [timer invalidate]; } //clean up timeouttimers [self.timeoutTimers removeAllObjects]; self.timeoutTimers = nil; //reset seq number self.nextSequenceNumber = 0; self.isStopped = YES; } } } #pragma mark - util static uint16_t in_cksum(const void *buffer, size_t bufferLen) // This is the standard BSD checksum code, modified to use modern types. { size_t bytesLeft; int32_t sum; const uint16_t * cursor; union { uint16_t us; uint8_t uc[2]; } last; uint16_t answer; bytesLeft = bufferLen; sum = 0; cursor = buffer; /* * Our algorithm is simple, using a 32 bit accumulator (sum), we add * sequential 16 bit words to it, and at the end, fold back all the * carry bits from the top 16 bits into the lower 16 bits. */ while (bytesLeft > 1) { sum += *cursor; cursor += 1; bytesLeft -= 2; } /* mop up an odd byte, if necessary */ if (bytesLeft == 1) { last.uc[0] = * (const uint8_t *) cursor; last.uc[1] = 0; sum += last.us; } /* add back carry outs from top 16 bits to low 16 bits */ sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */ sum += (sum >> 16); /* add carry */ answer = (uint16_t) ~sum; /* truncate to 16 bits */ return answer; } +(NSString *)sourceAddressInPacket:(NSData *)packet { // Returns the source address of the IP packet const struct IPHeader *ipPtr; const uint8_t *sourceAddress; if ([packet length] >= sizeof(IPHeader)) { ipPtr = (const IPHeader *)[packet bytes]; sourceAddress = ipPtr->sourceAddress;//dont need to swap byte order those cuz theyre the smallest atomic unit (1 byte) NSString *ipString = [NSString stringWithFormat:@"%d.%d.%d.%d", sourceAddress[0], sourceAddress[1], sourceAddress[2], sourceAddress[3]]; return ipString; } else return nil; } + (NSUInteger)icmp4HeaderOffsetInPacket:(NSData *)packet // Returns the offset of the ICMPHeader within an IP packet. { NSUInteger result; const struct IPHeader * ipPtr; size_t ipHeaderLength; result = NSNotFound; if ([packet length] >= (sizeof(IPHeader) + sizeof(ICMPHeader))) { ipPtr = (const IPHeader *) [packet bytes]; assert((ipPtr->versionAndHeaderLength & 0xF0) == 0x40); // IPv4 assert(ipPtr->protocol == 1); // ICMP ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t); if ([packet length] >= (ipHeaderLength + sizeof(ICMPHeader))) { result = ipHeaderLength; } } return result; } + (const struct ICMPHeader *)icmp4InPacket:(NSData *)packet // See comment in header. { const struct ICMPHeader * result; NSUInteger icmpHeaderOffset; result = nil; icmpHeaderOffset = [self icmp4HeaderOffsetInPacket:packet]; if (icmpHeaderOffset != NSNotFound) { result = (const struct ICMPHeader *) (((const uint8_t *)[packet bytes]) + icmpHeaderOffset); } return result; } - (BOOL)isValidPingResponsePacket:(NSMutableData *)packet { BOOL result; switch (self.hostAddressFamily) { case AF_INET: { result = [self isValidPing4ResponsePacket:packet]; } break; case AF_INET6: { result = [self isValidPing6ResponsePacket:packet]; } break; default: { result = NO; } break; } return result; } - (BOOL)isValidPing4ResponsePacket:(NSMutableData *)packet // Returns true if the packet looks like a valid ping response packet destined // for us. { BOOL result; NSUInteger icmpHeaderOffset; ICMPHeader * icmpPtr; uint16_t receivedChecksum; uint16_t calculatedChecksum; result = NO; icmpHeaderOffset = [[self class] icmp4HeaderOffsetInPacket:packet]; if (icmpHeaderOffset != NSNotFound) { icmpPtr = (struct ICMPHeader *) (((uint8_t *)[packet mutableBytes]) + icmpHeaderOffset); receivedChecksum = icmpPtr->checksum; icmpPtr->checksum = 0; calculatedChecksum = in_cksum(icmpPtr, [packet length] - icmpHeaderOffset); icmpPtr->checksum = receivedChecksum; if (receivedChecksum == calculatedChecksum) { if ( (icmpPtr->type == kICMPv4TypeEchoReply) && (icmpPtr->code == 0) ) { if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) { if ( OSSwapBigToHostInt16(icmpPtr->sequenceNumber) < self.nextSequenceNumber ) { result = YES; } } } } } // NSLog(@"valid: %@, type: %d", _b(result), icmpPtr->type); return result; } - (BOOL)isValidPing6ResponsePacket:(NSMutableData *)packet // Returns true if the IPv6 packet looks like a valid ping response packet destined // for us. { BOOL result; const ICMPHeader * icmpPtr; result = NO; if (packet.length >= sizeof(*icmpPtr)) { icmpPtr = packet.bytes; if ( (icmpPtr->type == kICMPv6TypeEchoReply) && (icmpPtr->code == 0) ) { if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) { if ( OSSwapBigToHostInt16(icmpPtr->sequenceNumber) < self.nextSequenceNumber ) { result = YES; } } } } return result; } -(NSData *)generateDataWithLength:(NSUInteger)length { //create a buffer full of 7's of specified length char tempBuffer[length]; memset(tempBuffer, 7, length); return [[NSData alloc] initWithBytes:tempBuffer length:length]; } - (void)_invokeTimeoutCallback:(NSTimer *)timer { dispatch_block_t callback = timer.userInfo; if (callback) { callback(); } } - (NSData *)pingPacketWithType:(uint8_t)type payload:(NSData *)payload requiresChecksum:(BOOL)requiresChecksum { NSMutableData * packet; ICMPHeader * icmpPtr; packet = [NSMutableData dataWithLength:sizeof(*icmpPtr) + payload.length]; if (packet == nil) { return nil; } icmpPtr = packet.mutableBytes; icmpPtr->type = type; icmpPtr->code = 0; icmpPtr->checksum = 0; icmpPtr->identifier = OSSwapHostToBigInt16(self.identifier); icmpPtr->sequenceNumber = OSSwapHostToBigInt16(self.nextSequenceNumber); memcpy(&icmpPtr[1], [payload bytes], [payload length]); if (requiresChecksum) { // The IP checksum routine returns a 16-bit number that's already in correct byte order // (due to wacky 1's complement maths), so we just put it into the packet as a 16-bit unit. icmpPtr->checksum = in_cksum(packet.bytes, packet.length); } return packet; } - (sa_family_t)hostAddressFamily { sa_family_t result = AF_UNSPEC; // Save a reference to a local variable, avoid crash when hostAddress is release by other thread. NSData *hostAddress = self.hostAddress; if (hostAddress != nil && hostAddress.length >= sizeof(struct sockaddr)) { result = ((const struct sockaddr *)hostAddress.bytes)->sa_family; } return result; } - (NSString*)ntop:(struct sockaddr *)sa len:(socklen_t)len { char ntop[NI_MAXHOST] = { 0 }; int ecode = getnameinfo(sa, len, ntop, sizeof(ntop), NULL, 0, NI_NUMERICHOST); if (ecode == 0) { return [[NSString alloc] initWithUTF8String:ntop]; } else { return nil; } } #pragma mark - memory -(id)init { if (self = [super init]) { self.setupQueue = dispatch_queue_create("GBPing setup queue", 0); self.isStopped = YES; self.identifier = arc4random(); self.useIpv4 = YES; self.useIpv6 = YES; } return self; } -(void)dealloc { self.delegate = nil; self.host = nil; @synchronized (self) { self.timeoutTimers = nil; self.pendingPings = nil; } self.hostAddress = nil; //clean up socket to be sure if (self.socket) { close(self.socket); self.socket = 0; } } @end ================================================ FILE: ios/ProxyPin/vpn/ping/GBPingHelper.swift ================================================ // // GBPingHelper.swift // import Foundation public typealias Handler = ((_ response: [String: Any]) -> Void) public class GBPingHelper: NSObject { private var ping: GBPing? private let delegate = PingDelegate() func start(withHost host: String, ipv4: Bool, ipv6: Bool, count: UInt, interval: TimeInterval, timeout: TimeInterval, ttl: UInt, handler: @escaping Handler) { ping?.stop() ping = GBPing() guard let ping = ping else { return } ping.host = host ping.useIpv4 = ipv4 ping.useIpv6 = ipv6 ping.count = count ping.pingPeriod = interval ping.timeout = timeout if ttl > 0 { ping.ttl = ttl } delegate.handler = handler ping.delegate = delegate ping.setup { success, err in if let err = err as NSError? { if err.domain == kCFErrorDomainCFNetwork as String { handler(["error": "UnknownHost"]) } else { handler(["error": "UnknownError"]) } return } if success { self.delegate.transmitted = 0 self.delegate.received = 0 ping.startPinging() } } } func stop() { ping?.stop() } } private class PingDelegate: NSObject, GBPingDelegate { public var handler: Handler? public var transmitted = 0 public var received = 0 func handle(_ summary: GBPingSummary, error: String? = nil) { guard let handler = handler else { return } var ret: [String: Any] = [:] ret["seq"] = summary.sequenceNumber ret["host"] = summary.host ret["ip"] = summary.ip ret["ttl"] = summary.ttl ret["time"] = summary.rtt ret["error"] = error handler(ret) } func ping(_ pinger: GBPing, didSendPingWith summary: GBPingSummary) { transmitted += 1 } func ping(_ pinger: GBPing, didTimeoutWith summary: GBPingSummary) { handle(summary, error: "RequestTimedOut") } func ping(_ pinger: GBPing, didReceiveReplyWith summary: GBPingSummary) { received += 1 handle(summary) } func ping(_ pinger: GBPing, didFinishWithTime time: TimeInterval) { guard let handler = handler else { return } var ret: [String: Any] = [:] ret["time"] = time ret["received"] = received ret["transmitted"] = transmitted handler(ret) } } ================================================ FILE: ios/ProxyPin/vpn/ping/GBPingSummary.h ================================================ // // GBPingSummary.h // GBPing // // Created by Luka Mirosevic on 05/11/2012. // Copyright (c) 2012 Goonbee. All rights reserved. // #import @interface GBPingSummary : NSObject typedef enum { GBPingStatusPending, GBPingStatusSuccess, GBPingStatusFail, } GBPingStatus; @property (assign, nonatomic) NSUInteger sequenceNumber; @property (assign, nonatomic) NSUInteger payloadSize; @property (assign, nonatomic) NSUInteger ttl; @property (strong, nonatomic, nullable) NSString *host; @property (strong, nonatomic, nullable) NSString *ip; @property (strong, nonatomic, nullable) NSDate *sendDate; @property (strong, nonatomic, nullable) NSDate *receiveDate; @property (assign, nonatomic) NSTimeInterval rtt; @property (assign, nonatomic) GBPingStatus status; @end ================================================ FILE: ios/ProxyPin/vpn/ping/GBPingSummary.m ================================================ // // GBPingSummary.m // GBPing // // Created by Luka Mirosevic on 05/11/2012. // Copyright (c) 2012 Goonbee. All rights reserved. // #import "GBPingSummary.h" @implementation GBPingSummary #pragma mark - custom acc -(void)setHost:(NSString *)host { _host = host; } -(NSTimeInterval)rtt { if (self.sendDate) { return [self.receiveDate timeIntervalSinceDate:self.sendDate]; } else { return 0; } } #pragma mark - copying -(id)copyWithZone:(NSZone *)zone { GBPingSummary *copy = [[[self class] allocWithZone:zone] init]; copy.sequenceNumber = self.sequenceNumber; copy.payloadSize = self.payloadSize; copy.ttl = self.ttl; copy.host = [self.host copy]; copy.ip = [self.ip copy]; copy.sendDate = [self.sendDate copy]; copy.receiveDate = [self.receiveDate copy]; copy.status = self.status; return copy; } #pragma mark - memory -(id)init { if (self = [super init]) { self.status = GBPingStatusPending; } return self; } -(void)dealloc { self.host = nil; self.ip = nil; self.sendDate = nil; self.receiveDate = nil; } #pragma mark - description -(NSString *)description { return [NSString stringWithFormat:@"host: %@, ip:%@, seq: %lu, status: %d, ttl: %lu, payloadSize: %lu, sendDate: %@, receiveDate: %@, rtt: %f", self.host, self.ip, (unsigned long)self.sequenceNumber, self.status, (unsigned long)self.ttl, (unsigned long)self.payloadSize, self.sendDate, self.receiveDate, self.rtt]; } @end ================================================ FILE: ios/ProxyPin/vpn/ping/ICMPHeader.h ================================================ // // ICMPHeader.h // GBPing // // Created by Luka Mirosevic on 15/11/2012. // Copyright (c) 2012 Goonbee. All rights reserved. // #ifndef GBPing_ICMPHeader_h #define GBPing_ICMPHeader_h #include #pragma mark - IP and ICMP On-The-Wire Format // The following declarations specify the structure of ping packets on the wire. // IP header structure: struct IPHeader { uint8_t versionAndHeaderLength; uint8_t differentiatedServices; uint16_t totalLength; uint16_t identification; uint16_t flagsAndFragmentOffset; uint8_t timeToLive; uint8_t protocol; uint16_t headerChecksum; uint8_t sourceAddress[4]; uint8_t destinationAddress[4]; // options... // data... }; typedef struct IPHeader IPHeader; __Check_Compile_Time(sizeof(IPHeader) == 20); __Check_Compile_Time(offsetof(IPHeader, versionAndHeaderLength) == 0); __Check_Compile_Time(offsetof(IPHeader, differentiatedServices) == 1); __Check_Compile_Time(offsetof(IPHeader, totalLength) == 2); __Check_Compile_Time(offsetof(IPHeader, identification) == 4); __Check_Compile_Time(offsetof(IPHeader, flagsAndFragmentOffset) == 6); __Check_Compile_Time(offsetof(IPHeader, timeToLive) == 8); __Check_Compile_Time(offsetof(IPHeader, protocol) == 9); __Check_Compile_Time(offsetof(IPHeader, headerChecksum) == 10); __Check_Compile_Time(offsetof(IPHeader, sourceAddress) == 12); __Check_Compile_Time(offsetof(IPHeader, destinationAddress) == 16); // ICMP type and code combinations: enum { kICMPv4TypeEchoRequest = 8, kICMPv4TypeEchoReply = 0 }; enum { kICMPv6TypeEchoRequest = 128, kICMPv6TypeEchoReply = 129 }; // ICMP header structure: struct ICMPHeader { uint8_t type; uint8_t code; uint16_t checksum; uint16_t identifier; uint16_t sequenceNumber; // data... }; typedef struct ICMPHeader ICMPHeader; __Check_Compile_Time(sizeof(ICMPHeader) == 8); __Check_Compile_Time(offsetof(ICMPHeader, type) == 0); __Check_Compile_Time(offsetof(ICMPHeader, code) == 1); __Check_Compile_Time(offsetof(ICMPHeader, checksum) == 2); __Check_Compile_Time(offsetof(ICMPHeader, identifier) == 4); __Check_Compile_Time(offsetof(ICMPHeader, sequenceNumber) == 6); #endif ================================================ FILE: ios/ProxyPin/vpn/socket/ClientPacketWriter.swift ================================================ // // ClientPacketWriter.swift // ProxyPin // // Created by wanghongen on 2024/9/ import Foundation import NetworkExtension class ClientPacketWriter: NSObject { private var packetFlow: NEPacketTunnelFlow private var isShutdown = false init(packetFlow: NEPacketTunnelFlow) { self.packetFlow = packetFlow } func write(data: Data) { self.packetFlow.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) } func shutdown() { self.isShutdown = true } } ================================================ FILE: ios/ProxyPin/vpn/socket/CloseableConnection.swift ================================================ // // CloseableConnection.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation protocol CloseableConnection { /// Closes the connection func closeConnection(connection: Connection) } ================================================ FILE: ios/ProxyPin/vpn/socket/SocketIOService.swift ================================================ // // ProxySocketIOService.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation import NetworkExtension import os.log class SocketIOService { // private static let maxReceiveBufferSize = 16384 private static let maxReceiveBufferSize = 1480 private let queue: DispatchQueue = DispatchQueue(label: "ProxyPin.SocketIOService", attributes: .concurrent) private var clientPacketWriter: NEPacketTunnelFlow private var shutdown = false init(clientPacketWriter: NEPacketTunnelFlow) { self.clientPacketWriter = clientPacketWriter } public func stop() { os_log("Stopping SocketIOService", log: OSLog.default, type: .default) queue.async(flags: .barrier) { self.shutdown = true } // queue.suspend() } //从connection接受数据 写到client public func registerSession(connection: Connection) { connection.channel!.stateUpdateHandler = { state in // os_log("Connection %{public}@ state changed to %{public}@", log: OSLog.default, type: .default, connection.description, String(describing: state)) switch state { case .ready: connection.isConnected = true os_log("Connected to %{public}@ on receiveMessage", log: OSLog.default, type: .default, connection.description) //接受远程服务器的数据 connection.sendToDestination() self.receiveMessage(connection: connection) case .cancelled: connection.isConnected = false os_log("Connection cancelled %{public}@", log: OSLog.default, type: .default, connection.description) connection.closeConnection() self.sendFin(connection: connection) case .failed(let error): connection.isConnected = false os_log("Failed to connect: %{public}@ %{public}@", log: OSLog.default, type: .error,connection.description, error.localizedDescription) connection.closeConnection() default: os_log("Connection %{public}@ entered unhandled state: %{public}@", log: OSLog.default, type: .default, connection.description, String(describing: state)) break } } connection.channel!.start(queue: self.queue) } private func receiveMessage(connection: Connection) { if (shutdown) { os_log("SocketIOService is shutting down", log: OSLog.default, type: .default) return } if (connection.nwProtocol == .UDP) { readUDP(connection: connection) } else { readTCP(connection: connection) } if (connection.isAbortingConnection) { os_log("Connection is aborting", log: OSLog.default, type: .default) connection.closeConnection() return } } func readTCP(connection: Connection) { // os_log("Reading from TCP socket") if connection.isAbortingConnection { os_log("Connection is aborting", log: OSLog.default, type: .default) return } guard let channel = connection.channel else { os_log("Invalid channel type", log: OSLog.default, type: .error) return } channel.receive(minimumIncompleteLength: 1, maximumLength: Self.maxReceiveBufferSize) { (data, context, isComplete, error) in self.queue.async(flags: .barrier) { // os_log("[SocketIOService] Received TCP data packet %{public}@ length %d", log: OSLog.default, type: .default, connection.description, data?.count ?? -1) if let error = error { os_log("Failed to read from TCP socket: %@", log: OSLog.default, type: .error, error as CVarArg) connection.isAbortingConnection = true return } self.pushDataToClient(buffer: data ?? Data() , connection: connection) // Recursively call readTCP to continue reading messages self.receiveMessage(connection: connection) if (isComplete) { connection.isAbortingConnection = true return } } } } func synchronized(_ lock: AnyObject, closure: () -> Void) { // objc_sync_enter(lock) closure() // objc_sync_exit(lock) } ///create packet data and send it to VPN client private func pushDataToClient(buffer: Data, connection: Connection) { // Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a // trigger to set PSH on the resulting TCP packet that goes to the VPN. connection.hasReceivedLastSegment = buffer.count <= 0 guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else { os_log("Invalid ipHeader or tcpHeader", log: OSLog.default, type: .error) return } synchronized(connection) { let unAck = connection.sendNext //处理益处问题 let nextUnAck = UInt32(truncatingIfNeeded: (connection.sendNext + UInt32(buffer.count)) % UInt32.max) connection.sendNext = nextUnAck let data = TCPPacketFactory.createResponsePacketData( ipHeader: ipHeader, tcpHeader: tcpHeader, packetData: buffer, isPsh: connection.hasReceivedLastSegment, ackNumber: connection.recSequence, seqNumber: unAck, timeSender: connection.timestampSender, timeReplyTo: connection.timestampReplyTo ) self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) // os_log("[SocketIOService] Sent TCP data packet to client %{public}@ length:%d seq:%u ack:%u", log: OSLog.default, type: .default, connection.description, buffer.count, unAck, connection.recSequence) } } private func sendFin(connection: Connection) { if (connection.nwProtocol != .TCP) { return } guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else { os_log("Invalid ipHeader or tcpHeader", log: OSLog.default, type: .error) return } synchronized(connection) { let data = TCPPacketFactory.createFinData( ipHeader: ipHeader, tcpHeader: tcpHeader, ackNumber: connection.recSequence, seqNumber: connection.sendNext, timeSender: connection.timestampSender, timeReplyTo: connection.timestampReplyTo ) self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) } } func readUDP(connection: Connection) { guard let channel = connection.channel else { os_log("Invalid channel type", log: OSLog.default, type: .error) return } channel.receive(minimumIncompleteLength: 1, maximumLength: 65507) { (data, context, isComplete, error) in self.queue.async(flags: .barrier) { if let error = error { os_log("Failed to read from UDP socket: %@", log: OSLog.default, type: .error, error as CVarArg) connection.isAbortingConnection = true return } // os_log("Received UDP data packet length %d", log: OSLog.default, type: .debug, data?.count ?? 0) guard let data = data, !data.isEmpty else { return } guard let ipHeader = connection.lastIpHeader, let udpHeader = connection.lastUdpHeader else { os_log("Missing IP or UDP header for connection %{public}@", log: OSLog.default, type: .error, connection.description) return } let packetData = UDPPacketFactory.createResponsePacket( ip: ipHeader, udp: udpHeader, packetData: data ) self.clientPacketWriter.writePackets([packetData], withProtocols: [NSNumber(value: AF_INET)]) // Recursively call receiveMessage to continue receiving messages self.receiveMessage(connection: connection) } } } } ================================================ FILE: ios/ProxyPin/vpn/transport/Packet.swift ================================================ // // Packet.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation class Packet { var ipHeader: IP4Header var transportHeader: TransportHeader var buffer: Data init(ipHeader: IP4Header, transportHeader: TransportHeader, buffer: Data) { self.ipHeader = ipHeader self.transportHeader = transportHeader self.buffer = buffer } } ================================================ FILE: ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift ================================================ // // ICMPPacket.swift // ProxyPin // // Created by wanghongen on 2024/10/3. // import Foundation class ICMPPacket { // Two ICMP packets we can handle: simple ping & pong static let ECHO_REQUEST_TYPE: UInt8 = 8 static let ECHO_SUCCESS_TYPE: UInt8 = 0 // One very common packet we ignore: connection rejection. Unclear why this happens, // random incoming connections that the phone tries to reply to? Nothing we can do though, // as we can't forward ICMP onwards, and we can't usefully respond or react. static let DESTINATION_UNREACHABLE_TYPE: UInt8 = 3 let type: UInt8 let code: UInt8 // 0 for request, 0 for success, 0 - 15 for error subtypes let checksum: UInt16 let identifier: UInt16 let sequenceNumber: UInt16 let data: [UInt8] init(type: UInt8, code: UInt8, checksum: UInt16, identifier: UInt16, sequenceNumber: UInt16, data: [UInt8]) { self.type = type self.code = code self.checksum = checksum self.identifier = identifier self.sequenceNumber = sequenceNumber self.data = data } var description: String { return "ICMP packet type \(type)/\(code) id:\(identifier) seq:\(sequenceNumber) and \(data.count) bytes of data" } } class ICMPPacketFactory { static func parseICMPPacket(_ stream: inout Data) -> ICMPPacket? { guard stream.count >= 8 else { return nil } let type = stream.removeFirst() let code = stream.removeFirst() let checksum = stream.withUnsafeBytes { $0.load(as: UInt16.self) } stream.removeFirst(2) let identifier = stream.withUnsafeBytes { $0.load(as: UInt16.self) } stream.removeFirst(2) let sequenceNumber = stream.withUnsafeBytes { $0.load(as: UInt16.self) } stream.removeFirst(2) let data = Array(stream) return ICMPPacket(type: type, code: code, checksum: checksum, identifier: identifier, sequenceNumber: sequenceNumber, data: data) } static func buildSuccessPacket(_ requestPacket: ICMPPacket) -> ICMPPacket { return ICMPPacket( type: ICMPPacket.ECHO_SUCCESS_TYPE, code: 0, checksum: 0, identifier: requestPacket.identifier, sequenceNumber: requestPacket.sequenceNumber, data: requestPacket.data ) } static func packetToBuffer(ipHeader: IP4Header, packet: ICMPPacket) -> Data { var buffer = Data() buffer.append(ipHeader.toBytes()) var icmpDataBuffer = Data() icmpDataBuffer.append(packet.type) icmpDataBuffer.append(packet.code) icmpDataBuffer.append(contentsOf: withUnsafeBytes(of: UInt16(0), Array.init)) if packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE { icmpDataBuffer.append(contentsOf: packet.identifier.bytes) icmpDataBuffer.append(contentsOf: packet.sequenceNumber.bytes) icmpDataBuffer.append(contentsOf: packet.data) } else { fatalError("Can't serialize unrecognized ICMP packet type") } let checksum = PacketUtil.calculateChecksum(data: icmpDataBuffer, offset: 0, length: icmpDataBuffer.count) icmpDataBuffer.replaceSubrange(2..<4, with: checksum) buffer.append(icmpDataBuffer) return buffer } } ================================================ FILE: ios/ProxyPin/vpn/transport/protocol/IP4Header.swift ================================================ // // IP4Header.swift // ProxyPin // // Created by wanghongen on 2024/9/16. // import Foundation import os.log // IPv4 header data structure class IP4Header { var ipVersion: UInt8 // 对于IPv4,其值为4(因此命名为IPv4)。 4bit var internetHeaderLength: UInt8 // 头部长度 4bit var diffTypeOfService: UInt8 // 差分服务代码点 =>6位 var ecn: UInt8 // 显式拥塞通知(ECN) var totalLength: UInt16 // 此IP数据包的总长度 16bit var identification: UInt16 // 主要用于唯一标识单个IP数据报的片段组。 16bit var mayFragment: Bool // 用于指示数据报是否可以分段。 1bit var lastFragment: Bool // 用于指示数据报是否是片段中的最后一个。 1bit var fragmentOffset: UInt16 // 指定特定片段相对于原始未分段的IP数据报的开始的偏移量。 13bit var timeToLive: UInt8 // 用于防止数据报持续存在。8bit var protocolNumber: UInt8 // 定义IP数据报的数据部分中使用的协议。 8bit var headerChecksum: UInt16 // 用于对头部进行错误检查的16位字段。 16bit var sourceIP: UInt32 // 发送者的IPv4地址。 32bit var destinationIP: UInt32 // 接收者的IPv4地址。 32bit //用于控制或识别片段的3比特字段。 //bit 0: 保留;必须为零 //bit 1: Don't Fragment (DF) //bit 2: More Fragments (MF) private var flag: UInt8 init( ipVersion: UInt8, internetHeaderLength: UInt8, diffTypeOfService: UInt8, ecn: UInt8, totalLength: UInt16, identification: UInt16, mayFragment: Bool, lastFragment: Bool, fragmentOffset: UInt16, timeToLive: UInt8, protocolNumber: UInt8, headerChecksum: UInt16, sourceIP: UInt32, destinationIP: UInt32 ) { self.ipVersion = ipVersion self.internetHeaderLength = internetHeaderLength self.diffTypeOfService = diffTypeOfService self.ecn = ecn self.totalLength = totalLength self.identification = identification self.mayFragment = mayFragment self.lastFragment = lastFragment self.fragmentOffset = fragmentOffset self.timeToLive = timeToLive self.protocolNumber = protocolNumber self.headerChecksum = headerChecksum self.sourceIP = sourceIP self.destinationIP = destinationIP self.flag = IP4Header.initFlag(mayFragment: mayFragment, lastFragment: lastFragment) } private static func initFlag(mayFragment: Bool, lastFragment: Bool) -> UInt8 { var initFlag: UInt8 = 0 if mayFragment { initFlag = 0x40 } if lastFragment { initFlag |= 0x20 } return initFlag } func setMayFragment(_ mayFragment: Bool) { self.mayFragment = mayFragment flag = mayFragment ? (flag | 0x40) : (flag & 0xBF) } func getIPHeaderLength() -> Int { return Int(internetHeaderLength * 4) } func copy() -> IP4Header { return IP4Header( ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification, mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: headerChecksum, sourceIP: sourceIP, destinationIP: destinationIP ) } func toBytes() -> Data { var buffer = Data() buffer.append(UInt8((ipVersion << 4) + internetHeaderLength)) buffer.append(UInt8((diffTypeOfService << 2) + ecn)) buffer.append(contentsOf: totalLength.bytes) buffer.append(contentsOf: identification.bytes) //组合标志和部分片段偏移 buffer.append(UInt8((fragmentOffset >> 8) & 0x1F) | flag) buffer.append(UInt8(fragmentOffset & 0xFF)) buffer.append(timeToLive) buffer.append(protocolNumber) buffer.append(contentsOf: headerChecksum.bytes) buffer.append(contentsOf: sourceIP.bytes) buffer.append(contentsOf: destinationIP.bytes) return buffer } } class IPPacketFactory { static let IP4_HEADER_SIZE = 20 static let IP4_VERSION: UInt8 = 0x04 //从给定的ByteBuffer流创建IPv4标头 static func createIP4Header(data: Data) -> IP4Header? { guard data.count >= IP4_HEADER_SIZE else { return nil } let buffer = [UInt8](data) let versionAndHeaderLength = buffer[0] let ipVersion = versionAndHeaderLength >> 4 guard ipVersion == IP4_VERSION else { return nil } let internetHeaderLength = versionAndHeaderLength & 0x0F let typeOfService = buffer[1] let diffTypeOfService = typeOfService >> 2 let ecn = typeOfService & 0x03 let totalLength = UInt16(buffer[2]) << 8 | UInt16(buffer[3]) let identification = UInt16(buffer[4]) << 8 | UInt16(buffer[5]) let flagsAndFragmentOffset = UInt16(buffer[6]) << 8 | UInt16(buffer[7]) let mayFragment = (flagsAndFragmentOffset & 0x4000) != 0 let lastFragment = (flagsAndFragmentOffset & 0x2000) != 0 let fragmentOffset = flagsAndFragmentOffset & 0x1FFF let timeToLive = buffer[8] let protocolNumber = buffer[9] let checksum = UInt16(buffer[10]) << 8 | UInt16(buffer[11]) let sourceIp = UInt32(buffer[12]) << 24 | UInt32(buffer[13]) << 16 | UInt32(buffer[14]) << 8 | UInt32(buffer[15]) let desIp = UInt32(buffer[16]) << 24 | UInt32(buffer[17]) << 16 | UInt32(buffer[18]) << 8 | UInt32(buffer[19]) if internetHeaderLength > 5 { // drop the IP option for _ in 0..<(internetHeaderLength - 5) { // Skip the IP options } } return IP4Header( ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification, mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: checksum, sourceIP: sourceIp, destinationIP: desIp ) } public static func printPacket(data: Data) { guard let ipHeader = createIP4Header(data: data) else { return } os_log("IP Header: version: %{public}d, internetHeaderLength: %{public}d, diffTypeOfService: %{public}d, ecn: %{public}d, totalLength: %{public}d, identification: %{public}d, mayFragment: %{public}d, lastFragment: %{public}d, fragmentOffset: %{public}d, timeToLive: %{public}d, protocolNumber: %{public}d, headerChecksum: %{public}d, sourceIP: %{public}@, destinationIP: %{public}@", log: OSLog.default, type: .default, ipHeader.ipVersion, ipHeader.internetHeaderLength, ipHeader.diffTypeOfService, ipHeader.ecn, ipHeader.totalLength, ipHeader.identification, ipHeader.mayFragment, ipHeader.lastFragment, ipHeader.fragmentOffset, ipHeader.timeToLive, ipHeader.protocolNumber, ipHeader.headerChecksum, PacketUtil.intToIPAddress(ipHeader.sourceIP), PacketUtil.intToIPAddress(ipHeader.destinationIP)) } } ================================================ FILE: ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift ================================================ // // TCPHeader.swift // ProxyPin // // Created by wanghongen on 2024/9/16. // import Foundation /// Represents a TCP header in a network packet. class TCPHeader : TransportHeader{ /// Source port number (16 bits) var sourcePort: UInt16 /// Destination port number (16 bits) var destinationPort: UInt16 /// Sequence number (32 bits) var sequenceNumber: UInt32 /// Acknowledgment number (32 bits) var ackNumber: UInt32 /// Data offset (4 bits) var dataOffset: UInt8 var isNS: Bool = false // ECN-nonce concealment protection (experimental: see RFC 3540) /// Flags (9 bits) var flags: UInt8 /// Window size (16 bits) var windowSize: UInt16 /// Checksum (16 bits) var checksum: UInt16 /// Urgent pointer (16 bits) var urgentPointer: UInt16 /// Options (variable length) var options: Data? var payload: Data? //Static section for constants static let END_OF_OPTIONS_LIST: UInt8 = 0 static let NO_OPERATION: UInt8 = 1 static let MAX_SEGMENT_SIZE: UInt8 = 2 static let WINDOW_SCALE: UInt8 = 3 static let SELECTIVE_ACK_PERMITTED: UInt8 = 4 static let TIME_STAMP: UInt8 = 8 init(sourcePort: UInt16, destinationPort: UInt16, sequenceNumber: UInt32, ackNumber: UInt32, dataOffset: UInt8, isNS: Bool, flags: UInt8, windowSize: UInt16, checksum: UInt16, urgentPointer: UInt16, options: Data?, payload: Data? = nil) { self.sourcePort = sourcePort self.destinationPort = destinationPort self.sequenceNumber = sequenceNumber self.ackNumber = ackNumber self.dataOffset = dataOffset self.isNS = isNS self.flags = flags self.windowSize = windowSize self.checksum = checksum self.urgentPointer = urgentPointer self.options = options self.payload = payload } //options var maxSegmentSize: UInt16 = 0 private var windowScale: UInt8 = 0 private var isSelectiveAckPermitted = false var timeStampSender = 0 var timeStampReplyTo = 0 func getSourcePort() -> Int { return Int(sourcePort) } func getDestinationPort() -> Int { return Int(destinationPort) } func isFIN() -> Bool { return flags & 0x01 != 0 } /// Checks if the SYN flag is set. func isSYN() -> Bool { return flags & 0x02 != 0 } /// Checks if the RST flag is set. func isRST() -> Bool { return flags & 0x04 != 0 } /// Checks if the PSH flag is set. func isPSH() -> Bool { return flags & 0x08 != 0 } /// Checks if the ACK flag is set. func isACK() -> Bool { return flags & 0x10 != 0 } /// Checks if the URG flag is set. func isURG() -> Bool { return flags & 0x20 != 0 } /// Checks if the ECE flag is set. func isECE() -> Bool { return flags & 0x40 != 0 } /// Checks if the CWR flag is set. func isCWR() -> Bool { return flags & 0x80 != 0 } /// Sets or clears the RST flag. func setIsRST(_ isRST: Bool) { flags = isRST ? (flags | 0x04) : (flags & 0xFB) } /// Sets or clears the SYN flag. func setIsSYN(_ isSYN: Bool) { flags = isSYN ? (flags | 0x02) : (flags & 0xFD) } /// Sets or clears the FIN flag. func setIsFIN(_ isFIN: Bool) { flags = isFIN ? (flags | 0x01) : (flags & 0xFE) } /// Sets or clears the PSH flag. func setIsPSH(_ isPSH: Bool) { flags = isPSH ? (flags | 0x08) : (flags & 0xF7) } /// Sets or clears the ACK flag. func setIsACK(_ isACK: Bool) { flags = isACK ? (flags | 0x10) : (flags & 0xEF) } /// Returns the length of the TCP header. func getTCPHeaderLength() -> Int { return Int(dataOffset) * 4 } /// Converts the TCP header to a byte array. func toBytes() -> Data { var buffer = Data() buffer.append(contentsOf: sourcePort.bytes) buffer.append(contentsOf: destinationPort.bytes) buffer.append(contentsOf: sequenceNumber.bytes) buffer.append(contentsOf: ackNumber.bytes) //is ns and data offset let headerLength = 5 buffer.append(UInt8((headerLength << 4) | (isNS ? 1 : 0))) buffer.append(flags) buffer.append(contentsOf: windowSize.bytes) buffer.append(contentsOf: checksum.bytes) buffer.append(contentsOf: urgentPointer.bytes) // if let options = options { // buffer.append(options) // } return buffer } /// Creates a copy of the TCP header. func copy() -> TCPHeader { return TCPHeader( sourcePort: sourcePort, destinationPort: destinationPort, sequenceNumber: sequenceNumber, ackNumber: ackNumber, dataOffset: dataOffset, isNS: isNS, flags: flags, windowSize: windowSize, checksum: checksum, urgentPointer: urgentPointer, options: options ) } } ================================================ FILE: ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift ================================================ // // TCPPacketFactory.swift // ProxyPin // // Created by wanghongen on 2024/9/16. // // import Foundation import os.log /// Factory class for creating TCP packets. class TCPPacketFactory { public static let TCP_HEADER_LENGTH = 20 //从tcp报文创建tcpHeader static func createTCPHeader(data: Data) -> TCPHeader? { if data.count < TCP_HEADER_LENGTH { os_log("Data is too short to be a TCP packet", log: OSLog.default, type: .error) return nil } var offset = 0 func readUInt16() -> UInt16 { let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt16.self).bigEndian } offset += 2 return value } func readUInt32() -> UInt32 { let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self).bigEndian } offset += 4 return value } let sourcePort = readUInt16() let destinationPort = readUInt16() let sequenceNumber = readUInt32() let ackNumber = readUInt32() let dataOffsetAndReserved = data[offset] offset += 1 let dataOffset = UInt8((dataOffsetAndReserved & 0xF0) >> 4) let isNs = (dataOffsetAndReserved & 0x01) == 1 let flags = UInt8(data[offset]) offset += 1 let windowSize = readUInt16() let checksum = readUInt16() let urgentPointer = readUInt16() var optionsSize = Int(dataOffset) - 5 var options: Data? if (optionsSize > 0) { optionsSize *= 4 options = data.subdata(in: offset.. Data { var ip = ipHeader.copy() var tcp = tcpHeader.copy() flipIp(ip: &ip, tcp: &tcp) let seqNumber = tcp.ackNumber tcp.ackNumber = ackToClient tcp.sequenceNumber = seqNumber ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) // Set TCP flags tcp.setIsACK(true) tcp.setIsSYN(false) tcp.setIsPSH(false) tcp.setIsFIN(false) tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes tcp.options = nil ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength()) return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) } ///创建带有RST标志的数据包,以便在需要重置时发送到客户端。 static func createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int) -> Data { var ip = ipHeader.copy() var tcp = tcpHeader.copy() var ackNumber: UInt32 = 0 var seqNumber: UInt32 = 0 if tcp.ackNumber > 0 { seqNumber = tcp.ackNumber } else { ackNumber = tcp.sequenceNumber + UInt32(dataLength) } tcp.ackNumber = ackNumber tcp.sequenceNumber = seqNumber // Flip IP from source to destination flipIp(ip: &ip, tcp: &tcp) ip.identification = 0 tcp.flags = 0 tcp.isNS = false tcp.setIsRST(true) tcp.dataOffset = 5 tcp.options = nil tcp.windowSize = 0 // Recalculate IP length let totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() ip.totalLength = UInt16(totalLength) return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) } //创建发送到客户端的FIN-ACK static func createFinAckData(ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: UInt32, seqToClient: UInt32, isFin: Bool, isAck: Bool) -> Data { var ip = ipHeader.copy() var tcp = tcpHeader.copy() flipIp(ip: &ip, tcp: &tcp) tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes tcp.options = nil tcp.ackNumber = ackToClient tcp.sequenceNumber = seqToClient ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) tcp.setIsACK(isAck) tcp.setIsSYN(false) tcp.setIsPSH(false) tcp.setIsFIN(isFin) ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength()) return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) } //通过写回客户端流创建SYN-ACK数据包数据 public static func createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Packet { var ip = ipHeader.copy() var tcp = tcpHeader.copy() flipIp(ip: &ip, tcp: &tcp) tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes tcp.options = nil // ack = received sequence + 1 let ackNumber = tcpHeader.sequenceNumber + 1 tcp.ackNumber = ackNumber // Server-generated initial sequence number let seqNumber = UInt64.random(in: 0..<100000) tcp.sequenceNumber = UInt32(seqNumber) // SYN-ACK tcp.setIsACK(true) tcp.setIsSYN(true) tcp.timeStampReplyTo = tcp.timeStampSender tcp.timeStampSender = PacketUtil.currentTime ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength()) return Packet(ipHeader: ip, transportHeader: tcp, buffer: createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)) } //创建数据包数据以发送回客户端 public static func createResponsePacketData( ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: Data?, isPsh: Bool, ackNumber: UInt32, seqNumber: UInt32, timeSender: Int, timeReplyTo: Int ) -> Data { var ip = ipHeader.copy() var tcp = tcpHeader.copy() flipIp(ip: &ip, tcp: &tcp) tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes tcp.options = nil tcp.ackNumber = ackNumber tcp.sequenceNumber = seqNumber ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) // ACK is always sent tcp.setIsACK(true) tcp.setIsSYN(false) tcp.setIsPSH(isPsh) tcp.setIsFIN(false) tcp.timeStampSender = timeSender tcp.timeStampReplyTo = timeReplyTo var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() if let packetData = packetData { totalLength += packetData.count } ip.totalLength = UInt16(totalLength) return createPacketData(ipHeader: ip, tcpHeader: tcp, data: packetData) } //将IP从源翻转到目标 private static func flipIp(ip: inout IP4Header, tcp: inout TCPHeader) { let sourceIp = ip.destinationIP let destIp = ip.sourceIP let sourcePort = tcp.destinationPort let destPort = tcp.sourcePort ip.destinationIP = destIp ip.sourceIP = sourceIp tcp.destinationPort = destPort tcp.sourcePort = sourcePort } public static func createFinData( ipHeader: IP4Header, tcpHeader: TCPHeader, ackNumber: UInt32, seqNumber: UInt32, timeSender: Int, timeReplyTo: Int ) -> Data { var ip = ipHeader.copy() var tcp = tcpHeader.copy() flipIp(ip: &ip, tcp: &tcp) tcp.ackNumber = ackNumber tcp.sequenceNumber = seqNumber ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) tcp.timeStampReplyTo = timeReplyTo tcp.timeStampSender = timeSender tcp.flags = 0 tcp.isNS = false tcp.setIsACK(true) tcp.setIsFIN(true) tcp.options = nil tcp.windowSize = 0 ip.totalLength = UInt16(ip.getIPHeaderLength() + TCP_HEADER_LENGTH) return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) } //从tcpHeader创建tcp报文 private static func createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: Data?) -> Data { let dataLength = data?.count ?? 0 var buffer = Data() // Add IP header let ipBuffer = ipHeader.toBytes() buffer.append(ipBuffer) // Add TCP header let tcpBuffer = tcpHeader.toBytes() buffer.append(tcpBuffer) // Add data if exists if let data = data { buffer.append(data) } // Zero out IP checksum buffer[10] = 0 buffer[11] = 0 // Calculate IP checksum let ipChecksum = PacketUtil.calculateChecksum(data: buffer, offset: 0, length: ipBuffer.count) buffer[10] = ipChecksum[0] buffer[11] = ipChecksum[1] // IPPacketFactory.printPacket(data: ipBuffer) // Zero out TCP checksum let tcpStart = ipBuffer.count buffer[tcpStart + 16] = 0 buffer[tcpStart + 17] = 0 // Calculate TCP checksum let tcpChecksum = PacketUtil.calculateTCPHeaderChecksum( data: buffer, offset: tcpStart, tcpLength: tcpBuffer.count + dataLength, sourceIP: ipHeader.sourceIP, destinationIP: ipHeader.destinationIP ) buffer[tcpStart + 16] = tcpChecksum[0] buffer[tcpStart + 17] = tcpChecksum[1] return buffer } static func printPacket(data: Data) { guard let tcpHeader = createTCPHeader(data: data) else { os_log("Failed to create TCP header", log: OSLog.default, type: .error) return } os_log("TCP Header: sourcePort: %{public}d, destinationPort: %{public}d, sequenceNumber: %{public}u, ackNumber: %{public}u, dataOffset: %{public}d, isNS: %{public}d, flags: %{public}d, windowSize: %{public}d, checksum: %{public}u, urgentPointer: %{public}u", log: OSLog.default, type: .default, tcpHeader.sourcePort, tcpHeader.destinationPort, tcpHeader.sequenceNumber, tcpHeader.ackNumber, tcpHeader.dataOffset, tcpHeader.isNS ? 1 : 0, tcpHeader.flags, tcpHeader.windowSize, tcpHeader.checksum, tcpHeader.urgentPointer) } } ================================================ FILE: ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift ================================================ // // TransportHeader.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation protocol TransportHeader { func getSourcePort() -> Int func getDestinationPort() -> Int } ================================================ FILE: ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift ================================================ // // UDPHeader.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation import os.log ///UDP报头的数据 struct UDPHeader { var sourcePort: UInt16 //源端口号 16bit var destinationPort: UInt16 //源端口号 16bit var length: UInt16 //UDP数据报长度 16bit var checksum: UInt16 //校验和 16bit init(sourcePort: UInt16, destinationPort: UInt16, length: UInt16, checksum: UInt16) { self.sourcePort = sourcePort self.destinationPort = destinationPort self.length = length self.checksum = checksum } } class UDPPacketFactory { static let UDP_HEADER_LENGTH = 8 static func createUDPHeader(from data: Data) -> UDPHeader? { guard data.count >= UDP_HEADER_LENGTH else { return nil } let srcPort = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt16.self).bigEndian } let destPort = data.withUnsafeBytes { $0.load(fromByteOffset: 2, as: UInt16.self).bigEndian } let length = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt16.self).bigEndian } let checksum = data.withUnsafeBytes { $0.load(fromByteOffset: 6, as: UInt16.self).bigEndian } return UDPHeader(sourcePort: srcPort, destinationPort: destPort, length: length, checksum: checksum) } // static func createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: Data?) -> Data { var udpLen = 8 if let packetData = packetData { udpLen += packetData.count } let srcPort = udp.destinationPort let destPort = udp.sourcePort let ipHeader = ip.copy() let srcIp = ip.destinationIP let destIp = ip.sourceIP ipHeader.setMayFragment(false) ipHeader.sourceIP = srcIp ipHeader.destinationIP = destIp ipHeader.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) //ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length let totalLength = ipHeader.getIPHeaderLength() + udpLen ipHeader.totalLength = UInt16(totalLength) var ipData = ipHeader.toBytes() // clear IP checksum ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in bytes[10] = 0 bytes[11] = 0 } // calculate checksum for IP header let ipChecksum = PacketUtil.calculateChecksum(data: ipData, offset: 0, length: ipData.count) // write result of checksum back to buffer ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in bytes[10] = ipChecksum[0] bytes[11] = ipChecksum[1] } var buffer = Data() // copy IP header to buffer buffer.append(ipData) // copy UDP header to buffer buffer.append(contentsOf: srcPort.bytes) buffer.append(contentsOf: destPort.bytes) buffer.append(contentsOf: UInt16(udpLen).bytes) // 计算UDP校验和 let udpChecksum: UInt16 = 0 buffer.append(contentsOf: udpChecksum.bytes) if let packetData = packetData { buffer.append(packetData) } return buffer } } ================================================ FILE: ios/ProxyPin/vpn/utils/PacketUtil.swift ================================================ // // PacketUtil.swift // ProxyPin // // Created by wanghongen on 2024/9/17. // import Foundation import os.log class PacketUtil { private static var packetId: Int = 0 static func getPacketId() -> Int { defer { packetId += 1 } return packetId } static var currentTime: Int { return Int(Date().timeIntervalSince1970) } static func writeIntToBytes(value: UInt32, buffer: inout Data, offset: Int) { guard buffer.count >= offset + 4 else { return } var intValue = value.bigEndian let intData = Data(bytes: &intValue, count: 4) buffer.replaceSubrange(offset.. String { return String(format: "%d.%d.%d.%d", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF) } static func calculateTCPHeaderChecksum(data: Data, offset: Int, tcpLength: Int, sourceIP: UInt32, destinationIP: UInt32) -> Data { var bufferSize = tcpLength + 12 var isOdd = false if bufferSize % 2 != 0 { bufferSize += 1 isOdd = true } var buffer = Data() // Add source IP buffer.append(contentsOf: sourceIP.bytes) // Add destination IP buffer.append(contentsOf: destinationIP.bytes) // Add reserved byte and protocol (6 for TCP) buffer.append(0) buffer.append(6) // Add TCP length buffer.append(contentsOf: UInt16(tcpLength).bytes) // Add TCP header and data buffer.append(contentsOf: data[offset.. Data { var start = offset var sum = 0 while start < length { sum += getNetworkInt(buffer: data, start: start, length: 2) start += 2 } // Carry over one's complement while (sum >> 16) > 0 { sum = (sum & 0xFFFF) + (sum >> 16) } // Flip the bits to get one's complement sum = ~sum // Extract the last two bytes of the int let checksum = Data([UInt8(truncatingIfNeeded: (sum >> 8) & 0xFF), UInt8(truncatingIfNeeded: sum & 0xFF)]) return checksum } static func getNetworkInt(buffer: Data, start: Int, length: Int) -> Int { var value = 0 var end = start + min(length, 4) if end > buffer.count { end = buffer.count } for i in start.. Bool { guard let options = tcpHeader.options else { return false } var i = 0 while i < options.count { let kind = options[i] switch kind { case 0, 1: break case 2: i += 3 case 3, 14: i += 2 case 4: i += 1 case 5, 15: i += Int(options[i + 1]) - 2 case 8: i += 9 case 23: return true default: print("Unknown option: \(kind)") } i += 1 } return false } } extension FixedWidthInteger { var bytes: [UInt8] { withUnsafeBytes(of: self.bigEndian) { Array($0) } } } ================================================ FILE: ios/ProxyPin/vpn/utils/TLS.swift ================================================ // // TLS.swift // Runner // // Created by wanghongen on 2025/5/31. // class TLS { static func isTLSClientHello(packetData: Data) -> Bool { // Ensure the packet has enough data for a TLS ClientHello message guard packetData.count >= 43 else { return false } // Check if the first byte is 0x16 (Handshake type: ClientHello) if packetData[0] != 0x16 { return false } // Check if the next two bytes represent a valid TLS version (e.g., 0x0301, 0x0302, 0x0303) let version = packetData[1...2] if version != Data([0x03, 0x01]) && version != Data([0x03, 0x02]) && version != Data([0x03, 0x03]) { return false } // Check if the handshake message type is ClientHello (0x01) if packetData[5] != 0x01 { return false } // Check if the record layer protocol version matches the expected TLS version let recordVersion = packetData[9...10] if recordVersion != Data([0x03, 0x01]) && recordVersion != Data([0x03, 0x02]) && recordVersion != Data([0x03, 0x03]) { return false } return true } } ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter import NetworkExtension @main @objc class AppDelegate: FlutterAppDelegate { var backgroundAudioEnable: Bool = true override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window?.rootViewController as! FlutterViewController let vpnChannel = FlutterMethodChannel.init(name: "com.proxy/proxyVpn", binaryMessenger: controller as! FlutterBinaryMessenger); vpnChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in if ("stopVpn" == call.method) { VpnManager.shared.disconnect() } else if ("isRunning" == call.method){ result(Bool(VpnManager.shared.isRunning())) } else if ("restartVpn" == call.method){ let arguments = call.arguments as? Dictionary // VpnManager.shared.disconnect() VpnManager.shared.restartConnect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool, proxyPassDomains: arguments?["proxyPassDomains"] as? [String]) } else { let arguments = call.arguments as? Dictionary VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool, proxyPassDomains: arguments?["proxyPassDomains"] as? [String]) } }) if #available(iOS 13.0.0, *) { PictureInPictureManager.regirst(flutter: controller as! FlutterBinaryMessenger) MethodHandler.register(with: self.registrar(forPlugin: MethodHandler.name)!) } if let window = self.window { window.rootViewController = controller } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func applicationWillTerminate(_ application: UIApplication) { VpnManager.shared.disconnect() } var timer: Timer? var bgTask: UIBackgroundTaskIdentifier? override func applicationDidEnterBackground(_ application: UIApplication) { if (!VpnManager.shared.isRunning()) { return } timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) RunLoop.current.add(timer!, forMode: RunLoop.Mode.common) bgTask = application.beginBackgroundTask(expirationHandler: nil) } @objc func timerAction() { print(UIApplication.shared.backgroundTimeRemaining) let application = UIApplication.shared if (bgTask != nil) { application.endBackgroundTask(bgTask!); bgTask = nil; } if (UIApplication.shared.backgroundTimeRemaining < 60 && VpnManager.shared.isRunning()) { bgTask = application.beginBackgroundTask(expirationHandler: nil) } if (application.backgroundTimeRemaining <= 0 || application.applicationState == .active || AudioManager.shared.openBackgroundAudioAutoplay) { timer?.invalidate(); timer = nil; } if (application.backgroundTimeRemaining <= 10) { self.backgroundAudio() } } override func applicationWillResignActive(_ application: UIApplication) { self.backgroundAudio(); } override func applicationDidBecomeActive(_ application: UIApplication) { self.endBackgroundUpdateTask() } private func backgroundAudio() { if (!VpnManager.shared.isRunning() || !self.backgroundAudioEnable) { return } if (AudioManager.shared.openBackgroundAudioAutoplay) { return; } AudioManager.shared.openBackgroundAudioAutoplay = true self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: { self.endBackgroundUpdateTask() }) } var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0) func endBackgroundUpdateTask() { if (!VpnManager.shared.isRunning() || !AudioManager.shared.openBackgroundAudioAutoplay) { return } AudioManager.shared.openBackgroundAudioAutoplay = false UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask) self.backgroundUpdateTask = UIBackgroundTaskIdentifier.invalid } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images": [ { "filename": "AppIcon@2x.png", "idiom": "iphone", "scale": "2x", "size": "60x60" }, { "filename": "AppIcon@3x.png", "idiom": "iphone", "scale": "3x", "size": "60x60" }, { "filename": "AppIcon~ipad.png", "idiom": "ipad", "scale": "1x", "size": "76x76" }, { "filename": "AppIcon-40@2x.png", "idiom": "iphone", "scale": "2x", "size": "40x40" }, { "filename": "AppIcon-40@3x.png", "idiom": "iphone", "scale": "3x", "size": "40x40" }, { "filename": "AppIcon-20@2x.png", "idiom": "iphone", "scale": "2x", "size": "20x20" }, { "filename": "AppIcon-20@3x.png", "idiom": "iphone", "scale": "3x", "size": "20x20" }, { "filename": "AppIcon-29.png", "idiom": "iphone", "scale": "1x", "size": "29x29" }, { "filename": "AppIcon-29@2x.png", "idiom": "iphone", "scale": "2x", "size": "29x29" }, { "filename": "AppIcon-29@3x.png", "idiom": "iphone", "scale": "3x", "size": "29x29" }, { "filename": "AppIcon-60@2x~car.png", "idiom": "car", "scale": "2x", "size": "60x60" }, { "filename": "AppIcon-60@3x~car.png", "idiom": "car", "scale": "3x", "size": "60x60" }, { "filename": "AppIcon@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "76x76" }, { "filename": "AppIcon-83.5@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "83.5x83.5" }, { "filename": "AppIcon~ios-marketing.png", "idiom": "ios-marketing", "scale": "1x", "size": "1024x1024" } ] } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: ios/Runner/AudioManager.swift ================================================ import Foundation import AVFoundation import UIKit class AudioManager: NSObject { static let shared = AudioManager() fileprivate let audioSession = AVAudioSession.sharedInstance() fileprivate var backgroundAudioPlayer: AVAudioPlayer? fileprivate var backgroundTimeLength = 0 fileprivate var timer: Timer? static let audioName = "" // 是否开启后台自动播放无声音乐 var openBackgroundAudioAutoplay = false { didSet { if self.openBackgroundAudioAutoplay { self.setupAudioSession() self.setupBackgroundAudioPlayer() } else { if let player = self.backgroundAudioPlayer { if player.isPlaying { player.stop() } } self.backgroundAudioPlayer = nil try? self.audioSession.setActive(false, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation) } } } override init() { super.init() self.setupListener() } deinit { NotificationCenter.default.removeObserver(self) } private func setupAudioSession() { do { try self.audioSession.setCategory(AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.mixWithOthers) try self.audioSession.setActive(false) } catch let error { debugPrint("\(type(of:self)):\(error)") } } private func setupBackgroundAudioPlayer() { do { self.backgroundAudioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: "silence", ofType: "mp3")!)) } catch let error { debugPrint("\(type(of:self)):\(error)") } self.backgroundAudioPlayer?.numberOfLoops = -1 self.backgroundAudioPlayer?.volume = 0 self.backgroundAudioPlayer?.delegate = self } private func setupListener() { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(audioSessionInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil) } } // MARK: - 扩展 监听通知 extension AudioManager { /// 进入后台 播放无声音乐 @objc public func didEnterBackground() { self.setupTimer() guard self.openBackgroundAudioAutoplay else {return} do { try self.audioSession.setActive(true) } catch let error { debugPrint("\(type(of:self)):\(error))") } self.backgroundAudioPlayer?.prepareToPlay() self.backgroundAudioPlayer?.play() } /// 进入前台,暂停播放音乐 @objc public func didBecomeActive() { self.removeTimer() self.hintBackgroundTimeLength() self.backgroundTimeLength = 0 guard self.openBackgroundAudioAutoplay else {return} self.backgroundAudioPlayer?.pause() do { try self.audioSession.setActive(false, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation) } catch let error { debugPrint("\(type(of:self)):\(error))") } } /// 音乐中断处理 @objc fileprivate func audioSessionInterruption(notification: NSNotification) { guard self.openBackgroundAudioAutoplay else {return} guard let userinfo = notification.userInfo else {return} guard let interruptionType: UInt = userinfo[AVAudioSessionInterruptionTypeKey] as! UInt? else {return} if interruptionType == AVAudioSession.InterruptionType.began.rawValue { // 中断开始,音乐被暂停 debugPrint("\(type(of:self)): 中断开始 userinfo:\(userinfo)") } else if interruptionType == AVAudioSession.InterruptionType.ended.rawValue { // 中断结束,恢复播放 debugPrint("\(type(of:self)): 中断结束 userinfo:\(userinfo)") guard let player = self.backgroundAudioPlayer else {return} if player.isPlaying == false { debugPrint("\(type(of:self)): 音乐未播放,准备开始播放") do { try self.audioSession.setActive(true) } catch let error { debugPrint("\(type(of:self)):\(error)") } player.prepareToPlay() player.play() } else { debugPrint("\(type(of:self)): 音乐正在播放") } } } } // MARK: - 扩展 定时器任务 extension AudioManager { fileprivate func setupTimer() { self.removeTimer() self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerTask), userInfo: nil, repeats: true) RunLoop.main.add(self.timer!, forMode: RunLoop.Mode.common) // RunLoop.main.add(self.timer!, forMode: RunLoop.Mode.init(rawValue: "")) } fileprivate func removeTimer() { self.timer?.invalidate() self.timer = nil; } @objc func timerTask() { self.backgroundTimeLength += 1 } fileprivate func hintBackgroundTimeLength() { let message = "本次后台持续时间:\(self.backgroundTimeLength)s" print(message) } } // MARK: - 扩展 播放代理 extension AudioManager: AVAudioPlayerDelegate { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { } func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { debugPrint("\(type(of:self))" + error.debugDescription) } } ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Handlers/MethodHandler.swift ================================================ // // MethodHandler.swift // Runner // // Created by wanghongen on 2025/5/30. // import Flutter import Network import SystemConfiguration.CaptiveNetwork import Security public class MethodHandler: NSObject, FlutterPlugin { public static let name = "com.proxypin/method" private var channel: FlutterMethodChannel? private var currentPathMonitor: NWPathMonitor? private var currentCompletionHandler: ((Bool) -> Void)? public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger()) let instance = MethodHandler() registrar.addMethodCallDelegate(instance, channel: channel) instance.channel = channel } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "requestLocalNetwork": // 调用异步函数,并在其完成时传递结果 self.requestLocalNetworkAccess { isAvailable in print("[MethodHandler] requestLocalNetwork result: \(isAvailable)") result(isAvailable) } case "isCaInstalled": guard let args = call.arguments as? [String: Any], let pem = args["pem"] as? String else { print("[MethodHandler] isCaInstalled ARG_ERROR: missing pem") result(FlutterError(code: "ARG_ERROR", message: "Missing pem", details: nil)) return } let ret = self.isCertificateInstalled(pem: pem) result(ret) case "evaluateChainTrusted": guard let args = call.arguments as? [String: Any], let leafPem = args["leafPem"] as? String, let caPem = args["caPem"] as? String else { print("[MethodHandler] evaluateChainTrusted ARG_ERROR: missing leafPem/caPem") result(FlutterError(code: "ARG_ERROR", message: "Missing leafPem/caPem", details: nil)) return } let host = args["host"] as? String let ret = self.isChainTrusted(leafPem: leafPem, caPem: caPem, host: host) // print("[MethodHandler] evaluateChainTrusted => \(ret)") result(ret) default: print("[MethodHandler] method not implemented: \(call.method)") result(FlutterMethodNotImplemented) } } // MARK: - iOS: Check certificate trust private func isCertificateInstalled(pem: String) -> Bool { guard let der = self.decodePemToDer(pem) as CFData?, let certificate = SecCertificateCreateWithData(nil, der) else { print("[MethodHandler] isCertificateTrusted decode/create cert failed") return false } let policy = SecPolicyCreateBasicX509() var trust: SecTrust? let status = SecTrustCreateWithCertificates(certificate, policy, &trust) if status != errSecSuccess || trust == nil { print("[MethodHandler] SecTrustCreateWithCertificates failed status=\(status)") return false } if #available(iOS 12.0, *) { var error: CFError? let ok = SecTrustEvaluateWithError(trust!, &error) if let e = error { print("[MethodHandler] SecTrustEvaluateWithError ok=\(ok) error=\(e)") } return ok } else { var trustResult = SecTrustResultType.invalid let evalStatus = SecTrustEvaluate(trust!, &trustResult) let ok = (evalStatus == errSecSuccess) && (trustResult == .unspecified || trustResult == .proceed) print("[MethodHandler] SecTrustEvaluate status=\(evalStatus) result=\(trustResult.rawValue) trusted=\(ok)") return ok } } // MARK: - iOS: Evaluate leaf+CA chain with SSL policy private func isChainTrusted(leafPem: String, caPem: String, host: String?) -> Bool { guard let leafDer = self.decodePemToDer(leafPem) as CFData?, let leaf = SecCertificateCreateWithData(nil, leafDer) else { print("[MethodHandler] isChainTrusted leaf decode/create failed") return false } guard let caDer = self.decodePemToDer(caPem) as CFData?, let ca = SecCertificateCreateWithData(nil, caDer) else { print("[MethodHandler] isChainTrusted ca decode/create failed") return false } let certs: [SecCertificate] = [leaf, ca] let policy = SecPolicyCreateSSL(true, host as CFString?) var trust: SecTrust? let status = SecTrustCreateWithCertificates(certs as CFTypeRef, policy, &trust) if status != errSecSuccess || trust == nil { print("[MethodHandler] isChainTrusted SecTrustCreateWithCertificates failed status=\(status)") return false } if #available(iOS 12.0, *) { var error: CFError? let ok = SecTrustEvaluateWithError(trust!, &error) if let e = error { print("[MethodHandler] isChainTrusted evaluate ok=\(ok) error=\(e)") } else { print("[MethodHandler] isChainTrusted evaluate ok=\(ok)") } return ok } else { var trustResult = SecTrustResultType.invalid let evalStatus = SecTrustEvaluate(trust!, &trustResult) let ok = (evalStatus == errSecSuccess) && (trustResult == .unspecified || trustResult == .proceed) // print("[MethodHandler] isChainTrusted evaluate status=\(evalStatus) result=\(trustResult.rawValue) trusted=\(ok)") return ok } } private func decodePemToDer(_ pem: String) -> Data? { // Strip header/footer and whitespace let lines = pem.components(separatedBy: "\n").filter { line in return !line.contains("-----BEGIN") && !line.contains("-----END") && !line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } let base64Str = lines.joined() let der = Data(base64Encoded: base64Str, options: .ignoreUnknownCharacters) return der } /// 异步检查本地网络(Wi-Fi 或以太网)是否可用。 /// - Parameter completion: 一个回调函数,当检查完成时调用,参数为 Bool 类型,true 表示本地网络可用,false 表示不可用。 func requestLocalNetworkAccess(completion: @escaping (Bool) -> Void) { // 如果已有正在进行的监视,先取消它 self.currentPathMonitor?.cancel() self.currentPathMonitor = NWPathMonitor() // 将 completion 存储起来,以便在 pathUpdateHandler 中调用 // 这是为了确保 completion 只被调用一次 self.currentCompletionHandler = completion self.currentPathMonitor?.pathUpdateHandler = { [weak self] path in guard let self = self else { return } // 确保 completionHandler 仍然存在(即尚未被调用和清除) guard let completionHandler = self.currentCompletionHandler else { // 可能已经被调用过了,或者监视器被意外触发 // 为安全起见,取消监视器 self.currentPathMonitor?.cancel() return } var isLocalNetworkAvailable = false print("Network path status: \(path.status)") if path.status == .satisfied { if path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet) { isLocalNetworkAvailable = true } } // 对于其他状态 (例如 .unsatisfied, .requiresConnection) 或其他接口类型 (例如 cellular), // isLocalNetworkAvailable 将保持 false。 // 调用存储的 completion handler completionHandler(isLocalNetworkAvailable) // 清理:取消监视器并清除存储的引用,以防止重复调用和内存泄漏 self.currentPathMonitor?.cancel() self.currentPathMonitor = nil self.currentCompletionHandler = nil } // 在主队列上启动监视器 self.currentPathMonitor?.start(queue: DispatchQueue.global()) } } ================================================ FILE: ios/Runner/Info.plist ================================================ BGTaskSchedulerPermittedIdentifiers $(PRODUCT_BUNDLE_IDENTIFIER) CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName ProxyPin CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleLocalizations en zh CFBundleName ProxyPin CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSCameraUsageDescription Scan QR code NSPhotoLibraryUsageDescription Access to Photo Library UIApplicationSupportsIndirectInputEvents UIBackgroundModes audio UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance NSAppTransportSecurity NSAllowsArbitraryLoads NSLocalNetworkUsageDescription Remote Device Connect ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner/Runner.entitlements ================================================ aps-environment development com.apple.developer.networking.networkextension packet-tunnel-provider com.apple.developer.networking.vpn.api allow-vpn com.apple.security.application-groups group.com.proxy.pin com.apple.security.network.client com.apple.security.network.server ================================================ FILE: ios/Runner/VpnManager.swift ================================================ let kProxyServiceVPNStatusNotification = "kProxyServiceVPNStatusNotification" import Foundation import NetworkExtension enum VPNStatus { case off case connecting case on case disconnecting } class VpnManager{ var activeVPN: NETunnelProviderManager?; public var proxyHost: String = "127.0.0.1" public var proxyPort: Int = 9099 public var ipProxy: Bool = false public var proxyPassDomains: [String]? static let shared = VpnManager() var observerAdded: Bool = false fileprivate(set) var vpnStatus = VPNStatus.off { didSet { NotificationCenter.default.post(name: Notification.Name(rawValue: kProxyServiceVPNStatusNotification), object: nil) } } init() { loadProviderManager{ guard let manager = $0 else{return} self.updateVPNStatus(manager) } addVPNStatusObserver() } deinit { NotificationCenter.default.removeObserver(self) } func addVPNStatusObserver() { guard !observerAdded else{ return } loadProviderManager { [unowned self] (manager) -> Void in if let manager = manager { self.observerAdded = true NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: OperationQueue.main, using: { [unowned self] (notification) -> Void in self.updateVPNStatus(manager) if (manager.connection.status == .invalid || manager.connection.status == .disconnected){ print("VPN断开: \(String(describing: manager.debugDescription))") } }) } } } func updateVPNStatus(_ manager: NEVPNManager) { switch manager.connection.status { case .connected: self.vpnStatus = .on case .connecting, .reasserting: self.vpnStatus = .connecting case .disconnecting: self.vpnStatus = .disconnecting case .disconnected, .invalid: self.vpnStatus = .off @unknown default: break } } } // load VPN Profiles extension VpnManager{ fileprivate func createProviderManager() -> NETunnelProviderManager { let manager = NETunnelProviderManager() let conf = NETunnelProviderProtocol() conf.serverAddress = "ProxyPin" manager.protocolConfiguration = conf manager.localizedDescription = "ProxyPin" return manager } func loadAndCreatePrividerManager(_ complete: @escaping (NETunnelProviderManager?) -> Void ){ NETunnelProviderManager.loadAllFromPreferences{ [self] (managers, error) in guard let managers = managers else{return} let manager: NETunnelProviderManager if managers.count > 0 { manager = managers[0] }else{ manager = self.createProviderManager() } var conf = [String:AnyObject]() conf["proxyHost"] = self.proxyHost as AnyObject conf["proxyPort"] = self.proxyPort as AnyObject conf["ipProxy"] = self.ipProxy as AnyObject // Bridge Swift [String] to NSArray (Objective-C) before inserting into AnyObject dictionary if let passDomains = self.proxyPassDomains { conf["proxyPassDomains"] = passDomains as NSArray } let orignConf = manager.protocolConfiguration as! NETunnelProviderProtocol orignConf.providerConfiguration = conf manager.protocolConfiguration = orignConf print(orignConf) manager.isEnabled = true manager.saveToPreferences{ if ($0 != nil){ // complete(nil); // return; } manager.loadFromPreferences{ if $0 != nil{ print("loadFromPreferences",$0.debugDescription) complete(nil);return; } self.addVPNStatusObserver() complete(manager) } } } } func loadProviderManager(_ complete: @escaping (NETunnelProviderManager?) -> Void){ NETunnelProviderManager.loadAllFromPreferences { (managers, error) in if let managers = managers { if managers.count > 0 { let manager = managers[0] complete(manager) return } } complete(nil) } } } // Actions extension VpnManager{ func connect(host: String?, port: Int?, ipProxy: Bool? = false, proxyPassDomains: [String]? = nil) { self.proxyHost = host ?? self.proxyHost self.proxyPort = port ?? self.proxyPort self.ipProxy = ipProxy ?? false self.proxyPassDomains = proxyPassDomains ?? self.proxyPassDomains self.loadAndCreatePrividerManager { (manager) in guard let manager = manager else{return} do{ self.activeVPN = manager try manager.connection.startVPNTunnel() }catch let err{ print("connect: ", err) } } } func restartConnect(host: String?, port: Int?, ipProxy: Bool? = false, proxyPassDomains: [String]? = nil) { self.proxyHost = host ?? self.proxyHost self.proxyPort = port ?? self.proxyPort self.ipProxy = ipProxy ?? false if (activeVPN != nil) { activeVPN?.connection.stopVPNTunnel() activeVPN = nil } self.connect(host: host, port: port, ipProxy: ipProxy, proxyPassDomains: proxyPassDomains) } func disconnect() { if (activeVPN != nil) { activeVPN?.connection.stopVPNTunnel() activeVPN = nil return } loadProviderManager{ $0?.connection.stopVPNTunnel() } } func isRunning() -> Bool { return vpnStatus == VPNStatus.on } } ================================================ FILE: ios/Runner/en.lproj/InfoPlist.strings ================================================ "NSCameraUsageDescription"="Scan QR code"; "NSPhotoLibraryUsageDescription"="Access to Photo Library"; "PhotoLibraryAddUsageDescription"= "Save image to Photo Library"; ================================================ FILE: ios/Runner/pip/PictureInPictureManager.swift ================================================ // // PictureInPicturePlugin.swift // Runner // // Created by wanghongen on 2024/1/8. // import AVKit import UIKit import Flutter import SnapKit import SwiftUI @available(iOS 13.0.0, *) class PictureInPictureManager: NSObject,AVPictureInPictureControllerDelegate { static var shared: PictureInPictureManager! private var channel: FlutterMethodChannel; //播放器 private var playerLayer: AVPlayerLayer? // 画中画 var pipController: AVPictureInPictureController! var pipView: PictureInPictureView? var proxyPort :Int = -1; static func regirst(flutter: FlutterBinaryMessenger) { let channel = FlutterMethodChannel.init(name: "com.proxy/pictureInPicture", binaryMessenger: flutter); shared = PictureInPictureManager(channel: channel) } private init(channel: FlutterMethodChannel) { self.channel = channel super.init() channel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in // print("画中画 {call.method} methodCallHandler:\(UIApplication.shared.windows)") if ("enterPictureInPictureMode" == call.method) { let arguments = call.arguments as? Dictionary self.proxyPort = arguments?["proxyPort"] as! Int self.starPiP() result(Bool(true)) } else if ("addData" == call.method) { self.pipView?.addData(text: call.arguments as! String) } }) if AVPictureInPictureController.isPictureInPictureSupported() { do { try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) } catch { print(error) } } } private func initPIP() { if (playerLayer == nil) { setupPlayer() } if (pipController == nil) { print("画中画初始化:\(UIApplication.shared.windows)") setupPip() } } // 配置播放器 private func setupPlayer() { let video = Bundle.main.url(forResource: "silience", withExtension: "mov") let asset = AVAsset.init(url: video!) let playerItem = AVPlayerItem.init(asset: asset) let player = AVPlayer.init(playerItem: playerItem) playerLayer = AVPlayerLayer(player: player) playerLayer?.frame = .init(x: 90, y: 390, width: 180, height: 280) playerLayer?.isHidden = true player.isMuted = true player.allowsExternalPlayback = true // player.play() let view = UIView() view.layer.addSublayer(playerLayer!) UIApplication.shared.keyWindow?.rootViewController?.view.addSubview(view) } // 配置画中画 private func setupPip() { pipController = AVPictureInPictureController.init(playerLayer: playerLayer!)! pipController.delegate = self // if #available(iOS 14.2, *) { // pipController.canStartPictureInPictureAutomaticallyFromInline = true // } // 隐藏播放按钮、快进快退按钮 pipController.setValue(1, forKey: "controlsStyle") //点击回到app //pipController.setValue(2, forKey: "controlsStyle") } // 开启/关闭 画中画 func starPiP() { self.initPIP(); if pipController.isPictureInPictureActive { pipController.stopPictureInPicture() } else { print("starPiP \(pipController.isPictureInPicturePossible)") if (pipController.isPictureInPicturePossible) { pipController.startPictureInPicture() return; } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [self] in if (self.pipController.isPictureInPicturePossible) { self.pipController.startPictureInPicture() return; } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) { self.pipController.startPictureInPicture() } } } } var playButton = UIButton(type: .custom) func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { // print("画中画初始化后:\(UIApplication.shared.windows)") // 把自定义view加到画中画上 if let window = UIApplication.shared.windows.first { pipView = PictureInPictureView() let vc = UIHostingController(rootView: pipView) let icon = VpnManager.shared.isRunning() ? "pause.fill" : "play.fill" playButton.setImage(UIImage(systemName: icon), for: .normal) playButton.addTarget(self, action: #selector(vpnAction), for: .touchUpInside) vc.view.addSubview(playButton) playButton.snp.makeConstraints{ (make) in make.left.equalTo(15) make.bottom.equalTo(-13) } let clearButton = UIButton(type: .custom) clearButton.setImage(UIImage(systemName: "trash.circle"), for: .normal) clearButton.addTarget(self, action: #selector(cleanAction), for: .touchUpInside) vc.view.addSubview(clearButton) clearButton.snp.makeConstraints{ (make) in make.right.equalTo(-13) make.bottom.equalTo(-13) } window.addSubview(vc.view!) // 使用自动布局 vc.view?.snp.makeConstraints { (make) -> Void in make.edges.equalToSuperview() } UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) } } @objc func cleanAction() { channel.invokeMethod("cleanSession", arguments: nil) pipView?.dataSource.clear() } @objc func vpnAction() { if (VpnManager.shared.isRunning()) { VpnManager.shared.disconnect() playButton.setImage(UIImage(systemName: "play.fill"), for: .normal) } else { VpnManager.shared.connect(host: nil, port: proxyPort, ipProxy: nil) playButton.setImage(UIImage(systemName: "pause.fill"), for: .normal) } // pipView?.addData(text: "hello") } func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { // print("pictureInPictureControllerWillStopPictureInPicture:") channel.invokeMethod("exitPictureInPictureMode", arguments: nil) } } ================================================ FILE: ios/Runner/pip/PictureInPictureView.swift ================================================ // // PictureInPictureView.swift // Runner // // Created by wanghongen on 2024/1/9. // import SwiftUI @available(iOS 13.0, *) class DataSource: ObservableObject { @Published var list: [String] = [] func clear() { list.removeAll() } } @available(iOS 13.0, *) struct PictureInPictureView: View { @ObservedObject var dataSource = DataSource() var body: some View { ScrollView { VStack(alignment: .leading, spacing: 1.3){ ForEach((0.. Void in // make.edges.equalToSuperview() // } // } // // func addData(text: String) { // let str = "• " + text + "\n" + (viewLabel.text ?? ""); // self.viewLabel.text = str; // } //} ================================================ FILE: ios/Runner/zh-Hans.lproj/InfoPlist.strings ================================================ "NSCameraUsageDescription"="扫描二维码"; "NSPhotoLibraryUsageDescription"="访问相册"; "PhotoLibraryAddUsageDescription"= "保存图片到相册"; ================================================ FILE: ios/Runner/zh-Hans.lproj/LaunchScreen.strings ================================================ ================================================ FILE: ios/Runner/zh-Hans.lproj/Main.strings ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1FBB39B834EBBDA7C793EA99 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 9B09121B2A5457B3001108B7 /* VpnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B09121A2A5457B3001108B7 /* VpnManager.swift */; }; 9B0912222A54593A001108B7 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B0912212A54593A001108B7 /* NetworkExtension.framework */; }; 9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */; }; 9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9B0912202A54593A001108B7 /* ProxyPin.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */; }; 9B2A10C82B4CBE32001C443F /* silience.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9B2A10C72B4CBE32001C443F /* silience.mov */; }; 9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5125A92CAEE3350027996E /* ICMPPacket.swift */; }; 9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B70772C2A5718FB00F184A9 /* AudioManager.swift */; }; 9B7077362A5728B900F184A9 /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9B7077352A5728B900F184A9 /* silence.mp3 */; }; 9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9B90F5822C183CDE007D7A81 /* InfoPlist.strings */; }; 9BAB4FC02DE75CFE0093BFBA /* GBPing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */; }; 9BAB4FC12DE75CFE0093BFBA /* GBPingSummary.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */; }; 9BAB4FC32DE75D220093BFBA /* GBPingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */; }; 9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */; }; 9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */; }; 9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28692C97748100C2B46C /* IP4Header.swift */; }; 9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */; }; 9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */; }; 9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */; }; 9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28722C988E9D00C2B46C /* Packet.swift */; }; 9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28742C988EC400C2B46C /* TransportHeader.swift */; }; 9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28772C98902900C2B46C /* PacketUtil.swift */; }; 9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28792C989A7200C2B46C /* Connection.swift */; }; 9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */; }; 9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */; }; 9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */; }; 9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */; }; 9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */; }; 9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28892C98C82000C2B46C /* SocketIOService.swift */; }; 9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */; }; 9BE87B5C2DEA480000F4FCEF /* MethodHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */; }; 9BE87B5E2DEA6BAE00F4FCEF /* TLS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */; }; B375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; 9B0912282A54593A001108B7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 9B09121F2A54593A001108B7; remoteInfo = ProxyPin; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 9B09122B2A54593B001108B7 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( 9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0B67A4E592FF13260AAFD656 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3E54CF83D4EE560125987C8A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 72900351EF1A3F028032459A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9B0912192A545757001108B7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 9B09121A2A5457B3001108B7 /* VpnManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VpnManager.swift; sourceTree = ""; }; 9B0912202A54593A001108B7 /* ProxyPin.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ProxyPin.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 9B0912212A54593A001108B7 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 9B0912262A54593A001108B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9B0912272A54593A001108B7 /* ProxyPin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProxyPin.entitlements; sourceTree = ""; }; 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureView.swift; sourceTree = ""; }; 9B2A10C72B4CBE32001C443F /* silience.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = silience.mov; sourceTree = ""; }; 9B5125A92CAEE3350027996E /* ICMPPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMPPacket.swift; sourceTree = ""; }; 9B70772C2A5718FB00F184A9 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; 9B7077352A5728B900F184A9 /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; 9B90F57C2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 9B90F57D2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; 9B90F5812C183CDE007D7A81 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 9B90F5832C183CE0007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; 9BAB4FBB2DE75CFE0093BFBA /* GBPing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GBPing.h; sourceTree = ""; }; 9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GBPing.m; sourceTree = ""; }; 9BAB4FBD2DE75CFE0093BFBA /* GBPingSummary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GBPingSummary.h; sourceTree = ""; }; 9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GBPingSummary.m; sourceTree = ""; }; 9BAB4FBF2DE75CFE0093BFBA /* ICMPHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICMPHeader.h; sourceTree = ""; }; 9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GBPingHelper.swift; sourceTree = ""; }; 9BAB4FC42DE75E9A0093BFBA /* ProxyPin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ProxyPin-Bridging-Header.h"; sourceTree = ""; }; 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureManager.swift; sourceTree = ""; }; 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHandler.swift; sourceTree = ""; }; 9BCA28692C97748100C2B46C /* IP4Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP4Header.swift; sourceTree = ""; }; 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPHeader.swift; sourceTree = ""; }; 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPPacketFactory.swift; sourceTree = ""; }; 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionManager.swift; sourceTree = ""; }; 9BCA28722C988E9D00C2B46C /* Packet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Packet.swift; sourceTree = ""; }; 9BCA28742C988EC400C2B46C /* TransportHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportHeader.swift; sourceTree = ""; }; 9BCA28772C98902900C2B46C /* PacketUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketUtil.swift; sourceTree = ""; }; 9BCA28792C989A7200C2B46C /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseableConnection.swift; sourceTree = ""; }; 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWProtocol.swift; sourceTree = ""; }; 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientPacketWriter.swift; sourceTree = ""; }; 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyVpnService.swift; sourceTree = ""; }; 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = ""; }; 9BCA28892C98C82000C2B46C /* SocketIOService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOService.swift; sourceTree = ""; }; 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPHeader.swift; sourceTree = ""; }; 9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodHandler.swift; sourceTree = ""; }; 9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLS.swift; sourceTree = ""; }; D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 2C2BB3BDC059E8FD67F7FF64 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1FBB39B834EBBDA7C793EA99 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 9B09121D2A54593A001108B7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9B0912222A54593A001108B7 /* NetworkExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 28892733E959FF4F4696A049 /* Frameworks */ = { isa = PBXGroup; children = ( 239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */, D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */, 9B0912212A54593A001108B7 /* NetworkExtension.framework */, ); name = Frameworks; sourceTree = ""; }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 8A601D60E7BAF3F69F98077D /* Pods */ = { isa = PBXGroup; children = ( 0B67A4E592FF13260AAFD656 /* Pods-Runner.debug.xcconfig */, 72900351EF1A3F028032459A /* Pods-Runner.release.xcconfig */, 3E54CF83D4EE560125987C8A /* Pods-Runner.profile.xcconfig */, 306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */, E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */, 8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 9B0912232A54593A001108B7 /* ProxyPin */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 8A601D60E7BAF3F69F98077D /* Pods */, 28892733E959FF4F4696A049 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, 9B0912202A54593A001108B7 /* ProxyPin.appex */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 9BE87B5A2DEA47DE00F4FCEF /* Handlers */, 9BC4B8D12B4C19ED0047DBDD /* pip */, 9B7077352A5728B900F184A9 /* silence.mp3 */, 9B09121A2A5457B3001108B7 /* VpnManager.swift */, 9B0912192A545757001108B7 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 9B70772C2A5718FB00F184A9 /* AudioManager.swift */, 9B90F5822C183CDE007D7A81 /* InfoPlist.strings */, ); path = Runner; sourceTree = ""; }; 9B0912232A54593A001108B7 /* ProxyPin */ = { isa = PBXGroup; children = ( 9BCA28642C97729000C2B46C /* vpn */, 9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */, 9B0912262A54593A001108B7 /* Info.plist */, 9B0912272A54593A001108B7 /* ProxyPin.entitlements */, 9BAB4FC42DE75E9A0093BFBA /* ProxyPin-Bridging-Header.h */, ); path = ProxyPin; sourceTree = ""; }; 9BAB4FB12DE74F570093BFBA /* ping */ = { isa = PBXGroup; children = ( 9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */, 9BAB4FBB2DE75CFE0093BFBA /* GBPing.h */, 9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */, 9BAB4FBD2DE75CFE0093BFBA /* GBPingSummary.h */, 9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */, 9BAB4FBF2DE75CFE0093BFBA /* ICMPHeader.h */, ); path = ping; sourceTree = ""; }; 9BC4B8D12B4C19ED0047DBDD /* pip */ = { isa = PBXGroup; children = ( 9B2A10C72B4CBE32001C443F /* silience.mov */, 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */, 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */, ); path = pip; sourceTree = ""; }; 9BCA28642C97729000C2B46C /* vpn */ = { isa = PBXGroup; children = ( 9BAB4FB12DE74F570093BFBA /* ping */, 9BCA287B2C989A8700C2B46C /* socket */, 9BCA28762C98901800C2B46C /* utils */, 9BCA28672C97746200C2B46C /* transport */, 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */, 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */, 9BCA28792C989A7200C2B46C /* Connection.swift */, 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */, 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */, 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */, ); path = vpn; sourceTree = ""; }; 9BCA28672C97746200C2B46C /* transport */ = { isa = PBXGroup; children = ( 9BCA28682C97747000C2B46C /* protocol */, 9BCA28722C988E9D00C2B46C /* Packet.swift */, ); path = transport; sourceTree = ""; }; 9BCA28682C97747000C2B46C /* protocol */ = { isa = PBXGroup; children = ( 9BCA28692C97748100C2B46C /* IP4Header.swift */, 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */, 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */, 9BCA28742C988EC400C2B46C /* TransportHeader.swift */, 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */, 9B5125A92CAEE3350027996E /* ICMPPacket.swift */, ); path = protocol; sourceTree = ""; }; 9BCA28762C98901800C2B46C /* utils */ = { isa = PBXGroup; children = ( 9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */, 9BCA28772C98902900C2B46C /* PacketUtil.swift */, ); path = utils; sourceTree = ""; }; 9BCA287B2C989A8700C2B46C /* socket */ = { isa = PBXGroup; children = ( 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */, 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */, 9BCA28892C98C82000C2B46C /* SocketIOService.swift */, ); path = socket; sourceTree = ""; }; 9BE87B5A2DEA47DE00F4FCEF /* Handlers */ = { isa = PBXGroup; children = ( 9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */, ); path = Handlers; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( E7E8C74F615A57D43D59596C /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 2C2BB3BDC059E8FD67F7FF64 /* Frameworks */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 35A8CB519E229982B14B0197 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9B09122B2A54593B001108B7 /* Embed Foundation Extensions */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 593E01BCFF86ADFAC59E51D5 /* [CP] Embed Pods Frameworks */, 298BDEFE069E2E1C3876CA2D /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( 9B0912292A54593A001108B7 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; 9B09121F2A54593A001108B7 /* ProxyPin */ = { isa = PBXNativeTarget; buildConfigurationList = 9B09122F2A54593B001108B7 /* Build configuration list for PBXNativeTarget "ProxyPin" */; buildPhases = ( 9B09121C2A54593A001108B7 /* Sources */, 9B09121D2A54593A001108B7 /* Frameworks */, 9B09121E2A54593A001108B7 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ProxyPin; productName = ProxyPin; productReference = 9B0912202A54593A001108B7 /* ProxyPin.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; 9B09121F2A54593A001108B7 = { CreatedOnToolsVersion = 14.2; LastSwiftMigration = 1630; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, "zh-Hans", ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, 9B09121F2A54593A001108B7 /* ProxyPin */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 9B2A10C82B4CBE32001C443F /* silience.mov in Resources */, 9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */, 9B7077362A5728B900F184A9 /* silence.mp3 in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 9B09121E2A54593A001108B7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 298BDEFE069E2E1C3876CA2D /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 35A8CB519E229982B14B0197 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 593E01BCFF86ADFAC59E51D5 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; E7E8C74F615A57D43D59596C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */, 9BE87B5C2DEA480000F4FCEF /* MethodHandler.swift in Sources */, 9B09121B2A5457B3001108B7 /* VpnManager.swift in Sources */, 9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 9B09121C2A54593A001108B7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */, 9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */, 9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */, 9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */, 9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */, 9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */, 9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */, 9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */, 9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */, 9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */, 9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */, 9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */, 9BAB4FC02DE75CFE0093BFBA /* GBPing.m in Sources */, 9BAB4FC12DE75CFE0093BFBA /* GBPingSummary.m in Sources */, 9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */, 9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */, 9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */, 9BAB4FC32DE75D220093BFBA /* GBPingHelper.swift in Sources */, 9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */, 9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */, 9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */, 9BE87B5E2DEA6BAE00F4FCEF /* TLS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; 9B0912292A54593A001108B7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9B09121F2A54593A001108B7 /* ProxyPin */; targetProxy = 9B0912282A54593A001108B7 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, 9B90F57C2C183C7E007D7A81 /* zh-Hans */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, 9B90F57D2C183C7E007D7A81 /* zh-Hans */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; 9B90F5822C183CDE007D7A81 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( 9B90F5812C183CDE007D7A81 /* en */, 9B90F5832C183CE0007D7A81 /* zh-Hans */, ); name = InfoPlist.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = DM3F8VR243; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = DM3F8VR243; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = DM3F8VR243; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; 9B09122C2A54593B001108B7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = DM3F8VR243; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ProxyPin/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ProxyPin; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "ProxyPin/ProxyPin-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 9B09122D2A54593B001108B7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = DM3F8VR243; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ProxyPin/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ProxyPin; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "ProxyPin/ProxyPin-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 9B09122E2A54593B001108B7 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = DM3F8VR243; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ProxyPin/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ProxyPin; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "ProxyPin/ProxyPin-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Profile; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 9B09122F2A54593B001108B7 /* Build configuration list for PBXNativeTarget "ProxyPin" */ = { isa = XCConfigurationList; buildConfigurations = ( 9B09122C2A54593B001108B7 /* Debug */, 9B09122D2A54593B001108B7 /* Release */, 9B09122E2A54593B001108B7 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: l10n.yaml ================================================ #synthetic-package: false arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart untranslated-messages-file: l10n_errors.txt ================================================ FILE: lib/l10n/app_en.arb ================================================ { "breakpoint": "Breakpoint", "breakpointRule": "Breakpoint Rule", "name": "Name", "requests": "Requests", "favorites": "Favorites", "history": "History", "toolbox": "Toolbox", "preference": "Preferences", "feedback": "Feedback", "about": "About", "filter": "Proxy Filter", "script": "Script", "share": "Share", "port": "Port: ", "proxy": "Proxy", "externalProxy": "External Proxy", "username": "Username", "password": "Password", "proxySetting": "Proxy Setting", "setAs": "Set as ", "systemProxy": "System Proxy", "enabledHTTP2": "Enable HTTP2", "serverNotStart": "Proxy server not started", "download": "Download", "config": "Configuration", "version": "Version", "start": "Start", "stop": "Stop", "clear": "Clear", "httpsProxy": "HTTPS Proxy", "setting": "Settings", "mobileConnect": "Mobile Connect", "connectRemote": "Connect Remote", "remoteDevice": "Remote Device", "remoteDeviceList": "Remote Device List", "myQRCode": "My QR Code", "theme": "Theme", "followSystem": "Follow System", "themeColor": "Theme Color", "themeLight": "Light", "themeDark": "Dark", "language": "Language", "autoStartup": "Auto Start Recording Traffic", "autoStartupDescribe": "Automatically start recording traffic when the program starts", "copied": "Copied to clipboard", "execute": "Execute", "cancel": "Cancel", "close": "Close", "save": "Save", "confirm": "Confirm", "confirmTitle": "Confirm operation", "confirmContent": "Are you sure about this operation?", "addSuccess": "Successfully added", "saveSuccess": "Saved successfully", "operationSuccess": "Operation succeeded", "import": "Import", "importSuccess": "Import successful", "importFailed": "Import failed", "export": "Export", "exportSuccess": "Export successful", "exportFailed": "Export failed", "deleteSuccess": "Delete successful", "send": "Send", "fail": "fail", "success": "success", "emptyData": "Empty Data", "requestSuccess": "Request successful", "add": "Add", "all": "All", "modify": "Modify", "responseType": "Response Type", "request": "Request", "response": "Response", "statusCode": "Status code", "duration": "Duration", "done": "Done", "type": "Type", "enable": "Enable", "example": "Example: ", "responseHeader": "Headers", "requestHeader": "Headers", "requestLine": "Request Line", "requestMethod": "Request Method", "param": "Param", "replaceBodyWith": "Replace Body With:", "redirectTo": "Redirect To:", "redirect": "Redirect", "cannotBeEmpty": "Cannot be empty", "requestRewriteList": "Request Rewrite List", "requestRewriteRule": "Request Rewrite Rule", "requestRewriteEnable": "Enable Request Rewrite", "action": "Action", "multiple": "Multiple", "edit": "Edit", "disabled": "Disabled", "requestRewriteDeleteConfirm": "Delete {size} rule(s)?", "useGuide": "Use Guide", "pleaseEnter": "Please Enter", "click": "Click", "replace": "Replace", "clickEdit": "Click Edit", "refresh": "Refresh", "selectFile": "Select file", "match": "Match", "value": "Value", "matchRule": "Match Rule", "emptyMatchAll": "Empty means match all", "newBuilt": "New", "reportServers": "Report Servers", "addReportServer": "Add Report Server", "editReportServer": "Edit Report Server", "serverUrl": "Server URL", "compression": "Compression", "compressionNone": "None", "newFolder": "New Folder", "enableSelect": "Enable Select", "disableSelect": "Disable Select", "deleteSelect": "Delete Select", "testData": "Test Data", "noChangesDetected": "No changes detected", "enterMatchData": "Enter the data to be matched", "modifyRequestHeader": "Modify Header", "headerName": "Header Name", "headerValue": "Header Value", "deleteHeaderConfirm": "Do you want to delete the request header?", "sequence": "All Requests", "domainList": "Domain List", "domainWhitelist": "Proxy Domain Whitelist", "domainBlacklist": "Proxy Domain Blacklist", "domainFilter": "Proxy Domain List", "appWhitelist": "App Whitelist", "appWhitelistDescribe": "Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid", "appBlacklist": "App Blacklist", "scanCode": "Scan Code Connect", "addBlacklist": "Add Proxy Blacklist", "addWhitelist": "Add Proxy Whitelist", "deleteWhitelist": "Delete Proxy Whitelist", "domainListSubtitle": "Last Request Time: {time}, Count: {count}", "selectAction": "Select action", "copy": "Copy", "copyHost": "Copy Host", "copyUrl": "Copy URL", "copyRawRequest": "Copy Raw Request", "copyRequestResponse": "Copy Request and Response", "copyCurl": "Copy cURL", "copyAsPythonRequests": "Copy as Python Requests", "delete": "Delete", "rename": "Rename", "repeat": "Repeat", "repeatAllRequests": "Repeat All Requests", "repeatDomainRequests": "Repeat Domain Requests", "customRepeat": "Custom Repeat", "repeatCount": "Iterations", "repeatInterval": "Interval(ms)", "repeatDelay": "Delay(ms)", "scheduleTime": "Schedule Time", "fixed": "fixed", "random": "random", "keepCustomSettings": "Keep custom settings", "editRequest": "Edit and Request", "reSendRequest": "The request has been resent", "viewExport": "View Export", "timeDesc": "Descending by time", "timeAsc": "Ascending by time", "search": "Search", "clearSearch": "Clear Search", "requestType": "Request type", "keyword": "Keyword", "keywordSearchScope": "Keyword search scope: ", "favorite": "Favorite", "deleteFavorite": "Delete Favorite", "emptyFavorite": "Empty Favorite", "deleteFavoriteSuccess": "Favorite deleted", "historyRecord": "History", "historyCacheTime": "Cache Time", "historyManualSave": "Manual Save", "historyDay": "{day} days", "historyForever": "Forever", "historyRecordTitle": "{name} Records {length}", "historyEmptyName": "Name cannot be empty", "historySubtitle": "Records {requestLength} file {size}", "historyUnSave": "Current record is not saved", "historyDeleteConfirm": "Do you want to delete this history?", "requestEdit": "Request Editing", "encode": "Encode", "requestBody": "Request Body", "responseBody": "Response Body", "requestRewrite": "Request Rewrite", "newWindow": "New Window", "httpRequest": "HTTP Request", "enabledHttps": "Enable HTTPS Proxy", "installRootCa": "Install Certificate", "installCaLocal": "Install Certificate to Local-Machine", "downloadRootCa": "Download Certificate", "downloadRootCaNote": "Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser", "generateCA": "Generate new root certificate", "generateCADescribe": "Are you sure you want to generate a new root certificate? If confirmed,\nYou need to reinstall and trust the new certificate", "resetDefaultCA": "Reset Default Root Certificate", "resetDefaultCADescribe": "Are you sure you want to reset to the default root certificate?\nProxyPin default root certificate is the same for all users.", "exportCaP12": "Export Root Certificate(.p12)", "importCaP12": "Import Root Certificate(.p12)", "trustCa": "Trust Certificate", "profileDownload": "Profile Download", "exportCA": "Export Root Certificate", "exportPrivateKey": "Export Private Key", "install": "Install", "installCaDescribe": "Install CA Setting > Profile Download > Install", "trustCaDescribe": "Trust CA Setting > General > About > Certificate Trust Setting", "androidRoot": "System Certificate (ROOT Device)", "androidRootMagisk": "Magisk module: \nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。", "androidRootRename": "If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate {name}", "androidRootCADownload": "Download System Certificate(.0)", "androidUserCA": "User Certificate", "androidUserCATips": "Tips: Android7+ many apps will not trust user certificates", "androidUserCAInstall": "Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate", "androidUserXposed": "It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki", "configWifiProxy": "Configure mobile Wi-Fi proxy", "caInstallGuide": "Certificate Installation Guide", "caAndroidBrowser": "Open Google Browser on Android devices:", "caIosBrowser": "Open Safari on iOS devices:", "localIP": "Local IP ", "mobileScan": "Configure Wi-Fi proxy or Scan with Mobile App", "decode": "Decode", "encodeInput": "Enter the content to be converted", "encodeResult": "Conversion Result", "encodeFail": "Encoding failed", "decodeFail": "Decoding failed", "shareUrl": "Share Request URL", "shareCurl": "Share cURL Request", "shareRequestResponse": "Share Request and Response", "captureDetail": "Capture Detail", "proxyPinSoftware": "ProxyPin Open source traffic capture software for all platforms", "prompt": "Prompt", "curlSchemeRequest": "If the curl format is recognized, should it be converted into an HTTP request?", "appExitTips": "Press again to exit the program", "remoteConnectDisconnect": "Check remote connection failed, disconnected", "connect": "Connect", "reconnect": "Reconnect", "remoteConnected": "Connected {os}, traffic will be forwarded to {os}", "remoteConnectForward": "Remote connection, forwarding requests to other terminals", "connectSuccess": "Connect successful", "connectedRemote": "Connected to remote", "connected": "Connected", "notConnected": "Not connected", "disconnect": "Disconnect", "ipLayerProxy": "IP Layer Proxy(Beta)", "ipLayerProxyDesc": "IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR", "inputAddress": "Input Address", "syncConfig": "Sync configuration", "pullConfigFail": "Failed to pull configuration, please check the network connection", "sync": "Sync", "invalidQRCode": "Unrecognized QR code", "remoteConnectFail": "Connection failed,Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions", "remoteConnectSuccessTips": "Your phone needs to enable packet capture in order to capture requests", "windowMode": "Window Mode", "windowModeSubTitle": "Enabled Packet Capture, Enter the background, Display a small window", "pipIcon": "Window shortcut icon", "pipIconDescribe": "Show quick access to small window Icon", "headerExpanded": "Headers Expanded", "headerExpandedSubtitle": "Details page Headers is expanded by default", "bottomNavigation": "Bottom Navigation", "bottomNavigationSubtitle": "Bottom navigation bar is displayed, effective after restart", "memoryCleanup": "Memory Cleanup", "memoryCleanupSubtitle": "Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning", "unlimited": "Unlimited", "custom": "Custom", "externalProxyAuth": "Proxy Auth (Optional)", "externalProxyServer": "Proxy Server", "externalProxyConnectFailure": "External Proxy Connect failure", "externalProxyFailureConfirm": "Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。", "mobileDisplayPacketCapture": "Mobile Display Packet Capture:", "proxyPortRepeat": "Startup failed, please check the port number {port} is occupied。", "reset": "Reset", "proxyIgnoreDomain": "Proxy ignores domain", "domainWhitelistDescribe": "Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid", "domainBlacklistDescribe": "Domain names on the blacklist will not be proxied", "domain": "Host", "enableScript": "Enable Script", "scriptUseDescribe": "Use JavaScript to modify requests and responses", "scriptEdit": "Edit script", "scrollEnd": "Scroll to End", "logger": "Log", "material3": "Material 3 is the latest version of Google’s open-source design system", "iosVpnBackgroundAudio": "After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?", "markRead": "Mark as read", "autoRead": "Auto read", "highlight": "Highlight", "blue" : "Blue", "green" : "Green", "yellow" : "Yellow", "red" : "Red", "pink" : "Pink", "gray" : "Gray", "underline" : "Underline", "requestBlock": "Request Block", "other": "Other", "certHashName": "CA Hash Name", "regExp": "RegExp", "systemCertName": "System Certificate Name", "qrCode": "QR Code", "scanQrCode": "Scan QR Code", "generateQrCode": "Generate", "saveImage": "Save Image", "selectImage": "Select Image", "inputContent": "Input Content", "errorCorrectLevel": "Error Correct", "output": "Output", "timestamp": "Timestamp", "convert": "Convert", "time": "DateTime", "nowTimestamp": "Now timestamp", "hosts": "Hosts", "toAddress": "To Address", "encrypt": "Encrypt", "decrypt": "Decrypt", "cipher": "Cipher", "appUpdateCheckVersion": "Check for Updates", "appUpdateNotAvailableMsg": "Already Using The Latest Version", "appUpdateDialogTitle": "Update Available", "appUpdateUpdateMsg": "A new version of ProxyPin is available. Would you like to update now?", "appUpdateCurrentVersionLbl": "Current Version", "appUpdateNewVersionLbl": "New Version", "appUpdateUpdateNowBtnTxt": "Update Now", "appUpdateLaterBtnTxt": "Later", "appUpdateIgnoreBtnTxt": "Ignore", "requestMap": "Request Map", "requestMapDescribe": "Do not request remote services, use local configuration or script for response", "automatic": "Automatic", "manual": "Manual", "certNotInstalled": "Certificate not installed", "openNewWindow": "Open New Window", "sponsorDonate": "Sponsor / Donate", "sponsorSupport": "Support ongoing development", "sponsorThanks": "Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.", "sponsorAfdian": "AFDIAN", "sponsorBuyMeCoffee": "Buy Me a Coffee", "privacyPolicy": "Privacy Policy", "privacyContent": "This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.", "requestCrypto": "Request Crypto", "cryptoDecoded": "Decoded", "cryptoDecodeToggle": "Decrypt", "optional": "Optional", "cryptoRuleField": "Field Name", "cryptoIvPrefixLabel": "IV Prefix", "cryptoIvPrefixTooltip": "Use the first N bytes of the response body as IV", "local": "Local", "remoteUrl": "Remote URL", "view": "View" } ================================================ FILE: lib/l10n/app_localizations.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'app_localizations_en.dart'; import 'app_localizations_zh.dart'; // ignore_for_file: type=lint /// Callers can lookup localized strings with an instance of AppLocalizations /// returned by `AppLocalizations.of(context)`. /// /// Applications need to include `AppLocalizations.delegate()` in their app's /// `localizationDelegates` list, and the locales they support in the app's /// `supportedLocales` list. For example: /// /// ```dart /// import 'l10n/app_localizations.dart'; /// /// return MaterialApp( /// localizationsDelegates: AppLocalizations.localizationsDelegates, /// supportedLocales: AppLocalizations.supportedLocales, /// home: MyApplicationHome(), /// ); /// ``` /// /// ## Update pubspec.yaml /// /// Please make sure to update your pubspec.yaml to include the following /// packages: /// /// ```yaml /// dependencies: /// # Internationalization support. /// flutter_localizations: /// sdk: flutter /// intl: any # Use the pinned version from flutter_localizations /// /// # Rest of dependencies /// ``` /// /// ## iOS Applications /// /// iOS applications define key application metadata, including supported /// locales, in an Info.plist file that is built into the application bundle. /// To configure the locales supported by your app, you’ll need to edit this /// file. /// /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. /// Then, in the Project Navigator, open the Info.plist file under the Runner /// project’s Runner folder. /// /// Next, select the Information Property List item, select Add Item from the /// Editor menu, then select Localizations from the pop-up menu. /// /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static AppLocalizations? of(BuildContext context) { return Localizations.of(context, AppLocalizations); } static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. /// /// Returns a list of localizations delegates containing this delegate along with /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, /// and GlobalWidgetsLocalizations.delegate. /// /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. static const List> localizationsDelegates = >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('en'), Locale('zh'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant') ]; /// No description provided for @breakpoint. /// /// In en, this message translates to: /// **'Breakpoint'** String get breakpoint; /// No description provided for @breakpointRule. /// /// In en, this message translates to: /// **'Breakpoint Rule'** String get breakpointRule; /// No description provided for @name. /// /// In en, this message translates to: /// **'Name'** String get name; /// No description provided for @requests. /// /// In en, this message translates to: /// **'Requests'** String get requests; /// No description provided for @favorites. /// /// In en, this message translates to: /// **'Favorites'** String get favorites; /// No description provided for @history. /// /// In en, this message translates to: /// **'History'** String get history; /// No description provided for @toolbox. /// /// In en, this message translates to: /// **'Toolbox'** String get toolbox; /// No description provided for @preference. /// /// In en, this message translates to: /// **'Preferences'** String get preference; /// No description provided for @feedback. /// /// In en, this message translates to: /// **'Feedback'** String get feedback; /// No description provided for @about. /// /// In en, this message translates to: /// **'About'** String get about; /// No description provided for @filter. /// /// In en, this message translates to: /// **'Proxy Filter'** String get filter; /// No description provided for @script. /// /// In en, this message translates to: /// **'Script'** String get script; /// No description provided for @share. /// /// In en, this message translates to: /// **'Share'** String get share; /// No description provided for @port. /// /// In en, this message translates to: /// **'Port: '** String get port; /// No description provided for @proxy. /// /// In en, this message translates to: /// **'Proxy'** String get proxy; /// No description provided for @externalProxy. /// /// In en, this message translates to: /// **'External Proxy'** String get externalProxy; /// No description provided for @username. /// /// In en, this message translates to: /// **'Username'** String get username; /// No description provided for @password. /// /// In en, this message translates to: /// **'Password'** String get password; /// No description provided for @proxySetting. /// /// In en, this message translates to: /// **'Proxy Setting'** String get proxySetting; /// No description provided for @setAs. /// /// In en, this message translates to: /// **'Set as '** String get setAs; /// No description provided for @systemProxy. /// /// In en, this message translates to: /// **'System Proxy'** String get systemProxy; /// No description provided for @enabledHTTP2. /// /// In en, this message translates to: /// **'Enable HTTP2'** String get enabledHTTP2; /// No description provided for @serverNotStart. /// /// In en, this message translates to: /// **'Proxy server not started'** String get serverNotStart; /// No description provided for @download. /// /// In en, this message translates to: /// **'Download'** String get download; /// No description provided for @config. /// /// In en, this message translates to: /// **'Configuration'** String get config; /// No description provided for @version. /// /// In en, this message translates to: /// **'Version'** String get version; /// No description provided for @start. /// /// In en, this message translates to: /// **'Start'** String get start; /// No description provided for @stop. /// /// In en, this message translates to: /// **'Stop'** String get stop; /// No description provided for @clear. /// /// In en, this message translates to: /// **'Clear'** String get clear; /// No description provided for @httpsProxy. /// /// In en, this message translates to: /// **'HTTPS Proxy'** String get httpsProxy; /// No description provided for @setting. /// /// In en, this message translates to: /// **'Settings'** String get setting; /// No description provided for @mobileConnect. /// /// In en, this message translates to: /// **'Mobile Connect'** String get mobileConnect; /// No description provided for @connectRemote. /// /// In en, this message translates to: /// **'Connect Remote'** String get connectRemote; /// No description provided for @remoteDevice. /// /// In en, this message translates to: /// **'Remote Device'** String get remoteDevice; /// No description provided for @remoteDeviceList. /// /// In en, this message translates to: /// **'Remote Device List'** String get remoteDeviceList; /// No description provided for @myQRCode. /// /// In en, this message translates to: /// **'My QR Code'** String get myQRCode; /// No description provided for @theme. /// /// In en, this message translates to: /// **'Theme'** String get theme; /// No description provided for @followSystem. /// /// In en, this message translates to: /// **'Follow System'** String get followSystem; /// No description provided for @themeColor. /// /// In en, this message translates to: /// **'Theme Color'** String get themeColor; /// No description provided for @themeLight. /// /// In en, this message translates to: /// **'Light'** String get themeLight; /// No description provided for @themeDark. /// /// In en, this message translates to: /// **'Dark'** String get themeDark; /// No description provided for @language. /// /// In en, this message translates to: /// **'Language'** String get language; /// No description provided for @autoStartup. /// /// In en, this message translates to: /// **'Auto Start Recording Traffic'** String get autoStartup; /// No description provided for @autoStartupDescribe. /// /// In en, this message translates to: /// **'Automatically start recording traffic when the program starts'** String get autoStartupDescribe; /// No description provided for @copied. /// /// In en, this message translates to: /// **'Copied to clipboard'** String get copied; /// No description provided for @execute. /// /// In en, this message translates to: /// **'Execute'** String get execute; /// No description provided for @cancel. /// /// In en, this message translates to: /// **'Cancel'** String get cancel; /// No description provided for @close. /// /// In en, this message translates to: /// **'Close'** String get close; /// No description provided for @save. /// /// In en, this message translates to: /// **'Save'** String get save; /// No description provided for @confirm. /// /// In en, this message translates to: /// **'Confirm'** String get confirm; /// No description provided for @confirmTitle. /// /// In en, this message translates to: /// **'Confirm operation'** String get confirmTitle; /// No description provided for @confirmContent. /// /// In en, this message translates to: /// **'Are you sure about this operation?'** String get confirmContent; /// No description provided for @addSuccess. /// /// In en, this message translates to: /// **'Successfully added'** String get addSuccess; /// No description provided for @saveSuccess. /// /// In en, this message translates to: /// **'Saved successfully'** String get saveSuccess; /// No description provided for @operationSuccess. /// /// In en, this message translates to: /// **'Operation succeeded'** String get operationSuccess; /// No description provided for @import. /// /// In en, this message translates to: /// **'Import'** String get import; /// No description provided for @importSuccess. /// /// In en, this message translates to: /// **'Import successful'** String get importSuccess; /// No description provided for @importFailed. /// /// In en, this message translates to: /// **'Import failed'** String get importFailed; /// No description provided for @export. /// /// In en, this message translates to: /// **'Export'** String get export; /// No description provided for @exportSuccess. /// /// In en, this message translates to: /// **'Export successful'** String get exportSuccess; /// No description provided for @exportFailed. /// /// In en, this message translates to: /// **'Export failed'** String get exportFailed; /// No description provided for @deleteSuccess. /// /// In en, this message translates to: /// **'Delete successful'** String get deleteSuccess; /// No description provided for @send. /// /// In en, this message translates to: /// **'Send'** String get send; /// No description provided for @fail. /// /// In en, this message translates to: /// **'fail'** String get fail; /// No description provided for @success. /// /// In en, this message translates to: /// **'success'** String get success; /// No description provided for @emptyData. /// /// In en, this message translates to: /// **'Empty Data'** String get emptyData; /// No description provided for @requestSuccess. /// /// In en, this message translates to: /// **'Request successful'** String get requestSuccess; /// No description provided for @add. /// /// In en, this message translates to: /// **'Add'** String get add; /// No description provided for @all. /// /// In en, this message translates to: /// **'All'** String get all; /// No description provided for @modify. /// /// In en, this message translates to: /// **'Modify'** String get modify; /// No description provided for @responseType. /// /// In en, this message translates to: /// **'Response Type'** String get responseType; /// No description provided for @request. /// /// In en, this message translates to: /// **'Request'** String get request; /// No description provided for @response. /// /// In en, this message translates to: /// **'Response'** String get response; /// No description provided for @statusCode. /// /// In en, this message translates to: /// **'Status code'** String get statusCode; /// No description provided for @duration. /// /// In en, this message translates to: /// **'Duration'** String get duration; /// No description provided for @done. /// /// In en, this message translates to: /// **'Done'** String get done; /// No description provided for @type. /// /// In en, this message translates to: /// **'Type'** String get type; /// No description provided for @enable. /// /// In en, this message translates to: /// **'Enable'** String get enable; /// No description provided for @example. /// /// In en, this message translates to: /// **'Example: '** String get example; /// No description provided for @responseHeader. /// /// In en, this message translates to: /// **'Headers'** String get responseHeader; /// No description provided for @requestHeader. /// /// In en, this message translates to: /// **'Headers'** String get requestHeader; /// No description provided for @requestLine. /// /// In en, this message translates to: /// **'Request Line'** String get requestLine; /// No description provided for @requestMethod. /// /// In en, this message translates to: /// **'Request Method'** String get requestMethod; /// No description provided for @param. /// /// In en, this message translates to: /// **'Param'** String get param; /// No description provided for @replaceBodyWith. /// /// In en, this message translates to: /// **'Replace Body With:'** String get replaceBodyWith; /// No description provided for @redirectTo. /// /// In en, this message translates to: /// **'Redirect To:'** String get redirectTo; /// No description provided for @redirect. /// /// In en, this message translates to: /// **'Redirect'** String get redirect; /// No description provided for @cannotBeEmpty. /// /// In en, this message translates to: /// **'Cannot be empty'** String get cannotBeEmpty; /// No description provided for @requestRewriteList. /// /// In en, this message translates to: /// **'Request Rewrite List'** String get requestRewriteList; /// No description provided for @requestRewriteRule. /// /// In en, this message translates to: /// **'Request Rewrite Rule'** String get requestRewriteRule; /// No description provided for @requestRewriteEnable. /// /// In en, this message translates to: /// **'Enable Request Rewrite'** String get requestRewriteEnable; /// No description provided for @action. /// /// In en, this message translates to: /// **'Action'** String get action; /// No description provided for @multiple. /// /// In en, this message translates to: /// **'Multiple'** String get multiple; /// No description provided for @edit. /// /// In en, this message translates to: /// **'Edit'** String get edit; /// No description provided for @disabled. /// /// In en, this message translates to: /// **'Disabled'** String get disabled; /// No description provided for @requestRewriteDeleteConfirm. /// /// In en, this message translates to: /// **'Delete {size} rule(s)?'** String requestRewriteDeleteConfirm(Object size); /// No description provided for @useGuide. /// /// In en, this message translates to: /// **'Use Guide'** String get useGuide; /// No description provided for @pleaseEnter. /// /// In en, this message translates to: /// **'Please Enter'** String get pleaseEnter; /// No description provided for @click. /// /// In en, this message translates to: /// **'Click'** String get click; /// No description provided for @replace. /// /// In en, this message translates to: /// **'Replace'** String get replace; /// No description provided for @clickEdit. /// /// In en, this message translates to: /// **'Click Edit'** String get clickEdit; /// No description provided for @refresh. /// /// In en, this message translates to: /// **'Refresh'** String get refresh; /// No description provided for @selectFile. /// /// In en, this message translates to: /// **'Select file'** String get selectFile; /// No description provided for @match. /// /// In en, this message translates to: /// **'Match'** String get match; /// No description provided for @value. /// /// In en, this message translates to: /// **'Value'** String get value; /// No description provided for @matchRule. /// /// In en, this message translates to: /// **'Match Rule'** String get matchRule; /// No description provided for @emptyMatchAll. /// /// In en, this message translates to: /// **'Empty means match all'** String get emptyMatchAll; /// No description provided for @newBuilt. /// /// In en, this message translates to: /// **'New'** String get newBuilt; /// No description provided for @reportServers. /// /// In en, this message translates to: /// **'Report Servers'** String get reportServers; /// No description provided for @addReportServer. /// /// In en, this message translates to: /// **'Add Report Server'** String get addReportServer; /// No description provided for @editReportServer. /// /// In en, this message translates to: /// **'Edit Report Server'** String get editReportServer; /// No description provided for @serverUrl. /// /// In en, this message translates to: /// **'Server URL'** String get serverUrl; /// No description provided for @compression. /// /// In en, this message translates to: /// **'Compression'** String get compression; /// No description provided for @compressionNone. /// /// In en, this message translates to: /// **'None'** String get compressionNone; /// No description provided for @newFolder. /// /// In en, this message translates to: /// **'New Folder'** String get newFolder; /// No description provided for @enableSelect. /// /// In en, this message translates to: /// **'Enable Select'** String get enableSelect; /// No description provided for @disableSelect. /// /// In en, this message translates to: /// **'Disable Select'** String get disableSelect; /// No description provided for @deleteSelect. /// /// In en, this message translates to: /// **'Delete Select'** String get deleteSelect; /// No description provided for @testData. /// /// In en, this message translates to: /// **'Test Data'** String get testData; /// No description provided for @noChangesDetected. /// /// In en, this message translates to: /// **'No changes detected'** String get noChangesDetected; /// No description provided for @enterMatchData. /// /// In en, this message translates to: /// **'Enter the data to be matched'** String get enterMatchData; /// No description provided for @modifyRequestHeader. /// /// In en, this message translates to: /// **'Modify Header'** String get modifyRequestHeader; /// No description provided for @headerName. /// /// In en, this message translates to: /// **'Header Name'** String get headerName; /// No description provided for @headerValue. /// /// In en, this message translates to: /// **'Header Value'** String get headerValue; /// No description provided for @deleteHeaderConfirm. /// /// In en, this message translates to: /// **'Do you want to delete the request header?'** String get deleteHeaderConfirm; /// No description provided for @sequence. /// /// In en, this message translates to: /// **'All Requests'** String get sequence; /// No description provided for @domainList. /// /// In en, this message translates to: /// **'Domain List'** String get domainList; /// No description provided for @domainWhitelist. /// /// In en, this message translates to: /// **'Proxy Domain Whitelist'** String get domainWhitelist; /// No description provided for @domainBlacklist. /// /// In en, this message translates to: /// **'Proxy Domain Blacklist'** String get domainBlacklist; /// No description provided for @domainFilter. /// /// In en, this message translates to: /// **'Proxy Domain List'** String get domainFilter; /// No description provided for @appWhitelist. /// /// In en, this message translates to: /// **'App Whitelist'** String get appWhitelist; /// No description provided for @appWhitelistDescribe. /// /// In en, this message translates to: /// **'Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid'** String get appWhitelistDescribe; /// No description provided for @appBlacklist. /// /// In en, this message translates to: /// **'App Blacklist'** String get appBlacklist; /// No description provided for @scanCode. /// /// In en, this message translates to: /// **'Scan Code Connect'** String get scanCode; /// No description provided for @addBlacklist. /// /// In en, this message translates to: /// **'Add Proxy Blacklist'** String get addBlacklist; /// No description provided for @addWhitelist. /// /// In en, this message translates to: /// **'Add Proxy Whitelist'** String get addWhitelist; /// No description provided for @deleteWhitelist. /// /// In en, this message translates to: /// **'Delete Proxy Whitelist'** String get deleteWhitelist; /// No description provided for @domainListSubtitle. /// /// In en, this message translates to: /// **'Last Request Time: {time}, Count: {count}'** String domainListSubtitle(Object count, Object time); /// No description provided for @selectAction. /// /// In en, this message translates to: /// **'Select action'** String get selectAction; /// No description provided for @copy. /// /// In en, this message translates to: /// **'Copy'** String get copy; /// No description provided for @copyHost. /// /// In en, this message translates to: /// **'Copy Host'** String get copyHost; /// No description provided for @copyUrl. /// /// In en, this message translates to: /// **'Copy URL'** String get copyUrl; /// No description provided for @copyRawRequest. /// /// In en, this message translates to: /// **'Copy Raw Request'** String get copyRawRequest; /// No description provided for @copyRequestResponse. /// /// In en, this message translates to: /// **'Copy Request and Response'** String get copyRequestResponse; /// No description provided for @copyCurl. /// /// In en, this message translates to: /// **'Copy cURL'** String get copyCurl; /// No description provided for @copyAsPythonRequests. /// /// In en, this message translates to: /// **'Copy as Python Requests'** String get copyAsPythonRequests; /// No description provided for @delete. /// /// In en, this message translates to: /// **'Delete'** String get delete; /// No description provided for @rename. /// /// In en, this message translates to: /// **'Rename'** String get rename; /// No description provided for @repeat. /// /// In en, this message translates to: /// **'Repeat'** String get repeat; /// No description provided for @repeatAllRequests. /// /// In en, this message translates to: /// **'Repeat All Requests'** String get repeatAllRequests; /// No description provided for @repeatDomainRequests. /// /// In en, this message translates to: /// **'Repeat Domain Requests'** String get repeatDomainRequests; /// No description provided for @customRepeat. /// /// In en, this message translates to: /// **'Custom Repeat'** String get customRepeat; /// No description provided for @repeatCount. /// /// In en, this message translates to: /// **'Iterations'** String get repeatCount; /// No description provided for @repeatInterval. /// /// In en, this message translates to: /// **'Interval(ms)'** String get repeatInterval; /// No description provided for @repeatDelay. /// /// In en, this message translates to: /// **'Delay(ms)'** String get repeatDelay; /// No description provided for @scheduleTime. /// /// In en, this message translates to: /// **'Schedule Time'** String get scheduleTime; /// No description provided for @fixed. /// /// In en, this message translates to: /// **'fixed'** String get fixed; /// No description provided for @random. /// /// In en, this message translates to: /// **'random'** String get random; /// No description provided for @keepCustomSettings. /// /// In en, this message translates to: /// **'Keep custom settings'** String get keepCustomSettings; /// No description provided for @editRequest. /// /// In en, this message translates to: /// **'Edit and Request'** String get editRequest; /// No description provided for @reSendRequest. /// /// In en, this message translates to: /// **'The request has been resent'** String get reSendRequest; /// No description provided for @viewExport. /// /// In en, this message translates to: /// **'View Export'** String get viewExport; /// No description provided for @timeDesc. /// /// In en, this message translates to: /// **'Descending by time'** String get timeDesc; /// No description provided for @timeAsc. /// /// In en, this message translates to: /// **'Ascending by time'** String get timeAsc; /// No description provided for @search. /// /// In en, this message translates to: /// **'Search'** String get search; /// No description provided for @clearSearch. /// /// In en, this message translates to: /// **'Clear Search'** String get clearSearch; /// No description provided for @requestType. /// /// In en, this message translates to: /// **'Request type'** String get requestType; /// No description provided for @keyword. /// /// In en, this message translates to: /// **'Keyword'** String get keyword; /// No description provided for @keywordSearchScope. /// /// In en, this message translates to: /// **'Keyword search scope: '** String get keywordSearchScope; /// No description provided for @favorite. /// /// In en, this message translates to: /// **'Favorite'** String get favorite; /// No description provided for @deleteFavorite. /// /// In en, this message translates to: /// **'Delete Favorite'** String get deleteFavorite; /// No description provided for @emptyFavorite. /// /// In en, this message translates to: /// **'Empty Favorite'** String get emptyFavorite; /// No description provided for @deleteFavoriteSuccess. /// /// In en, this message translates to: /// **'Favorite deleted'** String get deleteFavoriteSuccess; /// No description provided for @historyRecord. /// /// In en, this message translates to: /// **'History'** String get historyRecord; /// No description provided for @historyCacheTime. /// /// In en, this message translates to: /// **'Cache Time'** String get historyCacheTime; /// No description provided for @historyManualSave. /// /// In en, this message translates to: /// **'Manual Save'** String get historyManualSave; /// No description provided for @historyDay. /// /// In en, this message translates to: /// **'{day} days'** String historyDay(Object day); /// No description provided for @historyForever. /// /// In en, this message translates to: /// **'Forever'** String get historyForever; /// No description provided for @historyRecordTitle. /// /// In en, this message translates to: /// **'{name} Records {length}'** String historyRecordTitle(Object length, Object name); /// No description provided for @historyEmptyName. /// /// In en, this message translates to: /// **'Name cannot be empty'** String get historyEmptyName; /// No description provided for @historySubtitle. /// /// In en, this message translates to: /// **'Records {requestLength} file {size}'** String historySubtitle(Object requestLength, Object size); /// No description provided for @historyUnSave. /// /// In en, this message translates to: /// **'Current record is not saved'** String get historyUnSave; /// No description provided for @historyDeleteConfirm. /// /// In en, this message translates to: /// **'Do you want to delete this history?'** String get historyDeleteConfirm; /// No description provided for @requestEdit. /// /// In en, this message translates to: /// **'Request Editing'** String get requestEdit; /// No description provided for @encode. /// /// In en, this message translates to: /// **'Encode'** String get encode; /// No description provided for @requestBody. /// /// In en, this message translates to: /// **'Request Body'** String get requestBody; /// No description provided for @responseBody. /// /// In en, this message translates to: /// **'Response Body'** String get responseBody; /// No description provided for @requestRewrite. /// /// In en, this message translates to: /// **'Request Rewrite'** String get requestRewrite; /// No description provided for @newWindow. /// /// In en, this message translates to: /// **'New Window'** String get newWindow; /// No description provided for @httpRequest. /// /// In en, this message translates to: /// **'HTTP Request'** String get httpRequest; /// No description provided for @enabledHttps. /// /// In en, this message translates to: /// **'Enable HTTPS Proxy'** String get enabledHttps; /// No description provided for @installRootCa. /// /// In en, this message translates to: /// **'Install Certificate'** String get installRootCa; /// No description provided for @installCaLocal. /// /// In en, this message translates to: /// **'Install Certificate to Local-Machine'** String get installCaLocal; /// No description provided for @downloadRootCa. /// /// In en, this message translates to: /// **'Download Certificate'** String get downloadRootCa; /// No description provided for @downloadRootCaNote. /// /// In en, this message translates to: /// **'Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser'** String get downloadRootCaNote; /// No description provided for @generateCA. /// /// In en, this message translates to: /// **'Generate new root certificate'** String get generateCA; /// No description provided for @generateCADescribe. /// /// In en, this message translates to: /// **'Are you sure you want to generate a new root certificate? If confirmed,\nYou need to reinstall and trust the new certificate'** String get generateCADescribe; /// No description provided for @resetDefaultCA. /// /// In en, this message translates to: /// **'Reset Default Root Certificate'** String get resetDefaultCA; /// No description provided for @resetDefaultCADescribe. /// /// In en, this message translates to: /// **'Are you sure you want to reset to the default root certificate?\nProxyPin default root certificate is the same for all users.'** String get resetDefaultCADescribe; /// No description provided for @exportCaP12. /// /// In en, this message translates to: /// **'Export Root Certificate(.p12)'** String get exportCaP12; /// No description provided for @importCaP12. /// /// In en, this message translates to: /// **'Import Root Certificate(.p12)'** String get importCaP12; /// No description provided for @trustCa. /// /// In en, this message translates to: /// **'Trust Certificate'** String get trustCa; /// No description provided for @profileDownload. /// /// In en, this message translates to: /// **'Profile Download'** String get profileDownload; /// No description provided for @exportCA. /// /// In en, this message translates to: /// **'Export Root Certificate'** String get exportCA; /// No description provided for @exportPrivateKey. /// /// In en, this message translates to: /// **'Export Private Key'** String get exportPrivateKey; /// No description provided for @install. /// /// In en, this message translates to: /// **'Install'** String get install; /// No description provided for @installCaDescribe. /// /// In en, this message translates to: /// **'Install CA Setting > Profile Download > Install'** String get installCaDescribe; /// No description provided for @trustCaDescribe. /// /// In en, this message translates to: /// **'Trust CA Setting > General > About > Certificate Trust Setting'** String get trustCaDescribe; /// No description provided for @androidRoot. /// /// In en, this message translates to: /// **'System Certificate (ROOT Device)'** String get androidRoot; /// No description provided for @androidRootMagisk. /// /// In en, this message translates to: /// **'Magisk module: \nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。'** String get androidRootMagisk; /// No description provided for @androidRootRename. /// /// In en, this message translates to: /// **'If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate {name}'** String androidRootRename(Object name); /// No description provided for @androidRootCADownload. /// /// In en, this message translates to: /// **'Download System Certificate(.0)'** String get androidRootCADownload; /// No description provided for @androidUserCA. /// /// In en, this message translates to: /// **'User Certificate'** String get androidUserCA; /// No description provided for @androidUserCATips. /// /// In en, this message translates to: /// **'Tips: Android7+ many apps will not trust user certificates'** String get androidUserCATips; /// No description provided for @androidUserCAInstall. /// /// In en, this message translates to: /// **'Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate'** String get androidUserCAInstall; /// No description provided for @androidUserXposed. /// /// In en, this message translates to: /// **'It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki'** String get androidUserXposed; /// No description provided for @configWifiProxy. /// /// In en, this message translates to: /// **'Configure mobile Wi-Fi proxy'** String get configWifiProxy; /// No description provided for @caInstallGuide. /// /// In en, this message translates to: /// **'Certificate Installation Guide'** String get caInstallGuide; /// No description provided for @caAndroidBrowser. /// /// In en, this message translates to: /// **'Open Google Browser on Android devices:'** String get caAndroidBrowser; /// No description provided for @caIosBrowser. /// /// In en, this message translates to: /// **'Open Safari on iOS devices:'** String get caIosBrowser; /// No description provided for @localIP. /// /// In en, this message translates to: /// **'Local IP '** String get localIP; /// No description provided for @mobileScan. /// /// In en, this message translates to: /// **'Configure Wi-Fi proxy or Scan with Mobile App'** String get mobileScan; /// No description provided for @decode. /// /// In en, this message translates to: /// **'Decode'** String get decode; /// No description provided for @encodeInput. /// /// In en, this message translates to: /// **'Enter the content to be converted'** String get encodeInput; /// No description provided for @encodeResult. /// /// In en, this message translates to: /// **'Conversion Result'** String get encodeResult; /// No description provided for @encodeFail. /// /// In en, this message translates to: /// **'Encoding failed'** String get encodeFail; /// No description provided for @decodeFail. /// /// In en, this message translates to: /// **'Decoding failed'** String get decodeFail; /// No description provided for @shareUrl. /// /// In en, this message translates to: /// **'Share Request URL'** String get shareUrl; /// No description provided for @shareCurl. /// /// In en, this message translates to: /// **'Share cURL Request'** String get shareCurl; /// No description provided for @shareRequestResponse. /// /// In en, this message translates to: /// **'Share Request and Response'** String get shareRequestResponse; /// No description provided for @captureDetail. /// /// In en, this message translates to: /// **'Capture Detail'** String get captureDetail; /// No description provided for @proxyPinSoftware. /// /// In en, this message translates to: /// **'ProxyPin Open source traffic capture software for all platforms'** String get proxyPinSoftware; /// No description provided for @prompt. /// /// In en, this message translates to: /// **'Prompt'** String get prompt; /// No description provided for @curlSchemeRequest. /// /// In en, this message translates to: /// **'If the curl format is recognized, should it be converted into an HTTP request?'** String get curlSchemeRequest; /// No description provided for @appExitTips. /// /// In en, this message translates to: /// **'Press again to exit the program'** String get appExitTips; /// No description provided for @remoteConnectDisconnect. /// /// In en, this message translates to: /// **'Check remote connection failed, disconnected'** String get remoteConnectDisconnect; /// No description provided for @connect. /// /// In en, this message translates to: /// **'Connect'** String get connect; /// No description provided for @reconnect. /// /// In en, this message translates to: /// **'Reconnect'** String get reconnect; /// No description provided for @remoteConnected. /// /// In en, this message translates to: /// **'Connected {os}, traffic will be forwarded to {os}'** String remoteConnected(Object os); /// No description provided for @remoteConnectForward. /// /// In en, this message translates to: /// **'Remote connection, forwarding requests to other terminals'** String get remoteConnectForward; /// No description provided for @connectSuccess. /// /// In en, this message translates to: /// **'Connect successful'** String get connectSuccess; /// No description provided for @connectedRemote. /// /// In en, this message translates to: /// **'Connected to remote'** String get connectedRemote; /// No description provided for @connected. /// /// In en, this message translates to: /// **'Connected'** String get connected; /// No description provided for @notConnected. /// /// In en, this message translates to: /// **'Not connected'** String get notConnected; /// No description provided for @disconnect. /// /// In en, this message translates to: /// **'Disconnect'** String get disconnect; /// No description provided for @ipLayerProxy. /// /// In en, this message translates to: /// **'IP Layer Proxy(Beta)'** String get ipLayerProxy; /// No description provided for @ipLayerProxyDesc. /// /// In en, this message translates to: /// **'IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR'** String get ipLayerProxyDesc; /// No description provided for @inputAddress. /// /// In en, this message translates to: /// **'Input Address'** String get inputAddress; /// No description provided for @syncConfig. /// /// In en, this message translates to: /// **'Sync configuration'** String get syncConfig; /// No description provided for @pullConfigFail. /// /// In en, this message translates to: /// **'Failed to pull configuration, please check the network connection'** String get pullConfigFail; /// No description provided for @sync. /// /// In en, this message translates to: /// **'Sync'** String get sync; /// No description provided for @invalidQRCode. /// /// In en, this message translates to: /// **'Unrecognized QR code'** String get invalidQRCode; /// No description provided for @remoteConnectFail. /// /// In en, this message translates to: /// **'Connection failed,Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions'** String get remoteConnectFail; /// No description provided for @remoteConnectSuccessTips. /// /// In en, this message translates to: /// **'Your phone needs to enable packet capture in order to capture requests'** String get remoteConnectSuccessTips; /// No description provided for @windowMode. /// /// In en, this message translates to: /// **'Window Mode'** String get windowMode; /// No description provided for @windowModeSubTitle. /// /// In en, this message translates to: /// **'Enabled Packet Capture, Enter the background, Display a small window'** String get windowModeSubTitle; /// No description provided for @pipIcon. /// /// In en, this message translates to: /// **'Window shortcut icon'** String get pipIcon; /// No description provided for @pipIconDescribe. /// /// In en, this message translates to: /// **'Show quick access to small window Icon'** String get pipIconDescribe; /// No description provided for @headerExpanded. /// /// In en, this message translates to: /// **'Headers Expanded'** String get headerExpanded; /// No description provided for @headerExpandedSubtitle. /// /// In en, this message translates to: /// **'Details page Headers is expanded by default'** String get headerExpandedSubtitle; /// No description provided for @bottomNavigation. /// /// In en, this message translates to: /// **'Bottom Navigation'** String get bottomNavigation; /// No description provided for @bottomNavigationSubtitle. /// /// In en, this message translates to: /// **'Bottom navigation bar is displayed, effective after restart'** String get bottomNavigationSubtitle; /// No description provided for @memoryCleanup. /// /// In en, this message translates to: /// **'Memory Cleanup'** String get memoryCleanup; /// No description provided for @memoryCleanupSubtitle. /// /// In en, this message translates to: /// **'Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning'** String get memoryCleanupSubtitle; /// No description provided for @unlimited. /// /// In en, this message translates to: /// **'Unlimited'** String get unlimited; /// No description provided for @custom. /// /// In en, this message translates to: /// **'Custom'** String get custom; /// No description provided for @externalProxyAuth. /// /// In en, this message translates to: /// **'Proxy Auth (Optional)'** String get externalProxyAuth; /// No description provided for @externalProxyServer. /// /// In en, this message translates to: /// **'Proxy Server'** String get externalProxyServer; /// No description provided for @externalProxyConnectFailure. /// /// In en, this message translates to: /// **'External Proxy Connect failure'** String get externalProxyConnectFailure; /// No description provided for @externalProxyFailureConfirm. /// /// In en, this message translates to: /// **'Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。'** String get externalProxyFailureConfirm; /// No description provided for @mobileDisplayPacketCapture. /// /// In en, this message translates to: /// **'Mobile Display Packet Capture:'** String get mobileDisplayPacketCapture; /// No description provided for @proxyPortRepeat. /// /// In en, this message translates to: /// **'Startup failed, please check the port number {port} is occupied。'** String proxyPortRepeat(Object port); /// No description provided for @reset. /// /// In en, this message translates to: /// **'Reset'** String get reset; /// No description provided for @proxyIgnoreDomain. /// /// In en, this message translates to: /// **'Proxy ignores domain'** String get proxyIgnoreDomain; /// No description provided for @domainWhitelistDescribe. /// /// In en, this message translates to: /// **'Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid'** String get domainWhitelistDescribe; /// No description provided for @domainBlacklistDescribe. /// /// In en, this message translates to: /// **'Domain names on the blacklist will not be proxied'** String get domainBlacklistDescribe; /// No description provided for @domain. /// /// In en, this message translates to: /// **'Host'** String get domain; /// No description provided for @enableScript. /// /// In en, this message translates to: /// **'Enable Script'** String get enableScript; /// No description provided for @scriptUseDescribe. /// /// In en, this message translates to: /// **'Use JavaScript to modify requests and responses'** String get scriptUseDescribe; /// No description provided for @scriptEdit. /// /// In en, this message translates to: /// **'Edit script'** String get scriptEdit; /// No description provided for @scrollEnd. /// /// In en, this message translates to: /// **'Scroll to End'** String get scrollEnd; /// No description provided for @logger. /// /// In en, this message translates to: /// **'Log'** String get logger; /// No description provided for @material3. /// /// In en, this message translates to: /// **'Material 3 is the latest version of Google’s open-source design system'** String get material3; /// No description provided for @iosVpnBackgroundAudio. /// /// In en, this message translates to: /// **'After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?'** String get iosVpnBackgroundAudio; /// No description provided for @markRead. /// /// In en, this message translates to: /// **'Mark as read'** String get markRead; /// No description provided for @autoRead. /// /// In en, this message translates to: /// **'Auto read'** String get autoRead; /// No description provided for @highlight. /// /// In en, this message translates to: /// **'Highlight'** String get highlight; /// No description provided for @blue. /// /// In en, this message translates to: /// **'Blue'** String get blue; /// No description provided for @green. /// /// In en, this message translates to: /// **'Green'** String get green; /// No description provided for @yellow. /// /// In en, this message translates to: /// **'Yellow'** String get yellow; /// No description provided for @red. /// /// In en, this message translates to: /// **'Red'** String get red; /// No description provided for @pink. /// /// In en, this message translates to: /// **'Pink'** String get pink; /// No description provided for @gray. /// /// In en, this message translates to: /// **'Gray'** String get gray; /// No description provided for @underline. /// /// In en, this message translates to: /// **'Underline'** String get underline; /// No description provided for @requestBlock. /// /// In en, this message translates to: /// **'Request Block'** String get requestBlock; /// No description provided for @other. /// /// In en, this message translates to: /// **'Other'** String get other; /// No description provided for @certHashName. /// /// In en, this message translates to: /// **'CA Hash Name'** String get certHashName; /// No description provided for @regExp. /// /// In en, this message translates to: /// **'RegExp'** String get regExp; /// No description provided for @systemCertName. /// /// In en, this message translates to: /// **'System Certificate Name'** String get systemCertName; /// No description provided for @qrCode. /// /// In en, this message translates to: /// **'QR Code'** String get qrCode; /// No description provided for @scanQrCode. /// /// In en, this message translates to: /// **'Scan QR Code'** String get scanQrCode; /// No description provided for @generateQrCode. /// /// In en, this message translates to: /// **'Generate'** String get generateQrCode; /// No description provided for @saveImage. /// /// In en, this message translates to: /// **'Save Image'** String get saveImage; /// No description provided for @selectImage. /// /// In en, this message translates to: /// **'Select Image'** String get selectImage; /// No description provided for @inputContent. /// /// In en, this message translates to: /// **'Input Content'** String get inputContent; /// No description provided for @errorCorrectLevel. /// /// In en, this message translates to: /// **'Error Correct'** String get errorCorrectLevel; /// No description provided for @output. /// /// In en, this message translates to: /// **'Output'** String get output; /// No description provided for @timestamp. /// /// In en, this message translates to: /// **'Timestamp'** String get timestamp; /// No description provided for @convert. /// /// In en, this message translates to: /// **'Convert'** String get convert; /// No description provided for @time. /// /// In en, this message translates to: /// **'DateTime'** String get time; /// No description provided for @nowTimestamp. /// /// In en, this message translates to: /// **'Now timestamp'** String get nowTimestamp; /// No description provided for @hosts. /// /// In en, this message translates to: /// **'Hosts'** String get hosts; /// No description provided for @toAddress. /// /// In en, this message translates to: /// **'To Address'** String get toAddress; /// No description provided for @encrypt. /// /// In en, this message translates to: /// **'Encrypt'** String get encrypt; /// No description provided for @decrypt. /// /// In en, this message translates to: /// **'Decrypt'** String get decrypt; /// No description provided for @cipher. /// /// In en, this message translates to: /// **'Cipher'** String get cipher; /// No description provided for @appUpdateCheckVersion. /// /// In en, this message translates to: /// **'Check for Updates'** String get appUpdateCheckVersion; /// No description provided for @appUpdateNotAvailableMsg. /// /// In en, this message translates to: /// **'Already Using The Latest Version'** String get appUpdateNotAvailableMsg; /// No description provided for @appUpdateDialogTitle. /// /// In en, this message translates to: /// **'Update Available'** String get appUpdateDialogTitle; /// No description provided for @appUpdateUpdateMsg. /// /// In en, this message translates to: /// **'A new version of ProxyPin is available. Would you like to update now?'** String get appUpdateUpdateMsg; /// No description provided for @appUpdateCurrentVersionLbl. /// /// In en, this message translates to: /// **'Current Version'** String get appUpdateCurrentVersionLbl; /// No description provided for @appUpdateNewVersionLbl. /// /// In en, this message translates to: /// **'New Version'** String get appUpdateNewVersionLbl; /// No description provided for @appUpdateUpdateNowBtnTxt. /// /// In en, this message translates to: /// **'Update Now'** String get appUpdateUpdateNowBtnTxt; /// No description provided for @appUpdateLaterBtnTxt. /// /// In en, this message translates to: /// **'Later'** String get appUpdateLaterBtnTxt; /// No description provided for @appUpdateIgnoreBtnTxt. /// /// In en, this message translates to: /// **'Ignore'** String get appUpdateIgnoreBtnTxt; /// No description provided for @requestMap. /// /// In en, this message translates to: /// **'Request Map'** String get requestMap; /// No description provided for @requestMapDescribe. /// /// In en, this message translates to: /// **'Do not request remote services, use local configuration or script for response'** String get requestMapDescribe; /// No description provided for @automatic. /// /// In en, this message translates to: /// **'Automatic'** String get automatic; /// No description provided for @manual. /// /// In en, this message translates to: /// **'Manual'** String get manual; /// No description provided for @certNotInstalled. /// /// In en, this message translates to: /// **'Certificate not installed'** String get certNotInstalled; /// No description provided for @openNewWindow. /// /// In en, this message translates to: /// **'Open New Window'** String get openNewWindow; /// No description provided for @sponsorDonate. /// /// In en, this message translates to: /// **'Sponsor / Donate'** String get sponsorDonate; /// No description provided for @sponsorSupport. /// /// In en, this message translates to: /// **'Support ongoing development'** String get sponsorSupport; /// No description provided for @sponsorThanks. /// /// In en, this message translates to: /// **'Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.'** String get sponsorThanks; /// No description provided for @sponsorAfdian. /// /// In en, this message translates to: /// **'AFDIAN'** String get sponsorAfdian; /// No description provided for @sponsorBuyMeCoffee. /// /// In en, this message translates to: /// **'Buy Me a Coffee'** String get sponsorBuyMeCoffee; /// No description provided for @privacyPolicy. /// /// In en, this message translates to: /// **'Privacy Policy'** String get privacyPolicy; /// No description provided for @privacyContent. /// /// In en, this message translates to: /// **'This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.'** String get privacyContent; /// No description provided for @requestCrypto. /// /// In en, this message translates to: /// **'Request Crypto'** String get requestCrypto; /// No description provided for @cryptoDecoded. /// /// In en, this message translates to: /// **'Decoded'** String get cryptoDecoded; /// No description provided for @cryptoDecodeToggle. /// /// In en, this message translates to: /// **'Decrypt'** String get cryptoDecodeToggle; /// No description provided for @optional. /// /// In en, this message translates to: /// **'Optional'** String get optional; /// No description provided for @cryptoRuleField. /// /// In en, this message translates to: /// **'Field Name'** String get cryptoRuleField; /// No description provided for @cryptoIvPrefixLabel. /// /// In en, this message translates to: /// **'IV Prefix'** String get cryptoIvPrefixLabel; /// No description provided for @cryptoIvPrefixTooltip. /// /// In en, this message translates to: /// **'Use the first N bytes of the response body as IV'** String get cryptoIvPrefixTooltip; /// No description provided for @local. /// /// In en, this message translates to: /// **'Local'** String get local; /// No description provided for @remoteUrl. /// /// In en, this message translates to: /// **'Remote URL'** String get remoteUrl; /// No description provided for @view. /// /// In en, this message translates to: /// **'View'** String get view; } class _AppLocalizationsDelegate extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override Future load(Locale locale) { return SynchronousFuture(lookupAppLocalizations(locale)); } @override bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { // Lookup logic when language+script codes are specified. switch (locale.languageCode) { case 'zh': { switch (locale.scriptCode) { case 'Hant': return AppLocalizationsZhHant(); } break; } } // Lookup logic when only language code is specified. switch (locale.languageCode) { case 'en': return AppLocalizationsEn(); case 'zh': return AppLocalizationsZh(); } throw FlutterError('AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 'that was used.'); } ================================================ FILE: lib/l10n/app_localizations_en.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for English (`en`). class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override String get breakpoint => 'Breakpoint'; @override String get breakpointRule => 'Breakpoint Rule'; @override String get name => 'Name'; @override String get requests => 'Requests'; @override String get favorites => 'Favorites'; @override String get history => 'History'; @override String get toolbox => 'Toolbox'; @override String get preference => 'Preferences'; @override String get feedback => 'Feedback'; @override String get about => 'About'; @override String get filter => 'Proxy Filter'; @override String get script => 'Script'; @override String get share => 'Share'; @override String get port => 'Port: '; @override String get proxy => 'Proxy'; @override String get externalProxy => 'External Proxy'; @override String get username => 'Username'; @override String get password => 'Password'; @override String get proxySetting => 'Proxy Setting'; @override String get setAs => 'Set as '; @override String get systemProxy => 'System Proxy'; @override String get enabledHTTP2 => 'Enable HTTP2'; @override String get serverNotStart => 'Proxy server not started'; @override String get download => 'Download'; @override String get config => 'Configuration'; @override String get version => 'Version'; @override String get start => 'Start'; @override String get stop => 'Stop'; @override String get clear => 'Clear'; @override String get httpsProxy => 'HTTPS Proxy'; @override String get setting => 'Settings'; @override String get mobileConnect => 'Mobile Connect'; @override String get connectRemote => 'Connect Remote'; @override String get remoteDevice => 'Remote Device'; @override String get remoteDeviceList => 'Remote Device List'; @override String get myQRCode => 'My QR Code'; @override String get theme => 'Theme'; @override String get followSystem => 'Follow System'; @override String get themeColor => 'Theme Color'; @override String get themeLight => 'Light'; @override String get themeDark => 'Dark'; @override String get language => 'Language'; @override String get autoStartup => 'Auto Start Recording Traffic'; @override String get autoStartupDescribe => 'Automatically start recording traffic when the program starts'; @override String get copied => 'Copied to clipboard'; @override String get execute => 'Execute'; @override String get cancel => 'Cancel'; @override String get close => 'Close'; @override String get save => 'Save'; @override String get confirm => 'Confirm'; @override String get confirmTitle => 'Confirm operation'; @override String get confirmContent => 'Are you sure about this operation?'; @override String get addSuccess => 'Successfully added'; @override String get saveSuccess => 'Saved successfully'; @override String get operationSuccess => 'Operation succeeded'; @override String get import => 'Import'; @override String get importSuccess => 'Import successful'; @override String get importFailed => 'Import failed'; @override String get export => 'Export'; @override String get exportSuccess => 'Export successful'; @override String get exportFailed => 'Export failed'; @override String get deleteSuccess => 'Delete successful'; @override String get send => 'Send'; @override String get fail => 'fail'; @override String get success => 'success'; @override String get emptyData => 'Empty Data'; @override String get requestSuccess => 'Request successful'; @override String get add => 'Add'; @override String get all => 'All'; @override String get modify => 'Modify'; @override String get responseType => 'Response Type'; @override String get request => 'Request'; @override String get response => 'Response'; @override String get statusCode => 'Status code'; @override String get duration => 'Duration'; @override String get done => 'Done'; @override String get type => 'Type'; @override String get enable => 'Enable'; @override String get example => 'Example: '; @override String get responseHeader => 'Headers'; @override String get requestHeader => 'Headers'; @override String get requestLine => 'Request Line'; @override String get requestMethod => 'Request Method'; @override String get param => 'Param'; @override String get replaceBodyWith => 'Replace Body With:'; @override String get redirectTo => 'Redirect To:'; @override String get redirect => 'Redirect'; @override String get cannotBeEmpty => 'Cannot be empty'; @override String get requestRewriteList => 'Request Rewrite List'; @override String get requestRewriteRule => 'Request Rewrite Rule'; @override String get requestRewriteEnable => 'Enable Request Rewrite'; @override String get action => 'Action'; @override String get multiple => 'Multiple'; @override String get edit => 'Edit'; @override String get disabled => 'Disabled'; @override String requestRewriteDeleteConfirm(Object size) { return 'Delete $size rule(s)?'; } @override String get useGuide => 'Use Guide'; @override String get pleaseEnter => 'Please Enter'; @override String get click => 'Click'; @override String get replace => 'Replace'; @override String get clickEdit => 'Click Edit'; @override String get refresh => 'Refresh'; @override String get selectFile => 'Select file'; @override String get match => 'Match'; @override String get value => 'Value'; @override String get matchRule => 'Match Rule'; @override String get emptyMatchAll => 'Empty means match all'; @override String get newBuilt => 'New'; @override String get reportServers => 'Report Servers'; @override String get addReportServer => 'Add Report Server'; @override String get editReportServer => 'Edit Report Server'; @override String get serverUrl => 'Server URL'; @override String get compression => 'Compression'; @override String get compressionNone => 'None'; @override String get newFolder => 'New Folder'; @override String get enableSelect => 'Enable Select'; @override String get disableSelect => 'Disable Select'; @override String get deleteSelect => 'Delete Select'; @override String get testData => 'Test Data'; @override String get noChangesDetected => 'No changes detected'; @override String get enterMatchData => 'Enter the data to be matched'; @override String get modifyRequestHeader => 'Modify Header'; @override String get headerName => 'Header Name'; @override String get headerValue => 'Header Value'; @override String get deleteHeaderConfirm => 'Do you want to delete the request header?'; @override String get sequence => 'All Requests'; @override String get domainList => 'Domain List'; @override String get domainWhitelist => 'Proxy Domain Whitelist'; @override String get domainBlacklist => 'Proxy Domain Blacklist'; @override String get domainFilter => 'Proxy Domain List'; @override String get appWhitelist => 'App Whitelist'; @override String get appWhitelistDescribe => 'Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid'; @override String get appBlacklist => 'App Blacklist'; @override String get scanCode => 'Scan Code Connect'; @override String get addBlacklist => 'Add Proxy Blacklist'; @override String get addWhitelist => 'Add Proxy Whitelist'; @override String get deleteWhitelist => 'Delete Proxy Whitelist'; @override String domainListSubtitle(Object count, Object time) { return 'Last Request Time: $time, Count: $count'; } @override String get selectAction => 'Select action'; @override String get copy => 'Copy'; @override String get copyHost => 'Copy Host'; @override String get copyUrl => 'Copy URL'; @override String get copyRawRequest => 'Copy Raw Request'; @override String get copyRequestResponse => 'Copy Request and Response'; @override String get copyCurl => 'Copy cURL'; @override String get copyAsPythonRequests => 'Copy as Python Requests'; @override String get delete => 'Delete'; @override String get rename => 'Rename'; @override String get repeat => 'Repeat'; @override String get repeatAllRequests => 'Repeat All Requests'; @override String get repeatDomainRequests => 'Repeat Domain Requests'; @override String get customRepeat => 'Custom Repeat'; @override String get repeatCount => 'Iterations'; @override String get repeatInterval => 'Interval(ms)'; @override String get repeatDelay => 'Delay(ms)'; @override String get scheduleTime => 'Schedule Time'; @override String get fixed => 'fixed'; @override String get random => 'random'; @override String get keepCustomSettings => 'Keep custom settings'; @override String get editRequest => 'Edit and Request'; @override String get reSendRequest => 'The request has been resent'; @override String get viewExport => 'View Export'; @override String get timeDesc => 'Descending by time'; @override String get timeAsc => 'Ascending by time'; @override String get search => 'Search'; @override String get clearSearch => 'Clear Search'; @override String get requestType => 'Request type'; @override String get keyword => 'Keyword'; @override String get keywordSearchScope => 'Keyword search scope: '; @override String get favorite => 'Favorite'; @override String get deleteFavorite => 'Delete Favorite'; @override String get emptyFavorite => 'Empty Favorite'; @override String get deleteFavoriteSuccess => 'Favorite deleted'; @override String get historyRecord => 'History'; @override String get historyCacheTime => 'Cache Time'; @override String get historyManualSave => 'Manual Save'; @override String historyDay(Object day) { return '$day days'; } @override String get historyForever => 'Forever'; @override String historyRecordTitle(Object length, Object name) { return '$name Records $length'; } @override String get historyEmptyName => 'Name cannot be empty'; @override String historySubtitle(Object requestLength, Object size) { return 'Records $requestLength file $size'; } @override String get historyUnSave => 'Current record is not saved'; @override String get historyDeleteConfirm => 'Do you want to delete this history?'; @override String get requestEdit => 'Request Editing'; @override String get encode => 'Encode'; @override String get requestBody => 'Request Body'; @override String get responseBody => 'Response Body'; @override String get requestRewrite => 'Request Rewrite'; @override String get newWindow => 'New Window'; @override String get httpRequest => 'HTTP Request'; @override String get enabledHttps => 'Enable HTTPS Proxy'; @override String get installRootCa => 'Install Certificate'; @override String get installCaLocal => 'Install Certificate to Local-Machine'; @override String get downloadRootCa => 'Download Certificate'; @override String get downloadRootCaNote => 'Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser'; @override String get generateCA => 'Generate new root certificate'; @override String get generateCADescribe => 'Are you sure you want to generate a new root certificate? If confirmed,\nYou need to reinstall and trust the new certificate'; @override String get resetDefaultCA => 'Reset Default Root Certificate'; @override String get resetDefaultCADescribe => 'Are you sure you want to reset to the default root certificate?\nProxyPin default root certificate is the same for all users.'; @override String get exportCaP12 => 'Export Root Certificate(.p12)'; @override String get importCaP12 => 'Import Root Certificate(.p12)'; @override String get trustCa => 'Trust Certificate'; @override String get profileDownload => 'Profile Download'; @override String get exportCA => 'Export Root Certificate'; @override String get exportPrivateKey => 'Export Private Key'; @override String get install => 'Install'; @override String get installCaDescribe => 'Install CA Setting > Profile Download > Install'; @override String get trustCaDescribe => 'Trust CA Setting > General > About > Certificate Trust Setting'; @override String get androidRoot => 'System Certificate (ROOT Device)'; @override String get androidRootMagisk => 'Magisk module: \nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。'; @override String androidRootRename(Object name) { return 'If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate $name'; } @override String get androidRootCADownload => 'Download System Certificate(.0)'; @override String get androidUserCA => 'User Certificate'; @override String get androidUserCATips => 'Tips: Android7+ many apps will not trust user certificates'; @override String get androidUserCAInstall => 'Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate'; @override String get androidUserXposed => 'It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki'; @override String get configWifiProxy => 'Configure mobile Wi-Fi proxy'; @override String get caInstallGuide => 'Certificate Installation Guide'; @override String get caAndroidBrowser => 'Open Google Browser on Android devices:'; @override String get caIosBrowser => 'Open Safari on iOS devices:'; @override String get localIP => 'Local IP '; @override String get mobileScan => 'Configure Wi-Fi proxy or Scan with Mobile App'; @override String get decode => 'Decode'; @override String get encodeInput => 'Enter the content to be converted'; @override String get encodeResult => 'Conversion Result'; @override String get encodeFail => 'Encoding failed'; @override String get decodeFail => 'Decoding failed'; @override String get shareUrl => 'Share Request URL'; @override String get shareCurl => 'Share cURL Request'; @override String get shareRequestResponse => 'Share Request and Response'; @override String get captureDetail => 'Capture Detail'; @override String get proxyPinSoftware => 'ProxyPin Open source traffic capture software for all platforms'; @override String get prompt => 'Prompt'; @override String get curlSchemeRequest => 'If the curl format is recognized, should it be converted into an HTTP request?'; @override String get appExitTips => 'Press again to exit the program'; @override String get remoteConnectDisconnect => 'Check remote connection failed, disconnected'; @override String get connect => 'Connect'; @override String get reconnect => 'Reconnect'; @override String remoteConnected(Object os) { return 'Connected $os, traffic will be forwarded to $os'; } @override String get remoteConnectForward => 'Remote connection, forwarding requests to other terminals'; @override String get connectSuccess => 'Connect successful'; @override String get connectedRemote => 'Connected to remote'; @override String get connected => 'Connected'; @override String get notConnected => 'Not connected'; @override String get disconnect => 'Disconnect'; @override String get ipLayerProxy => 'IP Layer Proxy(Beta)'; @override String get ipLayerProxyDesc => 'IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR'; @override String get inputAddress => 'Input Address'; @override String get syncConfig => 'Sync configuration'; @override String get pullConfigFail => 'Failed to pull configuration, please check the network connection'; @override String get sync => 'Sync'; @override String get invalidQRCode => 'Unrecognized QR code'; @override String get remoteConnectFail => 'Connection failed,Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions'; @override String get remoteConnectSuccessTips => 'Your phone needs to enable packet capture in order to capture requests'; @override String get windowMode => 'Window Mode'; @override String get windowModeSubTitle => 'Enabled Packet Capture, Enter the background, Display a small window'; @override String get pipIcon => 'Window shortcut icon'; @override String get pipIconDescribe => 'Show quick access to small window Icon'; @override String get headerExpanded => 'Headers Expanded'; @override String get headerExpandedSubtitle => 'Details page Headers is expanded by default'; @override String get bottomNavigation => 'Bottom Navigation'; @override String get bottomNavigationSubtitle => 'Bottom navigation bar is displayed, effective after restart'; @override String get memoryCleanup => 'Memory Cleanup'; @override String get memoryCleanupSubtitle => 'Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning'; @override String get unlimited => 'Unlimited'; @override String get custom => 'Custom'; @override String get externalProxyAuth => 'Proxy Auth (Optional)'; @override String get externalProxyServer => 'Proxy Server'; @override String get externalProxyConnectFailure => 'External Proxy Connect failure'; @override String get externalProxyFailureConfirm => 'Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。'; @override String get mobileDisplayPacketCapture => 'Mobile Display Packet Capture:'; @override String proxyPortRepeat(Object port) { return 'Startup failed, please check the port number $port is occupied。'; } @override String get reset => 'Reset'; @override String get proxyIgnoreDomain => 'Proxy ignores domain'; @override String get domainWhitelistDescribe => 'Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid'; @override String get domainBlacklistDescribe => 'Domain names on the blacklist will not be proxied'; @override String get domain => 'Host'; @override String get enableScript => 'Enable Script'; @override String get scriptUseDescribe => 'Use JavaScript to modify requests and responses'; @override String get scriptEdit => 'Edit script'; @override String get scrollEnd => 'Scroll to End'; @override String get logger => 'Log'; @override String get material3 => 'Material 3 is the latest version of Google’s open-source design system'; @override String get iosVpnBackgroundAudio => 'After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?'; @override String get markRead => 'Mark as read'; @override String get autoRead => 'Auto read'; @override String get highlight => 'Highlight'; @override String get blue => 'Blue'; @override String get green => 'Green'; @override String get yellow => 'Yellow'; @override String get red => 'Red'; @override String get pink => 'Pink'; @override String get gray => 'Gray'; @override String get underline => 'Underline'; @override String get requestBlock => 'Request Block'; @override String get other => 'Other'; @override String get certHashName => 'CA Hash Name'; @override String get regExp => 'RegExp'; @override String get systemCertName => 'System Certificate Name'; @override String get qrCode => 'QR Code'; @override String get scanQrCode => 'Scan QR Code'; @override String get generateQrCode => 'Generate'; @override String get saveImage => 'Save Image'; @override String get selectImage => 'Select Image'; @override String get inputContent => 'Input Content'; @override String get errorCorrectLevel => 'Error Correct'; @override String get output => 'Output'; @override String get timestamp => 'Timestamp'; @override String get convert => 'Convert'; @override String get time => 'DateTime'; @override String get nowTimestamp => 'Now timestamp'; @override String get hosts => 'Hosts'; @override String get toAddress => 'To Address'; @override String get encrypt => 'Encrypt'; @override String get decrypt => 'Decrypt'; @override String get cipher => 'Cipher'; @override String get appUpdateCheckVersion => 'Check for Updates'; @override String get appUpdateNotAvailableMsg => 'Already Using The Latest Version'; @override String get appUpdateDialogTitle => 'Update Available'; @override String get appUpdateUpdateMsg => 'A new version of ProxyPin is available. Would you like to update now?'; @override String get appUpdateCurrentVersionLbl => 'Current Version'; @override String get appUpdateNewVersionLbl => 'New Version'; @override String get appUpdateUpdateNowBtnTxt => 'Update Now'; @override String get appUpdateLaterBtnTxt => 'Later'; @override String get appUpdateIgnoreBtnTxt => 'Ignore'; @override String get requestMap => 'Request Map'; @override String get requestMapDescribe => 'Do not request remote services, use local configuration or script for response'; @override String get automatic => 'Automatic'; @override String get manual => 'Manual'; @override String get certNotInstalled => 'Certificate not installed'; @override String get openNewWindow => 'Open New Window'; @override String get sponsorDonate => 'Sponsor / Donate'; @override String get sponsorSupport => 'Support ongoing development'; @override String get sponsorThanks => 'Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.'; @override String get sponsorAfdian => 'AFDIAN'; @override String get sponsorBuyMeCoffee => 'Buy Me a Coffee'; @override String get privacyPolicy => 'Privacy Policy'; @override String get privacyContent => 'This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.'; @override String get requestCrypto => 'Request Crypto'; @override String get cryptoDecoded => 'Decoded'; @override String get cryptoDecodeToggle => 'Decrypt'; @override String get optional => 'Optional'; @override String get cryptoRuleField => 'Field Name'; @override String get cryptoIvPrefixLabel => 'IV Prefix'; @override String get cryptoIvPrefixTooltip => 'Use the first N bytes of the response body as IV'; @override String get local => 'Local'; @override String get remoteUrl => 'Remote URL'; @override String get view => 'View'; } ================================================ FILE: lib/l10n/app_localizations_zh.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for Chinese (`zh`). class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); @override String get breakpoint => '断点'; @override String get breakpointRule => '断点规则'; @override String get name => '名称'; @override String get requests => '抓包'; @override String get favorites => '收藏'; @override String get history => '历史'; @override String get toolbox => '工具箱'; @override String get preference => '偏好设置'; @override String get feedback => '反馈'; @override String get about => '关于'; @override String get filter => '代理过滤'; @override String get script => '脚本'; @override String get share => '分享'; @override String get port => '端口号: '; @override String get proxy => '代理'; @override String get externalProxy => '外部代理设置'; @override String get username => '用户名'; @override String get password => '密码'; @override String get proxySetting => '代理设置'; @override String get setAs => '设置为'; @override String get systemProxy => '系统代理'; @override String get enabledHTTP2 => '启用HTTP2'; @override String get serverNotStart => '未开启抓包'; @override String get download => '下载'; @override String get config => '配置'; @override String get version => '版本'; @override String get start => '开始'; @override String get stop => '停止'; @override String get clear => '清空'; @override String get httpsProxy => 'HTTPS 代理'; @override String get setting => '设置'; @override String get mobileConnect => '手机连接'; @override String get connectRemote => '连接终端'; @override String get remoteDevice => '远程设备'; @override String get remoteDeviceList => '远程设备列表'; @override String get myQRCode => '我的二维码'; @override String get theme => '主题'; @override String get followSystem => '跟随系统'; @override String get themeColor => '主题颜色'; @override String get themeLight => '浅色'; @override String get themeDark => '深色'; @override String get language => '语言'; @override String get autoStartup => '自动开启抓包'; @override String get autoStartupDescribe => '程序启动时自动开始记录流量'; @override String get copied => '已复制到剪切板'; @override String get execute => '执行'; @override String get cancel => '取消'; @override String get close => '关闭'; @override String get save => '保存'; @override String get confirm => '确认'; @override String get confirmTitle => '确认操作'; @override String get confirmContent => '是否确认此操作?'; @override String get addSuccess => '添加成功'; @override String get saveSuccess => '保存成功'; @override String get operationSuccess => '操作成功'; @override String get import => '导入'; @override String get importSuccess => '导入成功'; @override String get importFailed => '导入失败'; @override String get export => '导出'; @override String get exportSuccess => '导出成功'; @override String get exportFailed => '导出失败'; @override String get deleteSuccess => '删除成功'; @override String get send => '发送'; @override String get fail => '失败'; @override String get success => '成功'; @override String get emptyData => '无数据'; @override String get requestSuccess => '请求成功'; @override String get add => '添加'; @override String get all => '全部'; @override String get modify => '修改'; @override String get responseType => '响应类型'; @override String get request => '请求'; @override String get response => '响应'; @override String get statusCode => '状态码'; @override String get duration => '耗时'; @override String get done => '完成'; @override String get type => '类型'; @override String get enable => '启用'; @override String get example => '示例: '; @override String get responseHeader => '响应头'; @override String get requestHeader => '请求头'; @override String get requestLine => '请求行'; @override String get requestMethod => '请求方法'; @override String get param => '参数'; @override String get replaceBodyWith => '消息体替换为:'; @override String get redirectTo => '重定向到:'; @override String get redirect => '重定向'; @override String get cannotBeEmpty => '不能为空'; @override String get requestRewriteList => '请求重写列表'; @override String get requestRewriteRule => '请求重写规则'; @override String get requestRewriteEnable => '是否启用请求重写'; @override String get action => '行为'; @override String get multiple => '多选'; @override String get edit => '编辑'; @override String get disabled => '禁用'; @override String requestRewriteDeleteConfirm(Object size) { return '是否删除$size条规则?'; } @override String get useGuide => '使用文档'; @override String get pleaseEnter => '请输入'; @override String get click => '点击'; @override String get replace => '替换'; @override String get clickEdit => '点击编辑'; @override String get refresh => '刷新'; @override String get selectFile => '选择文件'; @override String get match => '匹配'; @override String get value => '值'; @override String get matchRule => '匹配规则'; @override String get emptyMatchAll => '为空表示匹配全部'; @override String get newBuilt => '新建'; @override String get reportServers => '上报服务器'; @override String get addReportServer => '新增上报服务器'; @override String get editReportServer => '编辑上报服务器'; @override String get serverUrl => '服务器 URL'; @override String get compression => '压缩'; @override String get compressionNone => '无'; @override String get newFolder => '新建文件夹'; @override String get enableSelect => '启用选择'; @override String get disableSelect => '禁用选择'; @override String get deleteSelect => '删除选择'; @override String get testData => '测试数据'; @override String get noChangesDetected => '未检测到变更'; @override String get enterMatchData => '输入待匹配的数据'; @override String get modifyRequestHeader => '修改请求头'; @override String get headerName => '请求头名称'; @override String get headerValue => '请求头值'; @override String get deleteHeaderConfirm => '是否删除该请求头'; @override String get sequence => '全部请求'; @override String get domainList => '域名列表'; @override String get domainWhitelist => '代理域名白名单'; @override String get domainBlacklist => '代理域名黑名单'; @override String get domainFilter => '域名代理列表'; @override String get appWhitelist => '应用白名单'; @override String get appWhitelistDescribe => '只代理白名单中的应用, 白名单启用黑名单将会失效'; @override String get appBlacklist => '应用黑名单'; @override String get scanCode => '扫码连接'; @override String get addBlacklist => '添加代理黑名单'; @override String get addWhitelist => '添加代理白名单'; @override String get deleteWhitelist => '删除代理白名单'; @override String domainListSubtitle(Object count, Object time) { return '最后请求时间: $time, 次数: $count'; } @override String get selectAction => '选择操作'; @override String get copy => '复制'; @override String get copyHost => '复制域名'; @override String get copyUrl => '复制URL'; @override String get copyRawRequest => '复制 原始请求'; @override String get copyRequestResponse => '复制 请求和响应'; @override String get copyCurl => '复制 cURL'; @override String get copyAsPythonRequests => '复制 Python Requests'; @override String get delete => '删除'; @override String get rename => '重命名'; @override String get repeat => '重放'; @override String get repeatAllRequests => '重放所有请求'; @override String get repeatDomainRequests => '重放域名下请求'; @override String get customRepeat => '高级重放'; @override String get repeatCount => '次数'; @override String get repeatInterval => '间隔(ms)'; @override String get repeatDelay => '延时(ms)'; @override String get scheduleTime => '指定时间'; @override String get fixed => '固定'; @override String get random => '随机'; @override String get keepCustomSettings => '保持自定义设置'; @override String get editRequest => '编辑请求'; @override String get reSendRequest => '已重新发送请求'; @override String get viewExport => '视图导出'; @override String get timeDesc => '按时间降序'; @override String get timeAsc => '按时间升序'; @override String get search => '搜索'; @override String get clearSearch => '清除搜索'; @override String get requestType => '请求类型'; @override String get keyword => '关键词'; @override String get keywordSearchScope => '关键词搜索范围: '; @override String get favorite => '收藏'; @override String get deleteFavorite => '删除收藏'; @override String get emptyFavorite => '暂无收藏'; @override String get deleteFavoriteSuccess => '已删除收藏'; @override String get historyRecord => '历史记录'; @override String get historyCacheTime => '缓存时间'; @override String get historyManualSave => '手动保存'; @override String historyDay(Object day) { return '$day天'; } @override String get historyForever => '永久'; @override String historyRecordTitle(Object length, Object name) { return '$name 记录数 $length'; } @override String get historyEmptyName => '名称不能为空'; @override String historySubtitle(Object requestLength, Object size) { return '记录数 $requestLength 文件 $size'; } @override String get historyUnSave => '当前会话记录未保存'; @override String get historyDeleteConfirm => '是否删除该历史记录?'; @override String get requestEdit => '请求编辑'; @override String get encode => '编码'; @override String get requestBody => '请求体'; @override String get responseBody => '响应体'; @override String get requestRewrite => '请求重写'; @override String get newWindow => '新窗口打开'; @override String get httpRequest => 'HTTP请求'; @override String get enabledHttps => '启用HTTPS代理'; @override String get installRootCa => '安装根证书'; @override String get installCaLocal => '安装根证书到本机'; @override String get downloadRootCa => '下载根证书'; @override String get downloadRootCaNote => '注意:如果您将默认浏览器设置为 Safari 以外的浏览器,请单击此行复制并粘贴 Safari 浏览器的链接'; @override String get generateCA => '重新生成根证书'; @override String get generateCADescribe => '您确定要生成新的根证书吗? 如果确认,\n则需要重新安装并信任新的证书'; @override String get resetDefaultCA => '重置默认根证书'; @override String get resetDefaultCADescribe => '确定要重置为默认根证书吗? ProxyPin默认\n根证书对所有用户都是相同的.'; @override String get exportCaP12 => '导出根证书 (.p12)'; @override String get importCaP12 => '导入根证书 (.p12)'; @override String get trustCa => '信任证书'; @override String get profileDownload => '已下载描述文件'; @override String get exportCA => '导出根证书'; @override String get exportPrivateKey => '导出私钥'; @override String get install => '安装'; @override String get installCaDescribe => '安装证书 设置 > 已下载描述文件 > 安装'; @override String get trustCaDescribe => '信任证书 设置 > 通用 > 关于本机 > 证书信任设置'; @override String get androidRoot => '系统证书 (ROOT设备)'; @override String get androidRootMagisk => 'Magisk模块: \n安卓ROOT设备可以使用Magisk ProxyPinCA系统证书模块, 安装完重启手机后 在系统证书查看是否有ProxyPinCA证书,如果有说明证书安装成功。'; @override String androidRootRename(Object name) { return '模块不生效可以根据网上教程安装系统根证书, 根证书命名成 $name'; } @override String get androidRootCADownload => '下载系统根证书(.0)'; @override String get androidUserCA => '用户证书'; @override String get androidUserCATips => '提示:Android7+ 很多软件不会信任用户证书'; @override String get androidUserCAInstall => '打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书'; @override String get androidUserXposed => '推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki'; @override String get configWifiProxy => '配置手机Wi-Fi代理'; @override String get caInstallGuide => '证书安装指南'; @override String get caAndroidBrowser => '在 Android 设备上打开浏览器访问:'; @override String get caIosBrowser => '在 iOS 设备上打开 Safari访问:'; @override String get localIP => '本地IP '; @override String get mobileScan => '配置Wi-Fi代理或使用手机版扫描二维码'; @override String get decode => '解码'; @override String get encodeInput => '输入要转换的内容'; @override String get encodeResult => '转换结果'; @override String get encodeFail => '编码失败'; @override String get decodeFail => '解码失败'; @override String get shareUrl => '分享请求链接'; @override String get shareCurl => '分享 cURL 请求'; @override String get shareRequestResponse => '分享请求和响应'; @override String get captureDetail => '抓包详情'; @override String get proxyPinSoftware => 'ProxyPin全平台开源抓包软件'; @override String get prompt => '提示'; @override String get curlSchemeRequest => '识别到curl格式,是否转换为HTTP请求?'; @override String get appExitTips => '再按一次退出程序'; @override String get remoteConnectDisconnect => '检查远程连接失败,已断开'; @override String get connect => '连接'; @override String get reconnect => '重新连接'; @override String remoteConnected(Object os) { return '已连接$os,流量将转发到$os'; } @override String get remoteConnectForward => '远程连接,将其他设备流量转发到当前设备'; @override String get connectSuccess => '连接成功'; @override String get connectedRemote => '已连接远程'; @override String get connected => '已连接'; @override String get notConnected => '未连接'; @override String get disconnect => '断开连接'; @override String get ipLayerProxy => 'IP层代理(Beta)'; @override String get ipLayerProxyDesc => 'IP层代理可抓取Flutter应用请求,目前不是很稳定,欢迎提交PR'; @override String get inputAddress => '输入地址'; @override String get syncConfig => '同步配置'; @override String get pullConfigFail => '拉取配置失败, 请检查网络连接'; @override String get sync => '同步'; @override String get invalidQRCode => '无法识别的二维码'; @override String get remoteConnectFail => '连接失败,请检查是否在同一局域网和防火墙是否允许, ios需要开启本地网络权限'; @override String get remoteConnectSuccessTips => '手机需要开启抓包才可以抓取请求哦'; @override String get windowMode => '窗口模式'; @override String get windowModeSubTitle => '开启抓包后 如果应用退回到后台,显示一个小窗口'; @override String get pipIcon => '窗口快捷图标'; @override String get pipIconDescribe => '展示快捷进入小窗口Icon'; @override String get headerExpanded => 'Headers自动展开'; @override String get headerExpandedSubtitle => '详情页Headers栏是否自动展开'; @override String get bottomNavigation => '底部导航'; @override String get bottomNavigationSubtitle => '底部导航栏是否显示,重启后生效'; @override String get memoryCleanup => '内存清理'; @override String get memoryCleanupSubtitle => '到内存限制自动清理请求,清理后保留最近32条请求'; @override String get unlimited => '无限制'; @override String get custom => '自定义'; @override String get externalProxyAuth => '代理认证 (可选)'; @override String get externalProxyServer => '代理服务器'; @override String get externalProxyConnectFailure => '外部代理连接失败'; @override String get externalProxyFailureConfirm => '网络不通所有接口将会访问失败,是否继续设置外部代理。'; @override String get mobileDisplayPacketCapture => '手机端是否展示抓包:'; @override String proxyPortRepeat(Object port) { return '启动失败,请检查端口号$port是否被占用'; } @override String get reset => '重置'; @override String get proxyIgnoreDomain => '代理忽略域名'; @override String get domainWhitelistDescribe => '只代理白名单中的域名, 白名单启用黑名单将会失效'; @override String get domainBlacklistDescribe => '黑名单中的域名不会代理'; @override String get domain => '域名'; @override String get enableScript => '启用脚本工具'; @override String get scriptUseDescribe => '使用 JavaScript 修改请求和响应'; @override String get scriptEdit => '编辑脚本'; @override String get scrollEnd => '跟踪滚动'; @override String get logger => '日志'; @override String get material3 => 'Material3是谷歌开源设计系统的最新版本'; @override String get iosVpnBackgroundAudio => '开启抓包后,退出到后台。为了维护主UI线程的网络通信,将启用静音音频播放以保持主线程运行。否则,它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?'; @override String get markRead => '标记已读'; @override String get autoRead => '自动已读'; @override String get highlight => '高亮'; @override String get blue => '蓝色'; @override String get green => '绿色'; @override String get yellow => '黄色'; @override String get red => '红色'; @override String get pink => '粉色'; @override String get gray => '灰色'; @override String get underline => '下划线'; @override String get requestBlock => '请求屏蔽'; @override String get other => '其他'; @override String get certHashName => '证书Hash名称'; @override String get regExp => '正则表达式'; @override String get systemCertName => '系统证书名称'; @override String get qrCode => '二维码'; @override String get scanQrCode => '扫描二维码'; @override String get generateQrCode => '生成二维码'; @override String get saveImage => '保存图片'; @override String get selectImage => '选择图片'; @override String get inputContent => '输入内容'; @override String get errorCorrectLevel => '纠错等级'; @override String get output => '输出'; @override String get timestamp => '时间戳'; @override String get convert => '转换'; @override String get time => '时间'; @override String get nowTimestamp => '当前时间戳(秒)'; @override String get hosts => 'Hosts 映射'; @override String get toAddress => '映射地址'; @override String get encrypt => '加密'; @override String get decrypt => '解密'; @override String get cipher => '加解密'; @override String get appUpdateCheckVersion => '检查更新'; @override String get appUpdateNotAvailableMsg => '已是最新版本'; @override String get appUpdateDialogTitle => '有可用更新'; @override String get appUpdateUpdateMsg => 'ProxyPin 的新版本现已推出。您想现在更新吗?'; @override String get appUpdateCurrentVersionLbl => '当前版本'; @override String get appUpdateNewVersionLbl => '新版本'; @override String get appUpdateUpdateNowBtnTxt => '现在更新'; @override String get appUpdateLaterBtnTxt => '以后再说'; @override String get appUpdateIgnoreBtnTxt => '忽略'; @override String get requestMap => '请求映射'; @override String get requestMapDescribe => '不请求远程服务,使用本地配置或脚本进行响应'; @override String get automatic => '自动'; @override String get manual => '手动'; @override String get certNotInstalled => '证书未安装'; @override String get openNewWindow => '新窗口打开'; @override String get sponsorDonate => '赞助 / 捐赠'; @override String get sponsorSupport => '支持项目持续开发'; @override String get sponsorThanks => '感谢支持开源项目,可选择以下任意方式,帮助项目长期发展'; @override String get sponsorAfdian => '爱发电赞助'; @override String get sponsorBuyMeCoffee => 'Buy Me a Coffee'; @override String get privacyPolicy => '隐私协议'; @override String get privacyContent => '本项目为开源抓包工具,所有功能均在本地设备上运行;无任何后端服务器,不会收集、存储或上传任何用户信息。抓取的网络数据仅在本地处理,除非您主动使用远程转发功能。所需权限(如网络、存储、相机用于扫码)仅用于实现相应功能。您可在公开的源代码中审计其行为。'; @override String get requestCrypto => '请求解密'; @override String get cryptoDecoded => '已解密'; @override String get cryptoDecodeToggle => '解密'; @override String get optional => '可选'; @override String get cryptoRuleField => '字段名称'; @override String get cryptoIvPrefixLabel => 'IV 前缀'; @override String get cryptoIvPrefixTooltip => '使用响应体前 N 个字节作为 IV'; @override String get local => '本地'; @override String get remoteUrl => '远程URL'; @override String get view => '查看'; } /// The translations for Chinese, using the Han script (`zh_Hant`). class AppLocalizationsZhHant extends AppLocalizationsZh { AppLocalizationsZhHant() : super('zh_Hant'); @override String get breakpoint => '斷點'; @override String get breakpointRule => '斷點規則'; @override String get name => '名稱'; @override String get requests => '抓包'; @override String get favorites => '收藏'; @override String get history => '歷史'; @override String get toolbox => '工具箱'; @override String get preference => '偏好設定'; @override String get feedback => '意見回饋'; @override String get about => '關於'; @override String get filter => '代理過濾'; @override String get script => '腳本'; @override String get share => '分享'; @override String get port => '連接埠號: '; @override String get proxy => '代理'; @override String get externalProxy => '外部代理設定'; @override String get username => '使用者名稱'; @override String get password => '密碼'; @override String get proxySetting => '代理設定'; @override String get setAs => '設定為'; @override String get systemProxy => '系統代理'; @override String get enabledHTTP2 => '啟用HTTP2'; @override String get serverNotStart => '未開啟抓包'; @override String get download => '下載'; @override String get config => '設定'; @override String get version => '版本'; @override String get start => '開始'; @override String get stop => '停止'; @override String get clear => '清空'; @override String get httpsProxy => 'HTTPS 代理'; @override String get setting => '設定'; @override String get mobileConnect => '手機連接'; @override String get connectRemote => '連接終端'; @override String get remoteDevice => '遠端裝置'; @override String get remoteDeviceList => '遠端裝置列表'; @override String get myQRCode => '我的二維碼'; @override String get theme => '主題'; @override String get followSystem => '跟隨系統'; @override String get themeColor => '主題顏色'; @override String get themeLight => '淺色'; @override String get themeDark => '深色'; @override String get language => '語言'; @override String get autoStartup => '自動開啟抓包'; @override String get autoStartupDescribe => '程式啟動時自動開始記錄流量'; @override String get copied => '已複製到剪切板'; @override String get execute => '執行'; @override String get cancel => '取消'; @override String get close => '關閉'; @override String get save => '儲存'; @override String get confirm => '確認'; @override String get confirmTitle => '確認操作'; @override String get confirmContent => '是否確認此操作?'; @override String get addSuccess => '新增成功'; @override String get saveSuccess => '儲存成功'; @override String get operationSuccess => '操作成功'; @override String get import => '匯入'; @override String get importSuccess => '匯入成功'; @override String get importFailed => '匯入失敗'; @override String get export => '匯出'; @override String get exportSuccess => '匯出成功'; @override String get deleteSuccess => '刪除成功'; @override String get send => '傳送'; @override String get fail => '失敗'; @override String get success => '成功'; @override String get emptyData => '無資料'; @override String get requestSuccess => '請求成功'; @override String get add => '新增'; @override String get all => '全部'; @override String get modify => '修改'; @override String get responseType => '回應類型'; @override String get request => '請求'; @override String get response => '回應'; @override String get statusCode => '狀態碼'; @override String get duration => '耗時'; @override String get done => '完成'; @override String get type => '類型'; @override String get enable => '啟用'; @override String get example => '範例: '; @override String get responseHeader => '回應標頭'; @override String get requestHeader => '請求標頭'; @override String get requestLine => '請求行'; @override String get requestMethod => '請求方法'; @override String get param => '參數'; @override String get replaceBodyWith => '訊息體替換為:'; @override String get redirectTo => '重新導向到:'; @override String get redirect => '重新導向'; @override String get cannotBeEmpty => '不能為空'; @override String get requestRewriteList => '請求重寫列表'; @override String get requestRewriteRule => '請求重寫規則'; @override String get requestRewriteEnable => '是否啟用請求重寫'; @override String get action => '行為'; @override String get multiple => '多選'; @override String get edit => '編輯'; @override String get disabled => '停用'; @override String requestRewriteDeleteConfirm(Object size) { return '是否刪除$size條規則?'; } @override String get useGuide => '使用文件'; @override String get pleaseEnter => '請輸入'; @override String get click => '點選'; @override String get replace => '替換'; @override String get clickEdit => '點選編輯'; @override String get refresh => '重新整理'; @override String get selectFile => '選擇檔案'; @override String get match => '符合'; @override String get value => '值'; @override String get matchRule => '符合規則'; @override String get emptyMatchAll => '為空表示符合全部'; @override String get newBuilt => '新建'; @override String get reportServers => '上報伺服器'; @override String get addReportServer => '新增上報伺服器'; @override String get editReportServer => '編輯上報伺服器'; @override String get serverUrl => '伺服器 URL'; @override String get compression => '壓縮'; @override String get compressionNone => '無'; @override String get newFolder => '新建資料夾'; @override String get enableSelect => '啟用選擇'; @override String get disableSelect => '停用選擇'; @override String get deleteSelect => '刪除選擇'; @override String get testData => '測試資料'; @override String get noChangesDetected => '未檢測到變更'; @override String get enterMatchData => '輸入待符合的資料'; @override String get modifyRequestHeader => '修改請求標頭'; @override String get headerName => '請求標頭名稱'; @override String get headerValue => '請求標頭值'; @override String get deleteHeaderConfirm => '是否刪除該請求標頭'; @override String get sequence => '全部請求'; @override String get domainList => '網域名稱列表'; @override String get domainWhitelist => '代理網域名稱白名單'; @override String get domainBlacklist => '代理網域名稱黑名單'; @override String get domainFilter => '網域名稱代理列表'; @override String get appWhitelist => '應用程式白名單'; @override String get appWhitelistDescribe => '只代理白名單中的應用程式, 白名單啟用黑名單將會失效'; @override String get appBlacklist => '應用程式黑名單'; @override String get scanCode => '掃碼連接'; @override String get addBlacklist => '新增代理黑名單'; @override String get addWhitelist => '新增代理白名單'; @override String get deleteWhitelist => '刪除代理白名單'; @override String domainListSubtitle(Object count, Object time) { return '最後請求時間: $time, 次數: $count'; } @override String get selectAction => '選擇操作'; @override String get copy => '複製'; @override String get copyHost => '複製網域名稱'; @override String get copyUrl => '複製URL'; @override String get copyRawRequest => '複製原始請求'; @override String get copyRequestResponse => '複製 請求和回應'; @override String get copyCurl => '複製 cURL'; @override String get copyAsPythonRequests => '複製 Python Requests'; @override String get delete => '刪除'; @override String get rename => '重新命名'; @override String get repeat => '重放'; @override String get repeatAllRequests => '重放所有請求'; @override String get repeatDomainRequests => '重放網域名稱下請求'; @override String get customRepeat => '進階重放'; @override String get repeatCount => '次數'; @override String get repeatInterval => '間隔(ms)'; @override String get repeatDelay => '延遲(ms)'; @override String get scheduleTime => '指定時間'; @override String get fixed => '固定'; @override String get random => '隨機'; @override String get keepCustomSettings => '保持自訂設定'; @override String get editRequest => '編輯請求'; @override String get reSendRequest => '已重新傳送請求'; @override String get viewExport => '檢視匯出'; @override String get timeDesc => '按時間降序'; @override String get timeAsc => '按時間升序'; @override String get search => '搜尋'; @override String get clearSearch => '清除搜尋'; @override String get requestType => '請求類型'; @override String get keyword => '關鍵字'; @override String get keywordSearchScope => '關鍵字搜尋範圍: '; @override String get favorite => '收藏'; @override String get deleteFavorite => '刪除收藏'; @override String get emptyFavorite => '暫無收藏'; @override String get deleteFavoriteSuccess => '已刪除收藏'; @override String get historyRecord => '歷史記錄'; @override String get historyCacheTime => '快取時間'; @override String get historyManualSave => '手動儲存'; @override String historyDay(Object day) { return '$day天'; } @override String get historyForever => '永久'; @override String historyRecordTitle(Object length, Object name) { return '$name 記錄數 $length'; } @override String get historyEmptyName => '名稱不能為空'; @override String historySubtitle(Object requestLength, Object size) { return '記錄數 $requestLength 檔案 $size'; } @override String get historyUnSave => '目前對話記錄未儲存'; @override String get historyDeleteConfirm => '是否刪除該歷史記錄?'; @override String get requestEdit => '請求編輯'; @override String get encode => '編碼'; @override String get requestBody => '請求體'; @override String get responseBody => '回應體'; @override String get requestRewrite => '請求重寫'; @override String get newWindow => '新視窗開啟'; @override String get httpRequest => 'HTTP請求'; @override String get enabledHttps => '啟用HTTPS代理'; @override String get installRootCa => '安裝根憑證'; @override String get installCaLocal => '安裝根憑證到本機'; @override String get downloadRootCa => '下載根憑證'; @override String get downloadRootCaNote => '注意:如果您將預設瀏覽器設定為 Safari 以外的瀏覽器,請點選此行複製並貼上 Safari 瀏覽器的連結'; @override String get generateCA => '重新產生根憑證'; @override String get generateCADescribe => '您確定要產生新的根憑證嗎? 如果確認,\n則需要重新安裝並信任新的憑證'; @override String get resetDefaultCA => '重置預設根憑證'; @override String get resetDefaultCADescribe => '確定要重置為預設根憑證嗎? ProxyPin預設\n根憑證對所有使用者都是相同的.'; @override String get exportCaP12 => '匯出根憑證 (.p12)'; @override String get importCaP12 => '匯入根憑證 (.p12)'; @override String get trustCa => '信任憑證'; @override String get profileDownload => '已下載描述檔案'; @override String get exportCA => '匯出根憑證'; @override String get exportPrivateKey => '匯出私鑰'; @override String get install => '安裝'; @override String get installCaDescribe => '安裝憑證 設定 > 已下載描述檔案 > 安裝'; @override String get trustCaDescribe => '信任憑證 設定 > 一般 > 關於本機 > 憑證信任設定'; @override String get androidRoot => '系統憑證 (ROOT裝置)'; @override String get androidRootMagisk => 'Magisk模組: \n安卓ROOT裝置可以使用Magisk ProxyPinCA系統憑證模組, 安裝完重新開機後 在系統憑證檢視是否有ProxyPinCA憑證,如果有說明憑證安裝成功。'; @override String androidRootRename(Object name) { return '模組不生效可以根據網上教學安裝系統根憑證, 根憑證命名成 $name'; } @override String get androidRootCADownload => '下載系統根憑證(.0)'; @override String get androidUserCA => '使用者憑證'; @override String get androidUserCATips => '提示:Android7+ 很多軟體不會信任使用者憑證'; @override String get androidUserCAInstall => '開啟設定 -> 安全性 -> 加密和憑證 -> 安裝憑證 -> CA 憑證'; @override String get androidUserXposed => '推薦使用Xposed模組抓包(無需ROOT), 點選檢視wiki'; @override String get configWifiProxy => '設定手機Wi-Fi代理'; @override String get caInstallGuide => '憑證安裝指南'; @override String get caAndroidBrowser => '在 Android 裝置上開啟瀏覽器存取:'; @override String get caIosBrowser => '在 iOS 裝置上開啟 Safari存取:'; @override String get localIP => '本機IP '; @override String get mobileScan => '設定Wi-Fi代理或使用手機版掃描二維碼'; @override String get decode => '解碼'; @override String get encodeInput => '輸入要轉換的內容'; @override String get encodeResult => '轉換結果'; @override String get encodeFail => '編碼失敗'; @override String get decodeFail => '解碼失敗'; @override String get shareUrl => '分享請求連結'; @override String get shareCurl => '分享 cURL 請求'; @override String get shareRequestResponse => '分享請求和回應'; @override String get captureDetail => '抓包詳情'; @override String get proxyPinSoftware => 'ProxyPin全平台開源抓包軟體'; @override String get prompt => '提示'; @override String get curlSchemeRequest => '識別到curl格式,是否轉換為HTTP請求?'; @override String get appExitTips => '再按一次退出程式'; @override String get remoteConnectDisconnect => '檢查遠端連接失敗,已中斷連接'; @override String get connect => '連接'; @override String get reconnect => '重新連接'; @override String remoteConnected(Object os) { return '已連接$os,流量將轉發到$os'; } @override String get remoteConnectForward => '遠端連接,將其他裝置流量轉發到目前裝置'; @override String get connectSuccess => '連接成功'; @override String get connectedRemote => '已連接遠端'; @override String get connected => '已連接'; @override String get notConnected => '未連接'; @override String get disconnect => '中斷連接'; @override String get ipLayerProxy => 'IP層代理(Beta)'; @override String get ipLayerProxyDesc => 'IP層代理可抓取Flutter應用程式請求,目前不是很穩定,歡迎提交PR'; @override String get inputAddress => '輸入地址'; @override String get syncConfig => '同步設定'; @override String get pullConfigFail => '拉取設定失敗, 請檢查網路連接'; @override String get sync => '同步'; @override String get invalidQRCode => '無法識別的二維碼'; @override String get remoteConnectFail => '連接失敗,請檢查是否在同一區域網路和防火牆是否允許, ios需要開啟本機網路權限'; @override String get remoteConnectSuccessTips => '手機需要開啟抓包才可以抓取請求哦'; @override String get windowMode => '視窗模式'; @override String get windowModeSubTitle => '開啟抓包後 如果應用程式退回到背景,顯示一個小視窗'; @override String get pipIcon => '視窗快捷圖示'; @override String get pipIconDescribe => '展示快捷進入小視窗Icon'; @override String get headerExpanded => 'Headers自動展開'; @override String get headerExpandedSubtitle => '詳情頁Headers欄是否自動展開'; @override String get bottomNavigation => '底部導航'; @override String get bottomNavigationSubtitle => '底部導航欄是否顯示,重新啟動後生效'; @override String get memoryCleanup => '記憶體清理'; @override String get memoryCleanupSubtitle => '到記憶體限制自動清理請求,清理後保留最近32條請求'; @override String get unlimited => '無限制'; @override String get custom => '自訂'; @override String get externalProxyAuth => '代理認證 (可選)'; @override String get externalProxyServer => '代理伺服器'; @override String get externalProxyConnectFailure => '外部代理連接失敗'; @override String get externalProxyFailureConfirm => '網路不通所有介面將會存取失敗,是否繼續設定外部代理。'; @override String get mobileDisplayPacketCapture => '手機端是否展示抓包:'; @override String proxyPortRepeat(Object port) { return '啟動失敗,請檢查連接埠號$port是否被占用'; } @override String get reset => '重置'; @override String get proxyIgnoreDomain => '代理忽略網域名稱'; @override String get domainWhitelistDescribe => '只代理白名單中的網域名稱, 白名單啟用黑名單將會失效'; @override String get domainBlacklistDescribe => '黑名單中的網域名稱不會代理'; @override String get domain => '網域名稱'; @override String get enableScript => '啟用腳本工具'; @override String get scriptUseDescribe => '使用 JavaScript 修改請求和回應'; @override String get scriptEdit => '編輯腳本'; @override String get scrollEnd => '跟蹤滾動'; @override String get logger => '日誌'; @override String get material3 => 'Material3是Google開源設計系統的最新版本'; @override String get iosVpnBackgroundAudio => '開啟抓包後,退出到背景。為了維護主UI執行緒的網路通信,將啟用靜音音訊播放以保持主執行緒運作。否則,它將只在背景運作30秒。您同意在啟用抓包後在背景播放音訊嗎?'; @override String get markRead => '標記已讀'; @override String get autoRead => '自動已讀'; @override String get highlight => '高亮顯示'; @override String get blue => '藍色'; @override String get green => '綠色'; @override String get yellow => '黃色'; @override String get red => '紅色'; @override String get pink => '粉色'; @override String get gray => '灰色'; @override String get underline => '底線'; @override String get requestBlock => '請求阻擋'; @override String get other => '其他'; @override String get certHashName => '憑證Hash名稱'; @override String get regExp => '正規表示式'; @override String get systemCertName => '系統憑證名稱'; @override String get qrCode => '二維碼'; @override String get scanQrCode => '掃描二維碼'; @override String get generateQrCode => '產生二維碼'; @override String get saveImage => '儲存圖片'; @override String get selectImage => '選擇圖片'; @override String get inputContent => '輸入內容'; @override String get errorCorrectLevel => '糾錯等級'; @override String get output => '輸出'; @override String get timestamp => '時間戳'; @override String get convert => '轉換'; @override String get time => '時間'; @override String get nowTimestamp => '目前時間戳(秒)'; @override String get hosts => 'Hosts 對應'; @override String get toAddress => '對應地址'; @override String get encrypt => '加密'; @override String get decrypt => '解密'; @override String get cipher => '密文'; @override String get appUpdateCheckVersion => '檢查更新'; @override String get appUpdateNotAvailableMsg => '已是最新版本'; @override String get appUpdateDialogTitle => '有可用更新'; @override String get appUpdateUpdateMsg => 'ProxyPin 的新版本現已推出。您想現在更新嗎?'; @override String get appUpdateCurrentVersionLbl => '目前版本'; @override String get appUpdateNewVersionLbl => '新版本'; @override String get appUpdateUpdateNowBtnTxt => '現在更新'; @override String get appUpdateLaterBtnTxt => '稍後再說'; @override String get appUpdateIgnoreBtnTxt => '忽略'; @override String get requestMap => '請求映射'; @override String get requestMapDescribe => '不請求遠端服務,使用本地配置或腳本進行回應'; @override String get automatic => '自動'; @override String get manual => '手動'; @override String get certNotInstalled => '未安裝憑證'; @override String get openNewWindow => '新視窗開啟'; @override String get sponsorDonate => '贊助 / 捐贈'; @override String get sponsorSupport => '支持項目持續開發'; @override String get sponsorThanks => '感謝支持開源項目,可選擇以下任意方式,幫助項目長期發展'; @override String get sponsorAfdian => '愛發電贊助'; @override String get sponsorBuyMeCoffee => 'Buy Me a Coffee'; @override String get privacyPolicy => '隱私協議'; @override String get privacyContent => '本專案為開源抓包工具,所有功能均在本機裝置上運行;無任何後端伺服器,不會蒐集、儲存或上傳任何使用者資訊。擷取的網路資料僅在本機處理,除非您主動使用遠端轉發功能。所需權限(如網路、儲存、相機用於掃碼)僅用於實現相應功能。您可在公開的原始碼中稽核其行為。'; @override String get requestCrypto => '請求解密'; @override String get cryptoDecoded => '已解密'; @override String get cryptoDecodeToggle => '解密'; @override String get optional => '可選'; @override String get cryptoRuleField => '字段'; @override String get cryptoIvPrefixLabel => 'IV 前綴'; @override String get cryptoIvPrefixTooltip => '使用回應內容的前 N 個字節作為 IV'; @override String get local => '本地'; @override String get remoteUrl => '遠端URL'; @override String get view => '檢視'; } ================================================ FILE: lib/l10n/app_zh.arb ================================================ { "breakpoint": "断点", "breakpointRule": "断点规则", "name": "名称", "requests": "抓包", "favorites": "收藏", "history": "历史", "toolbox": "工具箱", "preference": "偏好设置", "feedback": "反馈", "about": "关于", "filter": "代理过滤", "script": "脚本", "share": "分享", "port": "端口号: ", "proxy": "代理", "externalProxy": "外部代理设置", "username": "用户名", "password": "密码", "proxySetting": "代理设置", "setAs": "设置为", "systemProxy": "系统代理", "enabledHTTP2": "启用HTTP2", "serverNotStart": "未开启抓包", "download": "下载", "config": "配置", "version": "版本", "start": "开始", "stop": "停止", "clear": "清空", "httpsProxy": "HTTPS 代理", "setting": "设置", "mobileConnect": "手机连接", "connectRemote": "连接终端", "remoteDevice": "远程设备", "remoteDeviceList": "远程设备列表", "myQRCode": "我的二维码", "theme": "主题", "themeColor": "主题颜色", "followSystem": "跟随系统", "themeLight": "浅色", "themeDark": "深色", "language": "语言", "autoStartup": "自动开启抓包", "autoStartupDescribe": "程序启动时自动开始记录流量", "copied": "已复制到剪切板", "execute": "执行", "cancel": "取消", "close": "关闭", "save": "保存", "confirm": "确认", "confirmTitle": "确认操作", "confirmContent": "是否确认此操作?", "addSuccess": "添加成功", "saveSuccess": "保存成功", "operationSuccess": "操作成功", "import": "导入", "importSuccess": "导入成功", "importFailed": "导入失败", "export": "导出", "exportSuccess": "导出成功", "exportFailed": "导出失败", "deleteSuccess": "删除成功", "send": "发送", "fail": "失败", "success": "成功", "emptyData": "无数据", "requestSuccess": "请求成功", "add": "添加", "all": "全部", "modify": "修改", "responseType": "响应类型", "request": "请求", "response": "响应", "statusCode": "状态码", "duration": "耗时", "done": "完成", "type": "类型", "enable": "启用", "example": "示例: ", "responseHeader": "响应头", "requestHeader": "请求头", "requestLine": "请求行", "requestMethod": "请求方法", "param": "参数", "replaceBodyWith": "消息体替换为:", "redirectTo": "重定向到:", "redirect": "重定向", "cannotBeEmpty": "不能为空", "requestRewriteList": "请求重写列表", "requestRewriteRule": "请求重写规则", "requestRewriteEnable": "是否启用请求重写", "action": "行为", "multiple": "多选", "edit": "编辑", "disabled": "禁用", "requestRewriteDeleteConfirm": "是否删除{size}条规则?", "useGuide": "使用文档", "pleaseEnter": "请输入", "click": "点击", "replace": "替换", "clickEdit": "点击编辑", "refresh": "刷新", "selectFile": "选择文件", "match": "匹配", "value": "值", "matchRule": "匹配规则", "emptyMatchAll": "为空表示匹配全部", "newBuilt": "新建", "reportServers": "上报服务器", "addReportServer": "新增上报服务器", "editReportServer": "编辑上报服务器", "serverUrl": "服务器 URL", "compression": "压缩", "compressionNone": "无", "newFolder": "新建文件夹", "enableSelect": "启用选择", "disableSelect": "禁用选择", "deleteSelect": "删除选择", "testData": "测试数据", "noChangesDetected": "未检测到变更", "enterMatchData": "输入待匹配的数据", "modifyRequestHeader": "修改请求头", "headerName": "请求头名称", "headerValue": "请求头值", "deleteHeaderConfirm": "是否删除该请求头", "sequence": "全部请求", "domainList": "域名列表", "domainWhitelist": "代理域名白名单", "domainBlacklist": "代理域名黑名单", "appWhitelist": "应用白名单", "appWhitelistDescribe": "只代理白名单中的应用, 白名单启用黑名单将会失效", "appBlacklist": "应用黑名单", "domainFilter": "域名代理列表", "scanCode": "扫码连接", "addBlacklist": "添加代理黑名单", "addWhitelist": "添加代理白名单", "deleteWhitelist": "删除代理白名单", "domainListSubtitle": "最后请求时间: {time}, 次数: {count}", "selectAction": "选择操作", "copy": "复制", "copyHost": "复制域名", "copyUrl": "复制URL", "copyRawRequest": "复制 原始请求", "copyRequestResponse": "复制 请求和响应", "copyCurl": "复制 cURL", "copyAsPythonRequests": "复制 Python Requests", "delete": "删除", "rename": "重命名", "repeat": "重放", "repeatAllRequests": "重放所有请求", "repeatDomainRequests": "重放域名下请求", "customRepeat": "高级重放", "repeatCount": "次数", "repeatInterval": "间隔(ms)", "repeatDelay": "延时(ms)", "scheduleTime": "指定时间", "fixed": "固定", "random": "随机", "keepCustomSettings": "保持自定义设置", "editRequest": "编辑请求", "reSendRequest": "已重新发送请求", "viewExport": "视图导出", "timeDesc": "按时间降序", "timeAsc": "按时间升序", "search": "搜索", "clearSearch": "清除搜索", "requestType": "请求类型", "keyword": "关键词", "keywordSearchScope": "关键词搜索范围: ", "favorite": "收藏", "deleteFavorite": "删除收藏", "emptyFavorite": "暂无收藏", "deleteFavoriteSuccess": "已删除收藏", "historyRecord": "历史记录", "historyManualSave": "手动保存", "historyDay": "{day}天", "historyForever": "永久", "historyCacheTime": "缓存时间", "historyEmptyName": "名称不能为空", "historyRecordTitle": "{name} 记录数 {length}", "historySubtitle": "记录数 {requestLength} 文件 {size}", "historyUnSave": "当前会话记录未保存", "historyDeleteConfirm": "是否删除该历史记录?", "requestEdit": "请求编辑", "encode": "编码", "decode": "解码", "requestBody": "请求体", "responseBody": "响应体", "requestRewrite": "请求重写", "newWindow": "新窗口打开", "httpRequest": "HTTP请求", "enabledHttps": "启用HTTPS代理", "installRootCa": "安装根证书", "installCaLocal": "安装根证书到本机", "downloadRootCa": "下载根证书", "downloadRootCaNote": "注意:如果您将默认浏览器设置为 Safari 以外的浏览器,请单击此行复制并粘贴 Safari 浏览器的链接", "generateCA": "重新生成根证书", "generateCADescribe": "您确定要生成新的根证书吗? 如果确认,\n则需要重新安装并信任新的证书", "resetDefaultCA": "重置默认根证书", "resetDefaultCADescribe": "确定要重置为默认根证书吗? ProxyPin默认\n根证书对所有用户都是相同的.", "exportCaP12": "导出根证书 (.p12)", "importCaP12": "导入根证书 (.p12)", "trustCa": "信任证书", "exportCA": "导出根证书", "exportPrivateKey": "导出私钥", "profileDownload": "已下载描述文件", "install": "安装", "installCaDescribe": "安装证书 设置 > 已下载描述文件 > 安装", "trustCaDescribe": "信任证书 设置 > 通用 > 关于本机 > 证书信任设置", "androidRoot": "系统证书 (ROOT设备)", "androidRootMagisk": "Magisk模块: \n安卓ROOT设备可以使用Magisk ProxyPinCA系统证书模块, 安装完重启手机后 在系统证书查看是否有ProxyPinCA证书,如果有说明证书安装成功。", "androidRootRename": "模块不生效可以根据网上教程安装系统根证书, 根证书命名成 {name}", "androidUserCA": "用户证书", "androidUserCATips": "提示:Android7+ 很多软件不会信任用户证书", "androidRootCADownload": "下载系统根证书(.0)", "androidUserCAInstall": "打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书", "androidUserXposed": "推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki", "configWifiProxy": "配置手机Wi-Fi代理", "caInstallGuide": "证书安装指南", "caAndroidBrowser": "在 Android 设备上打开浏览器访问:", "caIosBrowser": "在 iOS 设备上打开 Safari访问:", "localIP": "本地IP ", "mobileScan": "配置Wi-Fi代理或使用手机版扫描二维码", "encodeInput": "输入要转换的内容", "encodeResult": "转换结果", "encodeFail": "编码失败", "decodeFail": "解码失败", "shareUrl": "分享请求链接", "shareCurl": "分享 cURL 请求", "shareRequestResponse": "分享请求和响应", "captureDetail": "抓包详情", "proxyPinSoftware": "ProxyPin全平台开源抓包软件", "prompt": "提示", "curlSchemeRequest": "识别到curl格式,是否转换为HTTP请求?", "appExitTips": "再按一次退出程序", "remoteConnectDisconnect": "检查远程连接失败,已断开", "connect": "连接", "reconnect": "重新连接", "remoteConnected": "已连接{os},流量将转发到{os}", "remoteConnectForward": "远程连接,将其他设备流量转发到当前设备", "connectSuccess": "连接成功", "connectedRemote": "已连接远程", "connected": "已连接", "notConnected": "未连接", "inputAddress": "输入地址", "disconnect": "断开连接", "ipLayerProxy": "IP层代理(Beta)", "ipLayerProxyDesc": "IP层代理可抓取Flutter应用请求,目前不是很稳定,欢迎提交PR", "syncConfig": "同步配置", "pullConfigFail": "拉取配置失败, 请检查网络连接", "sync": "同步", "invalidQRCode": "无法识别的二维码", "remoteConnectFail": "连接失败,请检查是否在同一局域网和防火墙是否允许, ios需要开启本地网络权限", "remoteConnectSuccessTips": "手机需要开启抓包才可以抓取请求哦", "windowMode": "窗口模式", "windowModeSubTitle": "开启抓包后 如果应用退回到后台,显示一个小窗口", "pipIcon": "窗口快捷图标", "pipIconDescribe": "展示快捷进入小窗口Icon", "headerExpanded": "Headers自动展开", "headerExpandedSubtitle": "详情页Headers栏是否自动展开", "bottomNavigation": "底部导航", "bottomNavigationSubtitle": "底部导航栏是否显示,重启后生效", "memoryCleanup": "内存清理", "memoryCleanupSubtitle": "到内存限制自动清理请求,清理后保留最近32条请求", "unlimited": "无限制", "custom": "自定义", "externalProxyAuth": "代理认证 (可选)", "externalProxyServer": "代理服务器", "externalProxyConnectFailure": "外部代理连接失败", "externalProxyFailureConfirm": "网络不通所有接口将会访问失败,是否继续设置外部代理。", "mobileDisplayPacketCapture": "手机端是否展示抓包:", "proxyPortRepeat": "启动失败,请检查端口号{port}是否被占用", "reset": "重置", "proxyIgnoreDomain": "代理忽略域名", "domainWhitelistDescribe": "只代理白名单中的域名, 白名单启用黑名单将会失效", "domainBlacklistDescribe": "黑名单中的域名不会代理", "domain": "域名", "enableScript": "启用脚本工具", "scriptUseDescribe": "使用 JavaScript 修改请求和响应", "scriptEdit": "编辑脚本", "scrollEnd": "跟踪滚动", "logger": "日志", "material3": "Material3是谷歌开源设计系统的最新版本", "iosVpnBackgroundAudio": "开启抓包后,退出到后台。为了维护主UI线程的网络通信,将启用静音音频播放以保持主线程运行。否则,它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?", "markRead": "标记已读", "autoRead": "自动已读", "highlight": "高亮", "blue": "蓝色", "green": "绿色", "yellow": "黄色", "red": "红色", "pink": "粉色", "gray": "灰色", "underline": "下划线", "requestBlock": "请求屏蔽", "other": "其他", "certHashName": "证书Hash名称", "systemCertName": "系统证书名称", "regExp": "正则表达式", "qrCode": "二维码", "generateQrCode": "生成二维码", "scanQrCode": "扫描二维码", "saveImage": "保存图片", "selectImage": "选择图片", "inputContent": "输入内容", "errorCorrectLevel": "纠错等级", "output": "输出", "timestamp": "时间戳", "convert": "转换", "time": "时间", "nowTimestamp": "当前时间戳(秒)", "hosts": "Hosts 映射", "toAddress": "映射地址", "encrypt": "加密", "decrypt": "解密", "cipher": "加解密", "appUpdateCheckVersion": "检查更新", "appUpdateNotAvailableMsg": "已是最新版本", "appUpdateDialogTitle": "有可用更新", "appUpdateUpdateMsg": "ProxyPin 的新版本现已推出。您想现在更新吗?", "appUpdateCurrentVersionLbl": "当前版本", "appUpdateNewVersionLbl": "新版本", "appUpdateUpdateNowBtnTxt": "现在更新", "appUpdateLaterBtnTxt": "以后再说", "appUpdateIgnoreBtnTxt": "忽略", "requestMap": "请求映射", "requestMapDescribe": "不请求远程服务,使用本地配置或脚本进行响应", "automatic": "自动", "manual": "手动", "certNotInstalled": "证书未安装", "openNewWindow": "新窗口打开", "sponsorDonate": "赞助 / 捐赠", "sponsorSupport": "支持项目持续开发", "sponsorThanks": "感谢支持开源项目,可选择以下任意方式,帮助项目长期发展", "sponsorAfdian": "爱发电赞助", "sponsorBuyMeCoffee": "Buy Me a Coffee", "privacyPolicy": "隐私协议", "privacyContent": "本项目为开源抓包工具,所有功能均在本地设备上运行;无任何后端服务器,不会收集、存储或上传任何用户信息。抓取的网络数据仅在本地处理,除非您主动使用远程转发功能。所需权限(如网络、存储、相机用于扫码)仅用于实现相应功能。您可在公开的源代码中审计其行为。", "requestCrypto": "请求解密", "cryptoDecoded": "已解密", "cryptoDecodeToggle": "解密", "optional": "可选", "cryptoRuleField": "字段名称", "cryptoIvPrefixLabel": "IV 前缀", "cryptoIvPrefixTooltip": "使用响应体前 N 个字节作为 IV", "local": "本地", "remoteUrl": "远程URL", "view": "查看" } ================================================ FILE: lib/l10n/app_zh_Hant.arb ================================================ { "breakpoint": "斷點", "breakpointRule": "斷點規則", "requests": "抓包", "favorites": "收藏", "history": "歷史", "toolbox": "工具箱", "preference": "偏好設定", "feedback": "意見回饋", "about": "關於", "filter": "代理過濾", "script": "腳本", "share": "分享", "port": "連接埠號: ", "proxy": "代理", "externalProxy": "外部代理設定", "username": "使用者名稱", "password": "密碼", "proxySetting": "代理設定", "setAs": "設定為", "systemProxy": "系統代理", "enabledHTTP2": "啟用HTTP2", "serverNotStart": "未開啟抓包", "download": "下載", "config": "設定", "version": "版本", "start": "開始", "stop": "停止", "clear": "清空", "httpsProxy": "HTTPS 代理", "setting": "設定", "mobileConnect": "手機連接", "connectRemote": "連接終端", "remoteDevice": "遠端裝置", "remoteDeviceList": "遠端裝置列表", "myQRCode": "我的二維碼", "theme": "主題", "themeColor": "主題顏色", "followSystem": "跟隨系統", "themeLight": "淺色", "themeDark": "深色", "language": "語言", "autoStartup": "自動開啟抓包", "autoStartupDescribe": "程式啟動時自動開始記錄流量", "copied": "已複製到剪切板", "execute": "執行", "cancel": "取消", "close": "關閉", "save": "儲存", "confirm": "確認", "confirmTitle": "確認操作", "confirmContent": "是否確認此操作?", "addSuccess": "新增成功", "saveSuccess": "儲存成功", "operationSuccess": "操作成功", "import": "匯入", "importSuccess": "匯入成功", "importFailed": "匯入失敗", "export": "匯出", "exportSuccess": "匯出成功", "deleteSuccess": "刪除成功", "send": "傳送", "fail": "失敗", "success": "成功", "emptyData": "無資料", "requestSuccess": "請求成功", "add": "新增", "all": "全部", "modify": "修改", "responseType": "回應類型", "request": "請求", "response": "回應", "statusCode": "狀態碼", "duration": "耗時", "done": "完成", "type": "類型", "enable": "啟用", "example": "範例: ", "responseHeader": "回應標頭", "requestHeader": "請求標頭", "requestLine": "請求行", "requestMethod": "請求方法", "param": "參數", "replaceBodyWith": "訊息體替換為:", "redirectTo": "重新導向到:", "redirect": "重新導向", "cannotBeEmpty": "不能為空", "requestRewriteList": "請求重寫列表", "requestRewriteRule": "請求重寫規則", "requestRewriteEnable": "是否啟用請求重寫", "action": "行為", "multiple": "多選", "edit": "編輯", "disabled": "停用", "requestRewriteDeleteConfirm": "是否刪除{size}條規則?", "useGuide": "使用文件", "pleaseEnter": "請輸入", "click": "點選", "replace": "替換", "clickEdit": "點選編輯", "refresh": "重新整理", "selectFile": "選擇檔案", "match": "符合", "value": "值", "matchRule": "符合規則", "emptyMatchAll": "為空表示符合全部", "newBuilt": "新建", "reportServers": "上報伺服器", "addReportServer": "新增上報伺服器", "editReportServer": "編輯上報伺服器", "serverUrl": "伺服器 URL", "compression": "壓縮", "compressionNone": "無", "newFolder": "新建資料夾", "enableSelect": "啟用選擇", "disableSelect": "停用選擇", "deleteSelect": "刪除選擇", "testData": "測試資料", "noChangesDetected": "未檢測到變更", "enterMatchData": "輸入待符合的資料", "modifyRequestHeader": "修改請求標頭", "headerName": "請求標頭名稱", "headerValue": "請求標頭值", "deleteHeaderConfirm": "是否刪除該請求標頭", "sequence": "全部請求", "domainList": "網域名稱列表", "domainWhitelist": "代理網域名稱白名單", "domainBlacklist": "代理網域名稱黑名單", "appWhitelist": "應用程式白名單", "appWhitelistDescribe": "只代理白名單中的應用程式, 白名單啟用黑名單將會失效", "appBlacklist": "應用程式黑名單", "domainFilter": "網域名稱代理列表", "scanCode": "掃碼連接", "addBlacklist": "新增代理黑名單", "addWhitelist": "新增代理白名單", "deleteWhitelist": "刪除代理白名單", "domainListSubtitle": "最後請求時間: {time}, 次數: {count}", "selectAction": "選擇操作", "copy": "複製", "copyHost": "複製網域名稱", "copyUrl": "複製URL", "copyRawRequest": "複製原始請求", "copyRequestResponse": "複製 請求和回應", "copyCurl": "複製 cURL", "copyAsPythonRequests": "複製 Python Requests", "delete": "刪除", "rename": "重新命名", "repeat": "重放", "repeatAllRequests": "重放所有請求", "repeatDomainRequests": "重放網域名稱下請求", "customRepeat": "進階重放", "repeatCount": "次數", "repeatInterval": "間隔(ms)", "repeatDelay": "延遲(ms)", "scheduleTime": "指定時間", "fixed": "固定", "random": "隨機", "keepCustomSettings": "保持自訂設定", "editRequest": "編輯請求", "reSendRequest": "已重新傳送請求", "viewExport": "檢視匯出", "timeDesc": "按時間降序", "timeAsc": "按時間升序", "search": "搜尋", "clearSearch": "清除搜尋", "requestType": "請求類型", "keyword": "關鍵字", "keywordSearchScope": "關鍵字搜尋範圍: ", "favorite": "收藏", "deleteFavorite": "刪除收藏", "emptyFavorite": "暫無收藏", "deleteFavoriteSuccess": "已刪除收藏", "name": "名稱", "historyRecord": "歷史記錄", "historyManualSave": "手動儲存", "historyDay": "{day}天", "historyForever": "永久", "historyCacheTime": "快取時間", "historyEmptyName": "名稱不能為空", "historyRecordTitle": "{name} 記錄數 {length}", "historySubtitle": "記錄數 {requestLength} 檔案 {size}", "historyUnSave": "目前對話記錄未儲存", "historyDeleteConfirm": "是否刪除該歷史記錄?", "requestEdit": "請求編輯", "encode": "編碼", "decode": "解碼", "requestBody": "請求體", "responseBody": "回應體", "requestRewrite": "請求重寫", "newWindow": "新視窗開啟", "httpRequest": "HTTP請求", "enabledHttps": "啟用HTTPS代理", "installRootCa": "安裝根憑證", "installCaLocal": "安裝根憑證到本機", "downloadRootCa": "下載根憑證", "downloadRootCaNote": "注意:如果您將預設瀏覽器設定為 Safari 以外的瀏覽器,請點選此行複製並貼上 Safari 瀏覽器的連結", "generateCA": "重新產生根憑證", "generateCADescribe": "您確定要產生新的根憑證嗎? 如果確認,\n則需要重新安裝並信任新的憑證", "resetDefaultCA": "重置預設根憑證", "resetDefaultCADescribe": "確定要重置為預設根憑證嗎? ProxyPin預設\n根憑證對所有使用者都是相同的.", "exportCaP12": "匯出根憑證 (.p12)", "importCaP12": "匯入根憑證 (.p12)", "trustCa": "信任憑證", "exportCA": "匯出根憑證", "exportPrivateKey": "匯出私鑰", "profileDownload": "已下載描述檔案", "install": "安裝", "installCaDescribe": "安裝憑證 設定 > 已下載描述檔案 > 安裝", "trustCaDescribe": "信任憑證 設定 > 一般 > 關於本機 > 憑證信任設定", "androidRoot": "系統憑證 (ROOT裝置)", "androidRootMagisk": "Magisk模組: \n安卓ROOT裝置可以使用Magisk ProxyPinCA系統憑證模組, 安裝完重新開機後 在系統憑證檢視是否有ProxyPinCA憑證,如果有說明憑證安裝成功。", "androidRootRename": "模組不生效可以根據網上教學安裝系統根憑證, 根憑證命名成 {name}", "androidUserCA": "使用者憑證", "androidUserCATips": "提示:Android7+ 很多軟體不會信任使用者憑證", "androidRootCADownload": "下載系統根憑證(.0)", "androidUserCAInstall": "開啟設定 -> 安全性 -> 加密和憑證 -> 安裝憑證 -> CA 憑證", "androidUserXposed": "推薦使用Xposed模組抓包(無需ROOT), 點選檢視wiki", "configWifiProxy": "設定手機Wi-Fi代理", "caInstallGuide": "憑證安裝指南", "caAndroidBrowser": "在 Android 裝置上開啟瀏覽器存取:", "caIosBrowser": "在 iOS 裝置上開啟 Safari存取:", "localIP": "本機IP ", "mobileScan": "設定Wi-Fi代理或使用手機版掃描二維碼", "encodeInput": "輸入要轉換的內容", "encodeResult": "轉換結果", "encodeFail": "編碼失敗", "decodeFail": "解碼失敗", "shareUrl": "分享請求連結", "shareCurl": "分享 cURL 請求", "shareRequestResponse": "分享請求和回應", "captureDetail": "抓包詳情", "proxyPinSoftware": "ProxyPin全平台開源抓包軟體", "prompt": "提示", "curlSchemeRequest": "識別到curl格式,是否轉換為HTTP請求?", "appExitTips": "再按一次退出程式", "remoteConnectDisconnect": "檢查遠端連接失敗,已中斷連接", "connect": "連接", "reconnect": "重新連接", "remoteConnected": "已連接{os},流量將轉發到{os}", "remoteConnectForward": "遠端連接,將其他裝置流量轉發到目前裝置", "connectSuccess": "連接成功", "connectedRemote": "已連接遠端", "connected": "已連接", "notConnected": "未連接", "inputAddress": "輸入地址", "disconnect": "中斷連接", "ipLayerProxy": "IP層代理(Beta)", "ipLayerProxyDesc": "IP層代理可抓取Flutter應用程式請求,目前不是很穩定,歡迎提交PR", "syncConfig": "同步設定", "pullConfigFail": "拉取設定失敗, 請檢查網路連接", "sync": "同步", "invalidQRCode": "無法識別的二維碼", "remoteConnectFail": "連接失敗,請檢查是否在同一區域網路和防火牆是否允許, ios需要開啟本機網路權限", "remoteConnectSuccessTips": "手機需要開啟抓包才可以抓取請求哦", "windowMode": "視窗模式", "windowModeSubTitle": "開啟抓包後 如果應用程式退回到背景,顯示一個小視窗", "pipIcon": "視窗快捷圖示", "pipIconDescribe": "展示快捷進入小視窗Icon", "headerExpanded": "Headers自動展開", "headerExpandedSubtitle": "詳情頁Headers欄是否自動展開", "bottomNavigation": "底部導航", "bottomNavigationSubtitle": "底部導航欄是否顯示,重新啟動後生效", "memoryCleanup": "記憶體清理", "memoryCleanupSubtitle": "到記憶體限制自動清理請求,清理後保留最近32條請求", "unlimited": "無限制", "custom": "自訂", "externalProxyAuth": "代理認證 (可選)", "externalProxyServer": "代理伺服器", "externalProxyConnectFailure": "外部代理連接失敗", "externalProxyFailureConfirm": "網路不通所有介面將會存取失敗,是否繼續設定外部代理。", "mobileDisplayPacketCapture": "手機端是否展示抓包:", "proxyPortRepeat": "啟動失敗,請檢查連接埠號{port}是否被占用", "reset": "重置", "proxyIgnoreDomain": "代理忽略網域名稱", "domainWhitelistDescribe": "只代理白名單中的網域名稱, 白名單啟用黑名單將會失效", "domainBlacklistDescribe": "黑名單中的網域名稱不會代理", "domain": "網域名稱", "enableScript": "啟用腳本工具", "scriptUseDescribe": "使用 JavaScript 修改請求和回應", "scriptEdit": "編輯腳本", "scrollEnd": "跟蹤滾動", "logger": "日誌", "material3": "Material3是Google開源設計系統的最新版本", "iosVpnBackgroundAudio": "開啟抓包後,退出到背景。為了維護主UI執行緒的網路通信,將啟用靜音音訊播放以保持主執行緒運作。否則,它將只在背景運作30秒。您同意在啟用抓包後在背景播放音訊嗎?", "markRead": "標記已讀", "autoRead": "自動已讀", "highlight": "高亮顯示", "blue": "藍色", "green": "綠色", "yellow": "黃色", "red": "紅色", "pink": "粉色", "gray": "灰色", "underline": "底線", "requestBlock": "請求阻擋", "other": "其他", "certHashName": "憑證Hash名稱", "systemCertName": "系統憑證名稱", "regExp": "正規表示式", "qrCode": "二維碼", "generateQrCode": "產生二維碼", "scanQrCode": "掃描二維碼", "saveImage": "儲存圖片", "selectImage": "選擇圖片", "inputContent": "輸入內容", "errorCorrectLevel": "糾錯等級", "output": "輸出", "timestamp": "時間戳", "convert": "轉換", "time": "時間", "nowTimestamp": "目前時間戳(秒)", "hosts": "Hosts 對應", "toAddress": "對應地址", "encrypt": "加密", "decrypt": "解密", "cipher": "密文", "requestCrypto": "請求解密", "cryptoDecoded": "已解密", "cryptoDecodeToggle": "解密", "optional": "可選", "cryptoRuleField": "字段", "cryptoIvPrefixLabel": "IV 前綴", "cryptoIvPrefixTooltip": "使用回應內容的前 N 個字節作為 IV", "appUpdateCheckVersion": "檢查更新", "appUpdateNotAvailableMsg": "已是最新版本", "appUpdateDialogTitle": "有可用更新", "appUpdateUpdateMsg": "ProxyPin 的新版本現已推出。您想現在更新嗎?", "appUpdateCurrentVersionLbl": "目前版本", "appUpdateNewVersionLbl": "新版本", "appUpdateUpdateNowBtnTxt": "現在更新", "appUpdateLaterBtnTxt": "稍後再說", "appUpdateIgnoreBtnTxt": "忽略", "requestMap": "請求映射", "requestMapDescribe": "不請求遠端服務,使用本地配置或腳本進行回應", "automatic": "自動", "manual": "手動", "certNotInstalled": "未安裝憑證", "openNewWindow": "新視窗開啟", "sponsorDonate": "贊助 / 捐贈", "sponsorSupport": "支持項目持續開發", "sponsorThanks": "感謝支持開源項目,可選擇以下任意方式,幫助項目長期發展", "sponsorAfdian": "愛發電贊助", "sponsorBuyMeCoffee": "Buy Me a Coffee", "privacyPolicy": "隱私協議", "privacyContent": "本專案為開源抓包工具,所有功能均在本機裝置上運行;無任何後端伺服器,不會蒐集、儲存或上傳任何使用者資訊。擷取的網路資料僅在本機處理,除非您主動使用遠端轉發功能。所需權限(如網路、儲存、相機用於掃碼)僅用於實現相應功能。您可在公開的原始碼中稽核其行為。", "local": "本地", "remoteUrl": "遠端URL", "view": "檢視" } ================================================ FILE: lib/main.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/ui/component/chinese_font.dart'; import 'package:proxypin/ui/component/multi_window.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/desktop/desktop.dart'; import 'package:proxypin/ui/mobile/mobile.dart'; import 'package:proxypin/utils/desktop_support.dart'; import 'package:proxypin/utils/navigator.dart'; import 'package:proxypin/utils/platform.dart'; import 'l10n/app_localizations.dart'; ///主入口 ///@author wanghongen void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); //多窗口 if (args.firstOrNull == 'multi_window') { final windowId = int.parse(args[1]); final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map; runApp(FluentApp(multiWindow(windowId, argument), (await AppConfiguration.instance))); return; } var instance = AppConfiguration.instance; var configuration = Configuration.instance; //移动端 if (Platforms.isMobile()) { var appConfiguration = await instance; runApp(FluentApp(MobileHomePage((await configuration), appConfiguration), appConfiguration)); return; } var appConfiguration = await instance; if (Platforms.isDesktop()) { await DesktopSupport.initialize(appConfiguration); } runApp(FluentApp(DesktopHomePage(await configuration, appConfiguration), appConfiguration)); } class FluentApp extends StatelessWidget { final Widget home; final AppConfiguration appConfiguration; const FluentApp(this.home, this.appConfiguration, {super.key}); @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: appConfiguration.globalChange, builder: (_, current, __) { return MaterialApp( title: 'ProxyPin', debugShowCheckedModeBanner: false, navigatorKey: navigatorHelper.navigatorKey, theme: theme(Brightness.light), darkTheme: theme(Brightness.dark), themeMode: appConfiguration.themeMode, locale: appConfiguration.language, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: home, ); }); } ThemeData theme(Brightness brightness) { bool useMaterial3 = appConfiguration.useMaterial3; bool isDark = brightness == Brightness.dark; Color? themeColor = isDark ? appConfiguration.themeColor : appConfiguration.themeColor; Color? cardColor = isDark ? Color(0XFF3C3C3C) : Colors.white; Color? surfaceContainer = isDark ? Colors.grey[800] : Colors.white; Color? secondary = useMaterial3 ? null : themeColor; if (themeColor is MaterialColor) { secondary = themeColor[500]; } var colorScheme = ColorScheme.fromSeed( brightness: brightness, seedColor: themeColor, primary: themeColor, surface: cardColor, secondary: secondary, onPrimary: isDark ? Colors.white : null, surfaceContainer: surfaceContainer, surfaceContainerHigh: surfaceContainer, ); var themeData = ThemeData(brightness: brightness, useMaterial3: appConfiguration.useMaterial3, colorScheme: colorScheme); if (!appConfiguration.useMaterial3) { themeData = themeData.copyWith( appBarTheme: themeData.appBarTheme.copyWith( iconTheme: themeData.iconTheme.copyWith(size: 20), backgroundColor: themeData.canvasColor, elevation: 0, titleTextStyle: themeData.textTheme.titleMedium, ), tabBarTheme: themeData.tabBarTheme.copyWith( labelColor: themeData.colorScheme.primary, indicatorColor: themeColor, unselectedLabelColor: themeData.textTheme.titleMedium?.color, ), ); } if (Platform.isWindows) { themeData = themeData.useSystemChineseFont(); } return themeData.copyWith( dialogTheme: themeData.dialogTheme.copyWith(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)))); } } ================================================ FILE: lib/native/app_lifecycle.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:proxypin/network/util/logger.dart'; abstract interface class LifecycleListener { void onUserLeaveHint() {} void onPictureInPictureModeChanged(bool isInPictureInPictureMode) {} } class AppLifecycleBinding { static const MethodChannel _methodChannel = MethodChannel('com.proxy/appLifecycle'); //单例对象 static AppLifecycleBinding get instance { _instance ??= AppLifecycleBinding._(); return _instance!; } final List _listeners = []; static AppLifecycleBinding? _instance; AppLifecycleBinding._() { //注册方法 _methodChannel.setMethodCallHandler(_methodCallHandler); } static AppLifecycleBinding ensureInitialized() { return AppLifecycleBinding.instance; } void addListener(LifecycleListener listener) { if (_listeners.contains(listener)) return; _listeners.add(listener); } void removeListener(LifecycleListener listener) { _listeners.remove(listener); } Future _methodCallHandler(MethodCall call) async { logger.d("AppLifecycle methodCallHandler ${call.method}"); switch (call.method) { case 'appDetached': await WidgetsBinding.instance.handleRequestAppExit(); break; case 'onUserLeaveHint': for (var listener in _listeners) { listener.onUserLeaveHint(); } break; case 'onPictureInPictureModeChanged': for (var listener in _listeners) { listener.onPictureInPictureModeChanged(call.arguments); } break; } return Future.value(); } } ================================================ FILE: lib/native/installed_apps.dart ================================================ import 'package:flutter/services.dart'; class InstalledApps { static const MethodChannel _methodChannel = MethodChannel('com.proxy/installedApps'); static Future> getInstalledApps( bool withIcon, { String? packageNamePrefix, bool includeSystemApps = false, }) { return _methodChannel.invokeListMethod('getInstalledApps', { "withIcon": withIcon, "packageNamePrefix": packageNamePrefix, "includeSystemApps": includeSystemApps, }).then((value) => value?.map((e) => AppInfo.formJson(e)).toList() ?? []); } static Future getAppInfo(String packageName) async { return _methodChannel .invokeMethod('getAppInfo', {"packageName": packageName}).then((value) => AppInfo.formJson(value!)); } } class AppInfo { String? name; String? packageName; String? versionName; //icon Uint8List? icon; bool? inValid; AppInfo({ this.name, this.packageName, this.versionName, this.icon, this.inValid, }); AppInfo.formJson(Map json) { name = json['name']; packageName = json['packageName']; versionName = json['versionName']; icon = json['icon']; } Map toJson() { final Map data = {}; data['name'] = name; data['packageName'] = packageName; data['versionName'] = versionName; data['icon'] = icon; return data; } @override String toString() { return toJson().toString(); } @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other is AppInfo) { return packageName == other.packageName; } return false; } @override int get hashCode => packageName.hashCode; } ================================================ FILE: lib/native/native_method.dart ================================================ import 'package:flutter/services.dart'; import 'package:proxypin/network/util/logger.dart'; class NativeMethod { static const MethodChannel _channel = MethodChannel('com.proxypin/method'); /// 检查本地网络(Wi-Fi 或以太网)是否可用 (仅限 iOS)。 /// /// 返回 `true` 如果本地网络可用,否则返回 `false`。 static Future requestLocalNetworkAccess() async { try { final bool isAvailable = await _channel.invokeMethod('requestLocalNetwork'); logger.d("[NativeMethod] requestLocalNetworkAccess => $isAvailable"); return isAvailable; } on PlatformException catch (e) { logger.e("[NativeMethod] requestLocalNetworkAccess error: '${e.message}'."); return false; } } /// iOS: 检查给定 PEM 证书是否已安装到系统钥匙串 static Future isCaInstalled(String pem) async { try { final bool installed = await _channel.invokeMethod('isCaInstalled', {"pem": pem}); return installed; } on PlatformException catch (e) { logger.e("[NativeMethod] isCaInstalled error: ${e.message}"); return false; } } /// iOS: 基于 SSL 策略校验证书链(leaf + CA),仅当 CA 被系统信任时返回 true static Future evaluateChainTrusted(String leafPem, String caPem, {String? host}) async { try { final bool trusted = await _channel.invokeMethod('evaluateChainTrusted', { 'leafPem': leafPem, 'caPem': caPem, if (host != null) 'host': host, }); return trusted; } on PlatformException catch (e) { logger.e("[NativeMethod] evaluateChainTrusted error: ${e.message}"); return false; } } } ================================================ FILE: lib/native/pip.dart ================================================ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:proxypin/native/vpn.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/launch/launch.dart'; import 'package:proxypin/ui/mobile/mobile.dart'; import 'package:proxypin/utils/lang.dart'; ///画中画 class PictureInPicture { static bool inPip = false; static final MethodChannel _channel = const MethodChannel('com.proxy/pictureInPicture') ..setMethodCallHandler((call) async { logger.d("pictureInPicture MethodCallHandler ${call.method}"); if (call.method == 'cleanSession') { MobileApp.requestStateKey.currentState?.clean(); } else if (call.method == 'exitPictureInPictureMode') { inPip = false; Vpn.isRunning().then((value) { Vpn.isVpnStarted = value; SocketLaunch.startStatus.value = ValueWrap.of(value); }); } return Future.value(); }); ///进入画中画模式 static Future enterPictureInPictureMode(String host, int port, {List? appList, List? disallowApps}) async { final bool enterPictureInPictureMode = await _channel.invokeMethod('enterPictureInPictureMode', {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps}); inPip = true; return enterPictureInPictureMode; } ///退出画中画模式 static Future exitPictureInPictureMode() async { final bool exitPictureInPictureMode = await _channel.invokeMethod('exitPictureInPictureMode'); return exitPictureInPictureMode; } ///发送数据 static Future addData(String text) async { if (Platform.isIOS && inPip) { _channel.invokeMethod('addData', text.fixAutoLines()); } return false; } } ================================================ FILE: lib/native/process_info.dart ================================================ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/util/process_info.dart'; class ProcessInfoPlugin { static const MethodChannel _methodChannel = MethodChannel('com.proxy/processInfo'); static Future getProcessByPort(String host, int port) { return _methodChannel.invokeMethod('getProcessByPort', {"host": host, "port": port}).then((process) { if (process == null) return null; return ProcessInfo(process['packageName'], process['name'], process['packageName'], os: Platform.operatingSystem, icon: process['icon'], remoteHost: process['remoteHost'], remotePost: process['remotePort']); }); } static Future getRemoteAddressByPort(int port) async { if (!Platform.isAndroid) return null; return _methodChannel.invokeMethod('getRemoteAddressByPort', {"port": port}).then((process) { if (process == null) return null; return HostAndPort.host(process['remoteHost'], process['remotePort']); }); } } ================================================ FILE: lib/native/vpn.dart ================================================ import 'package:flutter/services.dart'; import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/util/logger.dart'; class Vpn { static const MethodChannel proxyVpnChannel = MethodChannel('com.proxy/proxyVpn'); static bool isVpnStarted = false; //vpn是否已经启动 static void startVpn(String host, int port, Configuration configuration, {bool? ipProxy = false}) { List? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : []; List? disallowApps; if (appList.isEmpty) { disallowApps = configuration.appBlacklist ?? []; } logger.d("Starting VPN with host: $host, port: $port, proxyPassDomains: ${configuration.proxyPassDomains.split(';')}"); proxyVpnChannel.invokeMethod("startVpn", { "proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps, "ipProxy": ipProxy, "setSystemProxy": configuration.enableSystemProxy, "proxyPassDomains": configuration.proxyPassDomains.split(';'), }); isVpnStarted = true; } static void stopVpn() { proxyVpnChannel.invokeMethod("stopVpn"); isVpnStarted = false; } //重启vpn static void restartVpn(String host, int port, Configuration configuration, {bool ipProxy = false}) { List? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : []; List? disallowApps; if (appList.isEmpty) { disallowApps = configuration.appBlacklist ?? []; } proxyVpnChannel.invokeMethod("restartVpn", { "proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps, "ipProxy": ipProxy, "setSystemProxy": configuration.enableSystemProxy, "proxyPassDomains": configuration.proxyPassDomains.split(';'), }); isVpnStarted = true; } static Future isRunning() async { return await proxyVpnChannel.invokeMethod("isRunning"); } } ================================================ FILE: lib/network/bin/configuration.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/util/file_read.dart'; import 'package:proxypin/network/components/host_filter.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/system_proxy.dart'; import 'package:proxypin/utils/platform.dart'; class Configuration { ///代理相关配置 int port = 9099; //是否启用https抓包 bool enableSsl = Platforms.isMobile(); //是否设置系统代理 bool enableSystemProxy = true; //代理忽略域名 String proxyPassDomains = SystemProxy.proxyPassDomains; //enabled socks5 proxy bool enableSocks5 = true; //外部代理 ProxyInfo? externalProxy; //白名单应用 List appWhitelist = []; //白名单应用是否启用 bool appWhitelistEnabled = true; //应用黑名单 List? appBlacklist; //远程连接 不持久化保存 String? remoteHost; bool enabledHttp2 = false; // 是否启用http2 //历史记录缓存时间 int historyCacheTime = 0; //默认是否启动 bool startup = false; Configuration._(); /// 单例 static Configuration? _instance; static Future get instance async { if (_instance == null) { try { var loadConfig = await _loadConfig(); _instance = Configuration.fromJson(loadConfig); } catch (e) { logger.e('初始化配置失败', error: e, stackTrace: StackTrace.current); _instance = Configuration._(); } } return _instance!; } /// 加载配置 Configuration.fromJson(Map config) { port = config['port'] ?? port; enableSsl = config['enableSsl'] == true; startup = config['startup'] ?? Platforms.isDesktop(); enableSystemProxy = config['enableSystemProxy'] ?? (config['enableDesktop'] ?? true); enableSocks5 = config['enableSocks5'] ?? true; enabledHttp2 = config['enabledHttp2'] ?? false; proxyPassDomains = config['proxyPassDomains'] ?? SystemProxy.proxyPassDomains; historyCacheTime = config['historyCacheTime'] ?? 0; if (config['externalProxy'] != null) { externalProxy = ProxyInfo.fromJson(config['externalProxy']); } appWhitelist = List.from(config['appWhitelist'] ?? []); appWhitelistEnabled = config['appWhitelistEnabled'] ?? true; appBlacklist = config['appBlacklist'] == null ? null : List.from(config['appBlacklist']); HostFilter.whitelist.load(config['whitelist']); HostFilter.blacklist.load(config['blacklist']); } /// 配置文件 static Future configFile() async { var separator = Platform.pathSeparator; var home = await FileRead.homeDir(); return File("${home.path}${separator}config.cnf"); } /// 刷新配置文件 Future flushConfig() async { var file = await configFile(); var exists = await file.exists(); if (!exists) { file = await file.create(recursive: true); } HostFilter.whitelist.toJson(); HostFilter.blacklist.toJson(); var json = jsonEncode(toJson()); logger.d('Refresh configuration file $runtimeType ${toJson()}'); file.writeAsString(json); } /// 加载配置文件 static Future> _loadConfig() async { var file = await configFile(); var exits = await file.exists(); if (!exits) { return {}; } Map config = jsonDecode(await file.readAsString()); logger.i('加载配置文件 [$file]'); return config; } Map toJson() { return { 'port': port, 'enableSsl': enableSsl, 'startup': startup, 'enableSystemProxy': enableSystemProxy, 'enableSocks5': enableSocks5, 'proxyPassDomains': proxyPassDomains, 'externalProxy': externalProxy?.toJson(), 'appWhitelist': appWhitelist, 'appWhitelistEnabled': appWhitelistEnabled, 'appBlacklist': appBlacklist, 'historyCacheTime': historyCacheTime, 'enabledHttp2': enabledHttp2, 'whitelist': HostFilter.whitelist.toJson(), 'blacklist': HostFilter.blacklist.toJson(), }; } } ================================================ FILE: lib/network/bin/listener.dart ================================================ import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; ///请求和响应事件监听 abstract class EventListener { void onRequest(Channel channel, HttpRequest request); void onResponse(ChannelContext channelContext, HttpResponse response); void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {} } class CombinedEventListener extends EventListener { final List listeners; CombinedEventListener(this.listeners); @override void onRequest(Channel channel, HttpRequest request) { for (var element in listeners) { element.onRequest(channel, request); } } @override void onResponse(ChannelContext channelContext, HttpResponse response) { for (var element in listeners) { element.onResponse(channelContext, response); } } @override void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) { for (var element in listeners) { element.onMessage(channel, message, frame); } } } ================================================ FILE: lib/network/bin/server.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/components/hosts.dart'; import 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/components/report_server_interceptor.dart'; import 'package:proxypin/network/components/request_block.dart'; import 'package:proxypin/network/components/request_rewrite.dart'; import 'package:proxypin/network/components/script.dart'; import 'package:proxypin/network/handle/http_proxy_handle.dart'; import 'package:proxypin/network/util/crts.dart'; import 'package:proxypin/utils/platform.dart'; import '../components/request_map.dart'; import '../http/codec.dart'; import '../channel/network.dart'; import '../util/logger.dart'; import '../util/system_proxy.dart'; import 'listener.dart'; import 'package:proxypin/network/components/request_breakpoint.dart'; Future main() async { var configuration = await Configuration.instance; ProxyServer(configuration).start(); } /// 代理服务器 class ProxyServer { static ProxyServer? current; //socket服务 Server? server; //请求事件监听 List listeners = []; //配置 final Configuration configuration; ProxyServer(this.configuration) { current = this; } //是否启动 bool get isRunning => server?.isRunning ?? false; ///是否启用https抓包 bool get enableSsl => configuration.enableSsl; int get port => configuration.port; set enableSsl(bool enableSsl) { configuration.enableSsl = enableSsl; if (server == null || server?.isRunning == false) { return; } if (configuration.enableSystemProxy) { SystemProxy.setSslProxyEnable(enableSsl, port); } } /// 启动代理服务 Future start() async { Server server = Server(configuration, listener: CombinedEventListener(listeners)); List interceptors = [ Hosts(), RequestMapInterceptor.instance, RequestRewriteInterceptor.instance, ScriptInterceptor(), RequestBlockInterceptor(), RequestBreakpointInterceptor.instance, // Register the interceptor ReportServerInterceptor() ]; interceptors.sort((a, b) => a.priority.compareTo(b.priority)); server.initChannel((channel) { channel.dispatcher.handle( HttpRequestCodec(), HttpResponseCodec(), HttpProxyChannelHandler(listener: CombinedEventListener(listeners), interceptors: interceptors), ); }); return server.bind(port).then((serverSocket) { logger.i("listen on $port"); this.server = server; if (configuration.enableSystemProxy) { setSystemProxyEnable(true); } //初始化证书 CertificateManager.initCAConfig(); return server; }); } /// 停止代理服务 Future stop() async { if (!isRunning) { return server; } if (configuration.enableSystemProxy) { await setSystemProxyEnable(false); } logger.i("stop on $port"); await server?.stop(); return server; } /// 设置系统代理 Future setSystemProxyEnable(bool enable) async { if (!Platforms.isDesktop()) { return; } //关闭系统代理 恢复成外部代理地址 if (!enable && configuration.externalProxy?.enabled == true) { await SystemProxy.setSystemProxy(configuration.externalProxy!.port!, enableSsl, configuration.proxyPassDomains); return; } await SystemProxy.setSystemProxyEnable(port, enable, enableSsl, passDomains: configuration.proxyPassDomains); } /// 重启代理服务 Future restart() async { await stop().whenComplete(() => start()); } ///检查是否监听端口 没有监听则启动 Future retryBind() async { try { await Socket.connect('127.0.0.1', port, timeout: const Duration(milliseconds: 350)); } catch (e) { logger.d('端口未被占用,尝试重新绑定 $port'); await restart(); } } ///添加监听器 void addListener(EventListener listener) { listeners.add(listener); } } ================================================ FILE: lib/network/channel/channel.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'dart:math'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/socket_address.dart'; import 'channel_dispatcher.dart'; ///处理I/O事件或截获I/O操作 ///[T] 读取的数据类型 ///@author wanghongen abstract class ChannelHandler { var log = logger; ///连接建立 void channelActive(ChannelContext context, Channel channel) {} ///读取数据事件 Future channelRead(ChannelContext channelContext, Channel channel, T msg) async {} ///连接断开 void channelInactive(ChannelContext channelContext, Channel channel) { //log.i("[${channel.id}] close $channel"); } void exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) { HostAndPort? host = channelContext.host; log.e("[${channel.id}] exceptionCaught $host $channel", error: error, stackTrace: trace); channel.close(); } } ///与网络套接字或组件的连接,能够进行读、写、连接和绑定等I/O操作。 class Channel { final int _id; final ChannelDispatcher dispatcher = ChannelDispatcher(); Socket _socket; //是否打开 bool isOpen = true; //此通道连接到的远程地址 final InetSocketAddress remoteSocketAddress; //是否写入中 bool isWriting = false; Object? error; //异常 //是否使用代理 bool useProxy = false; Channel(this._socket) : _id = DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999), remoteSocketAddress = InetSocketAddress(_socket.remoteAddress, _socket.remotePort); ///返回此channel的全局唯一标识符。 String get id => _id.toRadixString(36); Socket get socket => _socket; void serverSecureSocket(SecureSocket secureSocket, ChannelContext channelContext) { _socket = secureSocket; _socket.done.then((value) => isOpen = false); dispatcher.listen(this, channelContext); } //向远程发起ssl连接 Future secureSocket(ChannelContext channelContext, {String? host, List? supportedProtocols}) async { SecureSocket secureSocket = await SecureSocket.secure(socket, host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true); _socket = secureSocket; _socket.done.then((value) => isOpen = false); dispatcher.listen(this, channelContext); return secureSocket; } Future startSecureSocket(ChannelContext channelContext, {String? host, List? supportedProtocols}) async { SecureSocket secureSocket = await SecureSocket.secure(socket, host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true); _socket = secureSocket; _socket.done.then((value) => isOpen = false); return secureSocket; } void listen(ChannelContext channelContext) { dispatcher.listen(this, channelContext); } String? get selectedProtocol => isSsl && isOpen ? (_socket as SecureSocket).selectedProtocol : null; ///是否是ssl链接 bool get isSsl => _socket is SecureSocket; Future write(ChannelContext channelContext, Object obj) async { var data = dispatcher.encoder.encode(channelContext, obj); await writeBytes(data); } Future writeBytes(List bytes) async { if (isClosed) { logger.w("[$id] $remoteSocketAddress channel is closed", stackTrace: StackTrace.current); } //只能有一个写入 int retry = 0; while (isWriting && retry++ < 30) { await Future.delayed(const Duration(milliseconds: 100)); } if (isWriting) { logger.d("[$id] write busy"); } isWriting = true; try { if (!isClosed) { _socket.add(bytes); } } catch (e, t) { if (e is StateError && e.message == "StreamSink is closed") { logger.w("[$id] $remoteSocketAddress write error channel is closed $e", stackTrace: t); } else { logger.e("[$id] write error", error: e, stackTrace: t); } } finally { isWriting = false; } } ///写入并关闭此channel Future writeAndClose(ChannelContext channelContext, Object obj) async { await write(channelContext, obj); close(); } ///关闭此channel void close() async { if (isClosed) { return; } //写入中,延迟关闭 int retry = 0; while (isWriting && retry++ < 10) { await Future.delayed(const Duration(milliseconds: 150)); } isOpen = false; // if (!isWriting) { // await _socket.flush(); // } await _socket.close(); // _socket.destroy(); } ///返回此channel是否打开 bool get isClosed => !isOpen; @override String toString() { return 'Channel($id $remoteSocketAddress'; } } ================================================ FILE: lib/network/channel/channel_context.dart ================================================ import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/codec.dart'; import 'package:proxypin/network/http/h2/frame.dart'; import 'package:proxypin/network/http/h2/setting.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/attribute_keys.dart'; import 'package:proxypin/network/util/process_info.dart'; import 'package:proxypin/utils/lang.dart'; import '../bin/listener.dart'; import 'network.dart'; /// class ChannelContext { final Map _attributes = {}; //和本地客户端的连接 Channel? clientChannel; //和远程服务端的连接 Channel? serverChannel; EventListener? listener; //http2 stream final Map> _streams = {}; final Map _streamDependency = {}; ChannelContext(); //创建服务端连接 Future connectServerChannel(HostAndPort hostAndPort, ChannelHandler channelHandler) async { serverChannel = await startConnect(hostAndPort, channelHandler, this); putAttribute(clientChannel!.id, serverChannel); putAttribute(serverChannel!.id, clientChannel); return serverChannel!; } /// 建立连接 static Future startConnect( HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext) async { var client = Client()..initChannel((channel) => channel.dispatcher.channelHandle(HttpClientCodec(), handler)); return client.connect(hostAndPort, channelContext); } T? getAttribute(String key) { if (!_attributes.containsKey(key)) { return null; } return _attributes[key] as T; } void putAttribute(String key, Object? value) { if (value == null) { _attributes.remove(key); return; } _attributes[key] = value; } HostAndPort? get host => getAttribute(AttributeKeys.host); set host(HostAndPort? host) => putAttribute(AttributeKeys.host, host); HttpRequest? get currentRequest => getAttribute(AttributeKeys.request); 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) { var old = _streams[streamId]?.key; _streams[streamId] = Pair(request, null); return old; } void putStreamResponse(int streamId, HttpResponse response) { var pair = _streams[streamId]; if (pair == null) { pair = Pair(null, response); _streams[streamId] = pair; } pair.key?.response = response; response.request = pair.key; pair.value = response; } HttpRequest? getStreamRequest(int streamId) { return _streams[streamId]?.key; } HttpResponse? getStreamResponse(int streamId) { return _streams[streamId]?.value; } void removeStream(int streamId) { _streams.remove(streamId); } void put(int streamId, HeadersFrame frame) { _streamDependency[streamId] = frame; } HeadersFrame? removeStreamDependency(int streamId) { return _streamDependency.remove(streamId); } HeadersFrame? getStreamDependency(int streamId) { return _streamDependency[streamId]; } bool containsStreamDependency(int? streamId) { if (streamId == null) return false; return _streamDependency.containsKey(streamId); } } ================================================ FILE: lib/network/channel/channel_dispatcher.dart ================================================ import 'dart:async'; import 'dart:typed_data'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/handle/relay_handle.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/handle/websocket_handle.dart'; import 'package:proxypin/network/http/codec.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_client.dart'; import 'package:proxypin/network/util/attribute_keys.dart'; import 'package:proxypin/network/util/byte_buf.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/process_info.dart'; import 'package:proxypin/network/handle/sse_handle.dart'; import '../util/task_queue.dart'; class ChannelDispatcher extends ChannelHandler { late Decoder decoder; late Encoder encoder; late ChannelHandler handler; final ByteBuf buffer = ByteBuf(); //h2 stream dependency Sequential exec SequentialTaskQueue taskQueue = SequentialTaskQueue(); void handle(Decoder decoder, Encoder encoder, ChannelHandler handler) { this.encoder = encoder; this.decoder = decoder; this.handler = handler; } void channelHandle(Codec codec, ChannelHandler handler) { handle(codec, codec, handler); } /// 监听 void listen(Channel channel, ChannelContext channelContext) { buffer.clear(); channel.socket.done.onError((error, StackTrace trace) { logger.e('[${channelContext.clientChannel?.id}] secureSocket done error', error: error, stackTrace: trace); channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace); return null; }); channel.socket.listen((data) => channel.dispatcher.channelRead(channelContext, channel, data), onError: (error, trace) => channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace), onDone: () => channel.dispatcher.channelInactive(channelContext, channel)); } @override void channelActive(ChannelContext context, Channel channel) { handler.channelActive(context, channel); } ///远程转发请求 Future remoteForward(ChannelContext channelContext, HostAndPort remote) async { var clientChannel = channelContext.clientChannel!; Channel? remoteChannel = channelContext.serverChannel ?? await channelContext.connectServerChannel(remote, RelayHandler(clientChannel)); ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo); if (clientChannel.isSsl && !remoteChannel.isSsl) { //代理认证 if (proxyInfo?.isAuthenticated == true) { await HttpClients.connectRequest(channelContext, remote, remoteChannel, proxyInfo: proxyInfo); } await remoteChannel.secureSocket(channelContext, host: channelContext.getAttribute(AttributeKeys.domain)); } relay(channelContext, clientChannel, remoteChannel); } /// 转发请求 void relay(ChannelContext channelContext, Channel clientChannel, Channel remoteChannel) { var rawCodec = RawCodec(); clientChannel.dispatcher.channelHandle(rawCodec, RelayHandler(remoteChannel)); remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(clientChannel)); var body = buffer.bytes; buffer.clear(); handler.channelRead(channelContext, clientChannel, body); } @override Future channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async { //手机扫码连接转发远程 HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote); buffer.add(msg); try { if (remote != null) { await remoteForward(channelContext, remote); return; } Channel? remoteChannel = channelContext.getAttribute(channel.id); //大body 不解析直接转发 if (buffer.length > Codec.maxBodyLength && handler is! RelayHandler && remoteChannel != null) { logger.w("[$channel] forward large body"); relay(channelContext, channel, remoteChannel); return; } var decodeResult = decoder.decode(channelContext, buffer); //If the body does not support parsing, forward directly if (decodeResult.supportedParse == false) { notSupportedForward(channelContext, channel, decodeResult); return; } if (decodeResult.forward != null) { buffer.clearRead(); if (remoteChannel != null) { await remoteChannel.writeBytes(decodeResult.forward!); } else { logger.w("[$channel] forward remoteChannel is null"); } if (decodeResult.data == null) { return; } } if (!decodeResult.isDone) { return; } var length = buffer.length; buffer.clearRead(); var data = decodeResult.data; if (data is HttpMessage) { data.packageSize ??= length; data.remoteHost = channel.remoteSocketAddress.host; data.remotePort = channel.remoteSocketAddress.port; } if (data is HttpRequest) { channelContext.currentRequest = data; data.hostAndPort ??= channelContext.host ?? getHostAndPort(data, ssl: channel.isSsl); if (data.headers.host != null && data.headers.host?.contains(":") == false) { data.hostAndPort?.host = data.headers.host!; } data.processInfo ??= await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, data.remoteDomain()!); } if (data is HttpResponse) { data.requestId = channelContext.currentRequest?.requestId ?? data.requestId; data.request ??= channelContext.currentRequest; } //websocket协议 if (data is HttpResponse && data.isWebSocket && remoteChannel != null) { onWebSocketHandle(channelContext, channel, data); return; } if (data is HttpMessage && channelContext.containsStreamDependency(data.streamId)) { taskQueue.add(data.streamId!, channelContext.getStreamDependency(data.streamId!)?.streamDependency, () => handler.channelRead(channelContext, channel, data), onError: (error, stackTrace) => onError(channelContext, channel, error, trace: stackTrace)); } else { await handler.channelRead(channelContext, channel, data!); } } catch (error, trace) { onError(channelContext, channel, error, trace: trace); } } void onError(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) { logger.e( "[${channelContext.clientChannel?.id}] channelRead error isSsl:${channel.isSsl} client: ${channelContext.clientChannel?.selectedProtocol} server: ${channelContext.serverChannel?.selectedProtocol} ${String.fromCharCodes(buffer.bytes)}", error: error, stackTrace: trace); buffer.clear(); exceptionCaught(channelContext, channel, error, trace: trace); } /// websocket 处理 void onWebSocketHandle(ChannelContext channelContext, Channel channel, HttpResponse data) { Channel remoteChannel = channelContext.getAttribute(channel.id); data.request?.response = data; channelContext.host = channelContext.host?.copyWith(scheme: channel.isSsl ? HostAndPort.wssScheme : HostAndPort.wsScheme); channelContext.currentRequest?.hostAndPort = channelContext.host; logger.d("webSocket ${data.request?.hostAndPort}"); remoteChannel.write(channelContext, data); channelContext.listener?.onResponse(channelContext, data); var rawCodec = RawCodec(); channel.dispatcher.channelHandle(rawCodec, WebSocketChannelHandler(remoteChannel, data)); remoteChannel.dispatcher.channelHandle(rawCodec, WebSocketChannelHandler(channel, data.request!)); } /// SSE 处理 (text/event-stream) void onSseHandle(ChannelContext channelContext, Channel channel, HttpResponse response, List? initialBody) { Channel remoteChannel = channelContext.getAttribute(channel.id); channelContext.currentRequest?.response = response; response.request ??= channelContext.currentRequest; channelContext.listener?.onResponse(channelContext, response); remoteChannel.write(channelContext, response); // Switch to raw streaming: server->client uses SseChannelHandler; client->server just relays var rawCodec = RawCodec(); channel.dispatcher.channelHandle(rawCodec, SseChannelHandler(remoteChannel, response)); remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(channel)); // Flush any initial body bytes that were already read if (initialBody != null && initialBody.isNotEmpty) { // Place existing buffered bytes and let handler consume buffer.add(initialBody); var body = buffer.bytes; buffer.clear(); handler.channelRead(channelContext, channel, body); } } void notSupportedForward(ChannelContext channelContext, Channel channel, DecoderResult decodeResult) { Channel? remoteChannel = channelContext.getAttribute(channel.id); // If this is an SSE response, switch to SSE streaming mode instead of generic relay if (decodeResult.data is HttpResponse) { var response = decodeResult.data as HttpResponse; if (response.headers.contentType.toLowerCase().startsWith('text/event-stream') && remoteChannel != null) { logger.d("[$channel] switch to SSE streaming"); onSseHandle(channelContext, channel, response, decodeResult.forward); return; } } // Fallback: generic relay for unsupported body types buffer.add(decodeResult.forward ?? []); relay(channelContext, channel, remoteChannel!); if (decodeResult.data is HttpResponse) { var response = decodeResult.data as HttpResponse; logger.w("[$channel] not supported parse ${response.headers.contentType}"); response.request ??= channelContext.currentRequest; channelContext.currentRequest?.response = response; channelContext.listener?.onResponse(channelContext, response); } } @override exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) { handler.exceptionCaught(channelContext, channel, error, trace: trace); } @override channelInactive(ChannelContext channelContext, Channel channel) async { await taskQueue.waitForAll(); channel.isOpen = false; handler.channelInactive(channelContext, channel); } } class RawCodec extends Codec> { @override DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) { var decoderResult = DecoderResult()..data = byteBuf.readAvailableBytes(); return decoderResult; } @override List encode(ChannelContext channelContext, dynamic data) { return data as List; } } abstract interface class ChannelInitializer { void initChannel(Channel channel); } ================================================ FILE: lib/network/channel/host_port.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_headers.dart'; /// 获取主机和端口 HostAndPort getHostAndPort(HttpRequest request, {bool? ssl}) { String requestUri = request.uri; //有些请求直接是路径 /xxx, 从header取host if (request.uri.startsWith("/")) { requestUri = request.headers.get(HttpHeaders.HOST)!; } return HostAndPort.of(requestUri, ssl: ssl); } class HostAndPort { static final ipv6Pattern = r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|' r'([0-9a-fA-F]{1,4}:){1,7}:|' r'([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|' r'([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|' r'([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|' r'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|' r'([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|' r'[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|' r':((:[0-9a-fA-F]{1,4}){1,7}|:)|' r'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' r'::(ffff(:0{1,4}){0,1}:){0,1}' r'(([0-9]{1,3}\.){3,3}[0-9]{1,3})|' r'([0-9a-fA-F]{1,4}:){1,4}:' r'(([0-9]{1,3}\.){3,3}[0-9]{1,3}))$'; static final ipV6RegExp = RegExp(ipv6Pattern); static const String httpScheme = "http://"; static const String httpsScheme = "https://"; static const String wsScheme = "ws://"; static const String wssScheme = "wss://"; static const schemes = [httpsScheme, httpScheme, wssScheme, wsScheme]; String scheme; String host; final int port; bool? _ipv6; bool get isIPv6 { _ipv6 ??= _isIPv6(host); return _ipv6!; } static bool _isIPv6(String address) { return ipV6RegExp.hasMatch(address); } HostAndPort(this.scheme, this.host, this.port, {bool? ipv6}) : _ipv6 = ipv6; factory HostAndPort.host(String host, int port, {String? scheme}) { return HostAndPort(scheme ?? (port == 443 ? httpsScheme : httpScheme), host, port); } /// 是否是url static bool startsWithScheme(String url) { return schemes.any((scheme) => url.startsWith(scheme)); } bool isSsl() { return httpsScheme.startsWith(scheme); } /// 根据url构建 static HostAndPort of(String url, {bool? ssl}) { String domain = url; String? scheme; //域名格式 直接解析 if (startsWithScheme(url)) { try { Uri uri = Uri.parse(url); return HostAndPort('${uri.scheme}://', uri.host, uri.port); } catch (e) { //httpScheme scheme = schemes.firstWhere((element) => url.startsWith(element), orElse: () => httpScheme); domain = url.substring(scheme.length).split("/")[0]; } //说明支持ipv6 if (domain.startsWith('[') && domain.endsWith(']')) { return HostAndPort(scheme, domain, scheme == httpScheme ? 80 : 443, ipv6: true); } } //ip格式 host:port var indexOf = domain.lastIndexOf(':'); String host = domain.substring(0, indexOf == -1 ? domain.length : indexOf); String? port = indexOf == -1 ? null : domain.substring(indexOf + 1, domain.length); bool? ipv6 = host.startsWith('[') && host.endsWith(']') ? true : null; if (port != null) { bool isSsl = port == "443" || ssl == true; scheme ??= isSsl ? httpsScheme : httpScheme; return HostAndPort(scheme, host, int.parse(port), ipv6: ipv6); } scheme ??= (ssl == true ? httpsScheme : httpScheme); return HostAndPort(scheme, host, scheme == httpScheme ? 80 : 443, ipv6: ipv6); } String get domain { String host = this.host; if (isIPv6 && !host.startsWith('[') && !host.endsWith(']')) { host = '[$host]'; } return '$scheme$host${(port == 80 || port == 443) ? "" : ":$port"}'; } HostAndPort copyWith({String? scheme, String? host, int? port}) { return HostAndPort(scheme ?? this.scheme, host ?? this.host, port ?? this.port); } @override bool operator ==(Object other) => identical(this, other) || other is HostAndPort && runtimeType == other.runtimeType && scheme == other.scheme && host == other.host && port == other.port; @override int get hashCode => scheme.hashCode ^ host.hashCode ^ port.hashCode; @override String toString() { return domain; } } /// 代理信息 class ProxyInfo { bool enabled = false; //是否展示抓包 bool capturePacket = true; String host = '127.0.0.1'; int? port; //authorization String? username; String? password; ProxyInfo(); ProxyInfo.of(this.host, this.port) : enabled = true; bool get isAuthenticated => username?.isNotEmpty == true; ProxyInfo.fromJson(Map json) { enabled = json['enabled'] == true; capturePacket = json['capturePacket'] ?? true; host = json['host']; port = json['port']; username = json['username']; password = json['password']; } Map toJson() { return { 'enabled': enabled, 'capturePacket': capturePacket, 'host': host, 'port': port, 'username': username, 'password': password, }; } @override String toString() { return 'ProxyInfo{enabled: $enabled, capturePacket: $capturePacket, host: $host, port: $port, username: $username, password: $password}'; } } ================================================ FILE: lib/network/channel/network.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'dart:typed_data'; import 'package:proxypin/native/process_info.dart'; import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/channel/channel_dispatcher.dart'; import 'package:proxypin/network/components/host_filter.dart'; import 'package:proxypin/network/handle/relay_handle.dart'; import 'package:proxypin/network/socks/socks5.dart'; import 'package:proxypin/network/util/attribute_keys.dart'; import 'package:proxypin/network/util/crts.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/process_info.dart'; import 'package:proxypin/network/util/tls.dart'; import '../bin/listener.dart'; import 'host_port.dart'; abstract class Network { late Function _channelInitializer; Network initChannel(void Function(Channel channel) initializer) { _channelInitializer = initializer; return this; } Channel listen(Channel channel, ChannelContext channelContext) { _channelInitializer.call(channel); channel.dispatcher.channelActive(channelContext, channel); channel.socket.listen((data) => onEvent(data, channelContext, channel), onError: (error, StackTrace trace) => channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace), onDone: () => channel.dispatcher.channelInactive(channelContext, channel)); channel.socket.done.onError((error, StackTrace trace) { logger.e('[${channelContext.clientChannel?.id}] socket done error', error: error, stackTrace: trace); channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace); }); return channel; } Future onEvent(Uint8List data, ChannelContext channelContext, Channel channel); /// 转发请求 void relay(Channel clientChannel, Channel remoteChannel) { var rawCodec = RawCodec(); clientChannel.dispatcher.channelHandle(rawCodec, RelayHandler(remoteChannel)); remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(clientChannel)); } } class Server extends Network { Configuration configuration; late ServerSocket serverSocket; bool isRunning = false; EventListener? listener; StreamSubscription? serverSubscription; final List _connections = []; Timer? _connectionCleanupTimer; Server(this.configuration, {this.listener}); Future bind(int port) async { serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, port); serverSubscription = serverSocket.listen((socket) { var channel = Channel(socket); _connections.add(channel); socket.done.whenComplete(() => _connections.remove(channel)); ChannelContext channelContext = ChannelContext(); channelContext.clientChannel = channel; channelContext.listener = listener; listen(channel, channelContext); }); isRunning = true; _connectionCleanupTimer = Timer.periodic(const Duration(seconds: 120), (timer) { if (!isRunning) { timer.cancel(); _connectionCleanupTimer = null; return; } cleanupConnections(); }); return serverSocket; } Future stop() async { if (!isRunning) return serverSocket; isRunning = false; for (var channel in _connections) { if (channel.isClosed) continue; try { logger.d('Closing socket: ${channel.remoteSocketAddress.host}:${channel.remoteSocketAddress.port}'); channel.close(); } catch (e) { logger.e('Error closing socket: $e'); } } _connections.clear(); //关闭监听 serverSubscription?.cancel(); serverSubscription = null; await serverSocket.close(); _connectionCleanupTimer?.cancel(); _connectionCleanupTimer = null; return serverSocket; } void cleanupConnections() { _connections.removeWhere((channel) { if (channel.isClosed) { logger.i('Cleaning up closed channel: ${channel.remoteSocketAddress.host}:${channel.remoteSocketAddress.port}'); return true; } return false; }); } @override Future onEvent(Uint8List data, ChannelContext channelContext, Channel channel) async { //手机扫码转发远程地址 if (configuration.remoteHost != null) { channelContext.putAttribute(AttributeKeys.remote, HostAndPort.of(configuration.remoteHost!)); } //外部代理信息 if (configuration.externalProxy?.enabled == true) { ProxyInfo externalProxy = configuration.externalProxy!; channelContext.putAttribute(AttributeKeys.proxyInfo, externalProxy); if (externalProxy.capturePacket == false) { //不抓包直接转发 channelContext.putAttribute(AttributeKeys.remote, HostAndPort.host(externalProxy.host, externalProxy.port!)); } } HostAndPort? hostAndPort = channelContext.host; //黑名单 或 没开启https 直接转发 if ((HostFilter.filter(hostAndPort?.host)) || (hostAndPort?.isSsl() == true && configuration.enableSsl == false)) { var remoteChannel = channelContext.serverChannel ?? await channelContext.connectServerChannel(hostAndPort!, RelayHandler(channel)); relay(channel, remoteChannel); channel.dispatcher.channelRead(channelContext, channel, data); return; } //ssl握手 if (hostAndPort?.isSsl() == true || TLS.isTLSClientHello(data)) { ssl(channelContext, channel, data); return; } //socks5 if (configuration.enableSocks5 && Socks5.isSocks5(data) && channel.dispatcher.handler is! SocksServerHandler) { channel.dispatcher.channelHandle(RawCodec(), SocksServerHandler(channel.dispatcher.decoder, channel.dispatcher.encoder, channel.dispatcher.handler)); } channel.dispatcher.channelRead(channelContext, channel, data); } /// ssl握手 void ssl(ChannelContext channelContext, Channel channel, Uint8List data) async { var hostAndPort = channelContext.host; try { String? serviceName = TLS.getDomain(data) ?? hostAndPort?.host; bool isHttp = true; if (hostAndPort == null) { var domain = serviceName; var port = 443; if (domain == null) { var remote = await ProcessInfoPlugin.getRemoteAddressByPort(channel.remoteSocketAddress.port); domain = remote?.host; port = remote?.port ?? port; serviceName = domain; // DNS over HTTPS if (remote?.port == 853 && TLS.supportProtocols(data)?.contains("http/1.1") == false) { isHttp = false; } } hostAndPort = HostAndPort.host(domain!, port, scheme: HostAndPort.httpsScheme); } hostAndPort.scheme = HostAndPort.httpsScheme; channelContext.putAttribute(AttributeKeys.domain, hostAndPort.host); Channel? remoteChannel = channelContext.serverChannel; if (!isHttp || HostFilter.filter(hostAndPort.host) || !configuration.enableSsl) { remoteChannel = remoteChannel ?? await channelContext.connectServerChannel(hostAndPort, RelayHandler(channel)); relay(channel, remoteChannel); channel.dispatcher.channelRead(channelContext, channel, data); return; } if (remoteChannel != null && !remoteChannel.isSsl) { var supportProtocols = configuration.enabledHttp2 ? TLS.supportProtocols(data) : ['http/1.1']; await remoteChannel.startSecureSocket(channelContext, host: serviceName, supportedProtocols: supportProtocols); } //ssl自签证书 var certificate = await CertificateManager.getCertificateContext(serviceName!); var selectedProtocol = remoteChannel?.selectedProtocol; var supportedProtocols = selectedProtocol != null ? [selectedProtocol] : ['http/1.1']; certificate.setAlpnProtocols(supportedProtocols, true); //处理客户端ssl握手 var secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data, supportedProtocols: supportedProtocols); channel.serverSecureSocket(secureSocket, channelContext); remoteChannel?.listen(channelContext); if (selectedProtocol != secureSocket.selectedProtocol) { logger.i( '[${channelContext.clientChannel?.id}] $hostAndPort ssl handshake done, clientSelectedProtocol: ${secureSocket.selectedProtocol}, serverSelectedProtocols: $supportedProtocols'); } } catch (error, trace) { logger.e('[${channelContext.clientChannel?.id}] $hostAndPort ssl error', error: error, stackTrace: trace); try { channelContext.processInfo ??= await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, hostAndPort?.domain ?? 'unknown'); } catch (ignore) { /*ignore*/ } channelContext.host ??= hostAndPort; channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace); } } } class Client extends Network { Future connect(HostAndPort hostAndPort, ChannelContext channelContext, {Duration timeout = const Duration(seconds: 3)}) async { String host = hostAndPort.host; //说明支持ipv6 // if (host.startsWith("[") && host.endsWith(']')) { // host = host.substring(1, host.length - 1); // } // logger.d('Connecting to $host:${hostAndPort.port}'); return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) { if (socket.address.type != InternetAddressType.unix) { socket.setOption(SocketOption.tcpNoDelay, true); } var channel = Channel(socket); channelContext.serverChannel = channel; return listen(channel, channelContext); }); } /// ssl连接 Future secureConnect(HostAndPort hostAndPort, ChannelContext channelContext) async { return SecureSocket.connect(hostAndPort.host, hostAndPort.port, timeout: const Duration(seconds: 3), onBadCertificate: (certificate) => true).then((socket) { var channel = Channel(socket); channelContext.serverChannel = channel; return listen(channel, channelContext); }); } @override Future onEvent(Uint8List data, ChannelContext channelContext, Channel channel) async { channel.dispatcher.channelRead(channelContext, channel, data); } } ================================================ FILE: lib/network/components/host_filter.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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. */ void main() { print(HostFilter.filter("stackoverflow.com")); } /// @author wanghongen /// 2023/7/26 class HostFilter { /// 白名单 static final Whites whitelist = Whites(); /// 黑名单 static final Blacks blacklist = Blacks(); /// 是否过滤 static bool filter(String? host) { if (host == null) { return false; } //如果白名单不为空,不在白名单里都是黑名单 if (whitelist.enabled) { return whitelist.list.every((element) => !element.hasMatch(host)); } if (blacklist.enabled) { return blacklist.list.any((element) => element.hasMatch(host)); } return false; } } /// abstract class HostList { /// 列表 final List list = []; bool enabled = false; ///加载配置 void load(Map? map) { if (map == null) { return; } List? list = map['list']; this.list.clear(); list?.forEach((element) { this.list.add(RegExp(element)); }); enabled = map['enabled'] == true; } void add(String reg) { var regExp = RegExp(reg.replaceAll("*", ".*")); list.removeWhere((element) => element.pattern == regExp.pattern); list.add(regExp); } void remove(String reg) { list.removeWhere((element) => element.pattern == reg.replaceAll("*", ".*")); } void removeIndex(List index) { for (var element in index) { list.removeAt(element); } } // json序列化 Map toJson() { return { 'list': list.map((e) => e.pattern).toList(), 'enabled': enabled, }; } } ///白名单 class Whites extends HostList {} ///黑名单 class Blacks extends HostList { Blacks() { enabled = true; list.add(RegExp(".*.apple.com")); list.add(RegExp(".*.icloud.com")); } } ================================================ FILE: lib/network/components/hosts.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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 'package:proxypin/network/components/manager/hosts_manager.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/util/logger.dart'; import 'interceptor.dart'; /// Hosts interceptor /// @author wanghongen class Hosts extends Interceptor { Future get hostsManager async => await HostsManager.instance; @override int get priority => -1000; @override Future preConnect(HostAndPort hostAndPort) async { var host = hostAndPort.host; var hostsItem = await hostsManager.then((it) => it.getHosts(host)); if (hostsItem != null) { logger.d('Hosts: $host -> ${hostsItem.toAddress}'); return hostAndPort.copyWith(host: hostsItem.toAddress); } return hostAndPort; } } ================================================ FILE: lib/network/components/interceptor.dart ================================================ import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/http.dart'; /// A Interceptor that can intercept and modify the request and response. /// @author Hongen Wang abstract class Interceptor { /// The priority of the interceptor. int get priority => 0; Future preConnect(HostAndPort hostAndPort) async { return hostAndPort; } /// Called before the request is sent to the server. Future execute(HttpRequest request) async { return null; } /// Called before the request is sent to the server. Future onRequest(HttpRequest request) async { return request; } /// Called after the response is received from the server. Future onResponse(HttpRequest request, HttpResponse response) async { return response; } Future onError(HttpRequest? request, dynamic error, StackTrace? stackTrace) async { return; } } ================================================ FILE: lib/network/components/js/file.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'package:flutter_js/flutter_js.dart'; import 'package:path_provider/path_provider.dart'; import 'package:proxypin/network/util/logger.dart'; /// FileBridge for file operation /// @Author: Hongen Wang class FileBridge { static const String code = ''' function getApplicationSupportDirectory() { return sendMessage('getApplicationSupportDirectory', JSON.stringify('')); } function File(path) { return { path: path, readAsString: function() { return sendMessage('file.readAsString', JSON.stringify(this.path)); }, readAsStringSync: function() { return sendMessage('file.readAsStringSync', JSON.stringify(this.path)); }, readAsBytes: function() { return sendMessage('file.readAsBytes', JSON.stringify(this.path)); }, readAsBytesSync: function() { return sendMessage('file.readAsBytesSync', JSON.stringify(this.path)); }, writeAsString: function(content, append) { return sendMessage('file.writeAsString', JSON.stringify({path: this.path, content:content, append: append})); }, writeAsStringSync: function(content, append) { return sendMessage('file.writeAsStringSync', JSON.stringify({path: this.path, content: content, append: append})); }, writeAsBytes: function(bytes, append) { return sendMessage('file.writeAsBytes', JSON.stringify({path: this.path, bytes: bytes, append: append})); }, writeAsBytesSync: function(bytes, append) { return sendMessage('file.writeAsBytesSync', JSON.stringify({path: this.path, bytes: bytes, append: append})); }, length: function() { return sendMessage('file.length', JSON.stringify(this.path)); }, lengthSync: function() { return sendMessage('file.lengthSync', JSON.stringify(this.path)); }, delete: function() { return sendMessage('file.delete', JSON.stringify(this.path)); }, deleteSync: function() { return sendMessage('file.deleteSync', JSON.stringify(this.path)); }, exists: function() { return sendMessage('file.exists', JSON.stringify(this.path)); }, existsSync: function() { return sendMessage('file.existsSync', JSON.stringify(this.path)); }, create: function(recursive, exclusive) { return sendMessage('file.create', JSON.stringify({path: this.path, recursive: recursive, exclusive: exclusive})); }, createSync: function(recursive, exclusive) { return sendMessage('file.createSync', JSON.stringify({path: this.path, recursive: recursive, exclusive: exclusive})); }, rename: function(newPath) { return sendMessage('file.rename', JSON.stringify(this.path, newPath)); } }; } '''; ///register file operation static void registerFile(JavascriptRuntime flutterJs) { var channels = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()]; if (channels != null && channels.containsKey('file.readAsString')) { return; } var result = flutterJs.evaluate(code); if (result.isError) { logger.e('registerFile error: ${result.stringResult}'); } flutterJs.onMessage('getApplicationSupportDirectory', (args) { return getApplicationSupportDirectory().then((dir) => dir.path); }); flutterJs.onMessage('file.readAsString', (path) { return File(path).readAsString(); }); flutterJs.onMessage('file.readAsStringSync', (path) { var readAsStringSync = File(path).readAsStringSync(); return readAsStringSync; }); flutterJs.onMessage('file.readAsBytes', (path) { return File(path).readAsBytes(); }); flutterJs.onMessage('file.readAsBytesSync', (path) { return File(path).readAsBytesSync(); }); flutterJs.onMessage('file.writeAsString', (args) async { var path = args['path']; var content = args['content']; var append = args['append'] ?? false; await File(path).writeAsString(content, mode: append ? FileMode.append : FileMode.write); }); flutterJs.onMessage('file.writeAsStringSync', (args) { var path = args['path']; var content = args['content']; var append = args['append'] ?? false; File(path).writeAsStringSync(content, mode: append ? FileMode.append : FileMode.write); }); flutterJs.onMessage('file.writeAsBytes', (args) async { var path = args['path']; var bytes = List.from(args['bytes']); var append = args['append'] ?? false; await File(path).writeAsBytes(bytes, mode: append ? FileMode.append : FileMode.write); }); flutterJs.onMessage('file.writeAsBytesSync', (args) { var path = args['path']; var bytes = List.from(args['bytes']); var append = args['append'] ?? false; File(path).writeAsBytesSync(bytes, mode: append ? FileMode.append : FileMode.write); }); flutterJs.onMessage('file.length', (path) { return File(path).length(); }); flutterJs.onMessage('file.lengthSync', (path) { return File(path).lengthSync(); }); // flutterJs.onMessage('file.delete', (path) { // return File(path).delete(); // }); // // flutterJs.onMessage('file.deleteSync', (path) { // return File(path).deleteSync(); // }); flutterJs.onMessage('file.exists', (path) { return File(path).exists(); }); flutterJs.onMessage('file.existsSync', (path) { return File(path).existsSync(); }); flutterJs.onMessage('file.create', (args) { //bool recursive = false, bool exclusive = false var path = args['path']; var recursive = args['recursive'] ?? false; var exclusive = args['exclusive'] ?? false; File(path).create(recursive: recursive, exclusive: exclusive); }); flutterJs.onMessage('file.createSync', (args) { var path = args['path']; var recursive = args['recursive'] ?? false; var exclusive = args['exclusive'] ?? false; File(path).createSync(recursive: recursive, exclusive: exclusive); }); flutterJs.onMessage('file.rename', (args) async { var path = args['path']; var newPath = args['newPath']; await File(path).rename(newPath); }); } } ================================================ FILE: lib/network/components/js/md5.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'package:crypto/crypto.dart'; import 'package:flutter_js/flutter_js.dart'; import 'package:proxypin/network/util/lang.dart'; /// JsMd5 /// @author Hongen Wang class Md5Bridge { static const String _md5 = ''' function md5(input) { return sendMessage('md5', JSON.stringify(input)); } '''; ///注册js md5 static void registerMd5(JavascriptRuntime flutterJs) { var channels = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()]; if (channels != null && channels.containsKey('md5')) { return; } flutterJs.evaluate(_md5); flutterJs.onMessage('md5', (args) { List input; //判断是否是二进制 if (Lists.getElementType(args) == int) { input = Lists.convertList(args); } else { input = utf8.encode(args.toString()); } return md5.convert(input).toString(); }); } } ================================================ FILE: lib/network/components/js/script_engine.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_js/flutter_js.dart'; import 'package:proxypin/network/components/js/xhr.dart'; import '../../http/http.dart'; import '../../http/http.dart' as http; import '../../http/http_headers.dart'; import '../../util/lang.dart'; import '../../util/logger.dart'; import '../../util/uri.dart'; import 'file.dart'; import 'md5.dart'; class JavaScriptEngine { static Future getJavaScript({Function(dynamic args)? consoleLog}) async { final JavascriptRuntime flutterJs = getJavascriptRuntime(xhr: false); // register channel callback if (consoleLog != null) { final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()]; channelCallbacks!["ConsoleLog"] = consoleLog; } Md5Bridge.registerMd5(flutterJs); FileBridge.registerFile(flutterJs); flutterJs.enableFetch2(); return flutterJs; } /// js结果转换 static Future jsResultResolve(JavascriptRuntime flutterJs, JsEvalResult jsResult) async { try { if (jsResult.isPromise || jsResult.rawResult is Future) { jsResult = await flutterJs.handlePromise(jsResult); } if (jsResult.isPromise || jsResult.rawResult is Future) { jsResult = await flutterJs.handlePromise(jsResult); } } catch (e) { throw SignalException(jsResult.stringResult); } var result = jsResult.rawResult; if (Platform.isMacOS || Platform.isIOS) { result = flutterJs.convertValue(jsResult); } if (result is String) { result = jsonDecode(result); } if (jsResult.isError) { logger.e('jsResultResolve error: ${jsResult.stringResult}'); throw SignalException(jsResult.stringResult); } return result; } //转换js request static Future> convertJsRequest(HttpRequest request) async { var requestUri = request.requestUri; return { 'host': requestUri?.host, 'url': request.requestUrl, 'path': requestUri?.path, 'queries': requestUri?.queryParameters, 'headers': request.headers.toMap(), 'method': request.method.name, 'body': await request.decodeBodyString(), 'rawBody': request.body }; } //转换js response static Future> convertJsResponse(HttpResponse response) async { dynamic body = await response.decodeBodyString(); if (response.contentType.isBinary) { body = response.body; } return { 'headers': response.headers.toMap(), 'statusCode': response.status.code, 'body': body, 'rawBody': response.body }; } //http request static HttpRequest convertHttpRequest(HttpRequest request, Map map) { request.headers.clear(); request.method = http.HttpMethod.values.firstWhere((element) => element.name == map['method']); String query = UriUtils.mapToQuery(map['queries']); var requestUri = request.requestUri!.replace(path: map['path'], query: query); if (requestUri.isScheme('https')) { var query = requestUri.query; request.uri = requestUri.path + (query.isNotEmpty ? '?${requestUri.query}' : ''); } else { request.uri = requestUri.toString(); } map['headers'].forEach((key, value) { if (value is List) { request.headers.addValues(key, value.map((e) => e.toString()).toList()); return; } request.headers.set(key, value); }); request.headers.remove(HttpHeaders.CONTENT_ENCODING); //判断是否是二进制 if (Lists.getElementType(map['body']) == int) { request.body = Lists.convertList(map['body']); return request; } request.body = map['body']?.toString().codeUnits; if (request.body != null && (request.charset == 'utf-8' || request.charset == 'utf8')) { request.body = utf8.encode(map['body'].toString()); } return request; } //http response static HttpResponse convertHttpResponse(HttpResponse response, Map map) { response.headers.clear(); response.status = HttpStatus.valueOf(map['statusCode']); map['headers'].forEach((key, value) { if (value is List) { response.headers.addValues(key, value.map((e) => e.toString()).toList()); return; } response.headers.set(key, value); }); response.headers.remove(HttpHeaders.CONTENT_ENCODING); //判断是否是二进制 if (Lists.getElementType(map['body']) == int) { response.body = Lists.convertList(map['body']); return response; } response.body = map['body']?.toString().codeUnits; if (response.body != null && (response.charset == 'utf-8' || response.charset == 'utf8')) { response.body = utf8.encode(map['body'].toString()); } return response; } } ================================================ FILE: lib/network/components/js/xhr.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter_js/javascript_runtime.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/network/util/file_read.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/utils/platform.dart'; /* * Based on bits and pieces from different OSS sources * * 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. */ // ignore: non_constant_identifier_names var _XHR_DEBUG = false; setXhrDebug(bool value) => _XHR_DEBUG = value; const HTTP_GET = "get"; const HTTP_POST = "post"; const HTTP_PATCH = "patch"; const HTTP_DELETE = "delete"; const HTTP_PUT = "put"; const HTTP_HEAD = "head"; enum HttpMethod { put, get, post, delete, patch, head } String _debugSendNativeCallback() { if (_XHR_DEBUG) { return """console.log("XMLHttpRequest._send_native_callback"); console.log("arguments"); console.log(arguments); console.log(responseInfo); console.log(responseText); console.log(error);"""; } else return ""; } final String xhrJsCode = """ function XMLHttpRequest() { this._send_native = XMLHttpRequestExtension_send_native; this._httpMethod = null; this._url = null; this._requestHeaders = []; this._responseHeaders = []; this.response = null; this.responseText = null; this.responseXML = null; this.onreadystatechange = null; this.onloadstart = null; this.onprogress = null; this.onabort = null; this.onerror = null; this.onload = null; this.onloadend = null; this.ontimeout = null; this.readyState = 0; this.status = 0; this.statusText = ""; this.withCredentials = null; }; // readystate enum XMLHttpRequest.UNSENT = 0; XMLHttpRequest.OPENED = 1; XMLHttpRequest.HEADERS = 2; XMLHttpRequest.LOADING = 3; XMLHttpRequest.DONE = 4; XMLHttpRequest.prototype.constructor = XMLHttpRequest; XMLHttpRequest.prototype.open = function(httpMethod, url) { this._httpMethod = httpMethod; this._url = url; this.readyState = XMLHttpRequest.OPENED; if (typeof this.onreadystatechange === "function") { //console.log("Calling onreadystatechange(OPENED)..."); this.onreadystatechange(); } }; XMLHttpRequest.prototype.send = function(data) { this.readyState = XMLHttpRequest.LOADING; if (typeof this.onreadystatechange === "function") { //console.log("Calling onreadystatechange(LOADING)..."); this.onreadystatechange(); } if (typeof this.onloadstart === "function") { //console.log("Calling onloadstart()..."); this.onloadstart(); } var that = this; this._send_native(this._httpMethod, this._url, this._requestHeaders, data || null, function(responseInfo, responseText, error) { that._send_native_callback(responseInfo, responseText, error); }, this); }; XMLHttpRequest.prototype.abort = function() { this.readyState = XMLHttpRequest.UNSENT; // Note: this.onreadystatechange() is not supposed to be called according to the XHR specs } // responseInfo: {statusCode, statusText, responseHeaders} XMLHttpRequest.prototype._send_native_callback = function(responseInfo, responseText, error) { ${_debugSendNativeCallback()} if (this.readyState === XMLHttpRequest.UNSENT) { console.log("XHR native callback ignored because the request has been aborted"); if (typeof this.onabort === "function") { //console.log("Calling onabort()..."); this.onabort(); } return; } if (this.readyState != XMLHttpRequest.LOADING) { // Request was not expected console.log("XHR native callback ignored because the current state is not LOADING"); return; } // Response info // TODO: responseXML? this.responseURL = this._url; this.status = responseInfo.statusCode; this.statusText = responseInfo.statusText; this.responseBody = responseInfo.body; this._responseHeaders = responseInfo.responseHeaders || []; this.readyState = XMLHttpRequest.DONE; // Response this.response = null; this.responseText = null; this.responseXML = null; if (error) { this.responseText = error; } else { this.responseText = responseText; this.response = { body: responseInfo.body, } // console.log('RESPONSE TEXT: ' + responseText); } this.readyState = XMLHttpRequest.DONE; if (typeof this.onreadystatechange === "function") { //console.log("Calling onreadystatechange(DONE)..."); this.onreadystatechange(); } if (error === "timeout") { // Timeout console.warn("Got XHR timeout"); if (typeof this.ontimeout === "function") { //console.log("Calling ontimeout()..."); this.ontimeout(); } } else if (error) { // Error console.warn("Got XHR error:", error); if (typeof this.onerror === "function") { //console.log("Calling onerror()..."); this.onerror(); } } else { // Success //console.log("XHR success: response =", this.response); if (typeof this.onload === "function") { //console.log("Calling onload()..."); this.onload(); } } if (typeof this.onloadend === "function") { //console.log("Calling onloadend()..."); this.onloadend(); } }; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { this._requestHeaders.push([header, value]); }; XMLHttpRequest.prototype.getAllResponseHeaders = function() { var ret = ""; for (var i = 0; i < this._responseHeaders.length; i++) { var keyValue = this._responseHeaders[i]; ret += keyValue[0] + ": " + keyValue[1] + "\\r\\n"; } return ret; }; XMLHttpRequest.prototype.getResponseHeader = function(name) { var ret = ""; for (var i = 0; i < this._responseHeaders.length; i++) { var keyValue = this._responseHeaders[i]; if (keyValue[0] !== name) continue; if (ret === "") ret += ", "; ret += keyValue[1]; } return ret; }; // XMLHttpRequest.prototype.overrideMimeType = function() { // // TODO // }; this.XMLHttpRequest = XMLHttpRequest;"""; RegExp regexpHeader = RegExp("^([\\w-])+:(?!\\s*\$).+\$"); class XhrPendingCall { int? idRequest; String? method; String? url; Map headers; String? body; XhrPendingCall({ required this.idRequest, required this.method, required this.url, required this.headers, required this.body, }); } const XHR_PENDING_CALLS_KEY = "xhrPendingCalls"; http.Client? httpClient; xhrSetHttpClient(http.Client client) { httpClient = client; } extension JavascriptRuntimeXhrExtension on JavascriptRuntime { List? getPendingXhrCalls() { return dartContext[XHR_PENDING_CALLS_KEY]; } bool hasPendingXhrCalls() => getPendingXhrCalls()!.isNotEmpty; void clearXhrPendingCalls() { dartContext[XHR_PENDING_CALLS_KEY] = []; } Future enableFetch2({bool enabledProxy = false}) async { enableXhr2(enabledProxy: enabledProxy); final fetchPolyfill = await FileRead.readAsString('assets/js/fetch.js'); final evalFetchResult = evaluate(fetchPolyfill); logger.d('Eval Fetch Result: $evalFetchResult'); } Future createClient(bool enabledProxy) async { if (!enabledProxy) { return http.Client(); } // ProxyServer.current.isRunning var httpClient = HttpClient(); String proxy; if (Platforms.isDesktop()) { Map? proxyResult = await DesktopMultiWindow.invokeMethod(0, 'getProxyInfo'); if (proxyResult == null) { return http.Client(); } proxy = "${proxyResult['host']}:${proxyResult['port']}"; } else { if (ProxyServer.current?.isRunning == true) { proxy = "127.0.0.1:${ProxyServer.current!.port}"; } else { return http.Client(); } } httpClient.findProxy = (uri) { return "PROXY $proxy"; }; httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true; // 创建一个 IOClient 实例,将 HttpClient 传入 return IOClient(httpClient); } void enableXhr2({bool enabledProxy = false}) async { httpClient = httpClient ?? await createClient(enabledProxy); dartContext[XHR_PENDING_CALLS_KEY] = []; Timer.periodic(Duration(milliseconds: 40), (timer) { // exits if there is no pending call to remote if (!hasPendingXhrCalls()) return; // collect the pending calls into a local variable making copies List pendingCalls = List.from(getPendingXhrCalls()!); // clear the global pending calls list clearXhrPendingCalls(); // for each pending call, calls the remote http service pendingCalls.forEach((element) async { XhrPendingCall pendingCall = element as XhrPendingCall; HttpMethod eMethod = HttpMethod.values .firstWhere((e) => e.toString().toLowerCase() == ("HttpMethod.${pendingCall.method}".toLowerCase())); late http.Response response; switch (eMethod) { case HttpMethod.head: response = await httpClient!.head( Uri.parse(pendingCall.url!), headers: pendingCall.headers, ); break; case HttpMethod.get: response = await httpClient!.get( Uri.parse(pendingCall.url!), headers: pendingCall.headers, ); break; case HttpMethod.post: response = await httpClient!.post( Uri.parse(pendingCall.url!), body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body), headers: pendingCall.headers, ); break; case HttpMethod.put: response = await httpClient!.put( Uri.parse(pendingCall.url!), body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body), headers: pendingCall.headers, ); break; case HttpMethod.patch: response = await httpClient!.patch( Uri.parse(pendingCall.url!), body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body), headers: pendingCall.headers, ); break; case HttpMethod.delete: response = await httpClient!.delete( Uri.parse(pendingCall.url!), headers: pendingCall.headers, ); break; } // assuming request was successfully executed String? responseText; List? body; try { responseText = utf8.decode(response.bodyBytes); responseText = jsonEncode(json.decode(responseText)); } on Exception { // responseText = response.body; body = response.bodyBytes; } final xhrResult = XmlHttpRequestResponse( responseText: responseText, responseInfo: XhtmlHttpResponseInfo(statusCode: 200, statusText: "OK", body: body), ); response.headers.forEach((key, value) { xhrResult.responseInfo?.addResponseHeaders(key, value); }); final responseInfo = jsonEncode(xhrResult.responseInfo); final safeResponseText = responseText != null ? jsonEncode(responseText) : null; final error = xhrResult.error; // logger.d('XHR response for url: ${pendingCall.url}, status: ${xhrResult.responseInfo?.statusCode}'); // send back to the javascript environment the // response for the http pending callback var jsResult = evaluate( "globalThis.xhrRequests[${pendingCall.idRequest}].callback($responseInfo, $safeResponseText, $error);", ); if (jsResult.isError) { logger.e('jsResult error url:${pendingCall.url}, ${jsResult.stringResult}'); } }); }); this.evaluate(""" var xhrRequests = {}; var idRequest = -1; function XMLHttpRequestExtension_send_native() { idRequest += 1; var cb = arguments[4]; var context = arguments[5]; xhrRequests[idRequest] = { callback: function(responseInfo, responseText, error) { cb(responseInfo, responseText, error); } }; var args = []; args[0] = arguments[0]; args[1] = arguments[1]; args[2] = arguments[2]; args[3] = arguments[3]; args[4] = idRequest; sendMessage('SendNative', JSON.stringify(args)); } """); final evalXhrResult = this.evaluate(xhrJsCode); if (_XHR_DEBUG) print('RESULT evalXhrResult: $evalXhrResult'); this.onMessage('SendNative', (arguments) { try { String? method = arguments[0]; String? url = arguments[1]; dynamic headersList = arguments[2]; String? body = arguments[3]; int? idRequest = arguments[4]; Map headers = {}; headersList.forEach((header) { // final headerMatch = regexpHeader.allMatches(value).first; // String? headerName = headerMatch.group(0); // String? headerValue = headerMatch.group(1); // if (headerName != null) { // headers[headerName] = headerValue ?? ''; // } String headerKey = header[0]; headers[headerKey] = header[1]; }); (dartContext[XHR_PENDING_CALLS_KEY] as List).add( XhrPendingCall( idRequest: idRequest, method: method, url: url, headers: headers, body: body, ), ); } on Error catch (e) { if (_XHR_DEBUG) print('ERROR calling sendNative on Dart: >>>> $e'); } on Exception catch (e) { if (_XHR_DEBUG) print('Exception calling sendNative on Dart: >>>> $e'); } }); } } class XhtmlHttpResponseInfo { final int? statusCode; final String? statusText; final List? body; final List> responseHeaders = []; XhtmlHttpResponseInfo({ this.body, this.statusCode, this.statusText, }); void addResponseHeaders(String name, String value) { responseHeaders.add([name, value]); } Map toJson() { return { "statusCode": statusCode, "statusText": statusText, "body": body, "responseHeaders": responseHeaders }; } } class XmlHttpRequestResponse { final String? responseText; final String? error; // should be timeout in case of timeout final XhtmlHttpResponseInfo? responseInfo; XmlHttpRequestResponse({this.responseText, this.responseInfo, this.error}); Map toJson() { return {'responseText': responseText, 'responseInfo': responseInfo!.toJson(), 'error': error}; } } ================================================ FILE: lib/network/components/manager/hosts_manager.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:path_provider/path_provider.dart'; import 'package:proxypin/network/util/random.dart'; /// Hosts manager /// @author wanghongen class HostsManager { static String separator = Platform.pathSeparator; static HostsManager? _instance; bool enabled = true; final List list = []; final Map> _folderMap = {}; HostsManager._(); /// Singleton static Future get instance async { if (_instance == null) { _instance = HostsManager._(); await _instance?.load(); } return _instance!; } static File? _configFile; static Future homePath() async { if (Platform.isMacOS) { return await DesktopMultiWindow.invokeMethod(0, "getApplicationSupportDirectory"); } return await getApplicationSupportDirectory().then((it) => it.path); } static Future get configFile async { if (_configFile != null) return _configFile!; final path = await homePath(); var file = File('$path${separator}hosts.json'); if (!await file.exists()) { await file.create(); } _configFile = file; return file; } /// Load Future load() async { var json = await (await configFile).readAsString(); if (json.isEmpty) return; var config = jsonDecode(json); enabled = config['enabled'] == true; list.clear(); config['list']?.forEach((element) { var hostsItem = HostsItem.fromJson(element); if (hostsItem.parent != null) { var children = _folderMap[hostsItem.parent!] ??= []; children.add(hostsItem); return; } if (hostsItem.isFolder) { _folderMap[hostsItem.id] ??= []; } list.add(hostsItem); }); } /// Save Future flushConfig() async { var config = List.from(list); for (var values in _folderMap.values) { config.addAll(values); } var json = jsonEncode({ 'enabled': enabled, 'list': config.map((e) => e.toJson()).toList(), }); (await configFile).writeAsString(json); } List getFolderList(String parent) { return _folderMap[parent] ?? []; } Future addHosts(HostsItem item) async { if (item.parent == null) { list.add(item); } else { var children = _folderMap[item.parent!] ??= []; children.add(item); } } Future getHosts(String host) async { if (!enabled) return null; for (var item in list) { if (!item.enabled) continue; if (item.isFolder) { var list = _folderMap[item.id]; if (list == null) continue; for (var it in list) { if (it.enabled && it.match(host)) { return it; } } continue; } if (item.match(host)) { return item; } } return null; } removeHosts(Iterable items) async { if (items.isEmpty) return; for (var item in items) { if (item.parent == null) { list.remove(item); if (item.isFolder) { _folderMap.remove(item.id); } } else { var children = _folderMap[item.parent!] ??= []; children.remove(item); } } flushConfig(); } } class HostsItem { bool enabled = true; bool isFolder = false; final String id; String? parent; String host; String? toAddress; RegExp? _hostReg; HostsItem({String? id, required this.host, this.toAddress, required this.enabled, this.isFolder = false, this.parent}) : id = id ?? generateId(); static String generateId() { return DateTime.now().millisecondsSinceEpoch.toRadixString(36) + RandomUtil.randomString(4); } //匹配url bool match(String domain) { if (host != _hostReg?.pattern) _hostReg = null; _hostReg ??= RegExp(host.replaceAll("*", ".*")); return _hostReg!.hasMatch(domain); } factory HostsItem.fromJson(Map json) { return HostsItem( id: json['id'], host: json['host'], toAddress: json['toAddress'], enabled: json['enabled'], parent: json['parent'], isFolder: json['isFolder'] == true, ); } Map toJson() { return { 'id': id, 'parent': parent, 'enabled': enabled, 'isFolder': isFolder, 'host': host, 'toAddress': toAddress, }; } } ================================================ FILE: lib/network/components/manager/report_server_manager.dart ================================================ import 'dart:convert'; import '../../../storage/path.dart'; import '../../util/logger.dart'; class ReportServerManager { static ReportServerManager? _instance; List _list = []; ///单例 static Future get instance async { if (_instance == null) { _instance = ReportServerManager._internal(); await _instance!.loadConfig(); } return _instance!; } // Private constructor ReportServerManager._internal(); /// Get configured report servers List get servers => _list; Future matchServer(String url) async { final list = servers; for (var server in list) { if (server.match(url)) { return server; } } return null; } Future add(ReportServer server) async { _list.add(server); await _flush(); } Future removeAt(int index) async { final list = servers; list.removeAt(index); await _flush(); } Future update(int index, ReportServer server) async { final list = servers; server.updateUrlReg(); list[index] = server; await _flush(); } Future toggleEnabled(int index, bool enabled) async { final list = servers; list[index] = list[index].copyWith(enabled: enabled); await _flush(); } Future loadConfig() async { var list = []; final file = await Paths.getPath("report_servers.json"); if (await file.exists()) { final content = await file.readAsString(); if (content.trim().isNotEmpty) { try { final decoded = jsonDecode(content) as List; list = decoded.map((e) => ReportServer.fromJson(e as Map)).toList(); } catch (e, t) { logger.e('上报服务器配置解析失败', error: e, stackTrace: t); } } } _list = list; } Future _flush() async { final file = await Paths.getPath("report_servers.json"); final list = servers; await file.writeAsString(jsonEncode(list.map((e) => e.toJson()).toList())); } } class ReportServer { final String name; final String matchUrl; /// 服务器URL final String serverUrl; /// 是否启用 final bool enabled; /// 压缩方式:none/gzip,默认 none final String? compression; RegExp _urlReg; ReportServer({ required this.name, required this.matchUrl, required this.serverUrl, this.enabled = true, this.compression, }) : _urlReg = RegExp(matchUrl.replaceAll("*", ".*").replaceFirst('?', '\\?')); bool match(String url) { if (enabled) { return _urlReg.hasMatch(url); } return false; } void updateUrlReg() { _urlReg = RegExp(matchUrl.replaceAll("*", ".*").replaceFirst('?', '\\?')); } ReportServer copyWith({ String? name, String? serverUrl, bool? enabled, String? matchUrl, String? matchType, String? compression, Map? headers, }) { return ReportServer( name: name ?? this.name, matchUrl: matchUrl ?? this.matchUrl, serverUrl: serverUrl ?? this.serverUrl, enabled: enabled ?? this.enabled, compression: compression ?? this.compression, ); } factory ReportServer.fromJson(Map json) { return ReportServer( name: json['name'] ?? '', matchUrl: json['matchUrl'] ?? '', serverUrl: json['serverUrl'] ?? '', enabled: json['enabled'] ?? true, compression: (json['compression'] ?? 'none') as String, ); } Map toJson() { return { 'name': name, 'matchUrl': matchUrl, 'serverUrl': serverUrl, 'enabled': enabled, 'compression': compression, }; } } ================================================ FILE: lib/network/components/manager/request_block_manager.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; /// 请求屏蔽 /// @author wanghongen /// 2024/02/02 class RequestBlockManager { static RequestBlockManager? _instance; bool enabled = true; List list = []; final File _storageFile; RequestBlockManager._(this._storageFile); ///单例 static Future get instance async { if (_instance == null) { var file = await configFile(); _instance = RequestBlockManager._(file); await _instance?._load(); } return _instance!; } static Future configFile() async { var directory = await getApplicationSupportDirectory().then((it) => it.path); var file = File('$directory${Platform.pathSeparator}request_block.json'); if (!await file.exists()) { await file.create(recursive: true); } return file; } ///加载 Future _load() async { var json = await _storageFile.readAsString(); if (json.isEmpty) return; var config = jsonDecode(json); enabled = config['enabled'] == true; list.clear(); config['list']?.forEach((element) { list.add(RequestBlockItem.fromJson(element)); }); } addBlockRequest(RequestBlockItem item) { list.add(item); flushConfig(); } removeBlockRequest(int index) { list.removeAt(index); flushConfig(); } /// 是否启用 bool enableBlockRequest(String url) { if (!enabled) { return false; } return list.any((element) => element.match(url, BlockType.blockRequest)); } bool enableBlockResponse(String url) { if (!enabled) { return false; } return list.any((element) => element.match(url, BlockType.blockResponse)); } ///刷新配置 Future flushConfig() async { _storageFile.writeAsString(jsonEncode({'enabled': enabled, 'list': list})); } } enum BlockType { blockRequest('屏蔽请求'), blockResponse('屏蔽响应'); //名称 final String label; const BlockType(this.label); static BlockType nameOf(String name) { return BlockType.values.firstWhere((element) => element.name == name); } } class RequestBlockItem { bool enabled = true; String url; BlockType type; RegExp? urlReg; RequestBlockItem(this.enabled, this.url, this.type); //匹配url bool match(String url, BlockType blockType) { urlReg ??= RegExp(this.url.replaceAll("*", ".*")); return enabled && type == blockType && urlReg!.hasMatch(url); } factory RequestBlockItem.fromJson(Map json) { return RequestBlockItem(json['enabled'], json['url'], BlockType.nameOf(json['type'])); } Map toJson() { return {'enabled': enabled, 'url': url, 'type': type.name}; } @override String toString() { return toJson().toString(); } } ================================================ FILE: lib/network/components/manager/request_breakpoint_manager.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:path_provider/path_provider.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; class RequestBreakpointRule { bool enabled; String? name; String url; bool interceptRequest; bool interceptResponse; // Optional HTTP method matching; null means match any method HttpMethod? method; RequestBreakpointRule({ this.enabled = true, this.name, required this.url, this.interceptRequest = true, this.interceptResponse = true, this.method, }); bool match(String url, {HttpMethod? method}) { if (!enabled) return false; if (this.method != null && method != null && this.method != method) return false; return RegExp(this.url).hasMatch(url); } factory RequestBreakpointRule.fromJson(Map map) { HttpMethod? method; try { if (map['method'] != null) { method = HttpMethod.valueOf(map['method']); } } catch (e) { logger.e('Failed to parse HTTP method from request intercept rule', error: e); } return RequestBreakpointRule( enabled: map['enabled'] ?? true, name: map['name'], url: map['url'] ?? '', interceptRequest: map['interceptRequest'] ?? true, interceptResponse: map['interceptResponse'] ?? true, method: method, ); } Map toJson() { return { 'enabled': enabled, 'name': name, 'url': url, 'interceptRequest': interceptRequest, 'interceptResponse': interceptResponse, 'method': method?.name, }; } } class RequestBreakpointManager { static RequestBreakpointManager? _instance; RequestBreakpointManager._(); static Future get instance async { if (_instance == null) { _instance = RequestBreakpointManager._(); await _instance!.load(); } return _instance!; } bool enabled = true; List list = []; static Future homePath() async { if (Platform.isMacOS) { return await DesktopMultiWindow.invokeMethod(0, "getApplicationSupportDirectory"); } return await getApplicationSupportDirectory().then((it) => it.path); } Future load() async { try { var home = await homePath(); var file = File('$home${Platform.pathSeparator}request_breakpoint.json'); if (await file.exists()) { var json = jsonDecode(await file.readAsString()); enabled = json['enabled'] ?? false; list = (json['list'] as List? ?? []).map((e) => RequestBreakpointRule.fromJson(e)).toList(); } } catch (e) { logger.e('Failed to load request breakpoint config', error: e); } } Future save() async { try { var home = await homePath(); var file = File('$home${Platform.pathSeparator}request_breakpoint.json'); if (!await file.exists()) { await file.create(recursive: true); } var json = { 'enabled': enabled, 'list': list.map((e) => e.toJson()).toList(), }; await file.writeAsString(jsonEncode(json)); } catch (e) { logger.e('Failed to save request breakpoint config', error: e); } } void add(RequestBreakpointRule rule) { list.add(rule); save(); } void remove(RequestBreakpointRule rule) { list.remove(rule); save(); } } ================================================ FILE: lib/network/components/manager/request_crypto_manager.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/file_read.dart'; import 'package:proxypin/network/util/logger.dart'; class RequestCryptoManager { static String separator = Platform.pathSeparator; static RequestCryptoManager? _instance; RequestCryptoManager._(); static Future get instance async { if (_instance == null) { final config = await _loadRequestCryptoConfig(); _instance = RequestCryptoManager._(); await _instance!._reload(config); } return _instance!; } bool enabled = true; List rules = []; Future _reload(Map? map) async { if (map == null) { return; } enabled = map['enabled'] == true; final list = map['rules'] as List? ?? const []; rules = []; for (final element in list) { try { rules.add(CryptoRule.fromJson(Map.from(element))); } catch (e) { logger.e('加载请求加解密配置失败 $element', error: e); } } } Future reloadConfig() async { final config = await _loadRequestCryptoConfig(); await _reload(config); } static Future?> _loadRequestCryptoConfig() async { final home = await FileRead.homeDir(); final file = File('${home.path}${Platform.pathSeparator}request_crypto.json'); if (!await file.exists()) { return null; } try { final json = jsonDecode(await file.readAsString()) as Map; logger.i('加载请求加解密配置文件 [$file]'); return json; } catch (e, stack) { logger.e('解析请求加解密配置失败', error: e, stackTrace: stack); return null; } } Future flushConfig() async { final home = await FileRead.homeDir(); final file = File('${home.path}${Platform.pathSeparator}request_crypto.json'); if (!await file.exists()) { await file.create(recursive: true); } final json = jsonEncode(toJson()); logger.i('刷新请求加解密配置文件 ${file.path}'); await file.writeAsString(json); } /// Get the first matching rule for the given URL and optional field name CryptoRule? getMatchingRule(HttpMessage message) { final url = message.requestUrl; if (url == null) return null; if (!enabled) return null; for (final rule in rules) { if (!rule.enabled || !rule.matches(url)) continue; return rule; } return null; } /// Add a new crypto rule to the manager Future addRule(CryptoRule rule) async { rules.add(rule); } /// Update an existing rule at [index] Future updateRule(int index, CryptoRule rule) async { if (index < 0 || index >= rules.length) return; rules[index] = rule; } /// Remove a single rule by index Future removeRule(int index) async { if (index < 0 || index >= rules.length) return; rules.removeAt(index); } /// Remove multiple rules. Indexes should be sorted or will be sorted descending. Future removeIndex(List indexes) async { indexes.sort((a, b) => b.compareTo(a)); for (final i in indexes) { if (i >= 0 && i < rules.length) { rules.removeAt(i); } } } Map toJson() => { 'enabled': enabled, 'rules': rules.map((e) => e.toJson()).toList(), }; } class CryptoRule { final String name; final String urlPattern; final String? field; // single field supported bool enabled; final CryptoKeyConfig config; CryptoRule({ required this.name, required this.urlPattern, this.field, required this.enabled, required this.config, }); bool matches(String url) { try { return RegExp(urlPattern).hasMatch(url); } catch (_) { return url.contains(urlPattern); } } Map toJson() { final map = { 'name': name, 'urlPattern': urlPattern, 'field': field, 'enabled': enabled, 'config': config.toJson(), }; return map; } factory CryptoRule.fromJson(Map json) { return CryptoRule( name: json['name'] ?? '', urlPattern: json['urlPattern'] ?? '', field: json['field'], enabled: json['enabled'] ?? true, config: CryptoKeyConfig.fromJson(Map.from(json['config'] ?? {})), ); } CryptoRule copyWith({ String? name, String? urlPattern, String? field, bool? enabled, CryptoKeyConfig? config, }) { return CryptoRule( name: name ?? this.name, urlPattern: urlPattern ?? this.urlPattern, field: field ?? this.field, enabled: enabled ?? this.enabled, config: config ?? this.config, ); } /// Legacy constructor used by UI to create a default empty AesRule static CryptoRule newRule() { return CryptoRule( name: '', urlPattern: '', field: '', enabled: true, config: CryptoKeyConfig.defaults(), ); } } class CryptoKeyConfig { final String key; final String iv; final String ivSource; // 'manual' or 'prefix' final int ivPrefixLength; final String mode; final String padding; final int keyLength; const CryptoKeyConfig({ required this.key, required this.iv, required this.ivSource, required this.ivPrefixLength, required this.mode, required this.padding, required this.keyLength, }); factory CryptoKeyConfig.defaults() { return const CryptoKeyConfig( key: '', iv: '', ivSource: 'manual', ivPrefixLength: 16, mode: 'ECB', padding: 'PKCS7', keyLength: 128); } bool get isReady { if (key.trim().isEmpty) return false; if (mode != 'CBC') return true; // for CBC, either manual IV provided or prefix mode selected if (ivSource == 'prefix') return true; return iv.trim().isNotEmpty; } CryptoKeyConfig copyWith({ String? key, String? iv, String? ivSource, int? ivPrefixLength, String? mode, String? padding, int? keyLength, }) { return CryptoKeyConfig( key: key ?? this.key, iv: iv ?? this.iv, ivSource: ivSource ?? this.ivSource, ivPrefixLength: ivPrefixLength ?? this.ivPrefixLength, mode: mode ?? this.mode, padding: padding ?? this.padding, keyLength: keyLength ?? this.keyLength, ); } Map toJson() { return { 'key': key, 'iv': iv, 'ivSource': ivSource, 'ivPrefixLength': ivPrefixLength, 'mode': mode, 'padding': padding, 'keyLength': keyLength, }; } factory CryptoKeyConfig.fromJson(Map json) { return CryptoKeyConfig( key: json['key'] ?? '', iv: json['iv'] ?? '', ivSource: json['ivSource'] ?? 'manual', ivPrefixLength: json['ivPrefixLength'] ?? 16, mode: json['mode'] ?? 'ECB', padding: json['padding'] ?? 'PKCS7', keyLength: json['keyLength'] ?? 128, ); } } ================================================ FILE: lib/network/components/manager/request_map_manager.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:path_provider/path_provider.dart'; import '../../util/logger.dart'; import '../../util/random.dart'; class RequestMapManager { static RequestMapManager? _instance; static String separator = Platform.pathSeparator; RequestMapManager._internal(); final Map _mapItemsCache = {}; bool enabled = true; //存储所有的请求映射规则 List rules = []; ///单例 static Future get instance async { if (_instance == null) { _instance = RequestMapManager._internal(); await _instance?.reloadConfig(); } return _instance!; } //添加规则 Future addRule(RequestMapRule rule, RequestMapItem item) async { final path = await homePath(); String itemPath = "${separator}request_map$separator${RandomUtil.randomString(16)}.json"; var file = File(path + itemPath); await file.create(recursive: true); final itemJson = jsonEncode(item.toJson()); file.writeAsString(itemJson); rule.itemPath = itemPath; _mapItemsCache[rule] = item; rules.add(rule); await flushConfig(); } //update rule Future updateRule(RequestMapRule rule, RequestMapItem item) async { rule.updatePathReg(); if (rule.itemPath != null) { final path = await homePath(); var file = File('$path${rule.itemPath}'); await file.writeAsString(jsonEncode(item.toJson())); } _mapItemsCache[rule] = item; await flushConfig(); } //删除规则 Future deleteRule(int index) async { var item = rules.removeAt(index); final home = await homePath(); File(home + item.itemPath!).delete(); } //根据url和类型查找匹配的规则 RequestMapRule? findMatch(String url) { for (var rule in rules) { if (rule.match(url)) { return rule; } } return null; } Future getMapItem(RequestMapRule rule) async { if (_mapItemsCache.containsKey(rule)) { return _mapItemsCache[rule]; } if (rule.itemPath != null) { final path = await homePath(); var file = File('$path$separator${rule.itemPath}'); if (await file.exists()) { var content = await file.readAsString(); if (content.isNotEmpty) { var item = RequestMapItem.fromJson(jsonDecode(content)); _mapItemsCache[rule] = item; return item; } } } return null; } static String? _homePath; static Future homePath() async { if (_homePath != null) { return _homePath!; } if (Platform.isMacOS) { _homePath = await DesktopMultiWindow.invokeMethod(0, "getApplicationSupportDirectory"); } else { _homePath = await getApplicationSupportDirectory().then((it) => it.path); } return _homePath!; } static Future get _path async { final path = await homePath(); var file = File('$path${Platform.pathSeparator}request_map.json'); if (!await file.exists()) { await file.create(); } return file; } ///重新加载配置 Future reloadConfig() async { List list = []; var file = await _path; logger.d("reload request map config from ${file.path}"); if (await file.exists()) { var content = await file.readAsString(); if (content.isEmpty) { return; } var config = jsonDecode(content); enabled = config['enabled'] == true; for (var entry in config['list']) { list.add(RequestMapRule.fromJson(entry)); } } rules = list; _mapItemsCache.clear(); } ///保存配置 Future flushConfig() async { var file = await _path; if (!await file.exists()) { await file.create(recursive: true); } var config = { 'enabled': enabled, 'list': rules.map((e) => e.toJson()).toList(), }; await file.writeAsString(jsonEncode(config)); } } enum RequestMapType { local("本地"), script("脚本"), ; //名称 final String label; const RequestMapType(this.label); static RequestMapType fromName(String name) { return values.firstWhere((element) => element.name == name || element.label == name); } } class RequestMapRule { bool enabled; RequestMapType type; String? name; String url; RegExp _urlReg; String? itemPath; RequestMapRule({this.enabled = true, this.name, required this.url, required this.type, this.itemPath}) : _urlReg = RegExp(url.replaceAll("*", ".*").replaceFirst('?', '\\?')); bool match(String url) { if (enabled) { return _urlReg.hasMatch(url); } return false; } /// 从json中创建 factory RequestMapRule.fromJson(Map map) { return RequestMapRule( enabled: map['enabled'] == true, name: map['name'], url: map['url'], type: RequestMapType.fromName(map['type']), itemPath: map['itemPath']); } void updatePathReg() { _urlReg = RegExp(url.replaceAll("*", ".*").replaceFirst('?', '\\?')); } Map toJson() { return { 'name': name, 'enabled': enabled, 'url': url, 'type': type.name, 'itemPath': itemPath, }; } } class RequestMapItem { String? script; int? statusCode; Map? headers; //body String? body; String? bodyType; String? bodyFile; RequestMapItem({this.script, this.statusCode, this.headers, this.body, this.bodyType, this.bodyFile}); /// 从json中创建 factory RequestMapItem.fromJson(Map map) { return RequestMapItem( script: map['script'], statusCode: map['statusCode'], headers: (map['headers'] as Map?)?.cast(), body: map['body'], bodyType: map['bodyType'], bodyFile: map['bodyFile'], ); } Map toJson() { return { 'script': script, 'statusCode': statusCode, 'headers': headers, 'body': body, 'bodyType': bodyType, 'bodyFile': bodyFile, }; } } enum MapBodyType { text("文本"), file("文件"); final String label; const MapBodyType(this.label); } ================================================ FILE: lib/network/components/manager/request_rewrite_manager.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:proxypin/network/components/manager/rewrite_rule.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/file_read.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/random.dart'; /// @author wanghongen /// 2023/7/26 /// 请求重写 class RequestRewriteManager { static String separator = Platform.pathSeparator; //重写规则 final Map> rewriteItemsCache = {}; //单例 static RequestRewriteManager? _instance; RequestRewriteManager._(); static Future get instance async { if (_instance == null) { var config = await _loadRequestRewriteConfig(); _instance = RequestRewriteManager._(); await _instance!.reload(config); } return _instance!; } bool enabled = true; List rules = []; //重新加载配置 Future reload(Map? map) async { rewriteItemsCache.clear(); if (map == null) { return; } enabled = map['enabled'] == true; List list = map['rules'] ?? []; rules.clear(); for (var element in list) { try { rules.add(RequestRewriteRule.formJson(element)); } catch (e) { logger.e('加载请求重写配置失败 $element', error: e); } } } ///重新加载请求重写 Future reloadRequestRewrite() async { var config = await _loadRequestRewriteConfig(); reload(config); } ///同步配置 Future syncConfig(Map? config) async { if (config == null) { return; } rewriteItemsCache.clear(); enabled = config['enabled'] == true; List list = config['rules'] ?? []; rules.clear(); for (var element in list) { try { var rule = RequestRewriteRule.formJson(element); List list = element['items'] as List; List items = list.map((e) => RewriteItem.fromJson(e)).toList(); await addRule(rule, items); } catch (e) { logger.e('加载请求重写配置失败 $element', error: e); } } flushRequestRewriteConfig(); } /// 加载请求重写配置文件 static Future?> _loadRequestRewriteConfig() async { var home = await FileRead.homeDir(); var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); var exits = await file.exists(); if (!exits) { return null; } Map config = jsonDecode(await file.readAsString()); logger.i('加载请求重写配置文件 [$file]'); return config; } /// 保存请求重写配置文件 Future flushRequestRewriteConfig() async { var home = await FileRead.homeDir(); var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); bool exists = await file.exists(); if (!exists) { await file.create(recursive: true); } var json = jsonEncode(toJson()); logger.i('刷新请求重写配置文件 ${file.path}'); await file.writeAsString(json); } ///添加规则 Future addRule(RequestRewriteRule rule, List items) async { final home = await FileRead.homeDir(); String rewritePath = "${separator}rewrite$separator${RandomUtil.randomString(16)}.json"; var file = File(home.path + rewritePath); await file.create(recursive: true); file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList())); rule.rewritePath = rewritePath; rules.add(rule); rewriteItemsCache[rule] = items; } ///更新规则 Future updateRule(int index, RequestRewriteRule rule, List? items) async { rewriteItemsCache.remove(rules[index]); final home = await FileRead.homeDir(); rule.updatePathReg(); rules[index] = rule; if (items == null) { return; } bool isExist = rule.rewritePath != null; if (rule.rewritePath == null) { String rewritePath = "${separator}rewrite$separator${RandomUtil.randomString(16)}.json"; rule.rewritePath = rewritePath; } File file = File(home.path + rule.rewritePath!); if (!isExist) { await file.create(recursive: true); } await file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList())); rewriteItemsCache[rule] = items; } Future removeIndex(List indexes) async { for (var i in indexes) { var rule = rules.removeAt(i); rewriteItemsCache.remove(rule); //删除缓存 if (rule.rewritePath != null) { File home = await FileRead.homeDir(); try { await File(home.path + rule.rewritePath!).delete(); } catch (e) { logger.e('删除请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e); } rule.rewritePath = null; } } } RequestRewriteRule getRequestRewriteRule(HttpRequest request, RuleType type) { var url = request.domainPath; for (var rule in rules) { if (rule.match(url, type: type, method: request.method) && rule.type == type) { return rule; } } return RequestRewriteRule(type: type, url: url); } RequestRewriteRule? getRewriteRule(String? url, List types) { if (url == null || !enabled) { return null; } for (var rule in rules) { if (rule.match(url) && types.contains(rule.type)) { return rule; } } return null; } /// 获取重写规则 Future?> getRewriteItems(RequestRewriteRule rule) async { if (rewriteItemsCache.containsKey(rule)) { return rewriteItemsCache[rule]!; } if (rule.rewritePath == null) { return null; } final home = await FileRead.homeDir(); List items = []; try { var json = await File(home.path + rule.rewritePath!).readAsString(); List? list = jsonDecode(json); list?.forEach((element) => items.add(RewriteItem.fromJson(element))); rewriteItemsCache[rule] = items; } catch (e) { logger.e('加载请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e); } return items; } Map toJson() { return { 'enabled': enabled, 'rules': rules.map((e) => e.toJson()).toList(), }; } Future> toFullJson() async { var rulesJson = []; for (var rule in rules) { var json = rule.toJson(); json['items'] = await getRewriteItems(rule); rulesJson.add(json); } return { 'enabled': enabled, 'rules': rulesJson, }; } } ================================================ FILE: lib/network/components/manager/rewrite_rule.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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 'package:proxypin/network/http/http.dart'; import 'package:proxypin/utils/lang.dart'; ///重写规则 ///@author: wanghongen enum RuleType { // body("重写消息体"), //OLD VERSION requestReplace("替换请求"), responseReplace("替换响应"), requestUpdate("修改请求"), responseUpdate("修改响应"), redirect("重定向"); //名称 final String label; const RuleType(this.label); static RuleType fromName(String name) { return values.firstWhere((element) => element.name == name || element.label == name); } } class RequestRewriteRule { bool enabled; RuleType type; String? name; String url; RegExp _urlReg; String? rewritePath; // 可选的 HTTP 方法匹配;null 表示匹配任意方法 HttpMethod? method; RequestRewriteRule({this.enabled = true, this.name, required this.url, required this.type, this.rewritePath, this.method}) : _urlReg = RegExp(url.replaceAll("*", ".*").replaceFirst('?', '\\?')); bool match(String url, {RuleType? type, HttpMethod? method}) { if (!enabled) return false; if (type != null && this.type != type) return false; // 如果调用方提供了 method,则当规则定义了 method 时进行比较;如果调用方未提供 method,则不按方法过滤(向后兼容) if (method != null && this.method != null && this.method != method) return false; return _urlReg.hasMatch(url); } bool matchUrl(String url, RuleType type) { return this.type == type && _urlReg.hasMatch(url); } /// 从json中创建 factory RequestRewriteRule.formJson(Map map) { HttpMethod? method; try { if (map['method'] != null) { method = HttpMethod.valueOf(map['method'].toString()); } } catch (e) { // ignore invalid method } return RequestRewriteRule( enabled: map['enabled'] == true, name: map['name'], url: map['url'] ?? map['domain'] + map['path'], type: RuleType.fromName(map['type']), rewritePath: map['rewritePath'], method: method); } void updatePathReg() { _urlReg = RegExp(url.replaceAll("*", ".*").replaceFirst('?', '\\?')); } Map toJson() { var json = { 'name': name, 'enabled': enabled, 'url': url, 'type': type.name, 'rewritePath': rewritePath, }; if (method != null) { json['method'] = method!.name; } return json; } } enum ReplaceBodyType { text("文本"), file("文件"); final String label; const ReplaceBodyType(this.label); } class RewriteItem { bool enabled; RewriteType type; //key redirectUrl, method, path, queryParam, headers, body, statusCode final Map values = {}; RewriteItem(this.type, this.enabled, {Map? values}) { if (values != null) { this.values.addAll(Map.from(values)); } } factory RewriteItem.fromJson(Map map) { return RewriteItem(RewriteType.fromName(map['type']), map['enabled'], values: map['values']); } static List fromRequest(HttpRequest request) { List items = []; items.add(RewriteItem(RewriteType.replaceRequestLine, false)..path = request.requestUri?.path); items.add(RewriteItem(RewriteType.replaceRequestHeader, false)..headers = request.headers.toMap()); items.add(RewriteItem(RewriteType.replaceRequestBody, true)..body = request.getBodyString()); return items; } static List fromResponse(HttpResponse response) { List items = []; items.add(RewriteItem(RewriteType.replaceResponseStatus, false)..statusCode = response.status.code); items.add(RewriteItem(RewriteType.replaceResponseHeader, false)..headers = response.headers.toMap()); items.add(RewriteItem(RewriteType.replaceResponseBody, true)..body = response.getBodyString()); return items; } //key String? get key => values['key']; set key(String? key) => values['key'] = key; String? get value => values['value']; set value(String? value) => values['value'] = value; //redirectUrl String? get redirectUrl => values['redirectUrl']; set redirectUrl(String? redirectUrl) => values['redirectUrl'] = redirectUrl; //method HttpMethod? get method => values['method'] == null ? null : HttpMethod.values.firstWhereOrNull((element) => element.name == values['method']); set method(HttpMethod? method) => values['method'] = method?.name; String? get path => values['path']; set path(String? path) => values['path'] = path; //queryParam String? get queryParam => values['queryParam']; set queryParam(String? queryParam) => values['queryParam'] = queryParam; //statusCode int? get statusCode => values['statusCode']; set statusCode(int? statusCode) => values['statusCode'] = statusCode; //headers Map? get headers => values['headers'] == null ? null : Map.from(values['headers']); set headers(Map? headers) => values['headers'] = headers; //body String? get body => values['body']; set body(String? body) => values['body'] = body; String? get bodyType => values['bodyType']; set bodyType(String? bodyType) => values['bodyType'] = bodyType; String? get bodyFile => values['bodyFile']; set bodyFile(String? bodyFile) => values['bodyFile'] = bodyFile; Map toJson() { return { 'enabled': enabled, 'type': type.name, 'values': values, }; } @override String toString() { return toJson().toString(); } } enum RewriteType { //重定向 redirect("重定向"), //替换请求 replaceRequestLine("请求行"), replaceRequestHeader("请求头"), replaceRequestBody("请求体"), replaceResponseStatus("状态码"), replaceResponseHeader("响应头"), replaceResponseBody("响应体"), //修改请求 updateBody("修改Body"), addQueryParam("添加参数"), removeQueryParam("删除参数"), updateQueryParam("修改参数"), addHeader("添加头部"), removeHeader("删除头部"), updateHeader("修改头部"), ; static List updateRequest = [ updateBody, addQueryParam, updateQueryParam, removeQueryParam, addHeader, updateHeader, removeHeader ]; static List updateResponse = [updateBody, addHeader, updateHeader, removeHeader]; final String label; const RewriteType(this.label); static RewriteType fromName(String name) { return values.firstWhere((element) => element.name == name); } String getDescribe(bool isCN) { if (isCN) { return label; } return name.replaceFirst("replace", "").replaceFirst("Query", ""); } } ================================================ FILE: lib/network/components/manager/script_manager.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter_js/flutter_js.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/cache.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/random.dart'; import 'package:proxypin/ui/component/device.dart'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; import '../js/script_engine.dart'; /// @author wanghongen /// 2023/10/06 /// js脚本 class ScriptManager { static String template = """ // e.g. Add/Update/Remove:Queries、Headers、Body async function onRequest(context, request) { console.log(request.url); //Update or add Header //request.headers["X-New-Headers"] = "My-Value"; // Update Body use fetch API request,具体文档可网上搜索fetch API //request.body = await fetch('https://www.baidu.com/').then(response => response.text()); return request; } //You can modify the Response Data here before it goes to the client async function onResponse(context, request, response) { // response.statusCode = 200; //var body = JSON.parse(response.body); //body['key'] = "value"; //response.body = JSON.stringify(body); return response; } """; static String separator = Platform.pathSeparator; static ScriptManager? _instance; bool enabled = true; List list = []; final ExpiringCache _scriptMap = ExpiringCache(Duration(minutes: 15)); static late JavascriptRuntime flutterJs; static String? deviceId; static final List _logHandlers = []; ScriptManager._(); ///单例 static Future get instance async { if (_instance == null) { _instance = ScriptManager._(); await _instance?.reloadScript(); flutterJs = await JavaScriptEngine.getJavaScript(consoleLog: consoleLog); deviceId = await DeviceUtils.deviceId(); logger.d('init script manager $deviceId'); } return _instance!; } static void registerConsoleLog(int fromWindowId) { LogHandler logHandler = LogHandler( channelId: fromWindowId, handle: (logInfo) { DesktopMultiWindow.invokeMethod(fromWindowId, "consoleLog", logInfo.toJson()).onError((e, t) { logger.e("consoleLog error: $e"); removeLogHandler(fromWindowId); }); }); registerLogHandler(logHandler); } static void registerLogHandler(LogHandler logHandler) { if (_logHandlers.any((it) => it.channelId == logHandler.channelId)) { _logHandlers.removeWhere((it) => it.channelId == logHandler.channelId); } _logHandlers.add(logHandler); } static void removeLogHandler(int channelId) { _logHandlers.removeWhere((element) => channelId == element.channelId); } static dynamic consoleLog(dynamic args) async { if (_logHandlers.isEmpty) { return; } var level = args.removeAt(0); String output = args.join(' '); if (level == 'info') level = 'warn'; LogInfo logInfo = LogInfo(level, output); for (int i = 0; i < _logHandlers.length; i++) { _logHandlers[i].handle.call(logInfo); } } ///重新加载脚本 Future reloadScript() async { List scripts = []; var file = await _path; logger.d("reloadScript ${file.path}"); if (await file.exists()) { var content = await file.readAsString(); if (content.isEmpty) { return; } var config = jsonDecode(content); enabled = config['enabled'] == true; for (var entry in config['list']) { scripts.add(ScriptItem.fromJson(entry)); } } list = scripts; _scriptMap.clear(); } static String? _homePath; static Future homePath() async { if (_homePath != null) { return _homePath!; } if (Platform.isMacOS) { _homePath = await DesktopMultiWindow.invokeMethod(0, "getApplicationSupportDirectory"); } else { _homePath = await getApplicationSupportDirectory().then((it) => it.path); } return _homePath!; } static Future get _path async { final path = await homePath(); var file = File('$path${separator}script.json'); if (!await file.exists()) { await file.create(); } return file; } Future getScript(ScriptItem item) async { // Local script (existing behavior) if (_scriptMap.containsKey(item)) { return _scriptMap[item]!; } // Remote script if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) { var script = await _fetchRemoteScript(item); if (script != null) { _scriptMap[item] = script; } return script; } final home = await homePath(); var script = await File(home + item.scriptPath!).readAsString(); _scriptMap[item] = script; return script; } Future _fetchRemoteScript(ScriptItem item) async { final url = item.remoteUrl!.trim(); if (!_isHttpUrl(url)) { return null; } final resp = await http.get(Uri.parse(url)); final bytes = resp.bodyBytes; final content = utf8.decode(bytes); _scriptMap[item] = content; return content; } bool _isHttpUrl(String url) { final uri = Uri.tryParse(url); if (uri == null) return false; return uri.scheme == 'http' || uri.scheme == 'https'; } ///添加脚本 Future addScript(ScriptItem item, String? script) async { // Remote script: script is treated as initial cache (optional) if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) { list.add(item); return; } script ??= template; final path = await homePath(); String scriptPath = "${separator}scripts$separator${RandomUtil.randomString(16)}.js"; var file = File(path + scriptPath); await file.create(recursive: true); file.writeAsString(script); item.scriptPath = scriptPath; list.add(item); _scriptMap[item] = script; } ///更新脚本 Future updateScript(ScriptItem item, String script) async { // Remote scripts: update cache file (treat as local override of cache) if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) { _scriptMap[item] = script; return; } if (_scriptMap[item] == script) { return; } final home = await homePath(); File(home + item.scriptPath!).writeAsString(script); _scriptMap[item] = script; } ///删除脚本 Future removeScript(int index) async { var item = list.removeAt(index); _scriptMap.remove(item); if (item.scriptPath != null) { final home = await homePath(); File(home + item.scriptPath!).delete(); } } Future clean() async { _scriptMap.clear(); while (list.isNotEmpty) { var item = list.removeLast(); if (item.scriptPath != null) { final home = await homePath(); File(home + item.scriptPath!).delete(); } } await flushConfig(); } ///刷新配置 Future flushConfig() async { await _path.then((value) => value.writeAsString(jsonEncode({'enabled': enabled, 'list': list}))); } Map scriptSession = {}; ///脚本上下文 Map scriptContext(ScriptItem item) { return {'scriptName': item.name, 'os': Platform.operatingSystem, 'session': scriptSession, "deviceId": deviceId}; } ///运行脚本 Future runScript(HttpRequest request) async { if (!enabled) { return request; } var url = request.domainPath; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(scriptContext(item)); var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); String? script = await getScript(item); if (script == null) { continue; } var jsResult = await flutterJs.evaluateAsync( """var request = $jsRequest, context = $context; request['scriptContext'] = context; $script\n onRequest(context, request)"""); var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult); if (result == null) { return null; } request.attributes['scriptContext'] = result['scriptContext']; scriptSession = result['scriptContext']['session'] ?? {}; request = JavaScriptEngine.convertHttpRequest(request, result); } } return request; } ///运行脚本 Future runResponseScript(HttpResponse response) async { if (!enabled || response.request == null) { return response; } var request = response.request!; var url = request.domainPath; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item)); var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); var jsResponse = jsonEncode(await JavaScriptEngine.convertJsResponse(response)); String? script = await getScript(item); if (script == null) { continue; } var jsResult = await flutterJs.evaluateAsync( """var response = $jsResponse, context = $context; response['scriptContext'] = context; $script \n onResponse(context, $jsRequest, response);"""); // print("response: ${jsResult.isPromise} ${jsResult.isError} ${jsResult.rawResult}"); var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult); if (result == null) { return null; } scriptSession = result['scriptContext']['session'] ?? {}; response = JavaScriptEngine.convertHttpResponse(response, result); } } return response; } } class LogHandler { final int channelId; final Function(LogInfo logInfo) handle; LogHandler({required this.channelId, required this.handle}); } class LogInfo { final DateTime time; final String level; final String output; LogInfo(this.level, this.output, {DateTime? time}) : time = time ?? DateTime.now(); factory LogInfo.fromJson(Map json) { return LogInfo(json['level'], json['output'], time: DateTime.fromMillisecondsSinceEpoch(json['time'])); } Map toJson() { return {'time': time.millisecondsSinceEpoch, 'level': level, 'output': output}; } @override String toString() { return '{time: $time, level: $level, output: $output}'; } } class ScriptItem { bool enabled = true; String? name; List urls; String? scriptPath; List? urlRegs; String? remoteUrl; ScriptItem(this.enabled, this.name, dynamic urls, {this.scriptPath, this.remoteUrl}) : urls = urls is String ? (urls.contains(',') ? urls.split(',').map((e) => e.trim()).toList() : [urls]) : (urls is List ? urls : []); // 匹配url,任意一个规则匹配即可 bool match(String url) { urlRegs ??= urls.map((u) => RegExp(u.replaceAll("*", ".*"))).toList(); for (final reg in urlRegs!) { if (reg!.hasMatch(url)) return true; } return false; } factory ScriptItem.fromJson(Map json) { final urlField = json['url']; List urls; if (urlField is List) { urls = urlField.cast(); } else if (urlField is String) { urls = urlField.contains(',') ? urlField.split(',').map((e) => e.trim()).toList() : [urlField]; } else { urls = []; } return ScriptItem( json['enabled'], json['name'], urls, scriptPath: json['scriptPath'], remoteUrl: json['remoteUrl'], ); } Map toJson() { return { 'enabled': enabled, 'name': name, 'url': urls.length == 1 ? urls[0] : urls, 'scriptPath': scriptPath, if (remoteUrl != null) 'remoteUrl': remoteUrl, }; } @override String toString() { return 'ScriptItem{enabled: $enabled, name: $name, url: $urls, scriptPath: $scriptPath, remoteUrl: $remoteUrl}'; } } ================================================ FILE: lib/network/components/report_server_interceptor.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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'; import 'package:proxypin/network/util/compress.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/utils/har.dart'; import '../http/http.dart'; import 'interceptor.dart'; import 'manager/report_server_manager.dart'; /// Hosts interceptor /// @author wanghongen class ReportServerInterceptor extends Interceptor { Future get reportServerManager async => await ReportServerManager.instance; static HttpClient httpClient = HttpClient(); @override int get priority => 1000; @override Future onResponse(HttpRequest request, HttpResponse response) async { // Fire-and-forget reporting; don't block the proxy pipeline unawaited(reportServer(request, response)); return response; } @override Future onError(HttpRequest? request, error, StackTrace? stackTrace) async { if (request != null) { unawaited(reportServer(request, null, error: error, stackTrace: stackTrace)); } return; } Future reportServer(HttpRequest request, HttpResponse? response, {dynamic error, StackTrace? stackTrace}) async { String requestUrl = request.requestUrl; var manager = await reportServerManager; var server = await manager.matchServer(requestUrl); if (server == null) { return; } try { logger.i("reportServer start: $requestUrl -> ${server.name} (${server.serverUrl})"); // Prepare server URL (ensure scheme) var serverUrl = (server.serverUrl).trim(); if (serverUrl.isEmpty) { logger.w('reportServer skipped: serverUrl empty for ${server.name}'); return; } if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) { serverUrl = 'http://$serverUrl'; } final uri = Uri.parse(serverUrl); var payload = Har.toHar(request); List body = utf8.encode(jsonEncode(payload)); // Apply compression if configured final compression = server.compression?.toLowerCase(); if (compression == 'gzip') { try { body = gzipEncode(body); } catch (e) { logger.w('reportServer gzip compress failed: $e'); } } // Send POST final ioReq = await httpClient.postUrl(uri).timeout(const Duration(seconds: 5)); // Set headers final matchedRule = server.name; if (matchedRule.isNotEmpty) { ioReq.headers.set('X-Report-Name', matchedRule); } ioReq.headers.set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8'); if (compression == 'gzip') { ioReq.headers.set(HttpHeaders.contentEncodingHeader, 'gzip'); } // Write body and close ioReq.add(body); final ioResp = await ioReq.close().timeout(const Duration(seconds: 30)); final respText = await ioResp.transform(utf8.decoder).join(); if (ioResp.statusCode >= 200 && ioResp.statusCode < 300) { logger.i('reportServer delivered to ${server.name} (${uri.toString()}), status=${ioResp.statusCode}'); } else { logger.w('reportServer delivery to ${server.name} failed, status=${ioResp.statusCode}, body=$respText'); } } catch (e, st) { logger.e("reportServer error $requestUrl", error: e, stackTrace: st); } } } ================================================ FILE: lib/network/components/request_block.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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 'package:proxypin/network/components/manager/request_block_manager.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; import 'interceptor.dart'; /// RequestBlockInterceptor is a component that can block the request or response. /// @author Hongen Wang class RequestBlockInterceptor extends Interceptor { @override int get priority => 1000; @override Future onRequest(HttpRequest request) async { var uri = request.domainPath; var blockRequest = (await RequestBlockManager.instance).enableBlockRequest(uri); if (blockRequest) { logger.d("[${request.requestId}] 屏蔽请求 $uri"); return null; } return request; } @override Future onResponse(HttpRequest request, HttpResponse response) async { var uri = request.domainPath; var blockResponse = (await RequestBlockManager.instance).enableBlockResponse(uri); if (blockResponse) { logger.d("[${request.requestId}] 屏蔽响应 $uri"); return null; } return response; } } ================================================ FILE: lib/network/components/request_breakpoint.dart ================================================ import 'dart:async'; import 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/components/manager/request_breakpoint_manager.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/cache.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/component/multi_window.dart'; import '../http/http_headers.dart'; class RequestBreakpointInterceptor extends Interceptor { static RequestBreakpointInterceptor instance = RequestBreakpointInterceptor._(); final manager = RequestBreakpointManager.instance; final ExpiringCache> _pausedRequests = ExpiringCache(Duration(minutes: 10)); final ExpiringCache> _pausedResponses = ExpiringCache(Duration(minutes: 10)); RequestBreakpointInterceptor._(); @override Future onRequest(HttpRequest request) async { RequestBreakpointManager requestBreakpointManager = await manager; if (!requestBreakpointManager.enabled) return request; var url = request.requestUrl; for (var rule in requestBreakpointManager.list) { if (rule.match(url, method: request.method) && rule.interceptRequest) { Completer completer = Completer(); _pausedRequests[request.requestId] = completer; // Open Breakpoint Executor Window MultiWindow.openWindow("Breakpoint - Request", 'BreakpointExecutor', args: {'type': 'request', 'request': request.toJson(), 'requestId': request.requestId}); return completer.future.then((req) { if (req == null) { logger.d('Request ${request.requestId} was resumed null, aborting request'); return null; } request.method = req.method; Uri uri = req.requestUri!; if (uri.isScheme('https')) { request.uri = uri.path + (uri.hasQuery ? "?${uri.query}" : ""); } else { request.uri = uri.toString(); } request.headers.clear(); request.headers.addAll(req.headers); request.headers.remove(HttpHeaders.CONTENT_ENCODING); request.body = req.body; logger.d('Resuming request ${request.requestId} with modified request'); return request; }); } } return request; } @override Future onResponse(HttpRequest request, HttpResponse response) async { RequestBreakpointManager requestBreakpointManager = await manager; if (!requestBreakpointManager.enabled) return response; var url = request.requestUrl; for (var rule in requestBreakpointManager.list) { if (rule.match(url, method: request.method) && rule.interceptResponse) { Completer completer = Completer(); _pausedResponses[request.requestId] = completer; // Open Breakpoint Executor Window MultiWindow.openWindow("Breakpoint - Response", 'BreakpointExecutor', args: { 'type': 'response', 'request': request.toJson(), 'response': response.toJson(), 'requestId': request.requestId }); return completer.future.then((res) { if (res == null) { return null; } response.status = res.status; response.headers.clear(); response.headers.addAll(res.headers); response.headers.remove(HttpHeaders.CONTENT_ENCODING); response.body = res.body; logger.d('Resuming response for request ${request.requestId} with modified response'); return response; }); } } return response; } void resumeRequest(String requestId, HttpRequest? request) { if (_pausedRequests.containsKey(requestId)) { _pausedRequests.remove(requestId)?.complete(request); } } void resumeResponse(String requestId, HttpResponse? response) { if (_pausedResponses.containsKey(requestId)) { _pausedResponses.remove(requestId)?.complete(response); } } } ================================================ FILE: lib/network/components/request_map.dart ================================================ /* * Copyright 2025 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:flutter_js/flutter_js.dart'; import 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/file_read.dart'; import 'js/script_engine.dart'; import 'manager/request_map_manager.dart'; import 'manager/script_manager.dart'; /// RequestRewriteComponent is a component that can rewrite the request before sending it to the server. /// @author Hongen Wang class RequestMapInterceptor extends Interceptor { static RequestMapInterceptor instance = RequestMapInterceptor._(); static JavascriptRuntime? flutterJs; static Map scriptSession = {}; final managerInstance = RequestMapManager.instance; RequestMapInterceptor._(); ///脚本上下文 Map scriptContext(RequestMapRule rule) { return {'scriptName': rule.name, 'os': Platform.operatingSystem, 'session': scriptSession}; } @override Future execute(HttpRequest request) async { final manager = await managerInstance; if (!manager.enabled) { return null; } RequestMapRule? mapRule = manager.findMatch(request.requestUrl); if (mapRule == null) { return null; } var item = await manager.getMapItem(mapRule); if (item == null) { return null; } HttpResponse? response; if (mapRule.type == RequestMapType.local) { // 本地映射 response = await mapLocalResponse(mapRule, item); } else if (mapRule.type == RequestMapType.script && item.script != null) { response = await executeScript(request, mapRule, item.script!); } if (response == null) { return null; } response.request = request; request.response = response; return response; } /// 重写响应 Future mapLocalResponse(RequestMapRule rule, RequestMapItem item) async { HttpResponse response = HttpResponse(HttpStatus.valueOf(item.statusCode ?? 200)); item.headers?.forEach((key, value) { response.headers.set(key, value); }); if (item.bodyType == MapBodyType.file.name) { if (item.bodyFile == null) return response; response.body = await FileRead.readFile(item.bodyFile!); } else if (item.body != null) { response.body = response.charset == 'utf-8' || response.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits; } return response; } /// script执行 Future executeScript(HttpRequest request, RequestMapRule rule, String script) async { flutterJs ??= await JavaScriptEngine.getJavaScript(consoleLog: ScriptManager.consoleLog); var context = jsonEncode(scriptContext(rule)); var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); var jsResult = await flutterJs!.evaluateAsync( """var request = $jsRequest, context = $context; request['scriptContext'] = context; $script\n onRequest(context, request)"""); // print("response: ${jsResult.isPromise} ${jsResult.isError} ${jsResult.rawResult}"); var result = await JavaScriptEngine.jsResultResolve(flutterJs!, jsResult); if (result == null) { return null; } if (result['scriptContext']?['session'] != null) { scriptSession = result['scriptContext']['session']; } HttpResponse response = HttpResponse(HttpStatus.valueOf(200)); response = JavaScriptEngine.convertHttpResponse(response, result); return response; } } ================================================ FILE: lib/network/components/request_rewrite.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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:collection'; import 'dart:convert'; import 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/components/manager/request_rewrite_manager.dart'; import 'package:proxypin/network/http/constants.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/network/util/file_read.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/uri.dart'; import 'package:proxypin/utils/lang.dart'; import 'manager/rewrite_rule.dart'; /// RequestRewriteComponent is a component that can rewrite the request before sending it to the server. /// @author Hongen Wang class RequestRewriteInterceptor extends Interceptor { static RequestRewriteInterceptor instance = RequestRewriteInterceptor._(); final requestRewriteManager = RequestRewriteManager.instance; RequestRewriteInterceptor._(); @override Future onRequest(HttpRequest request) async { //重写请求 var url = request.requestUrl; await requestRewrite(url, request); return request; } @override Future onResponse(HttpRequest request, HttpResponse response) async { //重写响应 try { var url = request.requestUrl; await responseRewrite(url, response); } catch (e, t) { response.body = "$e".codeUnits; logger.e('[${request.requestId}] 响应重写异常 ', error: e, stackTrace: t); } return response; } ///获取重定向 Future getRedirectRule(String? url) async { var manager = await requestRewriteManager; var rewriteRule = manager.getRewriteRule(url, [RuleType.redirect]); if (rewriteRule == null) { return null; } var rewriteItems = await manager.getRewriteItems(rewriteRule); var redirectUrl = rewriteItems?.firstWhereOrNull((element) => element.enabled)?.redirectUrl; if (rewriteRule.url.contains("*") && redirectUrl?.contains("*") == true) { String ruleUrl = rewriteRule.url.replaceAll("*", ""); redirectUrl = redirectUrl?.replaceAll("*", url!.replaceAll(ruleUrl, "")); } return redirectUrl; } /// 重写请求 Future requestRewrite(String url, HttpRequest request) async { var manager = await RequestRewriteManager.instance; var rewriteRule = manager.getRewriteRule(url, [RuleType.requestReplace, RuleType.requestUpdate]); if (rewriteRule?.type == RuleType.requestReplace) { var rewriteItems = await manager.getRewriteItems(rewriteRule!); for (var item in rewriteItems!) { if (item.enabled) { await _replaceRequest(request, item); } } } if (rewriteRule?.type == RuleType.requestUpdate) { var rewriteItems = await manager.getRewriteItems(rewriteRule!); if (rewriteItems == null) { return; } for (var item in rewriteItems) { if (item.enabled) { await _updateRequest(request, item); } } } } /// 重写响应 Future responseRewrite(String? url, HttpResponse response) async { var manager = await RequestRewriteManager.instance; var rewriteRule = manager.getRewriteRule(url, [RuleType.responseReplace, RuleType.responseUpdate]); if (rewriteRule == null) { return; } if (rewriteRule.type == RuleType.responseReplace) { var rewriteItems = await manager.getRewriteItems(rewriteRule); for (var item in rewriteItems!) { if (item.enabled) { await _replaceResponse(response, item); } } } if (rewriteRule.type == RuleType.responseUpdate) { var rewriteItems = await manager.getRewriteItems(rewriteRule); if (rewriteItems == null) { return; } for (var item in rewriteItems) { if (item.enabled) { await _updateMessage(response, item); } } } } Future _updateRequest(HttpRequest request, RewriteItem item) async { var paramTypes = [RewriteType.addQueryParam, RewriteType.removeQueryParam, RewriteType.updateQueryParam]; if (paramTypes.contains(item.type)) { var requestUri = request.requestUri; Map queryParameters = LinkedHashMap.from(requestUri!.queryParameters); switch (item.type) { case RewriteType.addQueryParam: queryParameters[item.key!] = item.value; break; case RewriteType.removeQueryParam: if (item.value?.trim().isNotEmpty == true) { var val = queryParameters[item.key!]; if (val == null || !RegExp(item.value!).hasMatch(val)) { break; } } queryParameters.remove(item.key!); break; case RewriteType.updateQueryParam: var itemKey = item.key; if (itemKey == null || itemKey.trim().isEmpty) return; var entries = Map.of(queryParameters).entries; var regExp = RegExp(item.key!); for (var entry in entries) { var line = "${entry.key}=${entry.value}"; if (regExp.hasMatch(line)) { line = line.replaceAll(regExp, item.value ?? ''); var pair = line.splitFirst(HttpConstants.equal); if (pair.first != entry.key) queryParameters.remove(entry.key); queryParameters[pair.first] = pair.length > 1 ? pair.last : ''; break; } } break; default: break; } requestUri = requestUri.replace(query: UriUtils.mapToQuery(queryParameters)); if (requestUri.isScheme('https')) { request.uri = requestUri.path + (requestUri.hasQuery ? "?${requestUri.query}" : ""); } else { request.uri = requestUri.toString(); } return; } await _updateMessage(request, item); } //修改消息 Future _updateMessage(HttpMessage message, RewriteItem item) async { if (item.type == RewriteType.updateBody && message.body != null) { String body = (await message.decodeBodyString()).replaceAllMapped(RegExp(item.key!), (match) { if (match.groupCount > 0 && item.value?.contains("\$1") == true) { return item.value!.replaceAll("\$1", match.group(1)!); } return item.value ?? ''; }); message.body = message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(body) : body.codeUnits; message.headers.remove(HttpHeaders.CONTENT_ENCODING); message.headers.contentLength = message.body!.length; return; } if (item.type == RewriteType.addHeader) { message.headers.set(item.key!, item.value ?? ''); return; } if (item.type == RewriteType.removeHeader) { if (item.value?.trim().isNotEmpty == true) { var val = message.headers.get(item.key!); if (val == null || !RegExp(item.value!).hasMatch(val)) { return; } } message.headers.remove(item.key!); return; } if (item.type == RewriteType.updateHeader) { if (item.key == null || item.key?.trim().isEmpty == true) return; var headers = Map.of(message.headers.getHeaders()); var regExp = RegExp(item.key!, caseSensitive: false); headers.forEach((key, values) { var line = "$key: ${values.firstOrNull ?? ''}"; if (regExp.hasMatch(line)) { line = line.replaceAll(regExp, item.value ?? ''); var pair = line.splitFirst(HttpConstants.colon); if (pair.first != key) message.headers.remove(key); message.headers.set(pair.first, pair.length > 1 ? pair.last : ''); } }); return; } } //替换请求 Future _replaceRequest(HttpRequest request, RewriteItem item) async { if (item.type == RewriteType.replaceRequestLine) { request.method = item.method ?? request.method; Uri uri = Uri.parse(request.requestUrl).replace(path: item.path, query: item.queryParam); if (uri.isScheme('https')) { request.uri = uri.path + (uri.hasQuery ? "?${uri.query}" : ""); } else { request.uri = uri.toString(); } return; } await _replaceHttpMessage(request, item); } //替换相应 Future _replaceResponse(HttpResponse response, RewriteItem item) async { if (item.type == RewriteType.replaceResponseStatus && item.statusCode != null) { response.status = HttpStatus.valueOf(item.statusCode!); return; } await _replaceHttpMessage(response, item); } Future _replaceHttpMessage(HttpMessage message, RewriteItem item) async { if ((item.type == RewriteType.replaceRequestHeader || item.type == RewriteType.replaceResponseHeader) && item.headers != null) { item.headers?.forEach((key, value) => message.headers.set(key, value)); return; } if (item.type == RewriteType.replaceResponseBody || item.type == RewriteType.replaceRequestBody) { if (item.bodyType == ReplaceBodyType.file.name) { if (item.bodyFile == null) return; message.body = await FileRead.readFile(item.bodyFile!); message.headers.contentLength = message.body!.length; message.headers.remove(HttpHeaders.CONTENT_ENCODING); return; } if (item.body != null) { message.body = message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits; message.headers.contentLength = message.body!.length; message.headers.remove(HttpHeaders.CONTENT_ENCODING); } return; } } } ================================================ FILE: lib/network/components/script.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; import 'manager/script_manager.dart'; /// developers can write JS code to flexibly manipulate requests/responses ///@author Hongen Wang class ScriptInterceptor extends Interceptor { @override int get priority => 10; @override Future onRequest(HttpRequest request) async { //脚本替换 var scriptManager = await ScriptManager.instance; HttpRequest? httpRequest = await scriptManager.runScript(request); if (httpRequest == null) { return null; } return request; } @override Future onResponse(HttpRequest request, HttpResponse response) async { //脚本替换 var scriptManager = await ScriptManager.instance; try { HttpResponse? httpResponse = await scriptManager.runResponseScript(response); if (httpResponse == null) { return null; } return httpResponse; } catch (e, t) { response.status = HttpStatus(-1, 'Script exec error'); response.body = "$e\n${response.bodyAsString}".codeUnits; logger.e('[${request.requestId}] 执行脚本异常 ', error: e, stackTrace: t); } return response; } } ================================================ FILE: lib/network/handle/http_proxy_handle.dart ================================================ import 'dart:convert'; import 'package:proxypin/network/bin/listener.dart'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/components/host_filter.dart'; import 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/components/request_rewrite.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_client.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/proxy_helper.dart'; import 'package:proxypin/network/util/attribute_keys.dart'; import 'package:proxypin/network/util/uri.dart'; import 'package:proxypin/utils/ip.dart'; /// http请求处理器 class HttpProxyChannelHandler extends ChannelHandler { EventListener? listener; final List interceptors; HttpProxyChannelHandler({this.listener, required this.interceptors}); @override Future channelRead(ChannelContext channelContext, Channel channel, HttpRequest msg) async { //下载证书 if (msg.uri == 'http://proxy.pin/ssl' || msg.requestUrl == 'http://127.0.0.1:${channel.socket.port}/ssl') { ProxyHelper.crtDownload(channelContext, channel, msg); return; } //请求本服务 if (((await localIps()).contains(msg.hostAndPort?.host) || '127.0.0.1' == msg.hostAndPort?.host) && msg.hostAndPort?.port == channel.socket.port) { ProxyHelper.localRequest(channelContext, msg, channel); return; } //代理转发请求 try { await forward(channelContext, channel, msg).catchError((error, trace) { exceptionCaught(channelContext, channel, error, trace: trace); }); } catch (error, trace) { exceptionCaught(channelContext, channel, error, trace: trace); } } @override void exceptionCaught(ChannelContext channelContext, Channel channel, error, {StackTrace? trace}) { super.exceptionCaught(channelContext, channel, error, trace: trace); ProxyHelper.exceptionHandler(channelContext, channel, listener, channelContext.currentRequest, error); for (var interceptor in interceptors) { interceptor.onError(channelContext.currentRequest, error, trace); } } @override void channelInactive(ChannelContext channelContext, Channel channel) { Channel? remoteChannel = channelContext.serverChannel; remoteChannel?.close(); // log.d("[${channel.id}] close ${channel.error}"); } /// 转发请求 Future forward(ChannelContext channelContext, Channel channel, HttpRequest httpRequest) async { // log.d("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}"); if (channel.error != null) { ProxyHelper.exceptionHandler(channelContext, channel, listener, httpRequest, channel.error); return; } //获取远程连接 Channel remoteChannel; try { remoteChannel = await _getRemoteChannel(channelContext, channel, httpRequest); } catch (error, stackTrace) { log.e("[${channel.id}] 连接异常 ${httpRequest.method.name} ${httpRequest.requestUrl}", error: error, stackTrace: stackTrace); if (httpRequest.method == HttpMethod.connect) { channel.error = error; //记录异常 //https代理新建connect连接请求 返回ok 会继续发起正常请求 可以获取到请求内容 await channel.write(channelContext, HttpResponse(HttpStatus.ok.reason('Connection established'), protocolVersion: httpRequest.protocolVersion)); } else { rethrow; } return; } //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { // log.d( // "[${channel.id}] streamId:${httpRequest.streamId ?? ''} ${httpRequest.protocolVersion} ${httpRequest.method.name} ${httpRequest.requestUrl}"); if (HostFilter.filter(httpRequest.hostAndPort?.host)) { await remoteChannel.write(channelContext, httpRequest); return; } HttpRequest? request = httpRequest; //拦截器 for (var interceptor in interceptors) { request = await interceptor.onRequest(request!); if (request == null) { listener?.onRequest(channel, httpRequest); channel.close(); remoteChannel.close(); return; } } channelContext.currentRequest = request; listener?.onRequest(channel, request!); for (var interceptor in interceptors) { var response = await interceptor.execute(request!); if (response != null) { listener?.onResponse(channelContext, response); channel.writeAndClose(channelContext, response); return; } } //重定向 var uri = request!.domainPath; String? redirectUrl = await (RequestRewriteInterceptor.instance).getRedirectRule(uri); if (redirectUrl?.isNotEmpty == true) { await redirect(channelContext, channel, request, redirectUrl!); return; } //http1 直接请求 不需要携带域名 if (!remoteChannel.useProxy && request.protocolVersion == HttpMessage.http1Version && request.uri.startsWith(HostAndPort.httpScheme)) { final requestUri = request.requestUri!; request.uri = "${requestUri.path}${requestUri.hasQuery ? '?${requestUri.query}' : ''}"; } await remoteChannel.write(channelContext, request); } } //重定向 Future redirect( ChannelContext channelContext, Channel channel, HttpRequest httpRequest, String redirectUrl) async { var proxyHandler = HttpResponseProxyHandler(channel, interceptors, listener: listener); var redirectUri = UriBuild.build(redirectUrl, params: httpRequest.queries.isEmpty ? null : httpRequest.queries); log.d("[${channel.id}] 重定向 $redirectUri"); if (redirectUri.isScheme('https')) { httpRequest.uri = redirectUri.path + (redirectUri.hasQuery ? '?${redirectUri.query}' : ''); } else { httpRequest.uri = redirectUri.toString(); } httpRequest.headers.host = '${redirectUri.host}${redirectUri.hasPort ? ':${redirectUri.port}' : ''}'; var redirectChannel = await HttpClients.connect(redirectUri, proxyHandler, channelContext); channelContext.serverChannel = redirectChannel; await redirectChannel.write(channelContext, httpRequest); } /// 获取远程连接 Future _getRemoteChannel( ChannelContext channelContext, Channel clientChannel, HttpRequest httpRequest) async { //客户端连接 作为缓存 Channel? remoteChannel = channelContext.serverChannel; if (remoteChannel != null) { return remoteChannel; } var hostAndPort = httpRequest.hostAndPort ?? getHostAndPort(httpRequest); channelContext.host = hostAndPort; //远程转发 HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote); //外部代理 ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo); if (remote != null || proxyInfo != null) { HostAndPort connectHost = remote ?? HostAndPort.host(proxyInfo!.host, proxyInfo.port!); final proxyChannel = await connectRemote(channelContext, clientChannel, connectHost); proxyChannel.useProxy = true; //代理建立完连接判断是否是https 需要发起connect请求 if (httpRequest.method == HttpMethod.connect) { //proxy Authorization if (proxyInfo?.isAuthenticated == true) { String auth = base64Encode(utf8.encode("${proxyInfo?.username}:${proxyInfo?.password}")); httpRequest.headers.set(HttpHeaders.PROXY_AUTHORIZATION, 'Basic $auth'); } await proxyChannel.write(channelContext, httpRequest); } else { if (clientChannel.isSsl) { await HttpClients.connectRequest(channelContext, hostAndPort, proxyChannel, proxyInfo: proxyInfo); await proxyChannel.secureSocket(channelContext, host: hostAndPort.host, supportedProtocols: httpRequest.protocolVersion == "HTTP/2" ? ["h2"] : null); } } return proxyChannel; } HostAndPort remoteAddress = hostAndPort; final ProxyInfo? socksProxy = channelContext.getAttribute(AttributeKeys.socks5Proxy); if (socksProxy != null) { remoteAddress = hostAndPort.copyWith(host: socksProxy.host, port: socksProxy.port!); } for (var interceptor in interceptors) { remoteAddress = await interceptor.preConnect(remoteAddress); } final proxyChannel = await connectRemote(channelContext, clientChannel, remoteAddress); if (clientChannel.isSsl) { await proxyChannel.secureSocket(channelContext, host: hostAndPort.host, supportedProtocols: channelContext.clientChannel?.selectedProtocol == null ? null : [channelContext.clientChannel!.selectedProtocol!]); } //https代理新建连接请求 if (httpRequest.method == HttpMethod.connect) { await clientChannel.write(channelContext, HttpResponse(HttpStatus.ok.reason('Connection established'), protocolVersion: httpRequest.protocolVersion)); } return proxyChannel; } /// 连接远程 Future connectRemote(ChannelContext channelContext, Channel clientChannel, HostAndPort connectHost) async { var proxyHandler = HttpResponseProxyHandler(clientChannel, interceptors, listener: listener); var proxyChannel = await channelContext.connectServerChannel(connectHost, proxyHandler); return proxyChannel; } } /// http响应代理 class HttpResponseProxyHandler extends ChannelHandler { //客户端的连接 final Channel clientChannel; EventListener? listener; final List interceptors; HttpResponseProxyHandler(this.clientChannel, this.interceptors, {this.listener}); @override Future channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async { var request = msg.request ?? channelContext.currentRequest; request?.response = msg; //域名是否过滤 if (HostFilter.filter(request?.hostAndPort?.host) || request?.method == HttpMethod.connect) { await clientChannel.write(channelContext, msg); return; } // log.i("[${clientChannel.id}] Response $msg"); HttpResponse? response = msg; //拦截器 for (var interceptor in interceptors) { response = await interceptor.onResponse(request!, response!); if (response == null) { logger.d("[${clientChannel.id}] Interceptor returned null, stopping processing"); // Interceptor returned null, stopping processing listener?.onResponse(channelContext, msg); channel.close(); return; } } // Ensure request is linked if not present response?.request ??= request; listener?.onResponse(channelContext, response!); //发送给客户端 await clientChannel.write(channelContext, response!); } @override void channelInactive(ChannelContext channelContext, Channel channel) { clientChannel.close(); } @override void exceptionCaught(ChannelContext channelContext, Channel channel, error, {StackTrace? trace}) { super.exceptionCaught(channelContext, channel, error, trace: trace); for (var interceptor in interceptors) { interceptor.onError(channelContext.currentRequest, error, trace); } } } ================================================ FILE: lib/network/handle/relay_handle.dart ================================================ import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; class RelayHandler extends ChannelHandler { final Channel remoteChannel; RelayHandler(this.remoteChannel); @override Future channelRead(ChannelContext channelContext, Channel channel, Object msg) async { //发送给客户端 remoteChannel.write(channelContext, msg); } @override void channelInactive(ChannelContext channelContext, Channel channel) { remoteChannel.close(); } } ================================================ FILE: lib/network/handle/sse_handle.dart ================================================ import 'dart:typed_data'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/sse.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/network/util/logger.dart'; /// SSE (text/event-stream) handler: forwards raw bytes and emits parsed message frames. class SseChannelHandler extends ChannelHandler { final SseDecoder decoder = SseDecoder(); final Channel proxyChannel; final HttpMessage message; // HttpResponse on server->client, HttpRequest on client->server SseChannelHandler(this.proxyChannel, this.message); @override Future channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async { // Always forward the raw bytes first proxyChannel.writeBytes(msg); try { final frames = decoder.feed(msg); for (final WebSocketFrame frame in frames) { frame.isFromClient = message is HttpRequest; message.messages.add(frame); channelContext.listener?.onMessage(channel, message, frame); logger.d( "[${channelContext.clientChannel?.id}] sse channelRead ${frame.payloadLength} ${frame.payloadDataAsString}"); } } catch (e, stackTrace) { log.e("sse decode error", error: e, stackTrace: stackTrace); } } } ================================================ FILE: lib/network/handle/websocket_handle.dart ================================================ import 'dart:typed_data'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/network/util/logger.dart'; /// websocket处理器 class WebSocketChannelHandler extends ChannelHandler { final WebSocketDecoder decoder = WebSocketDecoder(); final Channel proxyChannel; final HttpMessage message; WebSocketChannelHandler(this.proxyChannel, this.message); @override Future channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async { proxyChannel.writeBytes(msg); WebSocketFrame? frame; try { frame = decoder.decode(msg); } catch (e, stackTrace) { log.e("websocket decode error", error: e, stackTrace: stackTrace); } if (frame == null) { return; } frame.isFromClient = message is HttpRequest; message.messages.add(frame); channelContext.listener?.onMessage(channel, message, frame); logger.d( "[${channelContext.clientChannel?.id}] websocket channelRead ${frame.payloadLength} ${frame.fin} ${frame.payloadDataAsString}"); } } ================================================ FILE: lib/network/http/codec.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:math'; import 'dart:typed_data'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/parse/body_reader.dart'; import 'package:proxypin/network/http/constants.dart'; import 'package:proxypin/network/http/h2/h2_codec.dart'; import 'package:proxypin/network/http/parse/http_parser.dart'; import 'package:proxypin/network/util/byte_buf.dart'; import 'http.dart'; import 'http_headers.dart'; class ParserException implements Exception { final String message; final String? source; ParserException(this.message, [this.source]); @override String toString() { return 'ParserException{message: $message source: $source}'; } } enum State { readInitial, readHeader, body, done, } class DecoderResult { bool isDone = true; T? data; bool supportedParse; //转发消息 List? forward; DecoderResult({this.isDone = true, this.supportedParse = true}); } /// 解码 abstract interface class Decoder { /// 解码 如果返回null说明数据不完整 DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf); } /// 编码 abstract interface class Encoder { List encode(ChannelContext channelContext, T data); } /// 编解码器 abstract class Codec implements Decoder, Encoder { static const int defaultMaxInitialLineLength = 1024000; // 1M static const int maxBodyLength = 4096000; // 4M } /// http编解码 abstract class HttpCodec implements Codec { final HttpParse _httpParse = HttpParse(); Http2Codec? _h2Codec; State _state = State.readInitial; late DecoderResult result; BodyReader? bodyReader; T createMessage(List reqLine); Http2Codec getH2Codec() { return _h2Codec ??= (this is HttpRequestCodec ? Http2RequestDecoder() : Http2ResponseDecoder()) as Http2Codec; } @override DecoderResult decode(ChannelContext channelContext, ByteBuf data) { var protocol = channelContext.clientChannel?.selectedProtocol; if (protocol == HttpConstants.h2 || protocol == HttpConstants.h2_14) { return getH2Codec().decode(channelContext, data); } //请求行 if (_state == State.readInitial) { init(); var initialLine = _readInitialLine(data); if (initialLine.isEmpty) { return result; } result.data = createMessage(initialLine); _state = State.readHeader; } //请求头 try { if (_state == State.readHeader) { _readHeader(data, result.data!); } //请求体 if (_state == State.body) { bool resolveBody = channelContext.currentRequest?.method != HttpMethod.head; var bodyResult = resolveBody ? bodyReader!.readBody(data.readAvailableBytes()) : null; if (!resolveBody || bodyResult?.isDone == true) { _state = State.done; result.data!.body = bodyResult?.body; } //If the body does not support parsing, forward directly if (bodyResult != null && !bodyResult.supportedParse) { result.supportedParse = false; result.forward = bodyResult.body; return result; } } if (_state == State.done) { result.data!.body = _convertBody(result.data!.body); _state = State.readInitial; result.isDone = true; return result; } } catch (e) { _state = State.readInitial; rethrow; } return result; } void init() { bodyReader = null; result = DecoderResult(isDone: false); } void initialLine(BytesBuilder buffer, T message); @override List encode(ChannelContext channelContext, T message) { if (message.protocolVersion == "HTTP/2") { return getH2Codec().encode(channelContext, message); } BytesBuilder builder = BytesBuilder(); //请求行 initialLine(builder, message); List? body = message.body; //请求头 bool isChunked = message.headers.isChunked; message.headers.remove(HttpHeaders.TRANSFER_ENCODING); if (body != null && (body.isNotEmpty || isChunked)) { message.headers.contentLength = body.length; } else if (message.contentLength != 0) { message.headers.remove(HttpHeaders.CONTENT_LENGTH); } message.headers.forEach((key, values) { for (var v in values) { builder ..add(key.codeUnits) ..addByte(HttpConstants.colon) ..addByte(HttpConstants.sp) ..add(v.codeUnits) ..addByte(HttpConstants.cr) ..addByte(HttpConstants.lf); } }); builder.addByte(HttpConstants.cr); builder.addByte(HttpConstants.lf); //请求体 builder.add(body ?? Uint8List(0)); return builder.toBytes(); } //读取起始行 List _readInitialLine(ByteBuf data) { int maxSize = min(data.readableBytes(), Codec.defaultMaxInitialLineLength); return _httpParse.parseInitialLine(data, maxSize); } //读取请求头 void _readHeader(ByteBuf data, T message) { if (_httpParse.parseHeaders(data, message.headers)) { _state = State.body; bodyReader = BodyReader(message); } } //转换body List? _convertBody(List? bytes) { if (bytes == null) { return null; } return bytes; } } /// http请求编解码 class HttpRequestCodec extends HttpCodec { @override HttpRequest createMessage(List reqLine) { HttpMethod httpMethod = HttpMethod.valueOf(reqLine[0]); return HttpRequest(httpMethod, reqLine[1], protocolVersion: reqLine[2]); } @override void initialLine(BytesBuilder buffer, HttpRequest message) { String uri = message.uri; //http scheme 输入地址和host不一致 if (uri.startsWith(HostAndPort.httpScheme) && (message.requestUri?.host != message.headers.host && message.headers.host?.contains(':') != true)) { uri = message.requestUri?.replace(host: message.headers.host).toString() ?? uri; } //请求行 buffer ..add(message.method.name.codeUnits) ..addByte(HttpConstants.sp) ..add(uri.codeUnits) ..addByte(HttpConstants.sp) ..add(message.protocolVersion.codeUnits) ..addByte(HttpConstants.cr) ..addByte(HttpConstants.lf); } } /// http响应编解码 class HttpResponseCodec extends HttpCodec { @override HttpResponse createMessage(List reqLine) { var httpStatus = HttpStatus(int.parse(reqLine[1]), reqLine[2]); return HttpResponse(httpStatus, protocolVersion: reqLine[0]); } @override void initialLine(BytesBuilder buffer, HttpResponse message) { //状态行 buffer.add(message.protocolVersion.codeUnits); buffer.addByte(HttpConstants.sp); buffer.add(message.status.code.toString().codeUnits); buffer.addByte(HttpConstants.sp); buffer.add(message.status.reasonPhrase.codeUnits); buffer.addByte(HttpConstants.cr); buffer.addByte(HttpConstants.lf); } } class HttpServerCodec extends Codec { HttpRequestCodec requestCodec = HttpRequestCodec(); HttpResponseCodec responseCodec = HttpResponseCodec(); @override DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf) { return requestCodec.decode(channelContext, byteBuf); } @override List encode(ChannelContext channelContext, HttpResponse data) { return responseCodec.encode(channelContext, data); } } class HttpClientCodec extends Codec { HttpRequestCodec requestCodec = HttpRequestCodec(); HttpResponseCodec responseCodec = HttpResponseCodec(); @override DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf) { return responseCodec.decode(channelContext, byteBuf); } @override List encode(ChannelContext channelContext, HttpRequest data) { return requestCodec.encode(channelContext, data); } } ================================================ FILE: lib/network/http/constants.dart ================================================ class HttpConstants { //h2协议 static const String h2 = 'h2'; static const String h2_14 = 'h2-14'; /// Line feed character /n static const int lf = 10; /// Carriage return /r static const int cr = 13; /// Horizontal space static const int sp = 32; /// Colon ':' static const int colon = 58; /// Colon '=' static const int equal = 61; } ================================================ FILE: lib/network/http/content_type.dart ================================================ /* * Copyright Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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 'package:proxypin/network/util/cache.dart'; import 'package:proxypin/network/util/logger.dart'; ///content type ///@author WangHongEn enum ContentType { json, formUrl, formData, js, html, text, css, font, image, video, http, sse ; static ContentType valueOf(String name) { return ContentType.values.firstWhere((element) => element.name == name.toLowerCase(), orElse: () => http); } //是否是二进制 bool get isBinary { return this == image || this == font || this == video; } bool get isImage => this == image; } class MediaType { static const String wildcardType = "*/*"; static LruCache cachedMediaTypes = LruCache(64); ///默认编码类型 static List defaultCharsetMediaTypes = [ MediaType("text", "plain", charset: "utf-8"), MediaType("text", "html", charset: "utf-8"), MediaType("text", "javascript", charset: "utf-8"), MediaType("text", "css", charset: "utf-8"), MediaType("application", "json", charset: "utf-8"), MediaType("application", "problem+json", charset: "utf-8"), MediaType("application", "xml", charset: "utf-8"), MediaType("application", "xhtml+xml", charset: "utf-8"), MediaType("application", "octet-stream", charset: "utf-16"), MediaType("image", "*", charset: "utf-16"), ]; final String type; final String subtype; final Map parameters; MediaType(this.type, this.subtype, {Map? parameters, String? charset}) : parameters = parameters ?? {} { if (charset != null) { this.parameters["charset"] = charset; } } static MediaType? valueOf(String mediaType) { if (mediaType.isEmpty) { throw InvalidMediaTypeException(mediaType, "'mediaType' must not be empty"); } // do not cache multipart mime types with random boundaries if (mediaType.startsWith("multipart")) { return _parseMediaTypeInternal(mediaType); } var parseMediaType = _parseMediaTypeInternal(mediaType); if (parseMediaType == null) { return null; } cachedMediaTypes.set(mediaType, parseMediaType); return parseMediaType; } ///编码 String? get charset { return parameters["charset"]?.toLowerCase(); } ///获取默认编码 static String? defaultCharset(MediaType mediaType) { for (var defaultMediaType in defaultCharsetMediaTypes) { if (defaultMediaType.equalsTypeAndSubtype(mediaType)) { return defaultMediaType.charset; } } return null; } static MediaType? _parseMediaTypeInternal(String mediaType) { int index = mediaType.indexOf(';'); String fullType = (index >= 0 ? mediaType.substring(0, index) : mediaType).trim(); if (fullType.isEmpty) { logger.d("Invalid media type: '$mediaType'"); return null; } if (MediaType.wildcardType == fullType) { fullType = "*/*"; } int subIndex = fullType.indexOf('/'); if (subIndex == -1) { logger.d("Invalid media type: '$mediaType'"); return null; } if (subIndex == fullType.length - 1) { logger.d("Invalid media type: '$mediaType'"); return null; } String type = fullType.substring(0, subIndex); String subtype = fullType.substring(subIndex + 1); if (MediaType.wildcardType == type && MediaType.wildcardType != subtype) { logger.d("Invalid media type: '$mediaType'"); return null; } Map parameters = {}; do { int nextIndex = index + 1; bool quoted = false; while (nextIndex < mediaType.length) { var ch = mediaType[nextIndex]; if (ch == ';') { if (!quoted) { break; } } else if (ch == '"') { quoted = !quoted; } nextIndex++; } String parameter = mediaType.substring(index + 1, nextIndex).trim(); if (parameter.isNotEmpty) { int eqIndex = parameter.indexOf('='); if (eqIndex >= 0) { String attribute = parameter.substring(0, eqIndex).trim(); String value = parameter.substring(eqIndex + 1).trim(); parameters[attribute] = value; } } index = nextIndex; } while (index < mediaType.length); try { return MediaType(type, subtype, parameters: parameters); } catch (e) { logger.d("Invalid media type: '$mediaType'", error: e); return null; } } ///类似于equals(Object),但仅基于类型和子类型,即忽略参数。 bool equalsTypeAndSubtype(MediaType other) { return type.toLowerCase() == other.type.toLowerCase() && subtype.toLowerCase() == other.subtype.toLowerCase(); } @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other is MediaType) { return type == other.type && subtype == other.subtype && parameters == other.parameters; } return false; } @override int get hashCode => type.hashCode ^ subtype.hashCode ^ parameters.hashCode; } class InvalidMediaTypeException implements Exception { final String mediaType; final String message; InvalidMediaTypeException(this.mediaType, this.message); @override String toString() { return "InvalidMediaTypeException: $message (mediaType: $mediaType)"; } } ================================================ FILE: lib/network/http/h2/frame.dart ================================================ /* * Copyright 2023 the original author or authors. * * 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 * * https://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. */ enum FrameType { data, headers, priority, rstStream, settings, pushPromise, ping, goaway, windowUpdate, continuation } class FrameHeader { static const flagsEndStream = 0x01; static const flagsEndHeaders = 0x04; static const flagsPriority = 0x20; final int length; final FrameType type; int flags; // 8 bits final int streamIdentifier; FrameHeader(this.length, this.type, this.flags, this.streamIdentifier); bool get hasPaddedFlag => (flags & 0x08) == 0x08; bool get hasPriorityFlag => (flags & flagsPriority) == flagsPriority; bool get hasEndHeadersFlag => (flags & flagsEndHeaders) == flagsEndHeaders; bool get hasEndStreamFlag => (flags & flagsEndStream) == flagsEndStream; bool get hasAckFlag => (flags & 0x01) == 0x01; List encode() { var result = []; result.addAll(_intToBytes(length, 3)); // length is 24 bits result.add(type.index); // type is 8 bits result.add(flags); // flags is 8 bits result.addAll(_intToBytes(streamIdentifier, 4)); // streamIdentifier is 32 bits return result; } List _intToBytes(int value, int byteCount) { var bytes = []; for (var i = 0; i < byteCount; i++) { bytes.insert(0, value & 0xff); value >>= 8; } return bytes; } } class Frame { final FrameHeader header; Frame(this.header); Map toJson() => { 'length': header.length, 'type': header.type.toString().split('.')[1], 'flags': header.flags, 'streamIdentifier': header.streamIdentifier }; } class HeadersFrame extends Frame { final int padLength; final bool exclusiveDependency; final int? streamDependency; final int? weight; List headerBlockFragment; HeadersFrame(super.header, this.padLength, this.exclusiveDependency, this.streamDependency, this.weight, this.headerBlockFragment); @override String toString() { return "HeadersFrame{padLength: $padLength, exclusiveDependency: $exclusiveDependency, streamDependency: $streamDependency, weight: $weight, headerBlockFragment: ${headerBlockFragment.length}}"; } } class DataFrame extends Frame { final int padLength; final List data; DataFrame(super.header, this.padLength, this.data); } ================================================ FILE: lib/network/http/h2/h2_codec.dart ================================================ /* * Copyright 2023 the original author or authors. * * 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 * * https://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:math'; import 'dart:typed_data'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/codec.dart'; import 'package:proxypin/network/http/h2/setting.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/network/util/byte_buf.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/http/sse.dart'; import 'package:proxypin/network/http/websocket.dart'; import '../../util/byte_utils.dart'; import 'frame.dart'; import 'hpack/hpack.dart'; /// http编解码 abstract class Http2Codec implements Codec { static const maxFrameSize = 16384; static final List connectionPrefacePRI = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".codeUnits; HPackDecoder decoder = HPackDecoder(); final HPackEncoder _hpackEncoder = HPackEncoder(); T createMessage(ChannelContext channelContext, FrameHeader frameHeader, Map> headers); T? getMessage(ChannelContext channelContext, FrameHeader frameHeader); // Per-stream SSE decoder instances keyed by HTTP/2 stream id final Map sseDecoders = {}; @override DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) { DecoderResult result = DecoderResult(); //Connection Preface PRI * HTTP/2.0 if (byteBuf.get(byteBuf.readerIndex) == 0x50 && byteBuf.get(byteBuf.readerIndex + 1) == 0x52 && byteBuf.get(byteBuf.readerIndex + 2) == 0x49 && isConnectionPrefacePRI(byteBuf)) { result.forward = byteBuf.readBytes(connectionPrefacePRI.length); // logger.d( // "Connection Preface ${connectionPrefacePRI.length} ${String.fromCharCodes(result.forward!)} ${byteBuf.readableBytes()}"); if (byteBuf.readableBytes() <= 0) { return result; } } List? forward = result.forward == null ? null : List.of(result.forward!); while (byteBuf.isReadable()) { FrameHeader? frameHeader = FrameReader.readFrameHeader(byteBuf); // logger.d( // "frameHeader streamId: ${frameHeader?.streamIdentifier} frame ${frameHeader?.type.name} ${frameHeader?.length} ${byteBuf.readableBytes()}"); if (frameHeader == null) { result.forward = forward; result.isDone = false; return result; } List? framePayload = FrameReader._readFramePayload(byteBuf, frameHeader.length); if (framePayload == null) { result.isDone = false; byteBuf.readerIndex -= FrameReader.headerLength; result.forward = forward; return result; } var parseResult = parseHttp2Packet(channelContext, frameHeader, framePayload); if (parseResult.forward != null) { forward ??= []; forward.addAll(parseResult.forward!); } if (parseResult.isDone) { parseResult.forward = forward; return parseResult; } } result.forward = forward; result.isDone = false; return result; } DecoderResult parseHttp2Packet(ChannelContext channelContext, FrameHeader frameHeader, List framePayload) { var result = DecoderResult(isDone: false); // logger.d( // "[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} streamId:${frameHeader.streamIdentifier} ${frameHeader.type} endHeaders: ${frameHeader.hasEndHeadersFlag} " // "endStream: ${frameHeader.hasEndStreamFlag} ${frameHeader.length}"); //根据帧类型进行处理 switch (frameHeader.type) { case FrameType.headers: //处理HEADERS帧 var headersFrame = _handleHeadersFrame(channelContext, frameHeader, ByteBuf(framePayload)); result.isDone = frameHeader.hasEndStreamFlag && frameHeader.hasEndHeadersFlag; if (headersFrame.streamDependency != null) { headersFrame.headerBlockFragment = []; channelContext.put(frameHeader.streamIdentifier, headersFrame); } //handle special case for SSE var possibleMessage = getMessage(channelContext, frameHeader); if (possibleMessage is HttpResponse && possibleMessage.headers.contentType.toLowerCase().startsWith('text/event-stream')) { result.forward = List.from(frameHeader.encode())..addAll(framePayload); result.data = possibleMessage; var currentRequest = channelContext.getStreamRequest(frameHeader.streamIdentifier); currentRequest?.response = possibleMessage; possibleMessage.request ??= channelContext.currentRequest; channelContext.listener?.onResponse(channelContext, possibleMessage); return result; } break; case FrameType.continuation: //处理CONTINUATION帧 var message = getMessage(channelContext, frameHeader); if (message == null) { logger.e("CONTINUATION frame but no message found"); result.forward = List.from(frameHeader.encode())..addAll(framePayload); return result; } Map> headers = _parseHeaders(channelContext, framePayload); headers.forEach((key, values) => message.headers.addValues(key, values)); message.packageSize = (message.packageSize ?? 0) + frameHeader.length; if (frameHeader.hasEndHeadersFlag && channelContext.getStreamRequest(frameHeader.streamIdentifier)?.method == HttpMethod.head) { result.isDone = true; } break; case FrameType.data: //处理DATA帧 var message = getMessage(channelContext, frameHeader)!; bool isSseResponse = message is HttpResponse && message.headers.contentType.toLowerCase().startsWith('text/event-stream'); if (isSseResponse) { _handleSseDataFrame(channelContext, frameHeader, message, ByteBuf(framePayload)); result.forward = List.from(frameHeader.encode())..addAll(framePayload); return result; } _handleDataFrame(channelContext, frameHeader, message, ByteBuf(framePayload)); result.isDone = frameHeader.hasEndStreamFlag; break; case FrameType.settings: SettingHandler.handleSettingsFrame(channelContext, frameHeader, ByteBuf(framePayload)); result.forward = List.from(frameHeader.encode())..addAll(framePayload); return result; case FrameType.goaway: var lastStreamId = readInt32(framePayload, 0); var errorCode = readInt32(framePayload, 4); var debugData = viewOrSublist(framePayload, 8, frameHeader.length - 8); logger.i( "[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} h2 goaway streamId: ${frameHeader.streamIdentifier} lastStreamId: $lastStreamId errorCode: $errorCode debugData: ${String.fromCharCodes(debugData)}"); result.forward = List.from(frameHeader.encode())..addAll(framePayload); return result; default: //其他帧类型 原文转发 result.forward = List.from(frameHeader.encode())..addAll(framePayload); return result; } if (result.isDone && frameHeader.streamIdentifier > 0) { result.data = getMessage(channelContext, frameHeader); result.data?.streamId = frameHeader.streamIdentifier; channelContext.currentRequest = channelContext.getStreamRequest(frameHeader.streamIdentifier); if (result.data is HttpResponse) { channelContext.removeStream(frameHeader.streamIdentifier); } } return result; } List
encodeHeaders(T message); @override Uint8List encode(ChannelContext channelContext, T data) { var bytesBuilder = BytesBuilder(); if (data.headers.getInt(HttpHeaders.CONTENT_LENGTH) != null) { data.headers.set(HttpHeaders.CONTENT_LENGTH.toLowerCase(), "${data.body?.length ?? 0}"); } var emptyBody = data.body == null || data.body!.isEmpty; //headers var headers = encodeHeaders(data); writeHeadersFrame(bytesBuilder, channelContext, data.streamId!, headers, endStream: emptyBody); //body if (!emptyBody) { var payload = data.body!; while (payload.length > maxFrameSize) { var chunkSize = min(maxFrameSize, payload.length); var chunk = payload.sublist(0, chunkSize); payload = payload.sublist(chunkSize); _writeFrame(channelContext, bytesBuilder, FrameType.data, 0, data.streamId!, chunk); } _writeFrame(channelContext, bytesBuilder, FrameType.data, FrameHeader.flagsEndStream, data.streamId!, payload); } return bytesBuilder.takeBytes(); } void writeHeadersFrame( BytesBuilder bytesBuilder, ChannelContext channelContext, int streamId, List
headers, { StreamSetting? setting, bool endStream = true, }) { var fragment = _hpackEncoder.encode(headers); var maxSize = channelContext.setting?.maxFrameSize ?? maxFrameSize; if (fragment.length < maxSize) { int flags = FrameHeader.flagsEndHeaders; if (endStream) { flags |= FrameHeader.flagsEndStream; } _writeHeadersFrame(bytesBuilder, channelContext, flags, streamId, fragment); } else { var chunk = fragment.sublist(0, maxSize); fragment = fragment.sublist(maxSize); _writeHeadersFrame(bytesBuilder, channelContext, 0, streamId, chunk); while (fragment.length > maxSize) { var chunk = fragment.sublist(0, maxSize); fragment = fragment.sublist(maxSize); _writeFrame(channelContext, bytesBuilder, FrameType.continuation, 0, streamId, chunk); } _writeFrame( channelContext, bytesBuilder, FrameType.continuation, FrameHeader.flagsEndHeaders, streamId, fragment); if (endStream) { //如果没有body,发送一个空的DATA帧 _writeFrame(channelContext, bytesBuilder, FrameType.data, FrameHeader.flagsEndStream, streamId, []); } } } void _writeHeadersFrame( BytesBuilder bytesBuilder, ChannelContext channelContext, int flags, int streamId, List payload) { var streamPriority = channelContext.removeStreamDependency(streamId); if (streamPriority != null) { flags |= FrameHeader.flagsPriority; bool exclusive = streamPriority.exclusiveDependency; int streamDependency = streamPriority.streamDependency!; payload = [ (exclusive ? 0x80 : 0) | (streamDependency & 0x7FFFFFFF) >> 24, (streamDependency & 0x00FF0000) >> 16, (streamDependency & 0x0000FF00) >> 8, (streamDependency & 0x000000FF), streamPriority.weight!, ...payload ]; } // logger.d( // "[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} _writeHeadersFrame streamId:$streamId flags:$flags originFlags:${streamPriority?.header.flags} ${streamPriority} ${payload.length}"); _writeFrame(channelContext, bytesBuilder, FrameType.headers, flags, streamId, payload); } void _writeFrame(ChannelContext channelContext, BytesBuilder bytesBuilder, FrameType type, int flags, int streamId, List payload) { FrameHeader frameHeader = FrameHeader(payload.length, type, flags, streamId); // logger.d( // "[${channelContext.clientChannel?.id}] ${this is Http2RequestDecoder ? 'request' : 'response'} _writeFrame streamId:${frameHeader.streamIdentifier} ${frameHeader.type} flags:${frameHeader.flags} endHeaders: ${frameHeader.hasEndHeadersFlag} endStream: ${frameHeader.hasEndStreamFlag} ${payload.length}"); bytesBuilder.add(frameHeader.encode()); bytesBuilder.add(payload); } bool isConnectionPrefacePRI(ByteBuf data) { if (data.readableBytes() < 9) { return false; } for (int i = 0; i < connectionPrefacePRI.length; i++) { if (data.get(data.readerIndex + i) != connectionPrefacePRI[i]) { return false; } } return true; } void _handleSseDataFrame( ChannelContext channelContext, FrameHeader frameHeader, HttpMessage message, ByteBuf payload) { // DATA 帧格式 int padLength = 0; if (frameHeader.hasPaddedFlag) { padLength = payload.readByte(); } int dataLength = payload.readableBytes() - padLength; var data = payload.readBytes(dataLength); // Incremental SSE parsing: do not accumulate full body to avoid large memory usage final decoder = sseDecoders.putIfAbsent(frameHeader.streamIdentifier, () => SseDecoder()); final frames = decoder.feed(Uint8List.fromList(data)); for (final WebSocketFrame frame in frames) { frame.isFromClient = false; // server -> client message.messages.add(frame); channelContext.listener?.onMessage(channelContext.clientChannel!, message, frame); logger.d( '[${channelContext.clientChannel?.id}] h2 sse streamId:${frameHeader.streamIdentifier} frame ${frame.payloadLength} ${frame.payloadDataAsString}'); } if (frameHeader.hasEndStreamFlag) { sseDecoders.remove(frameHeader.streamIdentifier); channelContext.removeStream(frameHeader.streamIdentifier); } } DataFrame _handleDataFrame( ChannelContext channelContext, FrameHeader frameHeader, HttpMessage message, ByteBuf payload) { // DATA 帧格式 int padLength = 0; if (frameHeader.hasPaddedFlag) { padLength = payload.readByte(); } //读取数据 int dataLength = payload.readableBytes() - padLength; var data = payload.readBytes(dataLength); // Regular body accumulation if (message.body == null) { message.body = data; } else { message.body = List.from(message.body!)..addAll(data); } message.packageSize = (message.packageSize ?? 0) + frameHeader.length; return DataFrame(frameHeader, padLength, data); } HeadersFrame _handleHeadersFrame(ChannelContext channelContext, FrameHeader frameHeader, ByteBuf payload) { // HEADERS 帧格式 int padLength = 0; //如果帧头部有PADDED标志位,则需要读取PADDED长度 if (frameHeader.hasPaddedFlag) { padLength = payload.readByte(); } int? streamDependency; bool exclusiveDependency = false; int? weight; //如果帧头部有PRIORITY标志位,则需要读取优先级信息 if (frameHeader.hasPriorityFlag) { if (payload.readableBytes() < 5) { throw Exception("Invalid PRIORITY frame: insufficient data"); } // 读取依赖流 ID 和权重 int dependency = payload.readInt(); exclusiveDependency = (dependency & 0x80000000) != 0; // 检查最高位是否为 1 streamDependency = dependency & 0x7FFFFFFF; // 获取低 31 位 weight = payload.readByte(); // 读取权重 logger.d( "PRIORITY frame parsed: streamId:${frameHeader.streamIdentifier} streamDependency=$streamDependency, weight=$weight $exclusiveDependency"); } var headerBlockLength = payload.length - payload.readerIndex - padLength; if (headerBlockLength < 0) { throw Exception("headerBlockLength < 0"); } var blockFragment = payload.readBytes(headerBlockLength); //读取头部信息 Map> headers = _parseHeaders(channelContext, blockFragment); T message = createMessage(channelContext, frameHeader, headers); headers.forEach((key, values) { if (!key.startsWith(":")) { message.headers.addValues(key, values); } }); message.streamId = frameHeader.streamIdentifier; message.packageSize = frameHeader.length; return HeadersFrame(frameHeader, padLength, exclusiveDependency, streamDependency, weight, blockFragment); } Map> _parseHeaders(ChannelContext channelContext, List payload) { if (channelContext.setting != null) { decoder.updateMaxReceivingHeaderTableSize(channelContext.setting!.headTableSize); } // Decode the headers List
headers = decoder.decode(payload); // Convert the headers to a map Map> headerMap = {}; for (Header header in headers) { final name = header.nameString; final value = header.valueString; headerMap[name] ??= []; headerMap[name]!.add(value); } return headerMap; } } class Http2RequestDecoder extends Http2Codec { @override HttpRequest createMessage(ChannelContext channelContext, FrameHeader frameHeader, Map> headers) { HttpMethod httpMethod = HttpMethod.valueOf(headers[":method"]!.first); var httpRequest = HttpRequest(httpMethod, headers[":path"]!.first, protocolVersion: headers[":version"]?.firstOrNull ?? "HTTP/2"); String? authority = headers[":authority"]?.firstOrNull; String? scheme = headers[":scheme"]?.firstOrNull; if (authority == null || scheme == null) { logger.e("Invalid HTTP/2 request headers: $headers"); } else { // 解析 authority,提取主机和端口 String host = authority; int port = (scheme == 'https' ? 443 : 80); if (authority.startsWith("[")) { int closeBracketIndex = authority.indexOf(']'); if (closeBracketIndex != -1) { host = authority.substring(0, closeBracketIndex + 1); if (authority.length > closeBracketIndex + 1 && authority[closeBracketIndex + 1] == ':') { port = int.tryParse(authority.substring(closeBracketIndex + 2)) ?? port; } } } else { int lastColonIndex = authority.lastIndexOf(':'); if (lastColonIndex != -1) { var p = int.tryParse(authority.substring(lastColonIndex + 1)); if (p != null) { host = authority.substring(0, lastColonIndex); port = p; } } } httpRequest.hostAndPort = HostAndPort("$scheme://", host, port); } var old = channelContext.putStreamRequest(frameHeader.streamIdentifier, httpRequest); assert(old == null, "old request is not null"); return httpRequest; } @override HttpRequest? getMessage(ChannelContext channelContext, FrameHeader frameHeader) { return channelContext.getStreamRequest(frameHeader.streamIdentifier); } @override List
encodeHeaders(HttpRequest message) { var headers =
[]; var uri = message.requestUri!; headers.add(Header.ascii(":method", message.method.name)); headers.add(Header.ascii(":scheme", uri.scheme)); headers.add(Header.ascii(":authority", uri.host)); headers.add(Header.ascii(":path", message.uri)); message.headers.forEach((key, values) { for (var value in values) { headers.add(Header.ascii(key.toLowerCase(), value)); } }); return headers; } } class Http2ResponseDecoder extends Http2Codec { @override HttpResponse createMessage( ChannelContext channelContext, FrameHeader frameHeader, Map> headers) { var httpResponse = HttpResponse(HttpStatus.valueOf(int.parse(headers[':status']!.first)), protocolVersion: headers[":version"]?.firstOrNull ?? 'HTTP/2'); final requestId = channelContext.getStreamRequest(frameHeader.streamIdentifier)?.requestId; if (requestId != null) { httpResponse.requestId = requestId; } channelContext.putStreamResponse(frameHeader.streamIdentifier, httpResponse); return httpResponse; } @override HttpResponse? getMessage(ChannelContext channelContext, FrameHeader frameHeader) { return channelContext.getStreamResponse(frameHeader.streamIdentifier); } @override List
encodeHeaders(HttpResponse message) { var headers =
[]; headers.add(Header.ascii(":status", message.status.code.toString())); message.headers.forEach((key, values) { for (var value in values) { headers.add(Header.ascii(key, value)); } }); return headers; } } class FrameReader { static int headerLength = 9; static List? _readFramePayload(ByteBuf data, int length) { if (data.readableBytes() < length) { return null; } var readBytes = data.readBytes(length); data.clearRead(); return readBytes; } static FrameHeader? readFrameHeader(ByteBuf data) { if (data.readableBytes() < headerLength) { return null; } int length = data.read() << 16 | data.read() << 8 | data.read(); FrameType type = FrameType.values[data.read()]; int flags = data.read(); int streamIdentifier = data.readInt(); return FrameHeader(length, type, flags, streamIdentifier); } } ================================================ FILE: lib/network/http/h2/hpack/hpack.dart ================================================ // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// HPACK specification. See here for more information: /// https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10 import 'dart:convert' show ascii; import 'dart:typed_data'; import '../../../util/byte_utils.dart'; import 'huffman.dart'; import 'huffman_table.dart'; /// Exception raised due to encoding/decoding errors. class HPackDecodingException implements Exception { final String _message; HPackDecodingException(this._message); @override String toString() => 'HPackDecodingException: $_message'; } /// A HPACK encoding/decoding context. /// /// This is a statefull class, so encoding/decoding changes internal state. class HPackContext { final HPackEncoder encoder = HPackEncoder(); final HPackDecoder decoder = HPackDecoder(); HPackContext({ int maxSendingHeaderTableSize = 4096, int maxReceivingHeaderTableSize = 4096, }) { encoder.updateMaxSendingHeaderTableSize(maxSendingHeaderTableSize); decoder.updateMaxReceivingHeaderTableSize(maxReceivingHeaderTableSize); } } /// A HTTP/2 header. class Header { final List name; final List value; final bool neverIndexed; Header(this.name, this.value, {this.neverIndexed = false}); String get nameString => ascii.decode(name); String get valueString => ascii.decode(value); factory Header.ascii(String name, String value) { // Specs: `However, header field names MUST be converted to lowercase prior // to their encoding in HTTP/2. A request or response containing uppercase // header field names MUST be treated as malformed (Section 8.1.2.6).` return Header(ascii.encode(name.toLowerCase()), ascii.encode(value)); } } /// A stateful HPACK decoder. class HPackDecoder { int _maxHeaderTableSize = 4096; final IndexTable _table = IndexTable(); void updateMaxReceivingHeaderTableSize(int newMaximumSize) { _maxHeaderTableSize = newMaximumSize; } List
decode(List data) { var offset = 0; int readInteger(int prefixBits) { assert(prefixBits <= 8 && prefixBits > 0); var byte = data[offset++] & ((1 << prefixBits) - 1); int integer; if (byte == ((1 << prefixBits) - 1)) { // Length encodeded. integer = 0; var shift = 0; while (true) { var done = (data[offset] & 0x80) != 0x80; integer += (data[offset++] & 0x7f) << shift; shift += 7; if (done) break; } integer += (1 << prefixBits) - 1; } else { // In place length. integer = byte; } return integer; } List readStringLiteral() { var isHuffmanEncoding = (data[offset] & 0x80) != 0; var length = readInteger(7); var sublist = viewOrSublist(data, offset, length); offset += length; if (isHuffmanEncoding) { return http2HuffmanCodec.decode(sublist); } else { return sublist; } } Header readHeaderFieldInternal(int index, {bool neverIndexed = false}) { List name, value; if (index > 0) { name = _table.lookup(index).name; value = readStringLiteral(); } else { name = readStringLiteral(); value = readStringLiteral(); } return Header(name, value, neverIndexed: neverIndexed); } try { var headers =
[]; while (offset < data.length) { var byte = data[offset]; var isIndexedField = (byte & 0x80) != 0; var isIncrementalIndexing = (byte & 0xc0) == 0x40; var isWithoutIndexing = (byte & 0xf0) == 0; var isNeverIndexing = (byte & 0xf0) == 0x10; var isDynamicTableSizeUpdate = (byte & 0xe0) == 0x20; if (isIndexedField) { var index = readInteger(7); var field = _table.lookup(index); headers.add(field); } else if (isIncrementalIndexing) { var field = readHeaderFieldInternal(readInteger(6)); _table.addHeaderField(field); headers.add(field); } else if (isWithoutIndexing) { headers.add(readHeaderFieldInternal(readInteger(4))); } else if (isNeverIndexing) { headers.add( readHeaderFieldInternal(readInteger(4), neverIndexed: true), ); } else if (isDynamicTableSizeUpdate) { var newMaxSize = readInteger(5); _table.updateMaxSize(newMaxSize); } else { throw HPackDecodingException('Invalid encoding of headers.'); } } return headers; // ignore: avoid_catching_errors } on RangeError catch (e) { throw HPackDecodingException('$e'); } on HuffmanDecodingException catch (e) { throw HPackDecodingException('$e'); } } } /// A stateful HPACK encoder. // Currently we encode all headers: // - without huffman encoding // - without using the dynamic table class HPackEncoder { void updateMaxSendingHeaderTableSize(int newMaximumSize) { // Once we start encoding via dynamic table we need to let the other // side know the maximum table size we're using. } List encode(List
headers) { var bytesBuilder = BytesBuilder(); var currentByte = 0; void writeInteger(int prefixBits, int value) { assert(prefixBits <= 8); if (value < (1 << prefixBits) - 1) { currentByte |= value; bytesBuilder.addByte(currentByte); } else { // Length encodeded. currentByte |= (1 << prefixBits) - 1; value -= (1 << prefixBits) - 1; bytesBuilder.addByte(currentByte); var done = false; while (!done) { currentByte = value & 0x7f; value = value >> 7; done = value == 0; if (!done) currentByte |= 0x80; bytesBuilder.addByte(currentByte); } } currentByte = 0; } void writeStringLiteral(List bytes) { // Support huffman encoding. currentByte = 0; // 1 would be huffman encoding writeInteger(7, bytes.length); bytesBuilder.add(bytes); } void writeLiteralHeaderWithoutIndexing(Header header) { bytesBuilder.addByte(0); writeStringLiteral(header.name); writeStringLiteral(header.value); } for (var header in headers) { writeLiteralHeaderWithoutIndexing(header); } return bytesBuilder.takeBytes(); } } class IndexTable { static final List _staticTable = [ null, Header(ascii.encode(':authority'), const []), Header(ascii.encode(':method'), ascii.encode('GET')), Header(ascii.encode(':method'), ascii.encode('POST')), Header(ascii.encode(':path'), ascii.encode('/')), Header(ascii.encode(':path'), ascii.encode('/index.html')), Header(ascii.encode(':scheme'), ascii.encode('http')), Header(ascii.encode(':scheme'), ascii.encode('https')), Header(ascii.encode(':status'), ascii.encode('200')), Header(ascii.encode(':status'), ascii.encode('204')), Header(ascii.encode(':status'), ascii.encode('206')), Header(ascii.encode(':status'), ascii.encode('304')), Header(ascii.encode(':status'), ascii.encode('400')), Header(ascii.encode(':status'), ascii.encode('404')), Header(ascii.encode(':status'), ascii.encode('500')), Header(ascii.encode('accept-charset'), const []), Header(ascii.encode('accept-encoding'), ascii.encode('gzip, deflate')), Header(ascii.encode('accept-language'), const []), Header(ascii.encode('accept-ranges'), const []), Header(ascii.encode('accept'), const []), Header(ascii.encode('access-control-allow-origin'), const []), Header(ascii.encode('age'), const []), Header(ascii.encode('allow'), const []), Header(ascii.encode('authorization'), const []), Header(ascii.encode('cache-control'), const []), Header(ascii.encode('content-disposition'), const []), Header(ascii.encode('content-encoding'), const []), Header(ascii.encode('content-language'), const []), Header(ascii.encode('content-length'), const []), Header(ascii.encode('content-location'), const []), Header(ascii.encode('content-range'), const []), Header(ascii.encode('content-type'), const []), Header(ascii.encode('cookie'), const []), Header(ascii.encode('date'), const []), Header(ascii.encode('etag'), const []), Header(ascii.encode('expect'), const []), Header(ascii.encode('expires'), const []), Header(ascii.encode('from'), const []), Header(ascii.encode('host'), const []), Header(ascii.encode('if-match'), const []), Header(ascii.encode('if-modified-since'), const []), Header(ascii.encode('if-none-match'), const []), Header(ascii.encode('if-range'), const []), Header(ascii.encode('if-unmodified-since'), const []), Header(ascii.encode('last-modified'), const []), Header(ascii.encode('link'), const []), Header(ascii.encode('location'), const []), Header(ascii.encode('max-forwards'), const []), Header(ascii.encode('proxy-authenticate'), const []), Header(ascii.encode('proxy-authorization'), const []), Header(ascii.encode('range'), const []), Header(ascii.encode('referer'), const []), Header(ascii.encode('refresh'), const []), Header(ascii.encode('retry-after'), const []), Header(ascii.encode('server'), const []), Header(ascii.encode('set-cookie'), const []), Header(ascii.encode('strict-transport-security'), const []), Header(ascii.encode('transfer-encoding'), const []), Header(ascii.encode('user-agent'), const []), Header(ascii.encode('vary'), const []), Header(ascii.encode('via'), const []), Header(ascii.encode('www-authenticate'), const []), ]; final List
_dynamicTable = []; /// The maximum size the dynamic table can grow to before entries need to be /// evicted. int _maximumSize = 4096; /// The current size of the dynamic table. int _currentSize = 0; IndexTable(); /// Updates the maximum size which the dynamic table can grow to. void updateMaxSize(int newMaxDynTableSize) { _maximumSize = newMaxDynTableSize; _reduce(); } /// Lookup an item by index. Header lookup(int index) { if (index <= 0) { throw HPackDecodingException( 'Invalid index (was: $index) for table lookup.', ); } if (index < _staticTable.length) { return _staticTable[index]!; } index -= _staticTable.length; if (index < _dynamicTable.length) { return _dynamicTable[index]; } throw HPackDecodingException( 'Invalid index (was: $index) for table lookup.', ); } /// Adds a new header field to the dynamic table - and evicts entries as /// necessary. void addHeaderField(Header header) { _dynamicTable.insert(0, header); _currentSize += _sizeOf(header); _reduce(); } /// Removes as many entries as required to be within the limit of /// [_maximumSize]. void _reduce() { while (_currentSize > _maximumSize) { var h = _dynamicTable.removeLast(); _currentSize -= _sizeOf(h); } } /// Returns the "size" a [header] has. /// /// This is specified to be the number of octets of name/value plus 32. int _sizeOf(Header header) => header.name.length + header.value.length + 32; } ================================================ FILE: lib/network/http/h2/hpack/huffman.dart ================================================ // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:typed_data'; import 'huffman_table.dart'; class HuffmanDecodingException implements Exception { final String _message; HuffmanDecodingException(this._message); @override String toString() => 'HuffmanDecodingException: $_message'; } /// A codec used for encoding/decoding using a huffman codec. class HuffmanCodec { final HuffmanEncoder _encoder; final HuffmanDecoder _decoder; HuffmanCodec(this._encoder, this._decoder); List decode(List bytes) => _decoder.decode(bytes); List encode(List bytes) => _encoder.encode(bytes); } /// A huffman decoder based on a [HuffmanTreeNode]. class HuffmanDecoder { final HuffmanTreeNode _root; HuffmanDecoder(this._root); /// Decodes [bytes] using a huffman tree. List decode(List bytes) { var buffer = BytesBuilder(); var currentByteOffset = 0; var node = _root; var currentDepth = 0; while (currentByteOffset < bytes.length) { var byte = bytes[currentByteOffset]; for (var currentBit = 7; currentBit >= 0; currentBit--) { var right = (byte >> currentBit) & 1 == 1; if (right) { node = node.right!; } else { node = node.left!; } currentDepth++; if (node.value != null) { if (node.value == EOS_BYTE) { throw HuffmanDecodingException( 'More than 7 bit padding is not allowed. Found entire EOS ' 'encoding', ); } buffer.addByte(node.value!); node = _root; currentDepth = 0; } } currentByteOffset++; } if (node != _root) { if (currentDepth > 7) { throw HuffmanDecodingException( 'Incomplete encoding of a byte or more than 7 bit padding.', ); } while (node.right != null) { node = node.right!; } if (node.value != 256) { throw HuffmanDecodingException('Incomplete encoding of a byte.'); } } return buffer.takeBytes(); } } /// A huffman encoder based on a list of codewords. class HuffmanEncoder { final List _codewords; HuffmanEncoder(this._codewords); /// Encodes [bytes] using a list of codewords. List encode(List bytes) { var buffer = BytesBuilder(); var currentByte = 0; var currentBitOffset = 7; void writeValue(int value, int numBits) { var i = numBits - 1; while (i >= 0) { if (currentBitOffset == 7 && i >= 7) { assert(currentByte == 0); buffer.addByte((value >> (i - 7)) & 0xff); currentBitOffset = 7; currentByte = 0; i -= 8; } else { currentByte |= ((value >> i) & 1) << currentBitOffset; currentBitOffset--; if (currentBitOffset == -1) { buffer.addByte(currentByte); currentBitOffset = 7; currentByte = 0; } i--; } } } for (var i = 0; i < bytes.length; i++) { var byte = bytes[i]; var value = _codewords[byte]; writeValue(value.encodedBytes, value.numBits); } if (currentBitOffset < 7) { writeValue(0xff, 1 + currentBitOffset); } return buffer.takeBytes(); } } /// Specifies the encoding of a specific value using huffman encoding. class EncodedHuffmanValue { /// An integer representation of the encoded bit-string. final int encodedBytes; /// The number of bits in [encodedBytes]. final int numBits; const EncodedHuffmanValue(this.encodedBytes, this.numBits); } /// A node in the huffman tree. class HuffmanTreeNode { HuffmanTreeNode? left; HuffmanTreeNode? right; int? value; } /// Generates a huffman decoding tree. HuffmanTreeNode generateHuffmanTree(List valueEncodings) { var root = HuffmanTreeNode(); for (var byteOffset = 0; byteOffset < valueEncodings.length; byteOffset++) { var entry = valueEncodings[byteOffset]; var current = root; for (var bitNr = 0; bitNr < entry.numBits; bitNr++) { var right = ((entry.encodedBytes >> (entry.numBits - bitNr - 1)) & 1) == 1; if (right) { current.right ??= HuffmanTreeNode(); current = current.right!; } else { current.left ??= HuffmanTreeNode(); current = current.left!; } } current.value = byteOffset; } return root; } ================================================ FILE: lib/network/http/h2/hpack/huffman_table.dart ================================================ // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'huffman.dart'; /// The huffman codec for encoding/decoding HTTP/2 header blocks. final HuffmanCodec http2HuffmanCodec = HuffmanCodec( HuffmanEncoder(_codeWords), HuffmanDecoder(generateHuffmanTree(_codeWords)), ); /// This is the integer representing the End-of-String symbol /// (it is not representable by a byte). const int EOS_BYTE = 256; /// This list of byte encodings via huffman encoding was generated from the /// HPACK specification. const List _codeWords = [ EncodedHuffmanValue(0x1ff8, 13), EncodedHuffmanValue(0x7fffd8, 23), EncodedHuffmanValue(0xfffffe2, 28), EncodedHuffmanValue(0xfffffe3, 28), EncodedHuffmanValue(0xfffffe4, 28), EncodedHuffmanValue(0xfffffe5, 28), EncodedHuffmanValue(0xfffffe6, 28), EncodedHuffmanValue(0xfffffe7, 28), EncodedHuffmanValue(0xfffffe8, 28), EncodedHuffmanValue(0xffffea, 24), EncodedHuffmanValue(0x3ffffffc, 30), EncodedHuffmanValue(0xfffffe9, 28), EncodedHuffmanValue(0xfffffea, 28), EncodedHuffmanValue(0x3ffffffd, 30), EncodedHuffmanValue(0xfffffeb, 28), EncodedHuffmanValue(0xfffffec, 28), EncodedHuffmanValue(0xfffffed, 28), EncodedHuffmanValue(0xfffffee, 28), EncodedHuffmanValue(0xfffffef, 28), EncodedHuffmanValue(0xffffff0, 28), EncodedHuffmanValue(0xffffff1, 28), EncodedHuffmanValue(0xffffff2, 28), EncodedHuffmanValue(0x3ffffffe, 30), EncodedHuffmanValue(0xffffff3, 28), EncodedHuffmanValue(0xffffff4, 28), EncodedHuffmanValue(0xffffff5, 28), EncodedHuffmanValue(0xffffff6, 28), EncodedHuffmanValue(0xffffff7, 28), EncodedHuffmanValue(0xffffff8, 28), EncodedHuffmanValue(0xffffff9, 28), EncodedHuffmanValue(0xffffffa, 28), EncodedHuffmanValue(0xffffffb, 28), EncodedHuffmanValue(0x14, 6), EncodedHuffmanValue(0x3f8, 10), EncodedHuffmanValue(0x3f9, 10), EncodedHuffmanValue(0xffa, 12), EncodedHuffmanValue(0x1ff9, 13), EncodedHuffmanValue(0x15, 6), EncodedHuffmanValue(0xf8, 8), EncodedHuffmanValue(0x7fa, 11), EncodedHuffmanValue(0x3fa, 10), EncodedHuffmanValue(0x3fb, 10), EncodedHuffmanValue(0xf9, 8), EncodedHuffmanValue(0x7fb, 11), EncodedHuffmanValue(0xfa, 8), EncodedHuffmanValue(0x16, 6), EncodedHuffmanValue(0x17, 6), EncodedHuffmanValue(0x18, 6), EncodedHuffmanValue(0x0, 5), EncodedHuffmanValue(0x1, 5), EncodedHuffmanValue(0x2, 5), EncodedHuffmanValue(0x19, 6), EncodedHuffmanValue(0x1a, 6), EncodedHuffmanValue(0x1b, 6), EncodedHuffmanValue(0x1c, 6), EncodedHuffmanValue(0x1d, 6), EncodedHuffmanValue(0x1e, 6), EncodedHuffmanValue(0x1f, 6), EncodedHuffmanValue(0x5c, 7), EncodedHuffmanValue(0xfb, 8), EncodedHuffmanValue(0x7ffc, 15), EncodedHuffmanValue(0x20, 6), EncodedHuffmanValue(0xffb, 12), EncodedHuffmanValue(0x3fc, 10), EncodedHuffmanValue(0x1ffa, 13), EncodedHuffmanValue(0x21, 6), EncodedHuffmanValue(0x5d, 7), EncodedHuffmanValue(0x5e, 7), EncodedHuffmanValue(0x5f, 7), EncodedHuffmanValue(0x60, 7), EncodedHuffmanValue(0x61, 7), EncodedHuffmanValue(0x62, 7), EncodedHuffmanValue(0x63, 7), EncodedHuffmanValue(0x64, 7), EncodedHuffmanValue(0x65, 7), EncodedHuffmanValue(0x66, 7), EncodedHuffmanValue(0x67, 7), EncodedHuffmanValue(0x68, 7), EncodedHuffmanValue(0x69, 7), EncodedHuffmanValue(0x6a, 7), EncodedHuffmanValue(0x6b, 7), EncodedHuffmanValue(0x6c, 7), EncodedHuffmanValue(0x6d, 7), EncodedHuffmanValue(0x6e, 7), EncodedHuffmanValue(0x6f, 7), EncodedHuffmanValue(0x70, 7), EncodedHuffmanValue(0x71, 7), EncodedHuffmanValue(0x72, 7), EncodedHuffmanValue(0xfc, 8), EncodedHuffmanValue(0x73, 7), EncodedHuffmanValue(0xfd, 8), EncodedHuffmanValue(0x1ffb, 13), EncodedHuffmanValue(0x7fff0, 19), EncodedHuffmanValue(0x1ffc, 13), EncodedHuffmanValue(0x3ffc, 14), EncodedHuffmanValue(0x22, 6), EncodedHuffmanValue(0x7ffd, 15), EncodedHuffmanValue(0x3, 5), EncodedHuffmanValue(0x23, 6), EncodedHuffmanValue(0x4, 5), EncodedHuffmanValue(0x24, 6), EncodedHuffmanValue(0x5, 5), EncodedHuffmanValue(0x25, 6), EncodedHuffmanValue(0x26, 6), EncodedHuffmanValue(0x27, 6), EncodedHuffmanValue(0x6, 5), EncodedHuffmanValue(0x74, 7), EncodedHuffmanValue(0x75, 7), EncodedHuffmanValue(0x28, 6), EncodedHuffmanValue(0x29, 6), EncodedHuffmanValue(0x2a, 6), EncodedHuffmanValue(0x7, 5), EncodedHuffmanValue(0x2b, 6), EncodedHuffmanValue(0x76, 7), EncodedHuffmanValue(0x2c, 6), EncodedHuffmanValue(0x8, 5), EncodedHuffmanValue(0x9, 5), EncodedHuffmanValue(0x2d, 6), EncodedHuffmanValue(0x77, 7), EncodedHuffmanValue(0x78, 7), EncodedHuffmanValue(0x79, 7), EncodedHuffmanValue(0x7a, 7), EncodedHuffmanValue(0x7b, 7), EncodedHuffmanValue(0x7ffe, 15), EncodedHuffmanValue(0x7fc, 11), EncodedHuffmanValue(0x3ffd, 14), EncodedHuffmanValue(0x1ffd, 13), EncodedHuffmanValue(0xffffffc, 28), EncodedHuffmanValue(0xfffe6, 20), EncodedHuffmanValue(0x3fffd2, 22), EncodedHuffmanValue(0xfffe7, 20), EncodedHuffmanValue(0xfffe8, 20), EncodedHuffmanValue(0x3fffd3, 22), EncodedHuffmanValue(0x3fffd4, 22), EncodedHuffmanValue(0x3fffd5, 22), EncodedHuffmanValue(0x7fffd9, 23), EncodedHuffmanValue(0x3fffd6, 22), EncodedHuffmanValue(0x7fffda, 23), EncodedHuffmanValue(0x7fffdb, 23), EncodedHuffmanValue(0x7fffdc, 23), EncodedHuffmanValue(0x7fffdd, 23), EncodedHuffmanValue(0x7fffde, 23), EncodedHuffmanValue(0xffffeb, 24), EncodedHuffmanValue(0x7fffdf, 23), EncodedHuffmanValue(0xffffec, 24), EncodedHuffmanValue(0xffffed, 24), EncodedHuffmanValue(0x3fffd7, 22), EncodedHuffmanValue(0x7fffe0, 23), EncodedHuffmanValue(0xffffee, 24), EncodedHuffmanValue(0x7fffe1, 23), EncodedHuffmanValue(0x7fffe2, 23), EncodedHuffmanValue(0x7fffe3, 23), EncodedHuffmanValue(0x7fffe4, 23), EncodedHuffmanValue(0x1fffdc, 21), EncodedHuffmanValue(0x3fffd8, 22), EncodedHuffmanValue(0x7fffe5, 23), EncodedHuffmanValue(0x3fffd9, 22), EncodedHuffmanValue(0x7fffe6, 23), EncodedHuffmanValue(0x7fffe7, 23), EncodedHuffmanValue(0xffffef, 24), EncodedHuffmanValue(0x3fffda, 22), EncodedHuffmanValue(0x1fffdd, 21), EncodedHuffmanValue(0xfffe9, 20), EncodedHuffmanValue(0x3fffdb, 22), EncodedHuffmanValue(0x3fffdc, 22), EncodedHuffmanValue(0x7fffe8, 23), EncodedHuffmanValue(0x7fffe9, 23), EncodedHuffmanValue(0x1fffde, 21), EncodedHuffmanValue(0x7fffea, 23), EncodedHuffmanValue(0x3fffdd, 22), EncodedHuffmanValue(0x3fffde, 22), EncodedHuffmanValue(0xfffff0, 24), EncodedHuffmanValue(0x1fffdf, 21), EncodedHuffmanValue(0x3fffdf, 22), EncodedHuffmanValue(0x7fffeb, 23), EncodedHuffmanValue(0x7fffec, 23), EncodedHuffmanValue(0x1fffe0, 21), EncodedHuffmanValue(0x1fffe1, 21), EncodedHuffmanValue(0x3fffe0, 22), EncodedHuffmanValue(0x1fffe2, 21), EncodedHuffmanValue(0x7fffed, 23), EncodedHuffmanValue(0x3fffe1, 22), EncodedHuffmanValue(0x7fffee, 23), EncodedHuffmanValue(0x7fffef, 23), EncodedHuffmanValue(0xfffea, 20), EncodedHuffmanValue(0x3fffe2, 22), EncodedHuffmanValue(0x3fffe3, 22), EncodedHuffmanValue(0x3fffe4, 22), EncodedHuffmanValue(0x7ffff0, 23), EncodedHuffmanValue(0x3fffe5, 22), EncodedHuffmanValue(0x3fffe6, 22), EncodedHuffmanValue(0x7ffff1, 23), EncodedHuffmanValue(0x3ffffe0, 26), EncodedHuffmanValue(0x3ffffe1, 26), EncodedHuffmanValue(0xfffeb, 20), EncodedHuffmanValue(0x7fff1, 19), EncodedHuffmanValue(0x3fffe7, 22), EncodedHuffmanValue(0x7ffff2, 23), EncodedHuffmanValue(0x3fffe8, 22), EncodedHuffmanValue(0x1ffffec, 25), EncodedHuffmanValue(0x3ffffe2, 26), EncodedHuffmanValue(0x3ffffe3, 26), EncodedHuffmanValue(0x3ffffe4, 26), EncodedHuffmanValue(0x7ffffde, 27), EncodedHuffmanValue(0x7ffffdf, 27), EncodedHuffmanValue(0x3ffffe5, 26), EncodedHuffmanValue(0xfffff1, 24), EncodedHuffmanValue(0x1ffffed, 25), EncodedHuffmanValue(0x7fff2, 19), EncodedHuffmanValue(0x1fffe3, 21), EncodedHuffmanValue(0x3ffffe6, 26), EncodedHuffmanValue(0x7ffffe0, 27), EncodedHuffmanValue(0x7ffffe1, 27), EncodedHuffmanValue(0x3ffffe7, 26), EncodedHuffmanValue(0x7ffffe2, 27), EncodedHuffmanValue(0xfffff2, 24), EncodedHuffmanValue(0x1fffe4, 21), EncodedHuffmanValue(0x1fffe5, 21), EncodedHuffmanValue(0x3ffffe8, 26), EncodedHuffmanValue(0x3ffffe9, 26), EncodedHuffmanValue(0xffffffd, 28), EncodedHuffmanValue(0x7ffffe3, 27), EncodedHuffmanValue(0x7ffffe4, 27), EncodedHuffmanValue(0x7ffffe5, 27), EncodedHuffmanValue(0xfffec, 20), EncodedHuffmanValue(0xfffff3, 24), EncodedHuffmanValue(0xfffed, 20), EncodedHuffmanValue(0x1fffe6, 21), EncodedHuffmanValue(0x3fffe9, 22), EncodedHuffmanValue(0x1fffe7, 21), EncodedHuffmanValue(0x1fffe8, 21), EncodedHuffmanValue(0x7ffff3, 23), EncodedHuffmanValue(0x3fffea, 22), EncodedHuffmanValue(0x3fffeb, 22), EncodedHuffmanValue(0x1ffffee, 25), EncodedHuffmanValue(0x1ffffef, 25), EncodedHuffmanValue(0xfffff4, 24), EncodedHuffmanValue(0xfffff5, 24), EncodedHuffmanValue(0x3ffffea, 26), EncodedHuffmanValue(0x7ffff4, 23), EncodedHuffmanValue(0x3ffffeb, 26), EncodedHuffmanValue(0x7ffffe6, 27), EncodedHuffmanValue(0x3ffffec, 26), EncodedHuffmanValue(0x3ffffed, 26), EncodedHuffmanValue(0x7ffffe7, 27), EncodedHuffmanValue(0x7ffffe8, 27), EncodedHuffmanValue(0x7ffffe9, 27), EncodedHuffmanValue(0x7ffffea, 27), EncodedHuffmanValue(0x7ffffeb, 27), EncodedHuffmanValue(0xffffffe, 28), EncodedHuffmanValue(0x7ffffec, 27), EncodedHuffmanValue(0x7ffffed, 27), EncodedHuffmanValue(0x7ffffee, 27), EncodedHuffmanValue(0x7ffffef, 27), EncodedHuffmanValue(0x7fffff0, 27), EncodedHuffmanValue(0x3ffffee, 26), EncodedHuffmanValue(0x3fffffff, 30), ]; ================================================ FILE: lib/network/http/h2/setting.dart ================================================ /* * Copyright 2023 the original author or authors. * * 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 * * https://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 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/http/h2/frame.dart'; import 'package:proxypin/network/util/byte_buf.dart'; class StreamSetting { /// 允许发送方通知远程端点用于解码头块的头压缩表的最大大小(以八位字节为单位)。 /// 初始值为4096个八位字节。 int headTableSize = 4096; ///如果一个端点接收到的这个参数设置为0,它就不能发送PUSH_PROMISE帧。 ///初始值为1,表示允许服务器推送。 bool enablePush = true; ///指示发送方允许的最大并发流数。这个限制是定向的:它适用于发送方允许接收方创建的流的数量。最初,对该值没有限制。建议此值不小于100,以免不必要地限制并行性。 int? maxConcurrentStreams; /// 指示发送方用于流级流控制的初始窗口大小(以八位字节为单位)。初始值为216-1(65,535)个八位字节。 int initialWindowSize = 65535; ///表示发送方愿意接收的最大帧有效负载的大小(以八位字节为单位)。 int maxFrameSize = 16384; ///建议设置通知对等方发送方准备接受的头列表的最大大小(以八位字节为单位)。 ///该值基于头字段的未压缩大小,包括名称和值的长度(以八位字节为单位)加上每个头字段32个八位字节的开销。 int? maxHeaderListSize; } class SettingHandler { static void handleSettingsFrame(ChannelContext channelContext, FrameHeader frameHeader, ByteBuf payload) { // SETTINGS frames must have a length that is a multiple of 6 bytes if (frameHeader.length % 6 != 0) { throw Exception("Invalid SETTINGS frame length"); } // If the SETTINGS frame has the ACK flag set, then it is an acknowledgement if (frameHeader.hasAckFlag) { // Handle SETTINGS ACK return; } var setting = channelContext.setting ??= StreamSetting(); // Otherwise, it is a SETTINGS frame that carries settings while (payload.isReadable()) { int identifier = payload.readShort(); int value = payload.readInt(); // print("SettingHandler.handleSettingsFrame identifier=$identifier value=$value"); // Handle the setting based on its identifier switch (identifier) { case 1: // SETTINGS_HEADER_TABLE_SIZE setting.headTableSize = value; break; case 2: // SETTINGS_ENABLE_PUSH setting.enablePush = value == 1; break; case 3: // SETTINGS_MAX_CONCURRENT_STREAMS setting.maxConcurrentStreams = value; break; case 4: // SETTINGS_INITIAL_WINDOW_SIZE setting.initialWindowSize = value; break; case 5: // SETTINGS_MAX_FRAME_SIZE setting.maxFrameSize = value; break; case 6: // SETTINGS_MAX_HEADER_LIST_SIZE setting.maxHeaderListSize = value; default: break; } } } } ================================================ FILE: lib/network/http/http.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:math'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/content_type.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/network/util/compress.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/process_info.dart'; import 'http_headers.dart'; ///定义HTTP消息的接口,为HttpRequest和HttpResponse提供公共属性。 ///@author WangHongEn abstract class HttpMessage { /// HTTP/1.1 static const String http1Version = "HTTP/1.1"; ///内容类型 static final Map contentTypes = { "javascript": ContentType.js, "text/css": ContentType.css, "font-woff": ContentType.font, "text/html": ContentType.html, "text/plain": ContentType.text, "application/x-www-form-urlencoded": ContentType.formUrl, "form-data": ContentType.formData, "image": ContentType.image, "video": ContentType.video, "application/json": ContentType.json, "text/event-stream": ContentType.sse, }; String protocolVersion; final HttpHeaders headers = HttpHeaders(); int get contentLength => headers.contentLength; String? get requestUrl; //报文大小 int? packageSize; List? _body; String? _bodyString; String? remoteHost; int? remotePort; String requestId = (DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999)).toRadixString(36); int? streamId; // http2 streamId HttpMessage(this.protocolVersion); //json序列化 factory HttpMessage.fromJson(Map json) { if (json["_class"] == "HttpRequest") { return HttpRequest.fromJson(json); } return HttpResponse.fromJson(json); } Map toJson(); /// 是否是websocket协议 bool get isWebSocket => headers.get("Upgrade") == 'websocket'; ContentType get contentType => contentTypes.entries .firstWhere((element) => headers.contentType.contains(element.key), orElse: () => const MapEntry("unknown", ContentType.http)) .value; List? get body => _body; set body(List? body) { _body = body; _bodyString = null; } ///获取消息体编码 String? get charset { var contentType = headers.contentType; if (contentType.isEmpty) { return 'utf-8'; } MediaType? mediaType = MediaType.valueOf(contentType); if (mediaType == null) { return 'utf-8'; } return mediaType.charset ?? MediaType.defaultCharset(mediaType); } ///获取消息 String get bodyAsString { return getBodyString(charset: 'utf-8'); } String getBodyString({String? charset}) { if (body == null || body?.isEmpty == true) { return ""; } if (_bodyString != null) { return _bodyString!; } charset ??= this.charset; try { List rawBody = body!; if (headers.isGzip) { rawBody = gzipDecode(body!); }else if (headers.contentEncoding == 'br') { rawBody = brDecode(body!); } else if (headers.contentEncoding == 'deflate') { rawBody = zlibDecode(body!); } if (charset == 'utf-8' || charset == 'utf8') { return utf8.decode(rawBody); } return String.fromCharCodes(rawBody); } catch (e) { return String.fromCharCodes(body!); } } Future decodeBodyString() async { if (body == null || body?.isEmpty == true) { return ""; } if (_bodyString != null) { return _bodyString!; } List rawBody = body!; if (headers.contentEncoding == 'zstd') { rawBody = await zstdDecode(body!) ?? []; if (charset == 'utf-8' || charset == 'utf8') { _bodyString = utf8.decode(rawBody); } else { _bodyString = String.fromCharCodes(rawBody); } return _bodyString!; } return getBodyString(); } List get cookies => headers.cookies; List messages = []; } ///HTTP请求。 class HttpRequest extends HttpMessage { String _uri; HttpMethod method; HostAndPort? hostAndPort; DateTime requestTime = DateTime.now(); //请求时间 HttpResponse? response; Map attributes = {}; ProcessInfo? processInfo; String get uri => _uri; set uri(String uri) { _uri = uri; _requestUri = null; } HttpRequest(this.method, this._uri, {String protocolVersion = "HTTP/1.1"}) : super(protocolVersion); String? remoteDomain() { if (hostAndPort == null && HostAndPort.startsWithScheme(uri)) { try { var uri = Uri.parse(this.uri); return '${uri.scheme}://${uri.host}${uri.hasPort ? ':${uri.port}' : ''}'; } catch (e) { return null; } } return hostAndPort?.domain; } @override String get requestUrl { if (HostAndPort.startsWithScheme(uri)) { return uri; } if (method == HttpMethod.connect) { return "${hostAndPort?.scheme ?? 'http://'}$uri"; } return '${remoteDomain()}$uri'; } /// 请求的uri Uri? _requestUri; Uri? get requestUri { try { _requestUri ??= Uri.parse(requestUrl); return _requestUri; } catch (e) { logger.w('parse uri error $requestUrl ${hostAndPort?.scheme} ${hostAndPort?.host}: $e'); return null; } } ///域名+路径 String get domainPath => '${remoteDomain()}$path'; /// 请求的path String get path => requestUri?.path ?? ''; /// path and query String get pathAndQuery => '${requestUri?.path}${requestUri?.hasQuery == true ? '?${requestUri?.query}' : ''}'; Map get queries => requestUri?.queryParameters ?? {}; ///获取消息体编码 @override String? get charset { return super.charset ?? 'utf-8'; } ///复制请求 HttpRequest copy({String? uri}) { var request = HttpRequest(method, uri ?? this.uri, protocolVersion: protocolVersion); request.headers.addAll(headers); if (uri != null && !uri.startsWith('/')) { request.hostAndPort = HostAndPort.of(uri); } request.hostAndPort ??= hostAndPort; request.streamId = streamId; request.body = body; request.messages = messages; return request; } @override Map toJson() { return { '_class': 'HttpRequest', '_id': requestId, 'uri': requestUrl, 'method': method.name, 'protocolVersion': protocolVersion, 'packageSize': packageSize, 'headers': headers.toJson(), 'body': body == null ? null : String.fromCharCodes(body!), 'requestTime': requestTime.millisecondsSinceEpoch, 'messages': messages.map((e) => e.toJson()).toList(), }; } factory HttpRequest.fromJson(Map json) { var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri'], protocolVersion: json['protocolVersion'] ?? "HTTP/1.1"); request.requestId = json['_id'] ?? request.requestId; request.headers.addAll(HttpHeaders.fromJson(json['headers'])); request.body = json['body']?.toString().codeUnits; if (json['requestTime'] != null) { request.requestTime = DateTime.fromMillisecondsSinceEpoch(json['requestTime']); } if (json['messages'] is List) { request.messages = (json['messages'] as List) .whereType() .map((e) => WebSocketFrame.fromJson(Map.from(e))) .toList(); } request.packageSize = json['packageSize']; return request; } @override String toString() { return 'HttpRequest{version: $protocolVersion, uri: $uri, method: ${method.name}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; } } ///HTTP响应。 class HttpResponse extends HttpMessage { HttpStatus status; DateTime responseTime = DateTime.now(); HttpRequest? request; String? _requestUrl; @override String? get requestUrl => request?.requestUrl ?? _requestUrl; HttpResponse(this.status, {String protocolVersion = "HTTP/1.1"}) : super(protocolVersion); /// 复制响应 HttpResponse copy() { var response = HttpResponse(status, protocolVersion: protocolVersion); response.headers.addAll(headers); response.body = body; response.request = request; response.messages = messages; return response; } String costTime() { if (request == null) { return ''; } var cost = responseTime.difference(request!.requestTime).inMilliseconds; if (cost > 1000) { return '${(cost / 1000).toStringAsFixed(2)}s'; } return '${cost}ms'; } //json序列化 factory HttpResponse.fromJson(Map json) { var httpResponse = HttpResponse(HttpStatus(json['status']['code'], json['status']['reasonPhrase']), protocolVersion: json['protocolVersion']) ..headers.addAll(HttpHeaders.fromJson(json['headers'])) ..body = json['body']?.toString().codeUnits; if (json['responseTime'] != null) { httpResponse.responseTime = DateTime.fromMillisecondsSinceEpoch(json['responseTime']); } if (json['messages'] is List) { httpResponse.messages = (json['messages'] as List) .where((e) => e is Map) .map((e) => WebSocketFrame.fromJson(Map.from(e))) .toList(); } httpResponse.packageSize = json['packageSize']; httpResponse._requestUrl = json['requestUrl']; return httpResponse; } @override Map toJson() { return { '_class': 'HttpResponse', 'requestUrl': request?.requestUrl ?? _requestUrl, 'protocolVersion': protocolVersion, 'packageSize': packageSize, 'status': { 'code': status.code, 'reasonPhrase': status.reasonPhrase, }, 'headers': headers.toJson(), 'body': body == null ? null : String.fromCharCodes(body!), 'responseTime': responseTime.millisecondsSinceEpoch, 'messages': messages.map((e) => e.toJson()).toList(), }; } @override String toString() { return 'HttpResponse{status: ${status.code}, protocolVersion: $protocolVersion headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; } } ///HTTP请求方法。 enum HttpMethod { get("GET"), post("POST"), put("PUT"), patch("PATCH"), delete("DELETE"), options("OPTIONS"), head("HEAD"), trace("TRACE"), connect("CONNECT"), propfind("PROPFIND"), report("REPORT"), ; final String name; const HttpMethod(this.name); static HttpMethod valueOf(String name) { try { return HttpMethod.values.firstWhere((element) => element.name == name.toUpperCase()); } catch (error) { logger.e("HttpMethod error $name :$error"); rethrow; } } static List methods() { return values.where((method) => method != HttpMethod.propfind && method != HttpMethod.report).toList(); } } ///HTTP响应状态。 class HttpStatus { /// 200 OK static final HttpStatus ok = newStatus(200, "OK"); /// 400 Bad Request static final HttpStatus badRequest = newStatus(400, "Bad Request"); /// 401 Unauthorized static final HttpStatus unauthorized = newStatus(401, "Unauthorized"); /// 403 Forbidden static final HttpStatus forbidden = newStatus(403, "Forbidden"); /// 404 Not Found static final HttpStatus notFound = newStatus(404, "Not Found"); /// 500 Internal Server Error static final HttpStatus internalServerError = newStatus(500, "Internal Server Error"); /// 502 Bad Gateway static final HttpStatus badGateway = newStatus(502, "Bad Gateway"); /// 503 Service Unavailable static final HttpStatus serviceUnavailable = newStatus(503, "Service Unavailable"); /// 504 Gateway Timeout static final HttpStatus gatewayTimeout = newStatus(504, "Gateway Timeout"); static HttpStatus newStatus(int statusCode, String? reasonPhrase) { if (reasonPhrase == null) { return HttpStatus.valueOf(statusCode); } return HttpStatus(statusCode, reasonPhrase); } static HttpStatus valueOf(int code) { switch (code) { case 200: return ok; case 400: return badRequest; case 401: return unauthorized; case 403: return forbidden; case 404: return notFound; case 500: return internalServerError; case 502: return badGateway; case 503: return serviceUnavailable; case 504: return gatewayTimeout; } return HttpStatus(code, ""); } final int code; String reasonPhrase; HttpStatus reason(String reasonPhrase) { this.reasonPhrase = reasonPhrase; return this; } HttpStatus(this.code, this.reasonPhrase); bool isSuccessful() { return code >= 200 && code < 300; } @override String toString() { return '$code $reasonPhrase'; } } ================================================ FILE: lib/network/http/http_client.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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'; import 'dart:typed_data'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/h2/h2_codec.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/network/channel/network.dart'; import 'package:proxypin/network/util/byte_buf.dart'; import 'package:proxypin/network/util/byte_utils.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/system_proxy.dart'; import 'package:proxy_manager/proxy_manager.dart'; import '../channel/channel.dart'; import 'codec.dart'; import 'h2/frame.dart'; import 'h2/setting.dart'; class HttpClients { static Future startConnect(HostAndPort hostAndPort, {Duration timeout = const Duration(seconds: 3)}) { String host = hostAndPort.host; //说明支持ipv6 if (host.startsWith("[") && host.endsWith(']')) { host = host.substring(1, host.length - 1); } return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) { if (socket.address.type != InternetAddressType.unix) { socket.setOption(SocketOption.tcpNoDelay, true); } return Channel(socket); }); } ///代理建立连接 static Future proxyConnect( HttpRequest request, HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext, {ProxyInfo? proxyInfo}) async { var client = Client()..initChannel((channel) => channel.dispatcher.channelHandle(HttpClientCodec(), handler)); if (proxyInfo == null) { var proxyTypes = hostAndPort.isSsl() ? ProxyTypes.https : ProxyTypes.http; proxyInfo = await SystemProxy.getSystemProxy(proxyTypes); } HostAndPort connectHost = proxyInfo == null ? hostAndPort : HostAndPort.host(proxyInfo.host, proxyInfo.port!); var channel = await client.connect(connectHost, channelContext); if (proxyInfo != null) { await connectRequest(channelContext, hostAndPort, channel, proxyInfo: proxyInfo); } if (hostAndPort.isSsl()) { await channel.startSecureSocket(channelContext, host: hostAndPort.host, supportedProtocols: request.protocolVersion == "HTTP/2" ? ["h2", "http/1.1"] : null); if (channelContext.serverChannel?.selectedProtocol == "h2") { await Http2ClientHandler(handler).listen(channel, channelContext); } else { request.protocolVersion = "HTTP/1.1"; channel.dispatcher.listen(channel, channelContext); } } logger.d( "request ${hostAndPort.host}:${hostAndPort.port} ${request.protocolVersion} ${channelContext.serverChannel?.selectedProtocol ?? ''}"); return channel; } ///发起代理连接请求 static Future connectRequest(ChannelContext channelContext, HostAndPort hostAndPort, Channel channel, {ProxyInfo? proxyInfo}) async { ChannelHandler handler = channel.dispatcher.handler; //代理 发送connect请求 var httpResponseHandler = HttpResponseHandler(); channel.dispatcher.handler = httpResponseHandler; HttpRequest proxyRequest = HttpRequest(HttpMethod.connect, '${hostAndPort.host}:${hostAndPort.port}'); proxyRequest.headers.set(HttpHeaders.HOST, '${hostAndPort.host}:${hostAndPort.port}'); //proxy Authorization if (proxyInfo?.isAuthenticated == true) { String auth = base64Encode(utf8.encode("${proxyInfo?.username}:${proxyInfo?.password}")); proxyRequest.headers.set(HttpHeaders.PROXY_AUTHORIZATION, 'Basic $auth'); } await channel.write(channelContext, proxyRequest); var response = await httpResponseHandler.getResponse(const Duration(seconds: 5)); channel.dispatcher.handler = handler; if (!response.status.isSuccessful()) { throw Exception("$hostAndPort Proxy failed to establish tunnel " "(${response.status.code} ${response..status.reasonPhrase})"); } return channel; } /// 建立连接 static Future connect(Uri uri, ChannelHandler handler, ChannelContext channelContext) async { Client client = Client() ..initChannel((channel) => channel.dispatcher.handle(HttpResponseCodec(), HttpRequestCodec(), handler)); if (uri.scheme == "https" || uri.scheme == "wss") { return client.secureConnect(HostAndPort.of(uri.toString()), channelContext); } return client.connect(HostAndPort.of(uri.toString()), channelContext); } /// 发送get请求 static Future get(String url, {Duration timeout = const Duration(seconds: 3)}) async { HttpRequest msg = HttpRequest(HttpMethod.get, url); return request(HostAndPort.of(url), msg, timeout: timeout); } /// 发送请求 static Future request(HostAndPort hostAndPort, HttpRequest request, {Duration timeout = const Duration(seconds: 3)}) async { var httpResponseHandler = HttpResponseHandler(); var client = Client() ..initChannel( (channel) => channel.dispatcher.handle(HttpResponseCodec(), HttpRequestCodec(), httpResponseHandler)); ChannelContext channelContext = ChannelContext(); Channel channel = await client.connect(hostAndPort, channelContext); await channel.write(channelContext, request); return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close()); } /// 发送代理请求 static Future proxyRequest(HttpRequest request, {ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 30)}) async { if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) { try { var uri = Uri.parse(request.requestUrl); request.headers.host = '${uri.host}${uri.hasPort ? ':${uri.port}' : ''}'; } catch (_) {} } ChannelContext channelContext = ChannelContext(); var httpResponseHandler = HttpResponseHandler(); request.hostAndPort ??= HostAndPort.of(request.requestUrl); Channel channel = await proxyConnect(request, proxyInfo: proxyInfo, request.hostAndPort!, httpResponseHandler, channelContext); if (!request.uri.startsWith("/")) { Uri? uri = request.requestUri; request = request.copy(uri: '${uri!.path}${uri.hasQuery ? '?${uri.query}' : ''}'); } if (channel.selectedProtocol == 'h2') { request.headers.remove(HttpHeaders.HOST); request.streamId = 1; } await channel.write(channelContext, request); return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close()); } } class Http2ClientHandler { static const int FLAG_ACK = 0x1; ByteBuf byteBuf = ByteBuf(); Http2ResponseDecoder decoder = Http2ResponseDecoder(); final ChannelHandler handler; Http2ClientHandler(this.handler); Future listen(Channel channel, ChannelContext channelContext) async { channel.dispatcher.encoder = Http2RequestDecoder(); channel.dispatcher.decoder = decoder; channel.socket.listen((data) => onData(channelContext, channel, data), onError: (error, trace) => handler.exceptionCaught(channelContext, channel, error, trace: trace), onDone: () => handler.channelInactive(channelContext, channel)); await channel.writeBytes(Http2Codec.connectionPrefacePRI); //发送setting final streamSetting = StreamSetting(); streamSetting.headTableSize = 65536; streamSetting.initialWindowSize = 1048896; streamSetting.maxHeaderListSize = 262144; var payload = Uint8List(6 * 3); int offset = 0; // SETTINGS_HEADER_TABLE_SIZE setInt16(payload, offset, 1); offset += 2; setInt32(payload, offset, streamSetting.headTableSize); offset += 4; // SETTINGS_INITIAL_WINDOW_SIZE setInt16(payload, offset, 4); offset += 2; setInt32(payload, offset, streamSetting.initialWindowSize); offset += 4; //SETTINGS_MAX_FRAME_SIZE setInt16(payload, offset, 6); offset += 2; setInt32(payload, offset, streamSetting.maxHeaderListSize!); offset += 4; var settingFrame = FrameHeader(payload.length, FrameType.settings, 0, 0); var buffer = settingFrame.encode()..addAll(payload); await channel.writeBytes(buffer); } void onData(ChannelContext channelContext, Channel channel, Uint8List data) { byteBuf.add(data); var decodeResult = decoder.decode(channelContext, byteBuf); if (!decodeResult.isDone) { return; } byteBuf.clearRead(); if (decodeResult.forward != null) { ByteBuf buffer = ByteBuf(decodeResult.forward); FrameHeader? frameHeader = FrameReader.readFrameHeader(buffer); logger.d("Http2ClientHandler forward ${frameHeader?.type}"); if (frameHeader?.type == FrameType.settings) { // 检查是否需要发送 ACK if (frameHeader!.hasAckFlag == false) { // 发送带有 ACK 标志的 SETTINGS 帧 var ackFrame = FrameHeader(0, FrameType.settings, FLAG_ACK, 0); channel.writeBytes(ackFrame.encode()); } } return; } handler.channelRead(channelContext, channel, decodeResult.data!); } } class HttpResponseHandler extends ChannelHandler { Completer _completer = Completer(); @override Future channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async { // log.i("[${channel.id}] Response $msg"); _completer.complete(msg); } Future getResponse(Duration duration) { return _completer.future.timeout(duration); } void resetResponse() { _completer = Completer(); } } ================================================ FILE: lib/network/http/http_headers.dart ================================================ // ignore_for_file: constant_identifier_names /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:collection'; class HttpHeaders { static const String CONTENT_LENGTH = "Content-Length"; static const String CONTENT_ENCODING = "Content-Encoding"; static const String CONTENT_TYPE = "Content-Type"; static const String HOST = "Host"; static const String TRANSFER_ENCODING = "Transfer-Encoding"; static const String Cookie = "Cookie"; static const String PROXY_AUTHORIZATION = "Proxy-Authorization"; static const List commonHeaderKeys = [ 'Accept', 'Accept-Charset', 'Accept-Encoding', 'Accept-Language', 'Accept-Ranges', 'Authorization', 'Cache-Control', 'Connection', 'Content-Type', 'Content-Length', 'Content-Encoding', 'Cookie', 'Date', 'Expect', 'From', 'Host', 'If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Range', 'If-Unmodified-Since', 'Max-Forwards', 'Origin', 'Pragma', 'Proxy-Authorization', 'Range', 'Referer', 'TE', 'Upgrade', 'User-Agent', 'Via', 'Warning', 'X-Requested-With', 'DNT', 'X-Forwarded-For', 'X-Forwarded-Host', 'X-Forwarded-Proto', 'Front-End-Https', 'X-Http-Method-Override', 'X-ATT-DeviceId', 'X-Wap-Profile', 'Proxy-Connection', 'X-UIDH', 'X-Csrf-Token', 'X-Request-ID', 'X-Correlation-ID', 'Save-Data' ]; static const Map> commonHeaderValues = { 'Accept': [ 'application/json, text/plain, */*', 'application/xml, text/xml, */*', 'text/html, application/xhtml+xml, */*', '*/*' ], 'Accept-Charset': ['utf-8, iso-8859-1;q=0.5', 'utf-8'], 'Accept-Encoding': ['gzip, deflate, br', 'gzip, deflate'], 'Accept-Language': ['en-US,en;q=0.9', 'zh-CN,zh;q=0.9'], 'Cache-Control': ['no-cache', 'max-age=0', 'no-store'], 'Connection': ['keep-alive', 'close'], 'Content-Type': [ 'application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain', 'text/html', 'application/xml' ], 'User-Agent': [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', 'Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0' ], }; final LinkedHashMap> _headers = LinkedHashMap>(); // 由小写标头名称键入的原始标头名称。 final Map _originalHeaderNames = {}; HttpHeaders(); ///设置header。 void set(String name, String value) { var original = _originalHeaderNames[name.toLowerCase()]; if (original != null && original != name) { _headers.remove(original); } _headers[name] = [value]; _originalHeaderNames[name.toLowerCase()] = name; } ///添加header。 void add(String name, String value) { addValues(name, [value]); } ///添加header。 void addValues(String name, List values) { var original = _originalHeaderNames[name.toLowerCase()]; if (original != null && original != name) { var old = _headers.remove(original); _headers[name] = List.from(old!); } if (_headers[name] == null) { _headers[name] = []; } _headers[name]?.addAll(values); _originalHeaderNames[name.toLowerCase()] = name; } ///从headers中添加 addAll(HttpHeaders? headers) { headers?.forEach((key, values) { for (var val in values) { add(key, val); } }); } String? get(String name) { return getList(name)?.first; } String getOriginalName(String name) { return _originalHeaderNames[name.toLowerCase()] ?? name; } List? getList(String name) { var originalHeaderName = _originalHeaderNames[name.toLowerCase()]; if (originalHeaderName == null) { return null; } return _headers[originalHeaderName]; } bool remove(String name) { var originalHeaderName = _originalHeaderNames.remove(name.toLowerCase()); _headers.remove(originalHeaderName); return originalHeaderName != null; } int? getInt(String name) { final value = get(name); if (value == null) { return null; } return int.parse(value); } bool getBool(String name) { final value = get(name); if (value == null) { return false; } return value.toLowerCase() == "true"; } int get contentLength => getInt(CONTENT_LENGTH) ?? 0; set contentLength(int contentLength) => set(CONTENT_LENGTH, contentLength.toString()); String? get contentEncoding => get(HttpHeaders.CONTENT_ENCODING)?.toLowerCase(); bool get isGzip => contentEncoding == "gzip"; bool get isChunked => get(HttpHeaders.TRANSFER_ENCODING)?.toLowerCase().trimLeft() == "chunked"; List get cookies => getList(Cookie) ?? []; void forEach(void Function(String name, List values) f) { _headers.forEach(f); } Iterable>> get entries => _headers.entries; set contentType(String contentType) => set(CONTENT_TYPE, contentType); String get contentType => get(CONTENT_TYPE) ?? ""; String? get host => get(HOST); set host(String? host) { if (host != null) { set(HOST, host); } } //清空 void clear() { _headers.clear(); _originalHeaderNames.clear(); } String headerLines() { StringBuffer sb = StringBuffer(); forEach((name, values) { for (var value in values) { sb.writeln("$name: $value"); } }); return sb.toString(); } ///转换json Map toJson() { Map json = {}; forEach((name, values) { json[name] = values; }); return json; } ///转换json Map toMap() { Map json = {}; forEach((name, values) { json[name] = values.join(";"); }); return json; } Map> getHeaders() { return _headers; } ///从json解析 factory HttpHeaders.fromJson(Map json) { HttpHeaders headers = HttpHeaders(); json.forEach((key, values) { for (var element in (values as List)) { headers.add(key, element.toString()); } }); return headers; } ///原始header文本 String toRawHeaders() { StringBuffer sb = StringBuffer(); forEach((name, values) { for (var value in values) { sb.writeln("$name: $value"); } }); return sb.toString(); } @override String toString() { return 'HttpHeaders{$_headers}'; } } ================================================ FILE: lib/network/http/parse/body_reader.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:math'; import 'dart:typed_data'; import 'package:proxypin/network/http/constants.dart'; import 'package:proxypin/network/http/http.dart'; import '../../../utils/num.dart'; import '../codec.dart'; class Result { final bool isDone; final bool supportedParse; Uint8List? body; Result(this.isDone, {this.body, this.supportedParse = true}); } class BodyReader { final HttpMessage message; // BytesBuilder msgBytes = BytesBuilder(); int _offset = 0; ReaderState _state; final BytesBuilder _bodyBuffer = BytesBuilder(); ///chunked编码 剩余未读取的chunk大小 int _chunkReadableSize = 0; BodyReader(this.message) : _state = message.headers.isChunked ? ReaderState.readChunkSize : ReaderState.readFixedLengthContent; Result readBody(Uint8List data) { if (_bodyBuffer.length > Codec.maxBodyLength) { _bodyBuffer.clear(); throw ParserException('Body length exceeds ${Codec.maxBodyLength}'); } _offset = 0; if (message.headers.contentType == 'video/x-flv' || message.headers.contentType.startsWith("text/event-stream")) { //Directly forward without processing for now return Result(false, supportedParse: false, body: data); } //chunked编码 if (message.headers.isChunked) { _readChunked(data); } else { _readFixedLengthContent(data); } if (_state == ReaderState.done) { var body = _bodyBuffer.toBytes(); _bodyBuffer.clear(); return Result(true, body: body); } return Result(false); } void _readFixedLengthContent(Uint8List data) { if (message.contentLength > 0) { _bodyBuffer.add(data.sublist(_offset)); } if (message.contentLength == -1 || _bodyBuffer.length >= message.contentLength) { _state = ReaderState.done; } } void _readChunked(Uint8List data) { while (_offset < data.length) { //读取chunk length if (_state == ReaderState.readChunkSize) { _chunkReadableSize = _readChunkSize(data); if (_chunkReadableSize == 0) { //chunked编码结束 _state = ReaderState.done; break; } if (_chunkReadableSize == -1) { continue; } _state = ReaderState.readChunkedContent; } //读取chunk内容 if (_state == ReaderState.readChunkedContent) { int end = min(data.length, _offset + _chunkReadableSize); _bodyBuffer.add(data.sublist(_offset, end)); //可读大小 _chunkReadableSize -= (end - _offset); _offset = end; if (_chunkReadableSize == 0) { _state = ReaderState.readChunkSize; _offset += 2; //内容结尾\r\n } } } } int _readChunkSize(Uint8List data) { if (_offset >= data.length) { return -1; } for (int i = _offset; i < data.length; i++) { /// chunked编码内容结尾\r\n if (data[i] == HttpConstants.lf) { if (i > 0 && data[i - 1] == HttpConstants.cr) { var line = data.sublist(_offset, i - 1); _offset = i + 1; if (line.isEmpty) { return -1; } return hexToInt(String.fromCharCodes(line)); } //可能上个包是结尾\r 最好做法是缓存上个不完整的包,先临时处理下 if (data.length == 1) { _offset = i + 1; return -1; } } } throw Exception('Invalid chunked encoding line: ${String.fromCharCodes(data)}'); } } enum ReaderState { readFixedLengthContent, readChunked, readChunkSize, readChunkedContent, done } ================================================ FILE: lib/network/http/parse/http_parser.dart ================================================ import 'dart:typed_data'; import 'package:proxypin/network/http/constants.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/network/util/byte_buf.dart'; /// http解析器 class HttpParse { static const int defaultMaxLength = 102400; /// 解析请求行 List parseInitialLine(ByteBuf data, int size) { List initialLine = []; var startIndex = data.readerIndex; for (int i = data.readerIndex; i < size; i++) { if (_isLineEnd(data, i)) { //请求行结束 Uint8List requestLine = data.readBytes(i - data.readerIndex); data.skipBytes(2); initialLine = _splitLine(requestLine); break; } } if (initialLine.length == 3) { return initialLine; } if (data.length > defaultMaxLength) { throw Exception("request line too long"); } data.readerIndex = startIndex; return []; } //分割行 List _splitLine(Uint8List data) { List lines = []; int start = 0; for (int i = 0; i < data.length; i++) { if (data[i] == HttpConstants.sp) { lines.add(String.fromCharCodes(data.sublist(start, i))); start = i + 1; if (lines.length == 2) { break; } } } lines.add(String.fromCharCodes(data.sublist(start))); return lines; } /// 解析请求头 bool parseHeaders(ByteBuf data, HttpHeaders headers) { if (!data.isReadable()) { return false; } int startIndex = data.readerIndex; for (int i = data.readerIndex; i < data.length; i++) { if ((i - startIndex) > defaultMaxLength) { throw Exception("header too long"); } if (_isLineEnd(data, i)) { Uint8List line = data.readBytes(i - data.readerIndex); data.skipBytes(2); if (line.isEmpty) { break; } var header = _splitHeader(line); headers.add(header[0], header[1]); } } //\r\n \r\n结束 return _isLineEnd(data, data.readerIndex - 4) && _isLineEnd(data, data.readerIndex - 2); } //是否行结束 bool _isLineEnd(ByteBuf data, int index) { return index + 1 < data.length && data.get(index) == HttpConstants.cr && data.get(index + 1) == HttpConstants.lf; } //分割头 List _splitHeader(List data) { List headers = []; for (int i = 0; i < data.length; i++) { if (data[i] == HttpConstants.colon) { headers.add(String.fromCharCodes(data.sublist(0, i))); if (data[i + 1] == HttpConstants.sp) { headers.add(String.fromCharCodes(data.sublist(i + 2))); } else { headers.add(String.fromCharCodes(data.sublist(i + 1))); } break; } } return headers; } } ================================================ FILE: lib/network/http/sse.dart ================================================ /* * Server-Sent Events (text/event-stream) incremental decoder */ import 'dart:convert'; import 'dart:typed_data'; import 'package:proxypin/network/http/websocket.dart'; /// Parse SSE stream chunks into message frames. /// We reuse WebSocketFrame as a generic message container so UI and listeners work. class SseDecoder { final StringBuffer _lineBuf = StringBuffer(); // current event fields final StringBuffer _data = StringBuffer(); String? _event; String? _id; int? _retry; /// Feed a chunk of bytes and return zero or more frames assembled. List feed(Uint8List bytes) { final List frames = []; // Append decoded text to buffer; allowMalformed to survive split UTF-8 sequences. _lineBuf.write(utf8.decode(bytes, allowMalformed: true)); while (true) { final String current = _lineBuf.toString(); final int nl = current.indexOf('\n'); if (nl == -1) break; String line = current.substring(0, nl); _lineBuf.clear(); if (nl + 1 < current.length) _lineBuf.write(current.substring(nl + 1)); if (line.endsWith('\r')) line = line.substring(0, line.length - 1); if (line.isEmpty) { // End of event: emit if any data collected if (_data.isNotEmpty) { String dataValue = _data.toString(); if (dataValue.endsWith('\n')) dataValue = dataValue.substring(0, dataValue.length - 1); // Build a text frame from the SSE event. Include event/id headers if present as a prefix comment. final String payloadText = _event == null && _id == null ? dataValue : _buildLabeledPayload(dataValue, event: _event, id: _id, retry: _retry); frames.add(_textFrame(payloadText)); } _resetEventState(); continue; } if (line.startsWith(':')) { // comment line – ignore continue; } final int colon = line.indexOf(':'); final String field = (colon == -1) ? line : line.substring(0, colon); String value = (colon == -1) ? '' : line.substring(colon + 1); if (value.startsWith(' ')) value = value.substring(1); switch (field) { case 'data': _data.write(value); _data.write('\n'); break; case 'event': _event = value; break; case 'id': _id = value; break; case 'retry': _retry = int.tryParse(value); break; default: // ignore unknown fields break; } } return frames; } void _resetEventState() { _data.clear(); _event = null; _id = null; _retry = null; } String _buildLabeledPayload(String data, {String? event, String? id, int? retry}) { final StringBuffer b = StringBuffer(); if (event != null && event.isNotEmpty) b.writeln('event: $event'); if (id != null && id.isNotEmpty) b.writeln('id: $id'); if (retry != null) b.writeln('retry: $retry'); b.write(data); return b.toString(); } WebSocketFrame _textFrame(String text) { final bytes = utf8.encode(text); return WebSocketFrame( fin: true, opcode: 0x01, // text mask: false, payloadLength: bytes.length, maskingKey: 0, payloadData: Uint8List.fromList(bytes), ); } } ================================================ FILE: lib/network/http/websocket.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:proxypin/network/util/logger.dart'; class WebSocketFrame { final bool fin; /* 0x00 denotes a continuation frame 0x01 表示一个text frame 0x02 表示一个binary frame 0x03 ~~ 0x07 are reserved for further non-control frames,为将来的非控制消息片段保留测操作码 0x08 表示连接关闭 0x09 表示 ping (心跳检测相关) 0x0a 表示 pong (心跳检测相关) */ final int opcode; //4bit final bool mask; //1bit final int maskingKey; final int payloadLength; final Uint8List payloadData; bool isFromClient = false; DateTime time; WebSocketFrame({ required this.fin, required this.opcode, required this.mask, required this.payloadLength, required this.maskingKey, required this.payloadData, DateTime? time, }) : time = time ?? DateTime.now(); bool get isText => opcode == 0x01; bool get isBinary => opcode == 0x02; String get payloadDataAsString { if (opcode == 0x08) { return '连接关闭'; } if (opcode == 0x02) { return '二进制数据'; } try { return utf8.decode(payloadData); } catch (e) { return String.fromCharCodes(payloadData); } } Map toJson() { return { 'fin': fin, 'opcode': opcode, 'mask': mask, 'maskingKey': maskingKey, 'payloadLength': payloadLength, // use base64 to avoid binary corruption in JSON 'payloadData': base64Encode(payloadData), 'isFromClient': isFromClient, 'time': time.millisecondsSinceEpoch, }; } factory WebSocketFrame.fromJson(Map json) { final payload = base64Decode(json['payloadData']?.toString() ?? ''); final frame = WebSocketFrame( fin: json['fin'] == true, opcode: (json['opcode'] ?? 0) as int, mask: json['mask'] == true, payloadLength: (json['payloadLength'] ?? payload.length) as int, maskingKey: (json['maskingKey'] ?? 0) as int, payloadData: Uint8List.fromList(payload), time: json['time'] == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch((json['time'] as num).toInt()), ); frame.isFromClient = json['isFromClient'] == true; return frame; } } ///websocket 解码器 class WebSocketDecoder { ByteBuffer buffer = ByteBuffer(); WebSocketFrame? decode(Uint8List newData) { buffer.putBytes(newData); if (!canParseWebSocketFrame(buffer.bytes)) { return null; } try { WebSocketFrame frame = _parseWebSocketFrame(buffer.bytes); buffer.clear(); return frame; } catch (e, stackTrace) { logger.e("WebSocket decode error", error: e, stackTrace: stackTrace); return null; } } bool canParseWebSocketFrame(Uint8List data) { if (data.length < 2) { return false; } var reader = ByteData.sublistView(data); var opcode = reader.getUint8(0) & 0x0f; if (opcode > 0xA) { return false; } var mask = reader.getUint8(1) >> 7; int payloadStart = 2; int payloadLength = reader.getUint8(1) & 0x7f; if (payloadLength == 126) { if (data.length < 4) return false; payloadLength = reader.getUint16(2); payloadStart += 2; } else if (payloadLength == 127) { if (data.length < 10) return false; payloadLength = reader.getUint64(2); payloadStart += 8; } if (mask == 1) { if (data.length < payloadStart + 4) { return false; } payloadStart += 4; } if (data.length < payloadStart + payloadLength) { return false; } return true; } WebSocketFrame _parseWebSocketFrame(Uint8List data) { var reader = ByteData.sublistView(data); var fin = reader.getUint8(0) >> 7; //解析 rsv1 var rsv1 = (reader.getUint8(0) >> 6) & 0x01; var opcode = reader.getUint8(0) & 0x0f; var mask = reader.getUint8(1) >> 7; int payloadLength = reader.getUint8(1) & 0x7f; int payloadStart = 2; if (payloadLength == 126) { payloadLength = reader.getUint16(2); payloadStart += 2; } else if (payloadLength == 127) { payloadLength = reader.getUint64(2); payloadStart += 8; } var maskingKey = 0; if (mask == 1) { maskingKey = reader.getUint32(payloadStart); payloadStart += 4; } int payloadDataLength = payloadLength; if (payloadStart + payloadDataLength > data.length) { payloadDataLength = data.length - payloadStart; logger.w("Payload data length exceeds available data, truncating."); } var payloadData = data.sublist(payloadStart, payloadStart + payloadDataLength); if (mask == 1) { payloadData = unmaskPayload(payloadData, maskingKey); } if (rsv1 == 1) { //inflate payloadData = decompress(payloadData); } return WebSocketFrame( fin: fin == 1, opcode: opcode, mask: mask == 1, maskingKey: maskingKey, payloadLength: payloadLength, payloadData: payloadData, ); } ZLibDecoder? _decoder; ZLibDecoder _ensureDecoder() => _decoder ?? ZLibDecoder(raw: true); Uint8List decompress(Uint8List msg) { try { return Uint8List.fromList(_ensureDecoder().convert(msg)); } catch (e) { logger.e("Decompression error", error: e); return msg; } } Uint8List unmaskPayload(Uint8List payloadData, int maskingKey) { var unmaskedData = Uint8List(payloadData.length); for (var i = 0; i < payloadData.length; i++) { var keyByte = (maskingKey >> ((3 - (i % 4)) * 8)) & 0xff; unmaskedData[i] = payloadData[i] ^ keyByte; } return unmaskedData; } } class ByteBuffer { Uint8List _bytes = Uint8List(0); Uint8List get bytes => _bytes; void putBytes(Uint8List newBytes) { Uint8List tmp = Uint8List(_bytes.length + newBytes.length); tmp.setAll(0, _bytes); tmp.setAll(_bytes.length, newBytes); _bytes = tmp; } void clear() { _bytes = Uint8List(0); } } ================================================ FILE: lib/network/socks/socks5.dart ================================================ /* * Copyright 2024 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:typed_data'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/http/codec.dart'; import 'package:proxypin/network/util/attribute_keys.dart'; import 'package:proxypin/network/util/logger.dart'; import '../channel/host_port.dart'; /// @author wanghongen class Socks5 { static const int version = 5; static const int methodNoAuth = 0; static const int methodNoAcceptable = 0xff; static const int cmdConnect = 1; static const int atypIpv4 = 1; static const int atypDomain = 3; static const int atypIpv6 = 4; static const int repSuccess = 0; static const int repCommandNotSupported = 7; static const int repAddressTypeNotSupported = 8; static const int repSocks5ServerAtypIpv4 = 0x01; static const int repSocks5ServerAtypDomain = 0x03; static const int repSocks5ServerAtypIpv6 = 0x04; static bool isSocks5(Uint8List data) { return (data.length == 3 || data.length == 4) && data[0] == version && (data[1] == 1 || data[1] == 2) && data[2] == methodNoAuth; } } ///Detects the version of the current SOCKS connection and initializes the pipeline with Socks5InitialRequestDecoder. class SocksServerHandler extends ChannelHandler { late Decoder originalDecoder; late Encoder originalEncoder; final ChannelHandler originalHandler; SocksState socksState = SocksState.init; SocksServerHandler(this.originalDecoder, this.originalEncoder, this.originalHandler); @override Future channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async { int idx = 0; final int version = msg[idx++]; if (version != Socks5.version) { await channel.writeBytes(Uint8List.fromList([Socks5.version, Socks5.methodNoAcceptable])); channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS version: $version')); return; } if (socksState == SocksState.init) { //no auth await channel.writeBytes(Uint8List.fromList([Socks5.version, Socks5.methodNoAuth])); socksState = SocksState.connect; return; } if (socksState == SocksState.connect) { final int cmd = msg[idx++]; if (cmd != Socks5.cmdConnect) { var out = encodeCommandResponse(Socks5.repCommandNotSupported); await channel.writeBytes(out); channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS cmd: $cmd')); return; } //skip RSV idx++; final int dstAddrType = msg[idx++]; String host; if (dstAddrType == Socks5.atypIpv4) { host = '${msg[idx++]}.${msg[idx++]}.${msg[idx++]}.${msg[idx++]}'; } else if (dstAddrType == Socks5.atypDomain) { int len = msg[idx++]; host = utf8.decode(msg.sublist(idx, idx + len)); idx += len; } else if (dstAddrType == Socks5.atypIpv6) { List parts = []; for (int i = 0; i < 8; i++) { int part = msg[idx++] << 8 | msg[idx++]; parts.add(part.toRadixString(16)); } host = parts.join(':'); } else { var out = encodeCommandResponse(Socks5.repAddressTypeNotSupported); await channel.writeBytes(out); channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS atyp: $dstAddrType')); return; } final int port = msg[idx++] << 8 | msg[idx++]; final proxyInfo = ProxyInfo.of(host, port); logger.d('[${channel.id}] Socks5 connect ${proxyInfo.host}:${proxyInfo.port}'); channelContext.putAttribute(AttributeKeys.socks5Proxy, proxyInfo); final out = encodeCommandResponse(Socks5.repSuccess, bndAddrType: Socks5.repSocks5ServerAtypIpv4); await channel.writeBytes(out); channel.dispatcher.handle(originalDecoder, originalEncoder, originalHandler); socksState = SocksState.connected; return; } } Uint8List encodeCommandResponse(int status, {int bndAddrType = 0, String? bndAddr, int bndPort = 0}) { var out = BytesBuilder(); out.addByte(Socks5.version); out.addByte(status); out.addByte(0x00); //RSV out.addByte(bndAddrType); if (bndAddr != null) { out.add(Int8List.fromList(bndAddr.split('.').map((e) => int.parse(e)).toList())); } else { out.add(Int8List.fromList([0, 0, 0, 0])); } out.addByte(bndPort >> 8); out.addByte(bndPort & 0xff); return out.takeBytes(); } } enum SocksState { init, auth, connect, connected, } ================================================ FILE: lib/network/util/attribute_keys.dart ================================================ /// @author wanghongen /// 2023/5/23 interface class AttributeKeys { static const String host = "HOST"; static const String domain = "DOMAIN"; static const String uri = "URI"; static const String request = "REQUEST"; static const String remote = "REMOTE"; static const String proxyInfo = "PROXY_INFO"; static const String socks5Proxy = "SOCKS5_PROXY"; static const String processInfo = "PROCESS_INFO"; } ================================================ FILE: lib/network/util/byte_buf.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:typed_data'; ///类似于netty ByteBuf class ByteBuf { late Uint8List _buffer; int readerIndex = 0; int writerIndex = 0; ByteBuf([List? bytes]) { if (bytes != null) { _buffer = Uint8List.fromList(bytes); writerIndex = bytes.length; } else { _buffer = Uint8List(0); // Initial buffer size } } int get length => writerIndex; Uint8List get bytes => Uint8List.sublistView(_buffer, 0, writerIndex); void add(List bytes) { _ensureCapacity(writerIndex + bytes.length); _buffer.setRange(writerIndex, writerIndex + bytes.length, bytes); writerIndex += bytes.length; } void clear() { readerIndex = 0; writerIndex = 0; _buffer = Uint8List(0); } ///释放已读的空间 void clearRead() { if (readerIndex == writerIndex) { clear(); return; } if (readerIndex > 0) { _buffer = Uint8List.sublistView(_buffer, readerIndex, writerIndex); writerIndex -= readerIndex; readerIndex = 0; } } bool isReadable() => readerIndex < writerIndex; int readableBytes() => writerIndex - readerIndex; Uint8List readAvailableBytes() => readBytes(readableBytes()); Uint8List readBytes(int length) { Uint8List result = Uint8List.sublistView(_buffer, readerIndex, readerIndex + length); readerIndex += length; return result; } void skipBytes(int length) { readerIndex += length; } int read() => _buffer[readerIndex++]; int readByte() => _buffer[readerIndex++]; int readShort() { int value = (_buffer[readerIndex] << 8) | _buffer[readerIndex + 1]; readerIndex += 2; return value; } int readInt() { int value = (_buffer[readerIndex] << 24) | (_buffer[readerIndex + 1] << 16) | (_buffer[readerIndex + 2] << 8) | _buffer[readerIndex + 3]; readerIndex += 4; return value; } int get(int index) => _buffer[index]; void truncate(int len) { if (len > readableBytes()) { throw Exception("Insufficient data"); } writerIndex = readerIndex + len; } ByteBuf dup() { ByteBuf buf = ByteBuf(); buf._buffer = Uint8List.fromList(_buffer); buf.readerIndex = readerIndex; buf.writerIndex = writerIndex; return buf; } void _ensureCapacity(int required) { if (_buffer.length < required) { int newSize = _buffer.length <= 1 ? required : _buffer.length * 2; while (newSize < required) { newSize *= 2; } Uint8List newBuffer = Uint8List(newSize); newBuffer.setRange(0, writerIndex, _buffer); _buffer = newBuffer; } } } ================================================ FILE: lib/network/util/byte_utils.dart ================================================ import 'dart:typed_data'; List viewOrSublist(List data, int offset, int length) { if (data is Uint8List) { return Uint8List.view(data.buffer, data.offsetInBytes + offset, length); } else { return data.sublist(offset, offset + length); } } int readInt64(List bytes, int offset) { var high = readInt32(bytes, offset); var low = readInt32(bytes, offset + 4); return high << 32 | low; } int readInt32(List bytes, int offset) { return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; } int readInt24(List bytes, int offset) { return (bytes[offset] << 16) | (bytes[offset + 1] << 8) | bytes[offset + 2]; } int readInt16(List bytes, int offset) { return (bytes[offset] << 8) | bytes[offset + 1]; } void setInt64(List bytes, int offset, int value) { setInt32(bytes, offset, value >> 32); setInt32(bytes, offset + 4, value & 0xffffffff); } void setInt32(List bytes, int offset, int value) { bytes[offset] = (value >> 24) & 0xff; bytes[offset + 1] = (value >> 16) & 0xff; bytes[offset + 2] = (value >> 8) & 0xff; bytes[offset + 3] = value & 0xff; } void setInt24(List bytes, int offset, int value) { bytes[offset] = (value >> 16) & 0xff; bytes[offset + 1] = (value >> 8) & 0xff; bytes[offset + 2] = value & 0xff; } void setInt16(List bytes, int offset, int value) { bytes[offset] = (value >> 8) & 0xff; bytes[offset + 1] = value & 0xff; } ================================================ FILE: lib/network/util/cache.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:collection'; /// A cache that expires entries after a given duration. /// The cache uses a timer to remove entries after the specified duration. /// @author WangHongEn class ExpiringCache { final Duration duration; final _cache = {}; final _expirationTimes = {}; ExpiringCache(this.duration); void set(K key, V value) { _expirationTimes[key]?.cancel(); _cache[key] = value; _expirationTimes[key] = Timer(duration, () => remove(key)); } void operator []=(K key, V value) => set(key, value); V? putIfAbsent(K key, V Function() ifAbsent) { if (_cache.containsKey(key)) { return _cache[key]; } final value = ifAbsent(); set(key, value); return value; } bool containsKey(K key) { return _cache.containsKey(key); } V? get(K key) { return _cache[key]; } V? operator [](K key) => get(key); V? remove(K key) { _expirationTimes[key]?.cancel(); _expirationTimes.remove(key); return _cache.remove(key); } void clear() { for (var timer in _expirationTimes.values) { timer.cancel(); } _expirationTimes.clear(); _cache.clear(); } } class LruCache { final int capacity; final _cache = LinkedHashMap(); LruCache(this.capacity); V? get(K key) { if (!_cache.containsKey(key)) { return null; } // Move the accessed key to the end to show that it was recently used final value = _cache.remove(key); _cache[key] = value as V; return value; } V pubIfAbsent(K key, V Function() ifAbsent) { if (_cache.containsKey(key)) { return _cache[key]!; } final value = ifAbsent(); set(key, value); return value; } void set(K key, V value) { if (_cache.containsKey(key)) { // Remove the old value _cache.remove(key); } else if (_cache.length == capacity) { // Remove the first key (least recently used) _cache.remove(_cache.keys.first); } _cache[key] = value; } void remove(K key) { _cache.remove(key); } int get length => _cache.length; void clear() { _cache.clear(); } } ================================================ FILE: lib/network/util/cert/basic_constraints.dart ================================================ /// @author wanghongen /// 2024/7/28 class BasicConstraints { final bool isCA; final int? pathLenConstraint; final bool critical; BasicConstraints({required this.isCA, this.pathLenConstraint, this.critical = true}); } ================================================ FILE: lib/network/util/cert/cert_data.dart ================================================ import 'dart:typed_data'; import 'extension.dart'; import 'key_usage.dart'; class X509CertificateData { /// The subject data of the certificate Map subject; /// The version of the certificate int version; BigInt serialNumber; /// The signatureAlgorithm of the certificate String signatureAlgorithm; /// The readable name of the signatureAlgorithm of the certificate String? signatureAlgorithmReadableName; Map issuer; /// The validity of the certificate X509CertificateValidity validity; /// The sha1 thumbprint for the certificate String? sha1Thumbprint; /// The sha256 thumbprint for the certificate String? sha256Thumbprint; /// The md5 thumbprint for the certificate String? md5Thumbprint; /// The public key data from the certificate X509CertificatePublicKeyData publicKeyData; /// The subject alternative names List? subjectAlternativNames; /// The plain certificate pem string, that was used to decode. String? plain; /// The extended key usage extension List? extKeyUsage; /// The certificate extensions X509CertificateDataExtensions? extensions; /// The signature String? signature; /// The tbsCertificateSeq as base64 string String? tbsCertificateSeqAsString; X509CertificateData({ required this.version, required this.serialNumber, required this.signatureAlgorithm, required this.issuer, required this.validity, required this.subject, // required this.tbsCertificate, this.signatureAlgorithmReadableName, this.sha1Thumbprint, this.sha256Thumbprint, this.md5Thumbprint, required this.publicKeyData, required this.subjectAlternativNames, this.plain, this.extKeyUsage, this.extensions, this.tbsCertificateSeqAsString, required this.signature, }); } class SubjectPublicKeyInfo { /// The algorithm of the public key String? algorithm; /// The readable name of the algorithm String? algorithmReadableName; /// The parameter of the public key String? parameter; /// The readable name of the parameter String? parameterReadableName; /// The key length of the public key int? length; /// The sha1 thumbprint of the public key String? sha1Thumbprint; /// The sha256 thumbprint of the public key String? sha256Thumbprint; /// The bytes representing the public key as String String? bytes; /// The exponent used on a RSA public key int? exponent; SubjectPublicKeyInfo({ this.algorithm, this.length, this.sha1Thumbprint, this.sha256Thumbprint, this.bytes, this.algorithmReadableName, this.parameter, this.parameterReadableName, this.exponent, }); } class X509CertificateValidity { /// The start date DateTime notBefore; /// The end date DateTime notAfter; X509CertificateValidity({required this.notBefore, required this.notAfter}); } // /// Model that represents the extensions of a x509Certificate /// class X509CertificateDataExtensions { /// The subject alternative names List? subjectAlternativNames; /// The extended key usage extension List? extKeyUsage; /// The key usage extension List? keyUsage; /// The cA field of the basic constraints extension bool? cA; /// The pathLenConstraint field of the basic constraints extension int? pathLenConstraint; /// The base64 encoded VMC logo VmcData? vmc; /// The distribution points for the crl files. Normally a url. List? cRLDistributionPoints; X509CertificateDataExtensions({ this.subjectAlternativNames, this.extKeyUsage, this.keyUsage, this.cA, this.pathLenConstraint, this.vmc, this.cRLDistributionPoints, }); } /// /// Model that a public key from a X509Certificate /// class X509CertificatePublicKeyData { /// The algorithm of the public key String? algorithm; /// The readable name of the algorithm String? algorithmReadableName; /// The parameter of the public key String? parameter; /// The readable name of the parameter String? parameterReadableName; /// The key length of the public key int? length; /// The sha1 thumbprint of the public key String? sha1Thumbprint; /// The sha256 thumbprint of the public key String? sha256Thumbprint; /// The bytes representing the public key as String String? bytes; Uint8List? plainSha1; /// The exponent used on a RSA public key int? exponent; X509CertificatePublicKeyData({ this.algorithm, this.length, this.sha1Thumbprint, this.sha256Thumbprint, this.bytes, this.plainSha1, this.algorithmReadableName, this.parameter, this.parameterReadableName, this.exponent, }); static Uint8List? plainSha1FromJson(List? json) { if (json == null) { return null; } return Uint8List.fromList(json); } static List? plainSha1ToJson(Uint8List? object) { if (object == null) { return null; } return object.toList(); } X509CertificatePublicKeyData.fromSubjectPublicKeyInfo( SubjectPublicKeyInfo info) { algorithm = info.algorithm; length = info.length; sha1Thumbprint = info.sha1Thumbprint; sha256Thumbprint = info.sha256Thumbprint; bytes = info.bytes; algorithmReadableName = info.algorithmReadableName; parameter = info.parameter; parameterReadableName = info.parameterReadableName; exponent = info.exponent; } } class VmcData { /// The base64 encoded logo String? base64Logo; /// The logo type String? type; /// The hash String? hash; /// The readable version of the algorithm of the hash String? hashAlgorithmReadable; /// The algorithm of the hash String? hashAlgorithm; VmcData({ this.base64Logo, this.hash, this.hashAlgorithm, this.hashAlgorithmReadable, this.type, }); String getFullSvgData() { return 'data:$type;base64,$base64Logo'; } } ================================================ FILE: lib/network/util/cert/der.dart ================================================ import 'dart:typed_data'; import 'dart:convert'; import 'package:proxypin/network/util/byte_buf.dart'; import 'package:pointycastle/asn1.dart'; import 'package:pointycastle/src//utils.dart'; class DerValue { /// Tag value indicating an ASN.1 "INTEGER" value. static const int tagInteger = 0x02; /// Tag value indicating an ASN.1 "OCTET STRING" value. static const int tagOctetString = 0x04; int tag; final Uint8List value; final ByteBuf buffer; late DerInputStream data; DerValue(this.tag, this.value, {ByteBuf? buffer}) : buffer = buffer ?? ByteBuf(value) { data = DerInputStream(this.buffer); } factory DerValue.fromBytes(Uint8List bytes) { return DerValue.getDerValue(ByteBuf(bytes)); } factory DerValue.getDerValue(ByteBuf inStream) { var tag = inStream.read(); int length = DerInputStream.getLength(inStream); var buffer = inStream.dup(); buffer.truncate(length); var value = inStream.readBytes(length); return DerValue(tag, value, buffer: buffer); } Uint8List toByteArray() { DerOutputStream out = DerOutputStream(); encode(out); return out.toByteArray(); } void encode(DerOutputStream out) { out.writeByte(tag); out.writeLength(value.length); out.writeBytes(value); } /// Returns true iff the CONSTRUCTED bit is set in the type tag. bool isConstructed() { return ((tag & 0x020) == 0x020); } bool isConstructedTag(int constructedTag) { if (!isConstructed()) { return false; } return ((tag & 0x01f) == constructedTag); } Uint8List getOctetString() { if (tag != tagOctetString && !isConstructedTag(tagOctetString)) { throw Exception("DerValue.getOctetString, not an Octet String: $tag"); } if (isConstructed()) { while (data.buffer.isReadable()) { return data.getOctetString(); } } return value; } //get oid ASN1ObjectIdentifier getOID() { if (tag != 0x06) { throw Exception('DER input, Object Identifier tag error'); } int length = value.length; int first = value[0] ~/ 40; int second = value[0] % 40; int oid = first * 40 + second; for (int i = 1; i < length; i++) { int byte = value[i]; if (byte < 128) { oid = oid * 128 + byte; } else { oid = oid * 128 + (byte & 0x7F); } } return ASN1ObjectIdentifier.fromIdentifierString(oid.toString()); } DerInputStream toDerInputStream() { return data; } @override String toString() { return 'DerValue(tag: $tag, value: ${base64.encode(value)})'; } } class DerOutputStream { final BytesBuilder _builder = BytesBuilder(); void writeByte(int byte) { _builder.addByte(byte); } void writeLength(int length) { if (length < 128) { _builder.addByte(length); } else { int numBytes = (length.bitLength + 7) >> 3; _builder.addByte(0x80 | numBytes); for (int i = numBytes - 1; i >= 0; i--) { _builder.addByte((length >> (8 * i)) & 0xFF); } } } void writeBytes(Uint8List bytes) { _builder.add(bytes); } Uint8List toByteArray() { return _builder.toBytes(); } } class DerInputStream { final ByteBuf buffer; DerInputStream(this.buffer); factory DerInputStream.fromBytes(Uint8List data) { return DerInputStream(ByteBuf(data)); } static int getLength(ByteBuf inStream) { int length = inStream.read(); if (length & 0x80 == 0) { return length; } int numBytes = length & 0x7F; length = 0; for (int i = 0; i < numBytes; i++) { length = (length << 8) | inStream.read(); } return length; } int getInteger() { if (buffer.read() != DerValue.tagInteger) { throw Exception("DER input, Integer tag error"); } var length = getLength(buffer); return decodeBigInt(buffer.readBytes(length)).toInt(); } List getSequence(int startLen) { int tag = buffer.read(); if (tag != 0x30) { // SEQUENCE tag throw Exception('Sequence tag error'); } int length = getLength(buffer); Uint8List sequenceData = buffer.readBytes(length); DerInputStream sequenceStream = DerInputStream.fromBytes(sequenceData); List values = []; while (sequenceStream.buffer.isReadable()) { int valueTag = sequenceStream.buffer.read(); int valueLength = getLength(sequenceStream.buffer); Uint8List valueData = sequenceStream.buffer.readBytes(valueLength); values.add(DerValue(valueTag, valueData)); } return values; } ASN1ObjectIdentifier getOID() { var oid = ASN1ObjectIdentifier.fromBytes(buffer.bytes); buffer.read(); var length = getLength(buffer); buffer.skipBytes(length); return oid; } DerValue getDerValue() { return DerValue.getDerValue(buffer); } /// Returns an ASN.1 OCTET STRING from the input stream. Uint8List getOctetString() { if (buffer.read() != DerValue.tagOctetString) { throw Exception("DER input not an octet string"); } int length = getLength(buffer); return buffer.readBytes(length); } } class DerIndefLenConverter { static const int LEN_LONG = 0x80; // bit 8 set static const int LEN_MASK = 0x7f; // bits 7 - 1 late Uint8List data; late Uint8List newData; int newDataPos = 0, dataPos = 0, dataSize = 0, index = 0; int unresolved = 0; List ndefsList = []; int numOfTotalLenBytes = 0; static bool isEOC(Uint8List data, int pos) { return data[pos] == 0 && data[pos + 1] == 0; } static bool isLongForm(int lengthByte) { return (lengthByte & LEN_LONG) == LEN_LONG; } static bool isIndefinite(int lengthByte) { return (isLongForm(lengthByte) && ((lengthByte & LEN_MASK) == 0)); } void parseTag() { if (isEOC(data, dataPos)) { int numOfEncapsulatedLenBytes = 0; var elem; int index; for (index = ndefsList.length - 1; index >= 0; index--) { elem = ndefsList[index]; if (elem is int) { break; } else { numOfEncapsulatedLenBytes += (elem as Uint8List).length - 3; } } if (index < 0) { throw Exception("EOC does not have matching indefinite-length tag"); } int sectionLen = dataPos - (elem as int) + numOfEncapsulatedLenBytes; Uint8List sectionLenBytes = getLengthBytes(sectionLen); ndefsList[index] = sectionLenBytes; unresolved--; numOfTotalLenBytes += (sectionLenBytes.length - 3); } dataPos++; } void writeTag() { while (dataPos < dataSize) { if (isEOC(data, dataPos)) { dataPos += 2; } else { newData[newDataPos++] = data[dataPos++]; break; } } } int parseLength() { if (dataPos == dataSize) { return 0; } int lenByte = data[dataPos++] & 0xff; if (isIndefinite(lenByte)) { ndefsList.add(dataPos); unresolved++; return 0; } int curLen = 0; if (isLongForm(lenByte)) { lenByte &= LEN_MASK; if (lenByte > 4) { throw Exception("Too much data"); } if ((dataSize - dataPos) < (lenByte + 1)) { return -1; } for (int i = 0; i < lenByte; i++) { curLen = (curLen << 8) + (data[dataPos++] & 0xff); } if (curLen < 0) { throw Exception("Invalid length bytes"); } } else { curLen = (lenByte & LEN_MASK); } return curLen; } void writeLengthAndValue() { if (dataPos == dataSize) { return; } int curLen = 0; int lenByte = data[dataPos++] & 0xff; if (isIndefinite(lenByte)) { Uint8List lenBytes = ndefsList[index++] as Uint8List; newData.setRange(newDataPos, newDataPos + lenBytes.length, lenBytes); newDataPos += lenBytes.length; } else { if (isLongForm(lenByte)) { lenByte &= LEN_MASK; for (int i = 0; i < lenByte; i++) { curLen = (curLen << 8) + (data[dataPos++] & 0xff); } if (curLen < 0) { throw Exception("Invalid length bytes"); } } else { curLen = (lenByte & LEN_MASK); } writeLength(curLen); writeValue(curLen); } } void writeLength(int curLen) { if (curLen < 128) { newData[newDataPos++] = curLen; } else if (curLen < (1 << 8)) { newData[newDataPos++] = 0x81; newData[newDataPos++] = curLen; } else if (curLen < (1 << 16)) { newData[newDataPos++] = 0x82; newData[newDataPos++] = (curLen >> 8); newData[newDataPos++] = curLen; } else if (curLen < (1 << 24)) { newData[newDataPos++] = 0x83; newData[newDataPos++] = (curLen >> 16); newData[newDataPos++] = (curLen >> 8); newData[newDataPos++] = curLen; } else { newData[newDataPos++] = 0x84; newData[newDataPos++] = (curLen >> 24); newData[newDataPos++] = (curLen >> 16); newData[newDataPos++] = (curLen >> 8); newData[newDataPos++] = curLen; } } Uint8List getLengthBytes(int curLen) { Uint8List lenBytes; int index = 0; if (curLen < 128) { lenBytes = Uint8List(1); lenBytes[index++] = curLen; } else if (curLen < (1 << 8)) { lenBytes = Uint8List(2); lenBytes[index++] = 0x81; lenBytes[index++] = curLen; } else if (curLen < (1 << 16)) { lenBytes = Uint8List(3); lenBytes[index++] = 0x82; lenBytes[index++] = (curLen >> 8); lenBytes[index++] = curLen; } else if (curLen < (1 << 24)) { lenBytes = Uint8List(4); lenBytes[index++] = 0x83; lenBytes[index++] = (curLen >> 16); lenBytes[index++] = (curLen >> 8); lenBytes[index++] = curLen; } else { lenBytes = Uint8List(5); lenBytes[index++] = 0x84; lenBytes[index++] = (curLen >> 24); lenBytes[index++] = (curLen >> 16); lenBytes[index++] = (curLen >> 8); lenBytes[index++] = curLen; } return lenBytes; } void writeValue(int curLen) { newData.setRange(newDataPos, newDataPos + curLen, data, dataPos); dataPos += curLen; newDataPos += curLen; } Uint8List? convertBytes(Uint8List indefData) { data = indefData; dataPos = 0; dataSize = data.length; while (dataPos < dataSize) { if (dataPos + 2 > dataSize) { return null; } parseTag(); int len = parseLength(); if (len < 0) { return null; } dataPos += len; if (dataPos < 0) { throw Exception("Data overflow"); } if (unresolved == 0) { break; } } if (unresolved != 0) { return null; } int unused = dataSize - dataPos; dataSize = dataPos; newData = Uint8List(dataSize + numOfTotalLenBytes + unused); dataPos = 0; newDataPos = 0; index = 0; while (dataPos < dataSize) { writeTag(); writeLengthAndValue(); } newData.setRange(dataSize + numOfTotalLenBytes, newData.length, data, dataSize); return newData; } } ================================================ FILE: lib/network/util/cert/extension.dart ================================================ import 'package:pointycastle/pointycastle.dart'; /// an object for the elements in the X.509 V3 extension block. class Extension { /// Key Usage static final ASN1ObjectIdentifier keyUsage = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.15"); /// Subject Alternative Name static final ASN1ObjectIdentifier subjectAlternativeName = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.17"); /// Basic Constraints static final ASN1ObjectIdentifier basicConstraints = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.19"); /// Extended Key Usage static final ASN1ObjectIdentifier extendedKeyUsage = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.37"); final ASN1ObjectIdentifier extnId; final bool critical; final ASN1OctetString value; Extension(this.extnId, this.critical, this.value); } enum ExtendedKeyUsage { SERVER_AUTH, CLIENT_AUTH, CODE_SIGNING, EMAIL_PROTECTION, TIME_STAMPING, OCSP_SIGNING, BIMI } ================================================ FILE: lib/network/util/cert/key_usage.dart ================================================ import 'dart:typed_data'; import 'package:pointycastle/pointycastle.dart'; enum KeyUsage { /// 0 DIGITAL_SIGNATURE, /// 1 (Also called contentCommitment now) NON_REPUDIATION, /// 2 KEY_ENCIPHERMENT, /// 3 DATA_ENCIPHERMENT, /// 4 KEY_AGREEMENT, /// 5 KEY_CERT_SIGN, /// 6 CRL_SIGN, /// 7 ENCIPHER_ONLY, /// 8 DECIPHER_ONLY } class ExtensionKeyUsage { static const int digitalSignature = (1 << 7); static const int nonRepudiation = (1 << 6); static const int keyEncipherment = (1 << 5); static const int dataEncipherment = (1 << 4); static const int keyAgreement = (1 << 3); static const int keyCertSign = (1 << 2); static const int cRLSign = (1 << 1); static const int encipherOnly = (1 << 0); static const int decipherOnly = (1 << 15); final ASN1BitString bitString; final bool critical; ExtensionKeyUsage(int usage, {this.critical = true}) : bitString = ASN1BitString.fromBytes(keyUsageBytes(usage)); static Uint8List keyUsageBytes(int valueBytes) { var bytes = [valueBytes]; if (valueBytes > 0xFF) { final int firstValueByte = (valueBytes & int.parse("ff00", radix: 16)) >> 8; final int secondValueByte = (valueBytes & int.parse("00ff", radix: 16)); bytes = [firstValueByte, secondValueByte]; } return Uint8List.fromList([ // BitString identifier 3, // Length bytes.length + 1, // Unused bytes at the end 1, ...bytes ]); } } ================================================ FILE: lib/network/util/cert/pkcs12.dart ================================================ import 'dart:typed_data'; import 'package:pointycastle/asn1.dart'; import 'package:pointycastle/export.dart'; import '../crypto.dart'; import '../lang.dart'; import 'der.dart'; import 'x509.dart'; class Pkcs12 { /// /// Generates a PKCS12 file according to RFC 7292. /// /// * privateKey = A private key in PEM format. /// * certificates = A list of certificates in PEM format. /// * password = The password used for encryption. /// * keyPbe = The encryption algorithm used to encrypt the private key. /// * certPbe = The encryption algorithm used to encrypt the certificates. /// * digetAlgorithm = The digest algorithm used for the mac key derivation /// * macIter = The iteration count for the key derivation /// * salt = The salt used for the key derivation, if left out, it will be generated /// * certSalt = The salt used for the key derivation for cert encryption, if left out salt will be used. /// * keySalt = The salt used for the key derivation for key encryption, if left out salt will be used. /// * friendlyName = The name to be used to place as an attribue. /// * localKeyId = The id to be used to place as an attribue. If left, it will be generated. /// /// Possible values for keyPbe and certPbe: /// * PBE-SHA1-RC4-128 /// * PBE-SHA1-RC4-40 /// * PBE-SHA1-3DES ( default for keyPbe ) /// * PBE-SHA1-2DES /// * PBE-SHA1-RC2-128 /// * PBE-SHA1-RC2-40 ( default for certPbe) /// /// Possible values for digestAlgorithm: /// * SHA-1 ( DEFAULT) /// * SHA-224 /// * SHA-256 /// * SHA-384 /// * SHA-512 /// /// **IMPORTANT:** This method generates a PKCS12 file that only supports PASSWORD PRIVACY and PASSWORD INTEGRITY mode. This /// means that the private key and certificates are encrypted with the given password and the HMAC is generated using the given password. /// /// If keyPbe or certPbe are set to NONE or the password is left out, there will be no encryption. /// If the password is left out, no HMAC is generated /// /// static Uint8List generatePkcs12( String privateKey, List certificates, { String? password, String keyPbe = 'PBE-SHA1-3DES', String certPbe = 'PBE-SHA1-RC2-40', String digestAlgorithm = 'SHA-1', int macIter = 2048, Uint8List? salt, Uint8List? certSalt, Uint8List? keySalt, String? friendlyName, Uint8List? localKeyId, }) { Uint8List? pwFormatted; if (password != null) { pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits)); } // GENERATE SALT salt ??= _generateSalt(); certSalt ??= salt; keySalt ??= salt; // GENERATE LOCAL KEY ID localKeyId ??= _generateLocalKeyId(); // CREATE SAFEBAGS WITH PEMS WRAPPED IN CERTBAG var safeBags = _generateSafeBagsForCerts(certificates, localKeyId, friendlyName: friendlyName); var safeContentsCert = ASN1SafeContents(safeBags); // CREATE CONTENT INFO ASN1ContentInfo contentInfoCert; ASN1ContentInfo contentInfoKey; if (certPbe != 'NONE' && pwFormatted != null) { var params = ASN1Sequence( elements: [ ASN1OctetString(octets: certSalt), ASN1Integer( BigInt.from(macIter), ), ], ); var contentEncryptionAlgorithm = ASN1AlgorithmIdentifier( _oiFromAlgorithm(certPbe), parameters: params, ); Uint8List encryptedContent = _encrypt( safeContentsCert.encode(), certPbe, pwFormatted, certSalt, macIter, 'SHA-1', ); var encryptedContentInfo = ASN1EncryptedContentInfo.forData(contentEncryptionAlgorithm, encryptedContent); var encryptedData = ASN1EncryptedData(encryptedContentInfo); contentInfoCert = ASN1ContentInfo.forEncryptedData(encryptedData); } else { contentInfoCert = ASN1ContentInfo.forData( ASN1OctetString( octets: safeContentsCert.encode(), ), ); } if (keyPbe != 'NONE' && pwFormatted != null) { var params = ASN1Sequence(elements: [ ASN1OctetString(octets: keySalt), ASN1Integer(BigInt.from(macIter)), ]); var contentEncryptionAlgorithm = ASN1AlgorithmIdentifier( _oiFromAlgorithm(keyPbe), parameters: params, ); var privateKeyInfo = _getPrivateKeyInfoFromPem(privateKey); Uint8List encryptedContent = _encrypt( privateKeyInfo.encode(), keyPbe, pwFormatted, keySalt, macIter, 'SHA-1', ); // CREATE SAFEBAG FOR PRIVATEKEY WRAPPED IN KEYBAG var safeBagsKey = _generateSafeBagsForShroudedKey( ASN1Sequence(elements: [contentEncryptionAlgorithm, ASN1OctetString(octets: encryptedContent)]), localKeyId, friendlyName: friendlyName, ); var safeContentsKey = ASN1SafeContents(safeBagsKey); contentInfoKey = ASN1ContentInfo.forData( ASN1OctetString( octets: safeContentsKey.encode(), ), ); } else { // CREATE SAFEBAG FOR PRIVATEKEY WRAPPED IN KEYBAG var safeBagsKey = _generateSafeBagsForKey( privateKey, localKeyId, friendlyName: friendlyName, ); var safeContentsKey = ASN1SafeContents(safeBagsKey); contentInfoKey = ASN1ContentInfo.forData( ASN1OctetString( octets: safeContentsKey.encode(), ), ); } // CREATE AUTHENTICATED SAFE WITH CONTENTINFO ( CERT AND KEY ) var authSafe = ASN1AuthenticatedSafe([contentInfoCert, contentInfoKey]); // WRAP AUTHENTICATED SAFE WITHIN A CONTENTINFO var T = ASN1ContentInfo.forData( ASN1OctetString( octets: authSafe.encode(), ), ); // GENERATE HMAC IF PASSWORD IS GIVEN ASN1MacData? macData; if (password != null) { var bytesForHmac = authSafe.encode(); var pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits)); var generator = PKCS12ParametersGenerator(Digest(digestAlgorithm)); generator.init(pwFormatted, salt, macIter); var key = generator.generateDerivedMacParameters(20); var m = _generateHmac(bytesForHmac, key.key, digestAlgorithm); macData = ASN1MacData( ASN1DigestInfo( m, _algorithmIdentifierFromDigest( digestAlgorithm, ), ), salt, BigInt.from(2048), ); } var pfx = ASN1Pfx( ASN1Integer(BigInt.from(3)), T, macData: macData, ); var bytes = pfx.encode(); return bytes; } static Uint8List _generateLocalKeyId() { return CryptoUtils.getSecureRandom().nextBytes(20); } static Uint8List _generateSalt() { return CryptoUtils.getSecureRandom().nextBytes(8); } static Uint8List _generateHmac(Uint8List bytesForHmac, Uint8List key, String digestAlgorithm) { final hmac = Mac('$digestAlgorithm/HMAC')..init(KeyParameter(key)); var m = hmac.process(bytesForHmac); return m; } /// /// Formats the given [password] according to RFC 7292 Appendix B.1 /// static Uint8List formatPkcs12Password(Uint8List password) { if (password.isNotEmpty) { // +1 for extra 2 pad bytes. var bytes = Uint8List((password.length + 1) * 2); for (var i = 0; i != password.length; i++) { bytes[i * 2] = (password[i] >>> 8); bytes[i * 2 + 1] = password[i]; } return bytes; } else { return Uint8List(0); } } static _generateSafeBagsForCerts(List certificates, Uint8List localKeyId, {String? friendlyName}) { var certBags = []; var safeBags = []; for (var pem in certificates) { certBags.add(ASN1CertBag.fromX509Pem(pem)); } for (var certBag in certBags) { var asn1Set = ASN1Set(elements: []); asn1Set.add(ASN1Pkcs12Attribute.localKeyID(localKeyId)); if (friendlyName != null) { asn1Set.add(ASN1Pkcs12Attribute.friendlyName(friendlyName)); } safeBags.add( ASN1SafeBag.forCertBag( certBag, bagAttributes: asn1Set, ), ); } return safeBags; } static List _generateSafeBagsForKey(String privateKey, Uint8List localKeyId, {String? friendlyName}) { late ASN1PrivateKeyInfo privateKeyInfo = _getPrivateKeyInfoFromPem(privateKey); var safeBagsKey = []; var asn1Set = ASN1Set(elements: []); asn1Set.add(ASN1Pkcs12Attribute.localKeyID(localKeyId)); if (friendlyName != null) { asn1Set.add(ASN1Pkcs12Attribute.friendlyName(friendlyName)); } safeBagsKey.add( ASN1SafeBag.forKeyBag( ASN1KeyBag(privateKeyInfo), bagAttributes: asn1Set, ), ); return safeBagsKey; } static _generateSafeBagsForShroudedKey(ASN1Object bagValue, Uint8List localKeyId, {String? friendlyName}) { var safeBagsKey = []; var asn1Set = ASN1Set(elements: []); asn1Set.add(ASN1Pkcs12Attribute.localKeyID(localKeyId)); if (friendlyName != null) { asn1Set.add(ASN1Pkcs12Attribute.friendlyName(friendlyName)); } safeBagsKey.add( ASN1SafeBag.forPkcs8ShroudedKeyBag( bagValue, bagAttributes: asn1Set, ), ); return safeBagsKey; } static ASN1PrivateKeyInfo _getPrivateKeyInfoFromPem(String pem) { late ASN1PrivateKeyInfo privateKeyInfo; switch (CryptoUtils.getPrivateKeyType(pem)) { case "RSA": privateKeyInfo = ASN1PrivateKeyInfo.fromPkcs8RsaPem(pem); break; case "RSA_PKCS1": privateKeyInfo = ASN1PrivateKeyInfo.fromPkcs1RsaPem(pem); break; case "ECC": privateKeyInfo = ASN1PrivateKeyInfo.fromEccPem(pem); break; } return privateKeyInfo; } static Uint8List _encryptRc2(Uint8List bytesToEncrypt, ParametersWithIV generateDerivedParametersWithIV) { return _processRc2(bytesToEncrypt, generateDerivedParametersWithIV, true); } static Uint8List _decryptRc2(Uint8List bytesToDecrypt, ParametersWithIV generateDerivedParametersWithIV) { return _processRc2(bytesToDecrypt, generateDerivedParametersWithIV, false); } static Uint8List _processRc2(Uint8List bytes, ParametersWithIV generateDerivedParametersWithIV, bool encrypt) { var engine = CBCBlockCipher(RC2Engine()); engine.reset(); engine.init(encrypt, generateDerivedParametersWithIV); var padded = CryptoUtils.addPKCS7Padding(bytes, 8); final encryptedContent = Uint8List(padded.length); var offset = 0; while (offset < padded.length) { offset += engine.processBlock(padded, offset, encryptedContent, offset); } return encryptedContent; } static Uint8List _encrypt3des(Uint8List bytesToEncrypt, ParametersWithIV generateDerivedParametersWithIV) { return _process3des(bytesToEncrypt, generateDerivedParametersWithIV, true); } static Uint8List _decrypt3des(Uint8List bytesToDecrypt, ParametersWithIV generateDerivedParametersWithIV) { return _process3des(bytesToDecrypt, generateDerivedParametersWithIV, false); } static Uint8List _process3des(Uint8List bytes, ParametersWithIV generateDerivedParametersWithIV, bool encrypt) { var engine = CBCBlockCipher(DESedeEngine()); engine.reset(); engine.init(encrypt, generateDerivedParametersWithIV); Uint8List padded; if (encrypt) { padded = CryptoUtils.addPKCS7Padding(bytes, 8); } else { padded = bytes; } final content = Uint8List(padded.length); var offset = 0; while (offset < padded.length) { offset += engine.processBlock(padded, offset, content, offset); } if (encrypt) { return content; } else { return CryptoUtils.removePKCS7Padding(content); } } static Uint8List _encryptRc4(Uint8List bytesToEncrypt, KeyParameter generateDerivedParameters) { return _processRc4(bytesToEncrypt, generateDerivedParameters, true); } static Uint8List _decryptRc4(Uint8List bytesToDecrypt, KeyParameter generateDerivedParameters) { return _processRc4(bytesToDecrypt, generateDerivedParameters, false); } static Uint8List _processRc4(Uint8List bytesToEncrypt, KeyParameter generateDerivedParameters, bool encrypt) { var engine = RC4Engine(); engine.init(true, generateDerivedParameters); engine.reset(); //var padded = CryptoUtils.addPKCS7Padding(bytesToEncrypt, 8); final encryptedContent = engine.process(bytesToEncrypt); return encryptedContent; } static Uint8List _encrypt( Uint8List encode, String algorithm, Uint8List pwFormatted, Uint8List salt, int macIter, String digetAlgorithm) { var pkcs12ParameterGenerator = PKCS12ParametersGenerator(Digest(digetAlgorithm)); pkcs12ParameterGenerator.init(pwFormatted, salt, macIter); switch (algorithm) { case 'PBE-SHA1-RC2-40': return _encryptRc2( encode, pkcs12ParameterGenerator.generateDerivedParametersWithIV(5, RC2Engine.BLOCK_SIZE), ); case 'PBE-SHA1-RC2-128': return _encryptRc2( encode, pkcs12ParameterGenerator.generateDerivedParametersWithIV(16, RC2Engine.BLOCK_SIZE), ); case 'PBE-SHA1-RC4-40': return _encryptRc4( encode, pkcs12ParameterGenerator.generateDerivedParameters(5), ); case 'PBE-SHA1-RC4-128': return _encryptRc4( encode, pkcs12ParameterGenerator.generateDerivedParameters(16), ); case 'PBE-SHA1-2DES': return _encrypt3des( encode, pkcs12ParameterGenerator.generateDerivedParametersWithIV( 16, DESedeEngine.BLOCK_SIZE, ), ); case 'PBE-SHA1-3DES': return _encrypt3des( encode, pkcs12ParameterGenerator.generateDerivedParametersWithIV( 24, DESedeEngine.BLOCK_SIZE, ), ); default: throw ArgumentError('unsupported algorithm $algorithm'); } } static Uint8List _decrypt(Uint8List toDecrypt, String algorithm, Uint8List pwFormatted, Uint8List salt, int macIter, String digetAlgorithm) { var pkcs12ParameterGenerator = PKCS12ParametersGenerator(Digest(digetAlgorithm)); pkcs12ParameterGenerator.init(pwFormatted, salt, macIter); switch (algorithm) { case 'PBE-SHA1-RC2-40': return _decryptRc2( toDecrypt, pkcs12ParameterGenerator.generateDerivedParametersWithIV(5, RC2Engine.BLOCK_SIZE), ); case 'PBE-SHA1-RC2-128': return _decryptRc2( toDecrypt, pkcs12ParameterGenerator.generateDerivedParametersWithIV(16, RC2Engine.BLOCK_SIZE), ); case 'PBE-SHA1-RC4-40': return _decryptRc4( toDecrypt, pkcs12ParameterGenerator.generateDerivedParameters(5), ); case 'PBE-SHA1-RC4-128': return _decryptRc4( toDecrypt, pkcs12ParameterGenerator.generateDerivedParameters(16), ); case 'PBE-SHA1-2DES': return _decrypt3des( toDecrypt, pkcs12ParameterGenerator.generateDerivedParametersWithIV( 16, DESedeEngine.BLOCK_SIZE, ), ); case 'PBE-SHA1-3DES': return _decrypt3des( toDecrypt, pkcs12ParameterGenerator.generateDerivedParametersWithIV( 24, DESedeEngine.BLOCK_SIZE, ), ); default: throw ArgumentError('unsupported algorithm $algorithm'); } } static ASN1AlgorithmIdentifier _algorithmIdentifierFromDigest(String digestAlgorithm) { switch (digestAlgorithm) { case 'SHA-1': return ASN1AlgorithmIdentifier.fromIdentifier('1.3.14.3.2.26'); case 'SHA-224': return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.4'); case 'SHA-256': return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.1'); case 'SHA-384': return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.2'); case 'SHA-512': return ASN1AlgorithmIdentifier.fromIdentifier('2.16.840.1.101.3.4.2.3'); default: return ASN1AlgorithmIdentifier.fromIdentifier('1.3.14.3.2.26'); } } static ASN1ObjectIdentifier _oiFromAlgorithm(String keyPbe) { switch (keyPbe) { case 'PBE-SHA1-RC2-40': // 1.2.840.113549.1.12.1.6 return ASN1ObjectIdentifier.fromBytes( Uint8List.fromList( HexUtils.decode("06 0A 2A 86 48 86 F7 0D 01 0C 01 06"), ), ); case 'PBE-SHA1-RC2-128': // 1.2.840.113549.1.12.1.5 return ASN1ObjectIdentifier.fromBytes( Uint8List.fromList( HexUtils.decode("06 0A 2A 86 48 86 F7 0D 01 0C 01 05"), ), ); case 'PBE-SHA1-RC4-40': // 1.2.840.113549.1.12.1.2 return ASN1ObjectIdentifier.fromBytes( Uint8List.fromList( HexUtils.decode("06 0A 2A 86 48 86 F7 0D 01 0C 01 02"), ), ); case 'PBE-SHA1-RC4-128': // 1.2.840.113549.1.12.1.1 return ASN1ObjectIdentifier.fromBytes( Uint8List.fromList( HexUtils.decode("06 0A 2A 86 48 86 F7 0D 01 0C 01 01"), ), ); case 'PBE-SHA1-2DES': // 1.2.840.113549.1.12.1.4 return ASN1ObjectIdentifier.fromBytes( Uint8List.fromList( HexUtils.decode("06 0A 2A 86 48 86 F7 0D 01 0C 01 04"), ), ); case 'PBE-SHA1-3DES': // 1.2.840.113549.1.12.1.3 return ASN1ObjectIdentifier.fromBytes( Uint8List.fromList( HexUtils.decode("06 0A 2A 86 48 86 F7 0D 01 0C 01 03"), ), ); default: throw ArgumentError('unsupported algorithm'); } } ///解析pkcs12文件 static List parsePkcs12( Uint8List pkcs12, { String? password, }) { Uint8List? pwFormatted; if (password != null) { pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits)); } var pems = []; var parser = ASN1Parser(pkcs12); var wrapperSeq = parser.nextObject() as ASN1Sequence; var pfx = ASN1Pfx.fromSequence(wrapperSeq); if (pfx.version.integer != BigInt.from(3)) { throw Exception("PKCS12 keystore not in version 3 format"); } var authSafeContent = pfx.authSafe.content as ASN1OctetString; parser = ASN1Parser(authSafeContent.valueBytes); ASN1Object asn1Object = parser.nextObject(); // Check the type before casting if (asn1Object is ASN1Sequence) { wrapperSeq = asn1Object; } else if (asn1Object is ASN1OctetString) { var octetString = authSafeContent; BytesBuilder authSafeData = BytesBuilder(); var parser = ASN1Parser(octetString.valueBytes); while (parser.hasNext()) { ASN1Object parsedContent = parser.nextObject() as ASN1OctetString; authSafeData.add(parsedContent.valueBytes!); } var data = authSafeData.toBytes(); // Check if the data is indefinite if (DerIndefLenConverter.isIndefinite(data[1])) { data = DerIndefLenConverter().convertBytes(data)!; } parser = ASN1Parser(data); ASN1Object asn1Object = parser.nextObject(); if (asn1Object is ASN1Sequence) { wrapperSeq = asn1Object; } else { throw Exception("Invalid PKCS12 keystore"); } } for (var e in wrapperSeq.elements!) { if (e is ASN1Sequence) { if (e.elements == null || e.elements!.isEmpty) { throw Exception("Invalid PKCS12 keystore"); } var contentInfo = ASN1ContentInfo.fromSequence(e); switch (contentInfo.contentType.objectIdentifierAsString) { case '1.2.840.113549.1.7.6': // encryptedData var encryptedData = ASN1EncryptedData.fromSequence(contentInfo.content as ASN1Sequence); var encryptedContentInfo = encryptedData.encryptedContentInfo; var seq = (contentInfo.content as ASN1Sequence).elements!.elementAt(1) as ASN1Sequence; // var startIndex = seq.elements!.elementAt(0).encodedBytes!.lengthInBytes; // startIndex += (seq.elements!.elementAt(1).encodedBytes!.lengthInBytes); // var encrypted = DerValue.fromBytes(seq.valueBytes!.sublist(startIndex)); var encrypted = DerValue.fromBytes(seq.elements!.elementAt(2).encodedBytes!); int newTag = DerValue.tagOctetString; if (encrypted.isConstructed()) { newTag |= 0x20; } encrypted.tag = newTag; var rawData = encrypted.getOctetString(); // DECRYPT var contentEncryptionAlgorithm = encryptedContentInfo.contentEncryptionAlgorithm; var decryptedContent = _decryptData(rawData, contentEncryptionAlgorithm, pwFormatted!); var contentType = encryptedContentInfo.contentType; switch (contentType.objectIdentifierAsString) { case '1.2.840.113549.1.7.1': // CERTIFICATES loadSafeContents(DerInputStream.fromBytes(decryptedContent), pems, pwFormatted); break; } break; case '1.2.840.113549.1.7.1': // data (PKCS #7) if (contentInfo.content!.isConstructed == true && contentInfo.content is ASN1OctetString) { var content = contentInfo.content as ASN1OctetString; loadSafeContents(DerInputStream.fromBytes(content.octets!), pems, pwFormatted); } else { var safeContents = ASN1SafeContents.fromSequence(ASN1Sequence.fromBytes(contentInfo.content!.valueBytes!)); for (var element in safeContents.safeBags) { var bagValueSeq = element.bagValue as ASN1Sequence; _parseSafaBag(element.bagId, bagValueSeq, pems, pwFormatted); } } break; } } } return pems; } static void loadSafeContents(DerInputStream stream, List pems, Uint8List? pwFormatted) { List safeBags = stream.getSequence(2); int count = safeBags.length; for (int i = 0; i < count; i++) { var sbi = safeBags[i].toDerInputStream(); var bagId = sbi.getOID(); var bagValue = sbi.getDerValue(); bagValue = bagValue.data.getDerValue(); var data = bagValue.toByteArray(); var bagValueSeq = ASN1Sequence.fromBytes(data); _parseSafaBag(bagId, bagValueSeq, pems, pwFormatted); } } static void _parseSafaBag( ASN1ObjectIdentifier bagId, ASN1Sequence bagValueSeq, List pems, Uint8List? pwFormatted) { //private key if (bagId.objectIdentifierAsString == "1.2.840.113549.1.12.10.1.2") { var contentEncryptionAlgorithm = ASN1AlgorithmIdentifier.fromSequence(bagValueSeq.elements!.elementAt(0) as ASN1Sequence); // DECRYPT var decryptedContent = _decryptData(bagValueSeq.elements!.elementAt(1).valueBytes!, contentEncryptionAlgorithm, pwFormatted!); var s = ASN1Sequence.fromBytes(decryptedContent); //private key pems.insert( 0, X509Utils.encodeASN1ObjectToPem(s, CryptoUtils.BEGIN_PRIVATE_KEY, CryptoUtils.END_PRIVATE_KEY), ); return; } //certificate if (bagId.objectIdentifierAsString == "1.2.840.113549.1.12.10.1.3") { var octet = ASN1OctetString.fromBytes(bagValueSeq.elements!.elementAt(1).valueBytes!); var x509Seq = ASN1Sequence.fromBytes(octet.valueBytes!); var cer = X509Utils.encodeASN1ObjectToPem(x509Seq, X509Utils.BEGIN_CERT, X509Utils.END_CERT); pems.add(cer); return; } // pkcs-12-keyBag if (bagId.objectIdentifierAsString == "1.2.840.113549.1.12.10.1.1") { var seq = bagValueSeq.elements!.elementAt(1) as ASN1Sequence; var identifier = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; switch (identifier.objectIdentifierAsString!) { case "1.2.840.113549.1.1.1": // rsaEncryption pems.insert( 0, X509Utils.encodeASN1ObjectToPem(bagValueSeq, CryptoUtils.BEGIN_PRIVATE_KEY, CryptoUtils.END_PRIVATE_KEY), ); break; } return; } } static Uint8List _decryptData( Uint8List data, ASN1AlgorithmIdentifier contentEncryptionAlgorithm, Uint8List pwFormatted) { // GET ALGORITHM var encryptionAlgorithm = _algorithmFromOi(contentEncryptionAlgorithm.algorithm.objectIdentifierAsString!); // GET SALT AND MACITER AND DIGEST ALGORITHM Uint8List salt = _getSaltFromAlgorithmParameters(contentEncryptionAlgorithm.parameters); int macIter = _getMacIterFromAlgorithmParameters(contentEncryptionAlgorithm.parameters); var digestAlgorithm = _getDigestAlgorithmFromEncryptionAlgorithm(encryptionAlgorithm); return _decrypt(data, encryptionAlgorithm, pwFormatted, salt, macIter, digestAlgorithm); } static String _algorithmFromOi(String keyPbe) { switch (keyPbe) { case '1.2.840.113549.1.12.1.6': return "PBE-SHA1-RC2-40"; case '1.2.840.113549.1.12.1.5': return "PBE-SHA1-RC2-128"; case '1.2.840.113549.1.12.1.2': return "PBE-SHA1-RC4-40"; case '1.2.840.113549.1.12.1.1': return "PBE-SHA1-RC4-128"; case '1.2.840.113549.1.12.1.4': return "PBE-SHA1-2DES"; case '1.2.840.113549.1.12.1.3': return "PBE-SHA1-3DES"; default: throw ArgumentError('unsupported algorithm'); } } static String _getDigestAlgorithmFromEncryptionAlgorithm(String keyPbe) { switch (keyPbe) { case 'PBE-SHA1-RC2-40': case 'PBE-SHA1-RC2-128': case "PBE-SHA1-RC4-40": case "PBE-SHA1-RC4-128": case "PBE-SHA1-2DES": case 'PBE-SHA1-3DES': return "SHA-1"; default: throw ArgumentError('unsupported algorithm'); } } static Uint8List _getSaltFromAlgorithmParameters(ASN1Object? parameters) { var seq = parameters as ASN1Sequence; if (seq.elements != null && seq.elements!.isNotEmpty) { var asn1Octet = seq.elements!.elementAt(0) as ASN1OctetString; return asn1Octet.valueBytes!; } return Uint8List.fromList([]); } static int _getMacIterFromAlgorithmParameters(ASN1Object? parameters) { var seq = parameters as ASN1Sequence; if (seq.elements != null && seq.elements!.isNotEmpty) { var asn1Int = seq.elements!.elementAt(1) as ASN1Integer; return asn1Int.integer!.toInt(); } return 1; } } ================================================ FILE: lib/network/util/cert/x509.dart ================================================ // ignore_for_file: constant_identifier_names, depend_on_referenced_packages import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:pointycastle/asn1/unsupported_object_identifier_exception.dart'; import 'package:pointycastle/pointycastle.dart'; import 'package:proxypin/network/util/cert/extension.dart'; import '../crypto.dart'; import '../lang.dart'; import 'basic_constraints.dart'; import 'cert_data.dart'; import 'key_usage.dart'; /// @author wanghongen /// 2023/7/26 class X509Utils { static const String BEGIN_CERT = '-----BEGIN CERTIFICATE-----'; static const String END_CERT = '-----END CERTIFICATE-----'; static const BEGIN_CRL = '-----BEGIN X509 CRL-----'; static const END_CRL = '-----END X509 CRL-----'; //所在国家 static const String COUNTRY_NAME = "2.5.4.6"; static const String SERIAL_NUMBER = "2.5.4.5"; static const String DN_QUALIFIER = "2.5.4.46"; ///android 系统证书名称 static String getSubjectHashName(Map subject) { // Add Issuer var issuerSeq = ASN1Sequence(); for (var k in subject.keys) { var s = X509Utils._identifier(k, subject[k]!); issuerSeq.add(s); } var derEncoded = issuerSeq.encode(); // Convert the hash to a long value var hashBytes = md5.convert(derEncoded).bytes; int hash = (hashBytes[0] & 0xff) | ((hashBytes[1] & 0xff) << 8) | ((hashBytes[2] & 0xff) << 16) | ((hashBytes[3] & 0xff) << 24); String hexString = hash.toRadixString(16).padLeft(8, '0'); return hexString; } /// /// Encode the given [asn1Object] to PEM format and adding the [begin] and [end]. /// static String encodeASN1ObjectToPem(ASN1Object asn1Object, String begin, String end, {String newLine = '\n'}) { var bytes = asn1Object.encode(); var chunks = Strings.chunk(base64.encode(bytes), 64); return '$begin$newLine${chunks.join(newLine)}$newLine$end'; } /// /// Converts the given DER encoded CRL to a PEM string with the corresponding /// headers. The given [bytes] can be taken directly from a .crl file. /// static String crlDerToPem(Uint8List bytes) { return formatKeyString(base64.encode(bytes), BEGIN_CRL, END_CRL); } /// /// Formats the given [key] by chunking the [key] and adding the [begin] and [end] to the [key]. /// /// The line length will be defined by the given [chunkSize]. The default value is 64. /// /// Each line will be delimited by the given [lineDelimiter]. The default value is '\n'.w /// static String formatKeyString(String key, String begin, String end, {int chunkSize = 64, String lineDelimiter = '\n'}) { var sb = StringBuffer(); var chunks = Strings.chunk(key, chunkSize); if (Strings.isNotEmpty(begin)) { sb.write(begin + lineDelimiter); } for (var s in chunks) { sb.write(s + lineDelimiter); } if (Strings.isNotEmpty(end)) { sb.write(end); return sb.toString(); } else { var tmp = sb.toString(); return tmp.substring(0, tmp.lastIndexOf(lineDelimiter)); } } /// /// Parses the given PEM to a [X509CertificateData] object. /// /// Throws an [ASN1Exception] if the pem could not be read by the [ASN1Parser]. /// static X509CertificateData x509CertificateFromPem(String pem) { var bytes = CryptoUtils.getBytesFromPEMString(pem); var asn1Parser = ASN1Parser(bytes); var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; var x509 = _x509FromAsn1Sequence(topLevelSeq); var sha1String = CryptoUtils.getHash(bytes, algorithmName: 'SHA-1'); var md5String = CryptoUtils.getHash(bytes, algorithmName: 'MD5'); var sha256String = CryptoUtils.getHash(bytes, algorithmName: 'SHA-256'); x509.plain = pem; x509.sha1Thumbprint = sha1String; x509.md5Thumbprint = md5String; x509.sha256Thumbprint = sha256String; return x509; } /// /// Generates a self signed certificate /// /// * [privateKey] = The private key used for signing /// * [csr] = The CSR containing the DN and public key /// * [days] = The validity in days /// * [sans] = Subject alternative names to place within the certificate /// * [extKeyUsage] = The extended key usage definition /// * [serialNumber] = The serialnumber. If not set the default will be 1. /// * [issuer] = The issuer. If null, the issuer will be the subject of the given csr. /// static String generateSelfSignedCertificate( X509CertificateData caRoot, RSAPublicKey publicKey, RSAPrivateKey privateKey, int days, { List? sans, String serialNumber = '1', Map? issuer, Map? subject, ExtensionKeyUsage? keyUsage, List? extKeyUsage, BasicConstraints? basicConstraints, }) { var data = ASN1Sequence(); // Add version var version = ASN1Object(tag: 0xA0); version.valueBytes = ASN1Integer.fromtInt(2).encode(); data.add(version); // Add serial number data.add(ASN1Integer(BigInt.parse(serialNumber))); // Add protocol var blockProtocol = ASN1Sequence(); blockProtocol.add(ASN1ObjectIdentifier.fromIdentifierString(caRoot.signatureAlgorithm)); blockProtocol.add(ASN1Null()); data.add(blockProtocol); issuer ??= Map.from(caRoot.subject); // Add Issuer var issuerSeq = ASN1Sequence(); for (var k in issuer.keys) { var s = _identifier(k, issuer[k]!); issuerSeq.add(s); } data.add(issuerSeq); // Add Validity var validitySeq = ASN1Sequence(); validitySeq.add(ASN1UtcTime(DateTime.now().subtract(const Duration(days: 3)).toUtc())); validitySeq.add(ASN1UtcTime(DateTime.now().add(Duration(days: days)).toUtc())); data.add(validitySeq); // Add Subject var subjectSeq = ASN1Sequence(); subject ??= Map.from(caRoot.subject); for (var k in subject.keys) { var s = _identifier(k, subject[k]!); subjectSeq.add(s); } data.add(subjectSeq); // Add Public Key data.add(_makePublicKeyBlock(publicKey)); // Add Extensions if (Lists.isNotEmpty(sans) || keyUsage != null || Lists.isNotEmpty(extKeyUsage)) { var extensionTopSequence = ASN1Sequence(); // Add basic constraints 2.5.29.19 if (basicConstraints != null) { var basicConstraintsValue = ASN1Sequence(); basicConstraintsValue.add(ASN1Boolean(basicConstraints.isCA)); if (basicConstraints.pathLenConstraint != null) { basicConstraintsValue.add(ASN1Integer(BigInt.from(basicConstraints.pathLenConstraint!))); } var octetString = ASN1OctetString(octets: basicConstraintsValue.encode()); var basicConstraintsSequence = ASN1Sequence(); basicConstraintsSequence.add(Extension.basicConstraints); if (basicConstraints.critical) { basicConstraintsSequence.add(ASN1Boolean(true)); } basicConstraintsSequence.add(octetString); extensionTopSequence.add(basicConstraintsSequence); } // Add key usage 2.5.29.15 if (keyUsage != null) { extensionTopSequence.add(keyUsageSequence(keyUsage)!); } //2.5.29.17 if (sans != null && sans.isNotEmpty) { var sanList = ASN1Sequence(); for (var s in sans) { sanList.add(ASN1PrintableString(stringValue: s, tag: 0x82)); } var octetString = ASN1OctetString(octets: sanList.encode()); var sanSequence = ASN1Sequence(); sanSequence.add(Extension.subjectAlternativeName); sanSequence.add(octetString); extensionTopSequence.add(sanSequence); } // Add ext key usage 2.5.29.37 var extKeyUsageSequence = extendedKeyUsageEncodings(extKeyUsage); if (extKeyUsageSequence != null) { extensionTopSequence.add(extKeyUsageSequence); } var extObj = ASN1Object(tag: 0xA3); extObj.valueBytes = extensionTopSequence.encode(); data.add(extObj); } var outer = ASN1Sequence(); outer.add(data); outer.add(blockProtocol); var encode = _rsaSign(data.encode(), privateKey, _getDigestFromOi(caRoot.signatureAlgorithm)); outer.add(ASN1BitString(stringValues: encode)); var chunks = Strings.chunk(base64Encode(outer.encode()), 64); return '$BEGIN_CERT\n${chunks.join('\r\n')}\n$END_CERT'; } static X509CertificateData _x509FromAsn1Sequence(ASN1Sequence topLevelSeq) { var tbsCertificateSeq = topLevelSeq.elements!.elementAt(0) as ASN1Sequence; var signatureAlgorithmSeq = topLevelSeq.elements!.elementAt(1) as ASN1Sequence; var signateureSeq = topLevelSeq.elements!.elementAt(2) as ASN1BitString; // tbsCertificate var element = 0; // Version var version = 1; if (tbsCertificateSeq.elements!.elementAt(0) is ASN1Integer) { // The version ASN1Object ist missing use version 1 version = 1; element = -1; } else { // Version 1 (int = 0), version 2 (int = 1) or version 3 (int = 2) var versionObject = tbsCertificateSeq.elements!.elementAt(element + 0); version = versionObject.valueBytes!.elementAt(2); version++; } // Serial Number var serialInteger = tbsCertificateSeq.elements!.elementAt(element + 1) as ASN1Integer; var serialNumber = serialInteger.integer; // Signature // var signatureSequence = tbsCertificateSeq.elements!.elementAt(element + 2) as ASN1Sequence; // var o = signatureSequence.elements!.elementAt(0) as ASN1ObjectIdentifier; // var signatureAlgorithm = o.objectIdentifierAsString!; // var signatureAlgorithmReadable = o.readableName!; // Issuer var issuerSequence = tbsCertificateSeq.elements!.elementAt(element + 3) as ASN1Sequence; var issuer = _getDnFromSeq(issuerSequence); // Validity var validitySequence = tbsCertificateSeq.elements!.elementAt(element + 4) as ASN1Sequence; var validity = _getValidityFromSeq(validitySequence); // Subject var subjectSequence = tbsCertificateSeq.elements!.elementAt(element + 5) as ASN1Sequence; var subject = _getDnFromSeq(subjectSequence); // Subject Public Key Info var pubKeySequence = tbsCertificateSeq.elements!.elementAt(element + 6) as ASN1Sequence; var subjectPublicKeyInfo = _getSubjectPublicKeyInfoFromSeq(pubKeySequence); X509CertificateDataExtensions? extensions; if (version > 1 && tbsCertificateSeq.elements!.length > element + 7) { var extensionObject = tbsCertificateSeq.elements!.elementAt(element + 7); var extParser = ASN1Parser(extensionObject.valueBytes); var extSequence = extParser.nextObject() as ASN1Sequence; extensions = _getExtensionsFromSeq(extSequence); } // signatureAlgorithm var pubKeyOid = signatureAlgorithmSeq.elements!.elementAt(0) as ASN1ObjectIdentifier; // signatureValue var sigAsString = _bytesAsString(signateureSeq.valueBytes!); return X509CertificateData( version: version, serialNumber: serialNumber!, signatureAlgorithm: pubKeyOid.objectIdentifierAsString!, signatureAlgorithmReadableName: pubKeyOid.readableName, signature: sigAsString, issuer: issuer, validity: validity, subject: subject, publicKeyData: X509CertificatePublicKeyData.fromSubjectPublicKeyInfo(subjectPublicKeyInfo), subjectAlternativNames: extensions?.subjectAlternativNames, extKeyUsage: extensions?.extKeyUsage, extensions: extensions, // tbsCertificate: tbsCertificate, tbsCertificateSeqAsString: base64.encode( tbsCertificateSeq.encode(), ), ); } static X509CertificateDataExtensions _getExtensionsFromSeq(ASN1Sequence extSequence) { List? sans; List? keyUsage; List? extKeyUsage; List basicConstraints; var extensions = X509CertificateDataExtensions(); for (var subseq in extSequence.elements!) { var seq = subseq as ASN1Sequence; var oi = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; if (oi.objectIdentifierAsString == '2.5.29.17') { if (seq.elements!.length == 3) { sans = _fetchSansFromExtension(seq.elements!.elementAt(2)); } else { sans = _fetchSansFromExtension(seq.elements!.elementAt(1)); } extensions.subjectAlternativNames = sans; } var keyUsageSequence = ASN1Sequence(); keyUsageSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.15')); if (oi.objectIdentifierAsString == '2.5.29.15') { if (seq.elements!.length == 3) { keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(2)); } else { keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(1)); } extensions.keyUsage = keyUsage; } if (oi.objectIdentifierAsString == '2.5.29.37') { if (seq.elements!.length == 3) { extKeyUsage = _fetchExtendedKeyUsageFromExtension(seq.elements!.elementAt(2)); } else { extKeyUsage = _fetchExtendedKeyUsageFromExtension(seq.elements!.elementAt(1)); } extensions.extKeyUsage = extKeyUsage; } if (oi.objectIdentifierAsString == '2.5.29.19') { if (seq.elements!.length == 3) { basicConstraints = _fetchBasicConstraintsFromExtension(seq.elements!.elementAt(2)); } else { basicConstraints = [null, null]; } extensions.cA = basicConstraints[0]; extensions.pathLenConstraint = basicConstraints[1]; } if (oi.objectIdentifierAsString == '1.3.6.1.5.5.7.1.12') { var vmcData = _fetchVmcLogo(seq.elements!.elementAt(1)); extensions.vmc = vmcData; } if (oi.objectIdentifierAsString == '2.5.29.31') { var cRLDistributionPoints = _fetchCrlDistributionPoints(seq.elements!.elementAt(1)); extensions.cRLDistributionPoints = cRLDistributionPoints; } } return extensions; } static ASN1Sequence? keyUsageSequence(ExtensionKeyUsage keyUsages) { var octetString = ASN1OctetString(octets: keyUsages.bitString.encode()); var keyUsageSequence = ASN1Sequence(); keyUsageSequence.add(Extension.keyUsage); if (keyUsages.critical) { keyUsageSequence.add(ASN1Boolean(true)); } keyUsageSequence.add(octetString); return keyUsageSequence; } static ASN1Sequence? extendedKeyUsageEncodings(List? extKeyUsage) { if (extKeyUsage == null || extKeyUsage.isEmpty) { return null; } var extKeyUsageList = ASN1Sequence(); for (var s in extKeyUsage) { var oi = []; switch (s) { case ExtendedKeyUsage.SERVER_AUTH: oi = [1, 3, 6, 1, 5, 5, 7, 3, 1]; break; case ExtendedKeyUsage.CLIENT_AUTH: oi = [1, 3, 6, 1, 5, 5, 7, 3, 2]; break; case ExtendedKeyUsage.CODE_SIGNING: oi = [1, 3, 6, 1, 5, 5, 7, 3, 3]; break; case ExtendedKeyUsage.EMAIL_PROTECTION: oi = [1, 3, 6, 1, 5, 5, 7, 3, 4]; break; case ExtendedKeyUsage.TIME_STAMPING: oi = [1, 3, 6, 1, 5, 5, 7, 3, 8]; break; case ExtendedKeyUsage.OCSP_SIGNING: oi = [1, 3, 6, 1, 5, 5, 7, 3, 9]; break; case ExtendedKeyUsage.BIMI: oi = [1, 3, 6, 1, 5, 5, 7, 3, 31]; break; } extKeyUsageList.add(ASN1ObjectIdentifier(oi)); } var octetString = ASN1OctetString(octets: extKeyUsageList.encode()); var extKeyUsageSequence = ASN1Sequence(); extKeyUsageSequence.add(Extension.extendedKeyUsage); extKeyUsageSequence.add(octetString); return extKeyUsageSequence; } static SubjectPublicKeyInfo _getSubjectPublicKeyInfoFromSeq(ASN1Sequence pubKeySequence) { var algSeq = pubKeySequence.elements!.elementAt(0) as ASN1Sequence; var algOi = algSeq.elements!.elementAt(0) as ASN1ObjectIdentifier; var asn1AlgParameters = algSeq.elements!.elementAt(1); var algParameters = ''; var algParametersReadable = ''; if (asn1AlgParameters is ASN1ObjectIdentifier) { algParameters = asn1AlgParameters.objectIdentifierAsString!; algParametersReadable = asn1AlgParameters.readableName!; } var pubBitString = pubKeySequence.elements!.elementAt(1) as ASN1BitString; var asn1PubKeyParser = ASN1Parser(pubBitString.stringValues as Uint8List?); ASN1Object? next; try { next = asn1PubKeyParser.nextObject(); } catch (e) { // continue } int pubKeyLength; int? exponent; var pubKeyAsBytes = pubKeySequence.encodedBytes; if (next != null && next is ASN1Sequence) { var s = next; var key = s.elements!.elementAt(0) as ASN1Integer; if (s.elements!.length == 2 && s.elements!.elementAt(1) is ASN1Integer) { var asn1Exponent = s.elements!.elementAt(1) as ASN1Integer; exponent = asn1Exponent.integer!.toInt(); } pubKeyLength = key.integer!.bitLength; //pubKeyAsBytes = s.encodedBytes; } else { //pubKeyAsBytes = pubBitString.valueBytes; var length = pubBitString.valueBytes!.elementAt(0) == 0 ? (pubBitString.valueByteLength! - 1) : pubBitString.valueByteLength; pubKeyLength = length! * 8; } var pubKeyThumbprint = CryptoUtils.getHash(pubKeySequence.encodedBytes!, algorithmName: 'SHA-1'); var pubKeySha256Thumbprint = CryptoUtils.getHash(pubKeySequence.encodedBytes!, algorithmName: 'SHA-256'); return SubjectPublicKeyInfo( algorithm: algOi.objectIdentifierAsString, algorithmReadableName: algOi.readableName, parameter: algParameters != '' ? algParameters : null, parameterReadableName: algParametersReadable != '' ? algParametersReadable : null, length: pubKeyLength, bytes: _bytesAsString(pubKeyAsBytes!), sha1Thumbprint: pubKeyThumbprint, sha256Thumbprint: pubKeySha256Thumbprint, exponent: exponent, ); } /// /// Converts the bytes to a hex string /// static String _bytesAsString(Uint8List bytes) { var b = StringBuffer(); for (var v in bytes) { var s = v.toRadixString(16); if (s.length == 1) { b.write('0$s'); } else { b.write(s); } } return b.toString().toUpperCase(); } /// /// Fetches the base64 encoded VMC logo from the given [extData] /// static VmcData _fetchVmcLogo(ASN1Object extData) { var octet = extData as ASN1OctetString; var vmcParser = ASN1Parser(octet.valueBytes); var topSeq = vmcParser.nextObject() as ASN1Sequence; var obj1 = topSeq.elements!.elementAt(0); var obj1Parser = ASN1Parser(obj1.valueBytes); var obj2 = obj1Parser.nextObject(); var obj2Parser = ASN1Parser(obj2.valueBytes); var obj2Seq = obj2Parser.nextObject() as ASN1Sequence; var nextSeq = obj2Seq.elements!.elementAt(0) as ASN1Sequence; var finalSeq = nextSeq.elements!.elementAt(0) as ASN1Sequence; var data = VmcData(); // Parse fileType var ia5 = finalSeq.elements!.elementAt(0) as ASN1IA5String; var fileType = ia5.stringValue!; // Parse hash var hashSeq = finalSeq.elements!.elementAt(1) as ASN1Sequence; var hasFinalSeq = hashSeq.elements!.elementAt(0) as ASN1Sequence; var algSeq = hasFinalSeq.elements!.elementAt(0) as ASN1Sequence; var oi = algSeq.elements!.elementAt(0) as ASN1ObjectIdentifier; data.hashAlgorithm = oi.objectIdentifierAsString; data.hashAlgorithmReadable = oi.readableName; var octetString = hasFinalSeq.elements!.elementAt(1) as ASN1OctetString; var hash = _bytesAsString(octetString.octets!); data.hash = hash; // Parse base64 logo var logoSeq = finalSeq.elements!.elementAt(2) as ASN1Sequence; var ia5Logo = logoSeq.elements!.elementAt(0) as ASN1IA5String; var base64LogoGzip = ia5Logo.stringValue; var gzip = base64LogoGzip!.substring(base64LogoGzip.indexOf(',') + 1); final decodedData = GZipCodec().decode(base64.decode(gzip)); var base64Logo = base64.encode(decodedData); data.base64Logo = base64Logo; data.type = fileType; return data; } /// /// Parses the given object identifier values to the internal enum /// static List _fetchKeyUsageFromExtension(ASN1Object extData) { var keyUsage = []; var octet = extData as ASN1OctetString; var keyUsageParser = ASN1Parser(octet.valueBytes); var keyUsageBitString = keyUsageParser.nextObject() as ASN1BitString; if (keyUsageBitString.valueBytes?.isEmpty ?? true) { return keyUsage; } final Uint8List bytes = keyUsageBitString.valueBytes!; final int lastBitsToSkip = bytes.first; final int amountOfBytes = bytes.length - 1; //don't count the first byte for (int bitCounter = 0; bitCounter < amountOfBytes * 8 - lastBitsToSkip; ++bitCounter) { final int byteIndex = bitCounter ~/ 8; // the current byte final int bitIndex = bitCounter % 8; // the current bit if (byteIndex >= amountOfBytes) { return keyUsage; } final int byte = bytes[1 + byteIndex]; //skip the first byte final bool keyBit = _getBitOfByte(byte, bitIndex); if (keyBit == true && KeyUsage.values.length > bitCounter) { keyUsage.add(KeyUsage.values[bitCounter]); } } return keyUsage; } /// From left to right. Returns [true] for 1 and [false] for [0]. static bool _getBitOfByte(int byte, int bitIndex) { final int shift = 7 - bitIndex; final int shiftedByte = byte >> shift; if (shiftedByte & 1 == 1) { return true; } else { return false; } } /// /// Parses the given object identifier values to the internal enum /// static List _fetchExtendedKeyUsageFromExtension(ASN1Object extData) { var extKeyUsage = []; var octet = extData as ASN1OctetString; var keyUsageParser = ASN1Parser(octet.valueBytes); var keyUsageSeq = keyUsageParser.nextObject() as ASN1Sequence; for (var oi in keyUsageSeq.elements!) { if (oi is ASN1ObjectIdentifier) { var s = oi.objectIdentifierAsString; switch (s) { case '1.3.6.1.5.5.7.3.1': extKeyUsage.add(ExtendedKeyUsage.SERVER_AUTH); break; case '1.3.6.1.5.5.7.3.2': extKeyUsage.add(ExtendedKeyUsage.CLIENT_AUTH); break; case '1.3.6.1.5.5.7.3.3': extKeyUsage.add(ExtendedKeyUsage.CODE_SIGNING); break; case '1.3.6.1.5.5.7.3.4': extKeyUsage.add(ExtendedKeyUsage.EMAIL_PROTECTION); break; case '1.3.6.1.5.5.7.3.8': extKeyUsage.add(ExtendedKeyUsage.TIME_STAMPING); break; case '1.3.6.1.5.5.7.3.9': extKeyUsage.add(ExtendedKeyUsage.OCSP_SIGNING); break; case '1.3.6.1.5.5.7.3.31': extKeyUsage.add(ExtendedKeyUsage.BIMI); break; default: } } } return extKeyUsage; } /// /// Parses the given ASN1Object to the two basic constraint /// fields cA and pathLenConstraint. Returns a list of types [bool, int] if /// cA is true and a valid pathLenConstraint is specified, else the /// corresponding element will be null. /// static List _fetchBasicConstraintsFromExtension(ASN1Object extData) { var basicConstraints = [null, null]; var octet = extData as ASN1OctetString; var constraintParser = ASN1Parser(octet.valueBytes); var constraintSeq = constraintParser.nextObject() as ASN1Sequence; for (var obj in constraintSeq.elements!) { if (obj is ASN1Boolean) { basicConstraints[0] = obj.boolValue; } if (obj is ASN1Integer) { basicConstraints[1] = obj.integer!.toInt(); } } return basicConstraints; } /// /// Fetches a list of subject alternative names from the given [extData] /// static List _fetchSansFromExtension(ASN1Object extData) { var sans = []; var octet = extData as ASN1OctetString; var sanParser = ASN1Parser(octet.valueBytes); var sanSeq = sanParser.nextObject() as ASN1Sequence; for (var san in sanSeq.elements!) { if (san.tag == 135) { var sb = StringBuffer(); if (san.valueByteLength == 16) { //IPv6 for (var i = 0; i < (san.valueByteLength ?? 0); i++) { if (sb.isNotEmpty && i % 2 == 0) { sb.write(':'); } sb.write(san.valueBytes![i].toRadixString(16).padLeft(2, '0')); } } else { //IPv4 and others for (var b in san.valueBytes!) { if (sb.isNotEmpty) { sb.write('.'); } sb.write(b); } } sans.add(sb.toString()); } else if (san.tag == 164) { // WE HAVE CONSTRUCTED SAN var constructedParser = ASN1Parser(san.valueBytes); var seq = constructedParser.nextObject() as ASN1Sequence; var sanValue = 'DirName:'; for (var san in seq.elements!) { var set = san as ASN1Set; var seq = set.elements!.elementAt(0) as ASN1Sequence; var oid = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; var object = seq.elements!.elementAt(1); var value = ''; sanValue = '$sanValue/'; if (object is ASN1UTF8String) { var objectAsUtf8 = object; value = objectAsUtf8.utf8StringValue!; } else if (object is ASN1PrintableString) { var objectPrintable = object; value = objectPrintable.stringValue!; } sanValue = '$sanValue${oid.readableName}=$value'; } sans.add(sanValue); } else { var s = String.fromCharCodes(san.valueBytes!); sans.add(s); } } return sans; } static List _fetchCrlDistributionPoints(ASN1Object extData) { var cRLDistributionPoints = []; var octet = extData as ASN1OctetString; var parser = ASN1Parser(octet.valueBytes); var topSeq = parser.nextObject() as ASN1Sequence; for (var e in topSeq.elements!) { var seq = e as ASN1Sequence; var o1 = seq.elements!.elementAt(0); var parser = ASN1Parser(o1.valueBytes); var o2 = parser.nextObject(); parser = ASN1Parser(o2.valueBytes); var o3 = parser.nextObject(); var point = String.fromCharCodes(o3.valueBytes!.toList()); cRLDistributionPoints.add(point); } return cRLDistributionPoints; } static X509CertificateValidity _getValidityFromSeq(ASN1Sequence validitySequence) { DateTime? asn1FromDateTime; DateTime? asn1ToDateTime; if (validitySequence.elements!.elementAt(0) is ASN1UtcTime) { var asn1From = validitySequence.elements!.elementAt(0) as ASN1UtcTime; asn1FromDateTime = asn1From.time; } else { var asn1From = validitySequence.elements!.elementAt(0) as ASN1GeneralizedTime; asn1FromDateTime = asn1From.dateTimeValue; } if (validitySequence.elements!.elementAt(1) is ASN1UtcTime) { var asn1To = validitySequence.elements!.elementAt(1) as ASN1UtcTime; asn1ToDateTime = asn1To.time; } else { var asn1To = validitySequence.elements!.elementAt(1) as ASN1GeneralizedTime; asn1ToDateTime = asn1To.dateTimeValue; } return X509CertificateValidity( notBefore: asn1FromDateTime!, notAfter: asn1ToDateTime!, ); } static Map _getDnFromSeq(ASN1Sequence issuerSequence) { var dnData = {}; for (var s in issuerSequence.elements as dynamic) { for (var ele in s.elements!) { var seq = ele as ASN1Sequence; var o = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; var object = seq.elements!.elementAt(1); String? value = ''; if (object is ASN1UTF8String) { var objectAsUtf8 = object; value = objectAsUtf8.utf8StringValue; } else if (object is ASN1PrintableString) { var objectPrintable = object; value = objectPrintable.stringValue; } else if (object is ASN1TeletextString) { var objectTeletext = object; value = objectTeletext.stringValue; } dnData.putIfAbsent(o.objectIdentifierAsString!, () => value ?? ''); } } return dnData; } static ASN1Set _identifier(String k, String value) { ASN1ObjectIdentifier oIdentifier; try { oIdentifier = ASN1ObjectIdentifier.fromName(k); } on UnsupportedObjectIdentifierException { oIdentifier = ASN1ObjectIdentifier.fromIdentifierString(k); } ASN1Object pString; var identifier = oIdentifier.objectIdentifierAsString; if (identifier == COUNTRY_NAME || SERIAL_NUMBER == identifier || identifier == DN_QUALIFIER) { pString = ASN1PrintableString(stringValue: value); } else { pString = ASN1UTF8String(utf8StringValue: value); } var innerSequence = ASN1Sequence(elements: [oIdentifier, pString]); return ASN1Set(elements: [innerSequence]); } static Uint8List _rsaSign(Uint8List inBytes, RSAPrivateKey privateKey, String signingAlgorithm) { var signer = Signer('$signingAlgorithm/RSA'); signer.init(true, PrivateKeyParameter(privateKey)); var signature = signer.generateSignature(inBytes) as RSASignature; return signature.bytes; } /// /// Create the public key ASN1Sequence for the csr. /// static ASN1Sequence _makePublicKeyBlock(RSAPublicKey publicKey) { var blockEncryptionType = ASN1Sequence(); blockEncryptionType.add(ASN1ObjectIdentifier.fromName('rsaEncryption')); blockEncryptionType.add(ASN1Null()); var publicKeySequence = ASN1Sequence(); publicKeySequence.add(ASN1Integer(publicKey.modulus)); publicKeySequence.add(ASN1Integer(publicKey.exponent)); var blockPublicKey = ASN1BitString(stringValues: publicKeySequence.encode()); var outer = ASN1Sequence(); outer.add(blockEncryptionType); outer.add(blockPublicKey); return outer; } static String _getDigestFromOi(String oi) { switch (oi) { case 'ecdsaWithSHA1': case 'sha1WithRSAEncryption': return 'SHA-1'; case 'ecdsaWithSHA224': case 'sha224WithRSAEncryption': return 'SHA-224'; case 'ecdsaWithSHA256': case 'sha256WithRSAEncryption': return 'SHA-256'; case 'ecdsaWithSHA384': case 'sha384WithRSAEncryption': return 'SHA-384'; case 'ecdsaWithSHA512': case 'sha512WithRSAEncryption': return 'SHA-512'; default: return 'SHA-256'; } } } ================================================ FILE: lib/network/util/compress.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:brotli/brotli.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:zstandard/zstandard.dart'; ///GZIP 解压缩 List gzipDecode(List byteBuffer) { GZipCodec gzipCodec = GZipCodec(); try { return gzipCodec.decode(byteBuffer); } catch (e) { logger.e("gzipDecode error: $e"); return byteBuffer; } } ///GZIP 压缩 List gzipEncode(List input) { return GZipCodec().encode(input); } ///br 解压缩 List brDecode(List byteBuffer) { try { return brotli.decode(byteBuffer); } catch (e) { logger.e("brDecode error: $e"); return byteBuffer; } } ///zstd 解压缩 Future?> zstdDecode(List byteBuffer) async { final zstandard = Zstandard(); try { return zstandard.decompress(Uint8List.fromList(byteBuffer)); } catch (e) { logger.e("zstdDecode error: $e"); return byteBuffer; } } ///zlib List zlibDecode(List byteBuffer) { try { final rawDeflateDecoder = ZLibDecoder(raw: true); return rawDeflateDecoder.convert(byteBuffer); } catch (e) { logger.e("zlibDecode error: $e"); return byteBuffer; } } ================================================ FILE: lib/network/util/crts.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:core'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:path_provider/path_provider.dart'; import 'package:pointycastle/export.dart'; import 'package:proxypin/network/util/cert/pkcs12.dart'; import 'package:proxypin/network/util/cert/x509.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/random.dart'; import 'package:proxypin/utils/lang.dart'; import 'cache.dart'; import 'cert/basic_constraints.dart'; import 'cert/cert_data.dart'; import 'cert/extension.dart'; import 'cert/key_usage.dart'; import 'crypto.dart'; import 'file_read.dart'; Future main() async { await CertificateManager.getCertificateContext('www.jianshu.com'); } enum StartState { uninitialized, initializing, initialized } class CertificateManager { /// 证书缓存 static final ExpiringCache _certificateMap = ExpiringCache(const Duration(minutes: 15)); /// 服务端密钥 static AsymmetricKeyPair _serverKeyPair = CryptoUtils.generateRSAKeyPair(); /// ca证书 static X509CertificateData? _caCert; /// ca私钥 static late RSAPrivateKey _caPriKey; /// 是否初始化 static StartState _state = StartState.uninitialized; static Completer _initializationCompleter = Completer(); static SecurityContext? get(String host) { return _certificateMap[host]; } static X509CertificateData? get caCert => _caCert; /// 清除缓存 static void cleanCache() { _certificateMap.clear(); } /// 获取域名自签名证书 static Future getCertificateContext(String host) async { SecurityContext? securityContext = _certificateMap[host]; if (securityContext != null) { return securityContext; } if (_state != StartState.initialized) { await initCAConfig(); } String cer = generate(_caCert!, _serverKeyPair.publicKey as RSAPublicKey, _caPriKey, host); var rsaPrivateKey = _serverKeyPair.privateKey as RSAPrivateKey; securityContext = SecurityContext(withTrustedRoots: true) ..useCertificateChainBytes(cer.codeUnits) ..allowLegacyUnsafeRenegotiation = true ..usePrivateKeyBytes(CryptoUtils.encodeRSAPrivateKeyToPemPkcs1(rsaPrivateKey).codeUnits); _certificateMap[host] = securityContext; return securityContext; } /// 生成域名证书 PEM(仅证书,不含私钥) static Future generateLeafCertificatePem(String host) async { if (_state != StartState.initialized) { await initCAConfig(); } return generate(_caCert!, _serverKeyPair.publicKey as RSAPublicKey, _caPriKey, host); } /// 生成证书 static String generate(X509CertificateData caRoot, RSAPublicKey serverPubKey, RSAPrivateKey caPriKey, String host) { //根据CA证书subject来动态生成目标服务器证书的issuer和subject Map x509Subject = { 'C': 'CN', 'ST': 'BJ', 'L': 'Beijing', 'O': 'Proxy', 'OU': 'ProxyPin', }; x509Subject['CN'] = host; var csrPem = X509Utils.generateSelfSignedCertificate(caRoot, serverPubKey, caPriKey, 365, sans: [host], serialNumber: Random().nextInt(1000000).toString(), subject: x509Subject); return csrPem; } /// 获取证书主题hash static Future systemCertificateName() async { if (_state != StartState.initialized) { await initCAConfig(); } var subject = caCert!.subject; return '${X509Utils.getSubjectHashName(subject)}.0'; } //重新生成根证书 static Future generateNewRootCA() async { if (_state != StartState.initialized) { await initCAConfig(); } var generateRSAKeyPair = CryptoUtils.generateRSAKeyPair(); var serverPubKey = generateRSAKeyPair.publicKey as RSAPublicKey; var serverPriKey = generateRSAKeyPair.privateKey as RSAPrivateKey; //根据CA证书subject来动态生成目标服务器证书的issuer和subject Map x509Subject = { 'C': 'CN', 'ST': 'BJ', 'L': 'Beijing', 'O': 'Proxy', 'OU': 'ProxyPin', }; x509Subject['CN'] = 'ProxyPin CA (${DateTime.now().dateFormat()},${RandomUtil.randomString(6).toUpperCase()})'; var csrPem = X509Utils.generateSelfSignedCertificate( _caCert!, serverPubKey, serverPriKey, 825, sans: [x509Subject['CN']!], serialNumber: DateTime.now().millisecondsSinceEpoch.toString(), issuer: x509Subject, subject: x509Subject, keyUsage: ExtensionKeyUsage(ExtensionKeyUsage.keyCertSign), extKeyUsage: [ExtendedKeyUsage.SERVER_AUTH], basicConstraints: BasicConstraints(isCA: true), ); //重新写入根证书 var caFile = await certificateFile(); await caFile.writeAsString(csrPem); //私钥 var serverPriKeyPem = CryptoUtils.encodeRSAPrivateKeyToPem(serverPriKey); var keyFile = await privateKeyFile(); await keyFile.writeAsString(serverPriKeyPem); cleanCache(); _state = StartState.uninitialized; } ///重置默认根证书 static Future resetDefaultRootCA() async { var caFile = await certificateFile(); await caFile.delete(); var keyFile = await privateKeyFile(); await keyFile.delete(); cleanCache(); _state = StartState.uninitialized; initCAConfig(); } static Future initCAConfig() async { if (_state == StartState.initialized || _state == StartState.initializing) { return _initializationCompleter.future; } var startTime = DateTime.now().millisecondsSinceEpoch; _state = StartState.initializing; _initializationCompleter = Completer(); try { _serverKeyPair = CryptoUtils.generateRSAKeyPair(); //从项目目录加入ca根证书 var caPemFile = await certificateFile(); _caCert = X509Utils.x509CertificateFromPem(await caPemFile.readAsString()); //根据CA证书subject来动态生成目标服务器证书的issuer和subject //从项目目录加入ca私钥 var keyFile = await privateKeyFile(); _caPriKey = CryptoUtils.rsaPrivateKeyFromPem(await keyFile.readAsString()); _state = StartState.initialized; _initializationCompleter.complete(); } catch (e) { logger.e('init ca config error:$e'); _state = StartState.uninitialized; _initializationCompleter.completeError(e); } logger.d('init ca config end cost:${DateTime.now().millisecondsSinceEpoch - startTime}'); return _initializationCompleter.future; } /// 证书文件 static Future certificateFile() async { final String appPath = await getApplicationSupportDirectory().then((value) => value.path); var caFile = File("$appPath${Platform.pathSeparator}ca.crt"); if (!(await caFile.exists())) { var body = await FileRead.read('assets/certs/ca.crt'); await caFile.writeAsBytes(body.buffer.asUint8List()); } return caFile; } ///证书pem格式内容 static Future certificatePem() async { var caFile = await certificateFile(); return caFile.readAsString(); } /// 私钥文件 static Future privateKeyFile() async { final String appPath = await getApplicationSupportDirectory().then((value) => value.path); var caFile = File("$appPath${Platform.pathSeparator}ca_key.pem"); if (!(await caFile.exists())) { var body = await FileRead.read('assets/certs/ca_key.pem'); await caFile.writeAsBytes(body.buffer.asUint8List()); } return caFile; } ///生成 p12文件 static Future generatePkcs12(String? password) async { var caFile = await CertificateManager.certificateFile(); var keyFile = await CertificateManager.privateKeyFile(); return Pkcs12.generatePkcs12(await keyFile.readAsString(), [await caFile.readAsString()], password: password); } ///import p12文件 static Future importPkcs12(Uint8List pkcs12, String? password) async { var decodePkcs12 = Pkcs12.parsePkcs12(pkcs12, password: password); var caFile = await CertificateManager.certificateFile(); var keyFile = await CertificateManager.privateKeyFile(); if (decodePkcs12.length != 2) { throw Exception('Invalid pkcs12 file'); } await keyFile.writeAsString(decodePkcs12[0]); await caFile.writeAsString(decodePkcs12[1]); cleanCache(); _state = StartState.uninitialized; initCAConfig(); } /// 获取证书详细信息 static Future getCertificateDetails() async { if (_state != StartState.initialized) { await initCAConfig(); } return caCert!; } } ================================================ FILE: lib/network/util/crypto.dart ================================================ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:pointycastle/api.dart'; import 'package:pointycastle/asn1/asn1_object.dart'; import 'package:pointycastle/asn1/asn1_parser.dart'; import 'package:pointycastle/asn1/primitives/asn1_bit_string.dart'; import 'package:pointycastle/asn1/primitives/asn1_integer.dart'; import 'package:pointycastle/asn1/primitives/asn1_object_identifier.dart'; import 'package:pointycastle/asn1/primitives/asn1_octet_string.dart'; import 'package:pointycastle/asn1/primitives/asn1_sequence.dart'; import 'package:pointycastle/asymmetric/api.dart'; import 'package:pointycastle/key_generators/api.dart'; import 'package:pointycastle/key_generators/rsa_key_generator.dart'; import 'package:pointycastle/paddings/pkcs7.dart'; import 'package:pointycastle/random/fortuna_random.dart'; import 'lang.dart'; class CryptoUtils { /// ignore: constant_identifier_names static const String BEGIN_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----'; static const String END_PUBLIC_KEY = '-----END PUBLIC KEY-----'; // ignore: constant_identifier_names static const BEGIN_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----'; static const END_PRIVATE_KEY = '-----END PRIVATE KEY-----'; static const BEGIN_RSA_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----'; static const END_RSA_PRIVATE_KEY = '-----END RSA PRIVATE KEY-----'; static const BEGIN_EC_PRIVATE_KEY = '-----BEGIN EC PRIVATE KEY-----'; static const String END_EC_PRIVATE_KEY = '-----END EC PRIVATE KEY-----'; /// /// Get a hash for the given [bytes] using the given [algorithm] /// /// The default [algorithm] used is **SHA-256**. All supported algorihms are : /// /// * SHA-1 /// * SHA-224 /// * SHA-256 /// * SHA-384 /// * SHA-512 /// * SHA-512/224 /// * SHA-512/256 /// * MD5 /// static String getHash(Uint8List bytes, {String algorithmName = 'SHA-256'}) { var hash = getHashPlain(bytes, algorithmName: algorithmName); const hexDigits = '0123456789abcdef'; var charCodes = Uint8List(hash.length * 2); for (var i = 0, j = 0; i < hash.length; i++) { var byte = hash[i]; charCodes[j++] = hexDigits.codeUnitAt((byte >> 4) & 0xF); charCodes[j++] = hexDigits.codeUnitAt(byte & 0xF); } return String.fromCharCodes(charCodes).toUpperCase(); } /// /// Get a hash for the given [bytes] using the given [algorithm] /// /// The default [algorithm] used is **SHA-256**. All supported algorihms are : /// /// * SHA-1 /// * SHA-224 /// * SHA-256 /// * SHA-384 /// * SHA-512 /// * SHA-512/224 /// * SHA-512/256 /// * MD5 /// static Uint8List getHashPlain(Uint8List bytes, {String algorithmName = 'SHA-256'}) { Uint8List hash; switch (algorithmName) { case 'SHA-1': hash = Digest('SHA-1').process(bytes); break; case 'SHA-224': hash = Digest('SHA-224').process(bytes); break; case 'SHA-256': hash = Digest('SHA-256').process(bytes); break; case 'SHA-384': hash = Digest('SHA-384').process(bytes); break; case 'SHA-512': hash = Digest('SHA-512').process(bytes); break; case 'SHA-512/224': hash = Digest('SHA-512/224').process(bytes); break; case 'SHA-512/256': hash = Digest('SHA-512/256').process(bytes); break; case 'MD5': hash = Digest('MD5').process(bytes); break; default: throw ArgumentError('Hash not supported'); } return hash; } /// /// Returns the private key type of the given [pem] /// static String getPrivateKeyType(String pem) { if (pem.startsWith(BEGIN_RSA_PRIVATE_KEY)) { return 'RSA_PKCS1'; } else if (pem.startsWith(BEGIN_PRIVATE_KEY)) { return 'RSA'; } else if (pem.startsWith(BEGIN_EC_PRIVATE_KEY)) { return 'ECC'; } return 'RSA'; } /// /// Generates a RSA [AsymmetricKeyPair] with the given [keySize]. /// The default value for the [keySize] is 2048 bits. /// /// The following keySize is supported: /// * 1024 /// * 2048 /// * 3072 /// * 4096 /// * 8192 /// static AsymmetricKeyPair generateRSAKeyPair({int keySize = 2048}) { var keyParams = RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 12); var secureRandom = getSecureRandom(); var rngParams = ParametersWithRandom(keyParams, secureRandom); var generator = RSAKeyGenerator(); generator.init(rngParams); return generator.generateKeyPair(); } /// /// Decode a [RSAPublicKey] from the given [pem] String. /// static RSAPublicKey rsaPublicKeyFromPem(String pem) { var bytes = CryptoUtils.getBytesFromPEMString(pem); return rsaPublicKeyFromDERBytes(bytes); } /// /// Decode the given [bytes] into an [RSAPublicKey]. /// static RSAPublicKey rsaPublicKeyFromDERBytes(Uint8List bytes) { var asn1Parser = ASN1Parser(bytes); var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; ASN1Sequence publicKeySeq; if (topLevelSeq.elements![1].runtimeType == ASN1BitString) { var publicKeyBitString = topLevelSeq.elements![1] as ASN1BitString; var publicKeyAsn = ASN1Parser(publicKeyBitString.stringValues as Uint8List?); publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence; } else { publicKeySeq = topLevelSeq; } var modulus = publicKeySeq.elements![0] as ASN1Integer; var exponent = publicKeySeq.elements![1] as ASN1Integer; var rsaPublicKey = RSAPublicKey(modulus.integer!, exponent.integer!); return rsaPublicKey; } /// /// Decode a [RSAPrivateKey] from the given [pem] String. /// static RSAPrivateKey rsaPrivateKeyFromPem(String pem) { var bytes = getBytesFromPEMString(pem); return rsaPrivateKeyFromDERBytes(bytes); } // /// Helper function for decoding the base64 in [pem]. /// /// Throws an ArgumentError if the given [pem] is not sourounded by begin marker -----BEGIN and /// endmarker -----END or the [pem] consists of less than two lines. /// /// The PEM header check can be skipped by setting the optional paramter [checkHeader] to false. /// static Uint8List getBytesFromPEMString(String pem, {bool checkHeader = true}) { var lines = LineSplitter.split(pem).map((line) => line.trim()).where((line) => line.isNotEmpty).toList(); String base64; if (checkHeader) { if (lines.length < 2 || !lines.first.startsWith('-----BEGIN') || !lines.last.startsWith('-----END')) { throw ArgumentError('The given string does not have the correct ' 'begin/end markers expected in a PEM file.'); } base64 = lines.sublist(1, lines.length - 1).join(''); } else { base64 = lines.join(''); } return Uint8List.fromList(base64Decode(base64)); } /// /// Decode the given [bytes] into an [RSAPrivateKey]. /// static RSAPrivateKey rsaPrivateKeyFromDERBytes(Uint8List bytes) { var asn1Parser = ASN1Parser(bytes); var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; //ASN1Object version = topLevelSeq.elements[0]; //ASN1Object algorithm = topLevelSeq.elements[1]; var privateKey = topLevelSeq.elements![2]; asn1Parser = ASN1Parser(privateKey.valueBytes); var pkSeq = asn1Parser.nextObject() as ASN1Sequence; var modulus = pkSeq.elements![1] as ASN1Integer; //ASN1Integer publicExponent = pkSeq.elements[2] as ASN1Integer; var privateExponent = pkSeq.elements![3] as ASN1Integer; var p = pkSeq.elements![4] as ASN1Integer; var q = pkSeq.elements![5] as ASN1Integer; //ASN1Integer exp1 = pkSeq.elements[6] as ASN1Integer; //ASN1Integer exp2 = pkSeq.elements[7] as ASN1Integer; //ASN1Integer co = pkSeq.elements[8] as ASN1Integer; var rsaPrivateKey = RSAPrivateKey(modulus.integer!, privateExponent.integer!, p.integer, q.integer); return rsaPrivateKey; } /// /// Enode the given [publicKey] to PEM format using the PKCS#8 standard. /// static String encodeRSAPublicKeyToPem(RSAPublicKey publicKey) { var algorithmSeq = ASN1Sequence(); var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0])); algorithmSeq.add(ASN1ObjectIdentifier.fromName('rsaEncryption')); algorithmSeq.add(paramsAsn1Obj); var publicKeySeq = ASN1Sequence(); publicKeySeq.add(ASN1Integer(publicKey.modulus)); publicKeySeq.add(ASN1Integer(publicKey.exponent)); var publicKeySeqBitString = ASN1BitString(stringValues: Uint8List.fromList(publicKeySeq.encode())); var topLevelSeq = ASN1Sequence(); topLevelSeq.add(algorithmSeq); topLevelSeq.add(publicKeySeqBitString); var dataBase64 = base64.encode(topLevelSeq.encode()); var chunks = Strings.chunk(dataBase64, 64); return '$BEGIN_PUBLIC_KEY\n${chunks.join('\n')}\n$END_PUBLIC_KEY'; } /// /// Enode the given [rsaPrivateKey] to PEM format using the PKCS#1 standard. /// /// The ASN1 structure is decripted at . /// /// ``` /// RSAPrivateKey ::= SEQUENCE { /// version Version, /// modulus INTEGER, -- n /// publicExponent INTEGER, -- e /// privateExponent INTEGER, -- d /// prime1 INTEGER, -- p /// prime2 INTEGER, -- q /// exponent1 INTEGER, -- d mod (p-1) /// exponent2 INTEGER, -- d mod (q-1) /// coefficient INTEGER, -- (inverse of q) mod p /// otherPrimeInfos OtherPrimeInfos OPTIONAL /// } /// ``` static String encodeRSAPrivateKeyToPemPkcs1(RSAPrivateKey rsaPrivateKey) { var version = ASN1Integer(BigInt.from(0)); var modulus = ASN1Integer(rsaPrivateKey.n); var publicExponent = ASN1Integer(BigInt.parse('65537')); var privateExponent = ASN1Integer(rsaPrivateKey.privateExponent); var p = ASN1Integer(rsaPrivateKey.p); var q = ASN1Integer(rsaPrivateKey.q); var dP = rsaPrivateKey.privateExponent! % (rsaPrivateKey.p! - BigInt.from(1)); var exp1 = ASN1Integer(dP); var dQ = rsaPrivateKey.privateExponent! % (rsaPrivateKey.q! - BigInt.from(1)); var exp2 = ASN1Integer(dQ); var iQ = rsaPrivateKey.q!.modInverse(rsaPrivateKey.p!); var co = ASN1Integer(iQ); var topLevelSeq = ASN1Sequence(); topLevelSeq.add(version); topLevelSeq.add(modulus); topLevelSeq.add(publicExponent); topLevelSeq.add(privateExponent); topLevelSeq.add(p); topLevelSeq.add(q); topLevelSeq.add(exp1); topLevelSeq.add(exp2); topLevelSeq.add(co); var dataBase64 = base64.encode(topLevelSeq.encode()); var chunks = Strings.chunk(dataBase64, 64); return '$BEGIN_RSA_PRIVATE_KEY\n${chunks.join('\n')}\n$END_RSA_PRIVATE_KEY'; } /// /// Enode the given [rsaPrivateKey] to PEM format using the PKCS#8 standard. /// /// The ASN1 structure is decripted at . /// ``` /// PrivateKeyInfo ::= SEQUENCE { /// version Version, /// algorithm AlgorithmIdentifier, /// PrivateKey BIT STRING /// } /// ``` /// static String encodeRSAPrivateKeyToPem(RSAPrivateKey rsaPrivateKey) { var version = ASN1Integer(BigInt.from(0)); var algorithmSeq = ASN1Sequence(); var algorithmAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1])); var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0])); algorithmSeq.add(algorithmAsn1Obj); algorithmSeq.add(paramsAsn1Obj); var privateKeySeq = ASN1Sequence(); var modulus = ASN1Integer(rsaPrivateKey.n); var publicExponent = ASN1Integer(BigInt.parse('65537')); var privateExponent = ASN1Integer(rsaPrivateKey.privateExponent); var p = ASN1Integer(rsaPrivateKey.p); var q = ASN1Integer(rsaPrivateKey.q); var dP = rsaPrivateKey.privateExponent! % (rsaPrivateKey.p! - BigInt.from(1)); var exp1 = ASN1Integer(dP); var dQ = rsaPrivateKey.privateExponent! % (rsaPrivateKey.q! - BigInt.from(1)); var exp2 = ASN1Integer(dQ); var iQ = rsaPrivateKey.q!.modInverse(rsaPrivateKey.p!); var co = ASN1Integer(iQ); privateKeySeq.add(version); privateKeySeq.add(modulus); privateKeySeq.add(publicExponent); privateKeySeq.add(privateExponent); privateKeySeq.add(p); privateKeySeq.add(q); privateKeySeq.add(exp1); privateKeySeq.add(exp2); privateKeySeq.add(co); var publicKeySeqOctetString = ASN1OctetString(octets: Uint8List.fromList(privateKeySeq.encode())); var topLevelSeq = ASN1Sequence(); topLevelSeq.add(version); topLevelSeq.add(algorithmSeq); topLevelSeq.add(publicKeySeqOctetString); var dataBase64 = base64.encode(topLevelSeq.encode()); var chunks = Strings.chunk(dataBase64, 64); return '$BEGIN_PRIVATE_KEY\n${chunks.join('\n')}\n$END_PRIVATE_KEY'; } /// /// Generates a secure [FortunaRandom] /// static SecureRandom getSecureRandom() { var secureRandom = FortunaRandom(); var random = Random.secure(); var seeds = List.generate(32, (_) => random.nextInt(256)); secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); return secureRandom; } /// /// Revomes the PKCS7 / PKCS5 padding from the [padded] bytes /// static Uint8List removePKCS7Padding(Uint8List padded) => padded.sublist(0, padded.length - PKCS7Padding().padCount(padded)); /// /// Adds a PKCS7 / PKCS5 padding to the given [bytes] and [blockSizeBytes] /// static Uint8List addPKCS7Padding(Uint8List bytes, int blockSizeBytes) { final padLength = blockSizeBytes - (bytes.length % blockSizeBytes); final padded = Uint8List(bytes.length + padLength)..setAll(0, bytes); PKCS7Padding().addPadding(padded, bytes.length); return padded; } } ================================================ FILE: lib/network/util/file_read.dart ================================================ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:proxypin/utils/platform.dart'; import 'package:path_provider/path_provider.dart'; class FileRead { static String? userHome; static Future homeDir() async { if (userHome != null) { return File("${userHome!}${Platform.pathSeparator}.proxypin"); } if (Platforms.isDesktop()) { userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; } else { userHome = (await getApplicationSupportDirectory()).path; } var separator = Platform.pathSeparator; return File("${userHome!}$separator.proxypin"); } static Future readAsString(String file) async { return rootBundle.loadString(file); // return File(file).readAsString(); } static Future read(String file) async { return rootBundle.load(file).then((bateData) => bateData.buffer.asUint8List()); // return File(file).readAsBytes(); } static String? _uuid; static Future get iosUuid async { if (_uuid == null) { var applicationPath = (await getApplicationSupportDirectory()).path; var uuidPattern = RegExp(r'/Application/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})/'); var match = uuidPattern.firstMatch(applicationPath); _uuid = match?.group(1); } return _uuid!; } static Future readFile(String path) async { if (Platform.isIOS) { var uuid = await iosUuid; //ios替换uuid var uuidPattern = RegExp(r'/Application/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/'); path = path.replaceAll(uuidPattern, '/Application/$uuid/'); } return File(path).readAsBytes(); } } ================================================ FILE: lib/network/util/lang.dart ================================================ import 'dart:typed_data'; dynamic getFirstElement(List? list) { return list?.firstOrNull; } ///获取list元素类型 /// @author wanghongen class Lists { static bool isNotEmpty(List? list) { return list != null && list.isNotEmpty; } static Type getElementType(dynamic list) { if (list == null || list.isEmpty || list is! List) { return Null; } var type = list.first.runtimeType; return type; } ///转换指定类型 static List convertList(List list) { return list.map((e) => e as T).toList(); } } class Strings { /// /// Splits the given String [s] in chunks with the given [chunkSize]. /// static List chunk(String s, int chunkSize) { var chunked = []; for (var i = 0; i < s.length; i += chunkSize) { var end = (i + chunkSize < s.length) ? i + chunkSize : s.length; chunked.add(s.substring(i, end)); } return chunked; } static bool isNotEmpty(String? s) { return s != null && s.isNotEmpty; } } class HexUtils { static String bytesToHex(List bytes) { return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); } static Uint8List decode(String hex) { var str = hex.replaceAll(" ", ""); str = str.toLowerCase(); if (str.length % 2 != 0) { str = "0$str"; } var l = str.length ~/ 2; var result = Uint8List(l); for (var i = 0; i < l; ++i) { var x = int.parse(str.substring(i * 2, (2 * (i + 1))), radix: 16); if (x.isNaN) { throw ArgumentError('Expected hex string'); } result[i] = x; } return result; } } ================================================ FILE: lib/network/util/localizations.dart ================================================ import 'dart:ui'; import 'package:proxypin/ui/configuration.dart'; /// @author wanghongen class Localizations { static bool get isZH { if (AppConfiguration.current?.language != null) { return AppConfiguration.current?.language!.languageCode == 'zh'; } return PlatformDispatcher.instance.locale.languageCode == 'zh'; } } ================================================ FILE: lib/network/util/logger.dart ================================================ import 'package:logger/logger.dart'; final logger = Logger( printer: PrettyPrinter( methodCount: 0, errorMethodCount: 15, lineLength: 120, colors: true, printEmojis: false, excludeBox: {Level.info: true, Level.debug: true}, )); ================================================ FILE: lib/network/util/process_info.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:proxypin/native/installed_apps.dart'; import 'package:proxypin/native/process_info.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/socket_address.dart'; import 'package:win32audio/win32audio.dart'; import 'cache.dart'; void main() async { var processInfo = await ProcessInfoUtils.getProcess(512); // await ProcessInfoUtils.getMacIcon(processInfo!.path); // print(await ProcessInfoUtils.getProcessByPort(63194)); print(processInfo); } /// 进程信息工具类 用于获取进程信息 ///@author wanghongen class ProcessInfoUtils { static final processInfoCache = ExpiringCache(const Duration(minutes: 5)); static Future getProcessByPort(InetSocketAddress socketAddress, String cacheKeyPre) async { try { if (Platform.isAndroid) { var app = await ProcessInfoPlugin.getProcessByPort(socketAddress.host, socketAddress.port); if (app != null) { return app; } if (socketAddress.host == '127.0.0.1') { return ProcessInfo('com.network.proxy', "ProxyPin", '', os: Platform.operatingSystem); } 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; } catch (e) { logger.e("getProcessByPort error: $e"); return null; } } // 获取进程 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', 'lsof -nP -iTCP:${socketAddress.port} |grep "${socketAddress.port}->"']); if (results.exitCode != 0) { return null; } var lines = LineSplitter.split(results.stdout); for (var line in lines) { var parts = line.trim().split(RegExp(r'\s+')); if (parts.length >= 9) { return int.tryParse(parts[1]); } } } return null; } 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(); String name = path.substring(path.lastIndexOf('\\') + 1); return ProcessInfo(name, name.split(".")[0], path, os: Platform.operatingSystem); } if (Platform.isMacOS) { 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) { var parts = line.trim().split(RegExp(r'\s+')); if (parts.length >= 2) { parts.removeAt(0).trim(); var path = parts.join(" ").split(".app/")[0]; String name = path.substring(path.lastIndexOf('/') + 1); return ProcessInfo(name, name, "$path.app", os: Platform.operatingSystem); } } } } return null; } } class ProcessInfo { static final _iconCache = ExpiringCache(const Duration(minutes: 5)); final String id; //应用包名 final String name; //应用名称 final String path; final String? os; Uint8List? icon; String? remoteHost; int? remotePost; ProcessInfo(this.id, this.name, this.path, {required this.os, this.icon, this.remoteHost, this.remotePost}); factory ProcessInfo.fromJson(Map json) { return ProcessInfo(json['id'], json['name'], json['path'], os: json['os']); } bool get hasCacheIcon => icon != null || _iconCache.get(id) != null; Uint8List? get cacheIcon => icon ?? _iconCache.get(id); Future getIcon() async { if (icon != null) return icon!; if (_iconCache.get(id) != null) return _iconCache.get(id)!; try { if (Platform.isAndroid) { icon = (await InstalledApps.getAppInfo(id)).icon; } if ('windows' == os || path.endsWith('.exe')) { icon = await _getWindowsIcon(path); } if (Platform.isMacOS) { var macIcon = await _getMacIcon(path); icon = await File(macIcon).readAsBytes(); } icon = icon ?? Uint8List(0); _iconCache.set(id, icon); } catch (e) { icon = Uint8List(0); } 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"; var indexOf = xml.indexOf(key); var iconName = xml.substring(indexOf + key.length, xml.indexOf("", indexOf)); iconName = iconName.trim().replaceAll("", ""); var icon = iconName.endsWith(".icns") ? iconName : "$iconName.icns"; String iconPath = "$path/Contents/Resources/$icon"; return iconPath; } Map toJson() { return {'id': id, 'name': name, 'path': path, 'os': os}; } @override String toString() { return toJson().toString(); } } ================================================ FILE: lib/network/util/proxy_helper.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'dart:io'; import 'package:proxypin/network/bin/listener.dart'; import 'package:proxypin/network/channel/channel.dart'; import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/components/manager/request_rewrite_manager.dart'; import 'package:proxypin/network/components/manager/script_manager.dart'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/http/codec.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/http_headers.dart'; import 'package:proxypin/network/util/crts.dart'; import 'package:proxypin/network/util/localizations.dart'; import '../components/host_filter.dart'; class ProxyHelper { //请求本服务 static localRequest(ChannelContext channelContext, HttpRequest msg, Channel channel) async { //获取配置 if (msg.path == '/config') { final requestRewrites = await RequestRewriteManager.instance; var response = HttpResponse(HttpStatus.ok, protocolVersion: msg.protocolVersion); var body = { "requestRewrites": await requestRewrites.toFullJson(), 'whitelist': HostFilter.whitelist.toJson(), 'blacklist': HostFilter.blacklist.toJson(), 'scripts': await ScriptManager.instance.then((script) { var list = script.list.map((e) async { return {'name': e.name, 'enabled': e.enabled, 'url': e.urls, 'script': await script.getScript(e)}; }); return Future.wait(list); }), }; response.body = utf8.encode(json.encode(body)); channel.writeAndClose(channelContext, response); return; } var response = HttpResponse(HttpStatus.ok, protocolVersion: msg.protocolVersion); response.body = utf8.encode('pong'); response.headers.set("os", Platform.operatingSystem); response.headers.set("hostname", Platform.isAndroid ? Platform.operatingSystem : Platform.localHostname); channel.writeAndClose(channelContext, response); } /// 下载证书 static void crtDownload(ChannelContext channelContext, Channel channel, HttpRequest request) async { const String fileMimeType = 'application/x-x509-ca-cert'; var response = HttpResponse(HttpStatus.ok); response.headers.set(HttpHeaders.CONTENT_TYPE, fileMimeType); response.headers.set("Content-Disposition", 'inline;filename=ProxyPinCA.crt'); response.headers.set("Connection", 'close'); var caFile = await CertificateManager.certificateFile(); var caBytes = await caFile.readAsBytes(); response.headers.set("Content-Length", caBytes.lengthInBytes.toString()); if (request.method == HttpMethod.head) { channel.writeAndClose(channelContext, response); return; } response.body = caBytes; channel.writeAndClose(channelContext, response); } ///异常处理 static Future 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); String message = error.toString(); HttpStatus status = HttpStatus(-1, message); if (error is HandshakeException) { status = HttpStatus( -2, Localizations.isZH ? 'SSL handshake failed, 请检查证书安装是否正确' : 'SSL handshake failed, please check the certificate'); } else if (error is ParserException) { status = HttpStatus(-3, error.message); } else if (error is SocketException) { status = HttpStatus(-4, error.message); } else if (error is SignalException) { status.reason(Localizations.isZH ? '执行脚本异常' : 'Execute script exception'); } request ??= HttpRequest(HttpMethod.connect, hostAndPort.domain) ..body = message.codeUnits ..headers.contentLength = message.codeUnits.length ..hostAndPort = hostAndPort; request.processInfo ??= channelContext.processInfo; if (request.method == HttpMethod.connect && !request.uri.startsWith("http")) { request.uri = hostAndPort.domain; } if (request.response == null || request.method == HttpMethod.connect) { request.response = HttpResponse(status) ..headers.contentType = 'text/plain' ..headers.contentLength = message.codeUnits.length ..body = message.codeUnits; } request.response?.request = request; channelContext.host = hostAndPort; listener?.onRequest(channel, request); listener?.onResponse(channelContext, request.response!); } } ================================================ FILE: lib/network/util/random.dart ================================================ import 'dart:math'; class RandomUtil { static const _characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; static String randomString(int length) { Random random = Random(); return String.fromCharCodes(Iterable.generate( length, (_) => _characters.codeUnitAt(random.nextInt(_characters.length)), )); } } ================================================ FILE: lib/network/util/socket_address.dart ================================================ import 'dart:io'; class InetSocketAddress { final InternetAddress address; final int port; InetSocketAddress(this.address, this.port); String get host => address.host; @override String toString() { return "InetSocketAddress($address:$port)"; } } ================================================ FILE: lib/network/util/system_proxy.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'package:proxypin/network/channel/host_port.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/utils/ip.dart'; import 'package:proxypin/utils/lang.dart'; import 'package:proxy_manager/proxy_manager.dart'; /// @author wanghongen /// 2023/7/26 class SystemProxy { static SystemProxy? _instance; ///单例 static SystemProxy get instance { if (_instance == null) { if (Platform.isMacOS) { _instance = MacSystemProxy(); } else if (Platform.isWindows) { _instance = WindowsSystemProxy(); } else if (Platform.isLinux) { _instance = LinuxSystemProxy(); } else { _instance = SystemProxy(); } } return _instance!; } ///获取代理忽略地址 static String get proxyPassDomains { if (Platform.isMacOS) { return '192.168.0.0/16;10.0.0.0/8;172.16.0.0/12;127.0.0.1;localhost;*.local;timestamp.apple.com'; } if (Platform.isWindows) { return '192.168.0.*;10.0.0.*;172.16.0.*;127.0.0.1;localhost;*.local;'; } if (Platform.isAndroid) { return '192.168.0.0/16;10.0.0.0/8;172.16.0.0/12;127.0.0.1;localhost'; } if (Platform.isIOS) { return '192.168.0.0/16;10.0.0.0/8;172.16.0.0/12;127.0.0.1;localhost;*.local;timestamp.apple.com'; } return ''; } ///获取系统代理 static Future getSystemProxy(ProxyTypes types) async { return instance._getSystemProxy(types); } ///设置系统代理 static Future setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async { await instance._setSystemProxy(port, sslSetting, proxyPassDomains); } ///设置Https代理启用状态 static void setSslProxyEnable(bool proxyEnable, port) { instance._setSslProxyEnable(proxyEnable, port); } /// 设置系统代理 /// @param sslSetting 是否设置https代理只在mac中有效 static Future setSystemProxyEnable(int port, bool enable, bool sslSetting, {required String passDomains}) async { //启用系统代理 if (enable) { await setSystemProxy(port, sslSetting, passDomains); return; } await instance._setProxyEnable(enable, sslSetting); } ///设置代理忽略地址 static Future setProxyPassDomains(String proxyPassDomains) async { instance._setProxyPassDomains(proxyPassDomains); } //子类抽象方法 ///获取系统代理 Future _getSystemProxy(ProxyTypes types) async { return null; } ///设置系统代理 Future _setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async { ProxyManager manager = ProxyManager(); await manager.setAsSystemProxy(sslSetting ? ProxyTypes.https : ProxyTypes.http, "127.0.0.1", port); setProxyPassDomains(proxyPassDomains); } ///设置代理是否启用 Future _setProxyEnable(bool proxyEnable, bool sslSetting) async { ProxyManager manager = ProxyManager(); await manager.cleanSystemProxy(); } ///设置Https代理启用状态 Future _setSslProxyEnable(bool proxyEnable, int port) async { return false; } ///设置代理忽略地址 Future _setProxyPassDomains(String proxyPassDomains) async {} } class MacSystemProxy implements SystemProxy { static String? _hardwarePort; // Helper to safely quote a string for sh (single-quote and escape any internal single quotes) static String _shellQuote(String s) { // Replace ' with '\'' which is the safe way to include single quotes inside single-quoted strings in shell return "'${s.replaceAll("'", "'\\''")}'"; } ///获取系统代理 @override Future _getSystemProxy(ProxyTypes proxyTypes) async { _hardwarePort = _hardwarePort ?? await hardwarePort(); // ensure we have a name if (_hardwarePort == null || _hardwarePort!.isEmpty) { logger.e('hardwarePort is empty, cannot get system proxy'); return null; } final quotedName = _shellQuote(_hardwarePort!); var result = await Process.run('bash', [ '-c', 'networksetup ${proxyTypes == ProxyTypes.http ? '-getwebproxy' : '-getsecurewebproxy'} $quotedName' ]).then((results) => results.stdout.toString().split('\n')); // defensive parsing: find lines safely String enabledLine = result.firstWhere((item) => item.contains('Enabled'), orElse: () => ''); if (enabledLine.isEmpty) { logger.e('Failed to parse Enabled line from networksetup output: ${result.join('\n')}'); return null; } var proxyEnableParts = enabledLine.trim().split(RegExp(r":\s*")); var proxyEnable = proxyEnableParts.length > 1 ? proxyEnableParts[1] : 'No'; if (proxyEnable == 'No') { return null; } String serverLine = result.firstWhere((item) => item.contains('Server'), orElse: () => ''); String portLine = result.firstWhere((item) => item.contains('Port'), orElse: () => ''); if (serverLine.isEmpty || portLine.isEmpty) { logger.e('Failed to parse Server/Port from networksetup output: ${result.join('\n')}'); return null; } var proxyServer = serverLine.trim().split(RegExp(r":\s*"))[1]; var proxyPort = portLine.trim().split(RegExp(r":\s*"))[1]; if (proxyEnable == 'Yes' && proxyServer.isNotEmpty) { return ProxyInfo.of(proxyServer, int.parse(proxyPort)); } return null; } ///mac设置代理地址 @override Future _setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async { _hardwarePort = _hardwarePort ?? await hardwarePort(); if (_hardwarePort == null || _hardwarePort!.isEmpty) { logger.e('hardwarePort is empty, cannot set system proxy'); return false; } final quotedName = _shellQuote(_hardwarePort!); List commands = [ 'networksetup -setwebproxy $quotedName 127.0.0.1 $port', sslSetting == true ? 'networksetup -setsecurewebproxy $quotedName 127.0.0.1 $port' : '', 'networksetup -setproxybypassdomains $quotedName ${proxyPassDomains.replaceAll(";", " ")}', 'networksetup -setsocksfirewallproxystate $quotedName off', ]; var results = await Process.run('bash', ['-c', _concatCommands(commands)]); logger.d('set proxyServer, name: $_hardwarePort, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); bool success = results.exitCode == 0; if (!success) { logger.e('setSystemProxy failed, stderr: ${results.stderr}'); return setProxyWithAuth(commands); } return success; } ///设置Https代理 @override Future _setSslProxyEnable(bool proxyEnable, port) async { var name = _hardwarePort ?? await hardwarePort(); if (name.isEmpty) { logger.e('hardwarePort is empty, cannot set ssl proxy state'); return false; } final quotedName = _shellQuote(name); List commands = [ proxyEnable ? 'networksetup -setsecurewebproxy $quotedName 127.0.0.1 $port' : 'networksetup -setsecurewebproxystate $quotedName off' ]; var results = await Process.run('bash', ['-c', _concatCommands(commands)]); bool success = results.exitCode == 0; if (!success) { logger.e('setSystemProxy failed, stderr: ${results.stderr}'); return setProxyWithAuth(commands); } return success; } ///mac获取当前网络名称 static Future hardwarePort() async { var name = await networkName(); // Use a safer pipeline that avoids embedding awk's $2 (which complicates Dart string quoting). // This command finds the Device line, takes the following Hardware Port line, and extracts the part after ':' var cmd = 'networksetup -listnetworkserviceorder | grep "Device: ${name}" -A 1 | grep "Hardware Port" | cut -d: -f2 | sed -n \'1p\''; var results = await Process.run('bash', ['-c', cmd]); var out = results.stdout.toString().trim(); if (out.isEmpty) return ''; // split on newlines or commas and take the first non-empty token var parts = out.split(RegExp(r"[\r\n,]+")); return parts.first.trim(); } ///设置代理忽略地址 @override Future _setProxyPassDomains(String proxyPassDomains) async { _hardwarePort ??= await hardwarePort(); if (_hardwarePort == null || _hardwarePort!.isEmpty) { logger.e('hardwarePort is empty, cannot set proxy bypass domains'); return; } final quotedName = _shellQuote(_hardwarePort!); var results = await Process.run( 'bash', ['-c', 'networksetup -setproxybypassdomains $quotedName ${proxyPassDomains.replaceAll(";", " ")}']); logger.d('set proxyPassDomains, name: $_hardwarePort, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); } ///mac设置代理是否启用 @override Future _setProxyEnable(bool proxyEnable, bool sslSetting) async { var proxyMode = proxyEnable ? 'on' : 'off'; _hardwarePort ??= await hardwarePort(); if (_hardwarePort == null || _hardwarePort!.isEmpty) { logger.e('hardwarePort is empty, cannot set proxy enable state'); return; } logger.d('set proxyEnable: $proxyEnable, name: $_hardwarePort'); final quotedName = _shellQuote(_hardwarePort!); List commands = [ 'networksetup -setwebproxystate $quotedName $proxyMode', sslSetting ? 'networksetup -setsecurewebproxystate $quotedName $proxyMode' : '' ]; var results = await Process.run('bash', ['-c', _concatCommands(commands)]); if (results.exitCode != 0) { logger.e('setProxyEnable failed, stderr: ${results.stderr}'); await setProxyWithAuth(commands); } } Future setProxyWithAuth(List commands) async { // 使用 quoted form of 确保 shell 指令被 AppleScript 正确转义 String script = 'do shell script "${commands.join('; ')}" with administrator privileges'; try { final result = await Process.run('osascript', ['-e', script]); bool success = result.exitCode == 0; if (!success) { logger.e("操作失败或用户取消: ${result.stderr}"); } return success; } catch (e) { logger.e("执行 AppleScript 出错: $e"); return false; } } static String _concatCommands(List commands) { return commands.where((element) => element.isNotEmpty).join(' && '); } } class WindowsSystemProxy extends SystemProxy { ///设置windows代理是否启用 @override Future _setProxyEnable(bool proxyEnable, bool sslSetting) async { await _internetSettings('add', ['ProxyEnable', '/t', 'REG_DWORD', '/f', '/d', proxyEnable ? '1' : '0']); } ///获取系统代理 @override Future _getSystemProxy(ProxyTypes types) async { var results = await _internetSettings('query', ['ProxyEnable']); var proxyEnableLine = results.split('\r\n').where((item) => item.contains('ProxyEnable')).first.trim(); if (proxyEnableLine.substring(proxyEnableLine.length - 1) != '1') { return null; } return _internetSettings('query', ['ProxyServer']).then((results) { var proxyServerLine = results.split('\r\n').where((item) => item.contains('ProxyServer')).firstOrNull; var proxyServerLineSplits = proxyServerLine?.split(RegExp(r"\s+")); if (proxyServerLineSplits == null || proxyServerLineSplits.length < 2) { return null; } var proxyLine = proxyServerLineSplits[proxyServerLineSplits.length - 1]; if (proxyLine.startsWith("http://") || proxyLine.startsWith("https:///")) { proxyLine = proxyLine.replaceFirst("http://", "").replaceFirst("https:///", ""); } var proxyServer = proxyLine.split(":")[0]; var proxyPort = proxyLine.split(":")[1]; logger.d("$proxyServer:$proxyPort"); return ProxyInfo.of(proxyServer, int.parse(proxyPort)); }).catchError((e) { logger.e('getSystemProxy error', error: e, stackTrace: StackTrace.current); return null; }); } ///设置代理忽略地址 @override Future _setProxyPassDomains(String proxyPassDomains) async { var results = await _internetSettings('add', ['ProxyOverride', '/t', 'REG_SZ', '/d', proxyPassDomains, '/f']); logger.i('set proxyPassDomains, stdout: $results'); } static Future _internetSettings(String cmd, List args) async { return Process.run('reg', [ cmd, 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', ...args, ]).then((results) => results.stdout.toString()); } } class LinuxSystemProxy extends SystemProxy { @override Future _setSystemProxy(int port, bool sslSetting, String proxyPassDomains) async { ProxyManager manager = ProxyManager(); await manager.setAsSystemProxy(ProxyTypes.http, "127.0.0.1", port); if (sslSetting) await manager.setAsSystemProxy(ProxyTypes.https, "127.0.0.1", port); SystemProxy.setProxyPassDomains(proxyPassDomains); } ///linux 获取代理 @override Future _getSystemProxy(ProxyTypes types) async { var mode = await Process.run("gsettings", ["get", "org.gnome.system.proxy", "mode"]) .then((value) => value.stdout.toString().trim()); if (mode.contains("manual")) { var hostFuture = Process.run("gsettings", ["get", "org.gnome.system.proxy.${types.name}", "host"]) .then((value) => value.stdout.toString().trim()); var portFuture = Process.run("gsettings", ["get", "org.gnome.system.proxy.${types.name}", "port"]) .then((value) => value.stdout.toString().trim()); return Future.wait([hostFuture, portFuture]).then((value) { var host = Strings.trimWrap(value[0], "'"); var port = Strings.trimWrap(value[1], "'"); if (host.isNotEmpty && port.isNotEmpty) { return ProxyInfo.of(host, int.parse(port)); } return null; }); } return null; } } void main() async { // single instance ProxyManager manager = ProxyManager(); // set a http proxy await manager.setAsSystemProxy(ProxyTypes.http, "127.0.0.1", 1087); } ================================================ FILE: lib/network/util/task_queue.dart ================================================ import 'dart:async'; import 'dart:collection'; class SequentialTaskQueue { final Queue<_Task> _tasks = Queue(); bool _isProcessing = false; bool _isCancelled = false; Completer? _completer; final Set completedTasks = {}; final Map> dependencyTasks = {}; /// Adds a task to the queue with a priority (e.g., streamId). void add(int id, int? dependency, Future Function() task, {void Function(dynamic error, StackTrace stackTrace)? onError}) { if (_isCancelled) return; _tasks.addLast(_Task(id, task, dependency: dependency, onError: onError)); // Sort tasks by priority (e.g., streamId). // _tasks.sort((a, b) => a.key.compareTo(b.key)); runAllTask(); } runAllTask() async { if (!_isProcessing) { _isProcessing = true; _completer ??= Completer(); while (_tasks.isNotEmpty) { final currentTask = _tasks.removeFirst(); await runTask(currentTask); } _isProcessing = false; _completer?.complete(); _completer = null; } } Future runTask(_Task task) async { if (_isCancelled) return; if (task.dependency != null && task.dependency! > 0 && !completedTasks.contains(task.dependency)) { dependencyTasks[task.dependency!] ??= []; dependencyTasks[task.dependency]!.add(task); } else { try { await task.task(); } catch (error, stackTrace) { task.onError?.call(error, stackTrace); } finally { completedTasks.add(task.id); } if (dependencyTasks[task.id] != null) { for (var dependencyTask in dependencyTasks[task.id]!) { await runTask(dependencyTask); } dependencyTasks.remove(task.id); } } } Future waitForAll() async { if (_isProcessing) { _completer ??= Completer(); return _completer?.future; } return; } void cancel() { _isCancelled = true; _tasks.clear(); } void reset() { _isCancelled = false; _tasks.clear(); } } class _Task { final int id; final int? dependency; final Future Function() task; final Function(dynamic error, StackTrace stackTrace)? onError; _Task(this.id, this.task, {this.dependency, this.onError}); } ================================================ FILE: lib/network/util/tls.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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:typed_data'; class TLS { ///从TLS Client Hello 获取支持的协议 static List? supportProtocols(Uint8List data) { try { int sessionLength = data[43]; int pos = 44 + sessionLength; if (data.length < pos + 2) return null; int cipherSuitesLength = data.buffer.asByteData().getUint16(pos); pos += 2 + cipherSuitesLength; if (data.length < pos + 1) return null; int compressionMethodsLength = data[pos]; pos += 1 + compressionMethodsLength; if (data.length < pos + 2) return null; int extensionsLength = data.buffer.asByteData().getUint16(pos); pos += 2; if (data.length < pos + extensionsLength) return null; List protocols = []; int end = pos + extensionsLength; while (pos + 4 <= end) { int extensionType = data.buffer.asByteData().getUint16(pos); int extensionLength = data.buffer.asByteData().getUint16(pos + 2); pos += 4; if (extensionType == 16 /* ALPN */) { if (pos + 2 > end) return protocols; int alpnExtensionLength = data.buffer.asByteData().getUint16(pos); pos += 2; if (pos + alpnExtensionLength > end) return protocols; int alpnEnd = pos + alpnExtensionLength; while (pos + 1 <= alpnEnd) { int protocolLength = data[pos]; pos += 1; if (pos + protocolLength > alpnEnd) return protocols; String protocol = String.fromCharCodes(data.sublist(pos, pos + protocolLength)); protocols.add(protocol); pos += protocolLength; } } else { pos += extensionLength; } } return protocols; } catch (_) { // Ignore errors, just return empty list } return null; } ///判断是否是TLS Client Hello static bool isTLSClientHello(Uint8List data) { if (data.length < 43) return false; if (data[0] != 0x16 /* handshake */) return false; if (data[1] != 0x03 || data[2] < 0x00 || data[2] > 0x03) return false; if (data[5] != 0x01 /* client_hello */) return false; if (data[9] != 0x03 || data[10] < 0x00 || data[10] > 0x03) return false; return true; } ///从TLS Client Hello 解析域名 static String? getDomain(Uint8List data) { try { int sessionLength = data[43]; int pos = 44 + sessionLength; if (data.length < pos + 2) return null; int cipherSuitesLength = data.buffer.asByteData().getUint16(pos); pos += 2 + cipherSuitesLength; if (data.length < pos + 1) return null; int compressionMethodsLength = data[pos]; pos += 1 + compressionMethodsLength; if (data.length < pos + 2) return null; int extensionsLength = data.buffer.asByteData().getUint16(pos); pos += 2; if (data.length < pos + extensionsLength) return null; int end = pos + extensionsLength; while (pos + 4 <= end) { int extensionType = data.buffer.asByteData().getUint16(pos); int extensionLength = data.buffer.asByteData().getUint16(pos + 2); pos += 4; if (extensionType == 0 /* server_name */) { if (pos + 5 > end) return null; int serverNameListLength = data.buffer.asByteData().getUint16(pos); pos += 2; if (pos + serverNameListLength > end) return null; int serverNameType = data[pos]; int serverNameLength = data.buffer.asByteData().getUint16(pos + 1); pos += 3; if (serverNameType != 0 /* host_name */) return null; if (pos + serverNameLength > end) return null; return String.fromCharCodes(data.sublist(pos, pos + serverNameLength)); } else { pos += extensionLength; } } } catch (_) { // Ignore errors, just return null } return null; } } ================================================ FILE: lib/network/util/uri.dart ================================================ import 'dart:collection'; /// Uri构建工具类 class UriBuild { /// 构建Uri static Uri build(String url, {Map? params}) { var uri = Uri.parse(url); if (params == null) { return uri; } var queries = HashMap(); queries.addAll(uri.queryParameters); queries.addAll(params); return uri.replace(queryParameters: queries); } } class UriUtils { //map转url参数 static String mapToQuery(Map? map) { if (map == null) { return ''; } List list = []; map.forEach((key, value) { list.add('$key=${Uri.encodeComponent(value.toString())}'); }); return list.join('&'); } } ================================================ FILE: lib/storage/favorites.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:collection'; import 'dart:convert'; import 'dart:io'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/storage/path.dart'; import 'package:proxypin/utils/har.dart'; /// 收藏存储 /// @author WangHongEn class FavoriteStorage { static Queue? list; // Keep only recent websocket/sse messages per favorite to control favorites.json size. static const int maxWebSocketMessagesPerFavorite = 200; static const int maxWebSocketPayloadBytesPerFavorite = 1 * 1024 * 1024; // 1 MB static Function()? addNotifier; /// 获取收藏列表 static Future> get favorites async { if (list == null) { list = ListQueue(); var file = await Paths.getPath("favorites.json"); if (await file.exists()) { var value = await file.readAsString(); if (value.isEmpty) { return list!; } try { var config = jsonDecode(value) as List; for (var element in config) { list?.add(Favorite.fromJson(element)); } } catch (e, t) { logger.e('收藏列表解析失败', error: e, stackTrace: t); } } } return list!; } /// 添加收藏 static Future addFavorite(HttpRequest request) async { var favorites = await FavoriteStorage.favorites; if (favorites.any((element) => element.request.requestId == request.requestId)) { return; } // Snapshot to avoid mutating the live request/response when trimming persisted messages. final favorite = _snapshotFavorite(request); trimFavoriteMessages(favorite); favorites.addFirst(favorite); flushConfig(); //通知 addNotifier?.call(); } static Favorite _snapshotFavorite(HttpRequest request) { final copiedRequest = request.copy(); final copiedResponse = request.response?.copy(); return Favorite(copiedRequest, response: copiedResponse); } static Future removeFavorite(Favorite favorite) async { var list = await favorites; list.remove(favorite); flushConfig(); } //刷新配置 static Future flushConfig() async { var list = await favorites; await Paths.getPath("favorites.json").then((file) => file.writeAsString(toJson(list))); } static String toJson(Queue list) { return jsonEncode(list.map((e) => e.toJson()).toList()); } /// Export all favorites to a given file path static Future exportToFile(String path) async { var current = await favorites; var content = toJson(current); await File(path).writeAsString(content, flush: true); } /// Export all favorites as HAR to a given file path static Future exportToHarFile(String path, {String title = 'Favorites'}) async { var current = await favorites; final requests = current.map((f) => f.request).toList(growable: false); await Har.writeFile(requests, File(path), title: title); } /// Import favorites from a JSON or HAR file (merges with current list, de-duping by requestId) static Future importFromFile(String path) async { final file = File(path); if (!await file.exists()) { throw Exception('File not found'); } final lower = path.toLowerCase(); List imported; if (lower.endsWith('.har')) { // HAR import final requests = await Har.readFile(file); imported = requests.map((r) => Favorite(r)).toList(growable: false); } else { // JSON import (old format) final content = await file.readAsString(); if (content.trim().isEmpty) { return; } final decoded = jsonDecode(content) as List; imported = decoded.map((e) => Favorite.fromJson(e as Map)).toList(growable: false); } final current = await favorites; final existingIds = current.map((e) => e.request.requestId).toSet(); // Merge without replacing current entries; skip duplicates by requestId for (var fav in imported.reversed) { final rid = fav.request.requestId; if (existingIds.contains(rid)) { continue; } trimFavoriteMessages(fav); existingIds.add(rid); current.addFirst(fav); } await flushConfig(); addNotifier?.call(); } static bool trimFavoriteMessages(Favorite favorite) { final response = favorite.response; final requestFrames = List.of(favorite.request.messages); final responseFrames = List.of(response?.messages ?? const []); if (requestFrames.isEmpty && responseFrames.isEmpty) { return false; } final refs = <_FrameRef>[ ...requestFrames.map((e) => _FrameRef(isRequest: true, frame: e)), ...responseFrames.map((e) => _FrameRef(isRequest: false, frame: e)), ]..sort((a, b) => a.frame.time.compareTo(b.frame.time)); final totalBytes = refs.fold(0, (sum, e) => sum + e.frame.payloadData.length); if (refs.length <= maxWebSocketMessagesPerFavorite && totalBytes <= maxWebSocketPayloadBytesPerFavorite) { return false; } final kept = <_FrameRef>[]; int keptBytes = 0; for (int i = refs.length - 1; i >= 0; i--) { final ref = refs[i]; final bytes = ref.frame.payloadData.length; final hitCount = kept.length >= maxWebSocketMessagesPerFavorite; final hitBytes = kept.isNotEmpty && (keptBytes + bytes > maxWebSocketPayloadBytesPerFavorite); if (hitCount || hitBytes) { continue; } kept.add(ref); keptBytes += bytes; if (kept.length >= maxWebSocketMessagesPerFavorite) { break; } } kept.sort((a, b) => a.frame.time.compareTo(b.frame.time)); favorite.request.messages = kept.where((e) => e.isRequest).map((e) => e.frame).toList(growable: false); response?.messages = kept.where((e) => !e.isRequest).map((e) => e.frame).toList(growable: false); return true; } } class _FrameRef { final bool isRequest; final WebSocketFrame frame; _FrameRef({required this.isRequest, required this.frame}); } class Favorite { String? name; final HttpRequest request; HttpResponse? response; Favorite(this.request, {this.name, this.response}) { response ??= request.response; request.response = response; response?.request = request; } factory Favorite.fromJson(Map json) { return Favorite(HttpRequest.fromJson(json['request']), name: json['name'], response: json['response'] == null ? null : HttpResponse.fromJson(json['response'])); } Map toJson() { return { 'name': name, 'request': request.toJson(), 'response': response?.toJson(), }; } int get websocketMessageCount => request.messages.length + (response?.messages.length ?? 0); } ================================================ FILE: lib/storage/histories.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:collection'; import 'dart:convert'; import 'dart:io'; import 'package:date_format/date_format.dart'; import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/storage/path.dart'; import 'package:proxypin/utils/files.dart'; import 'package:proxypin/utils/har.dart'; import 'package:proxypin/utils/listenable_list.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; ///历史存储 ///@Author WangHongEn class HistoryStorage { static HistoryStorage? _instance; final File _storageFile; HistoryStorage._internal(this._storageFile); static final ListenableList _histories = ListenableList(); ///单例 static Future get instance async { if (_instance == null) { var file = await Paths.getPath("histories.json"); _instance = HistoryStorage._internal(file); await _instance!._init(); } return _instance!; } //初始化 Future _init() async { if (await _storageFile.exists()) { var content = await _storageFile.readAsString(); if (content.trim().isEmpty) { return; } try { var list = jsonDecode(content) as List; for (var entry in list) { _histories.add(HistoryItem.formJson(entry)); } } catch (e) { logger.e("历史记录解析错误", error: e); } } } static Future _homePath() async { final home = await getApplicationSupportDirectory(); return '${home.path}${Platform.pathSeparator}history'; } /// 获取历史记录 List get histories { return _histories.source; } addListener(ListenerListEvent listener) async { _histories.addListener(listener); } ///打开文件 static Future openFile(String name) async { final homePath = await _homePath(); var file = File('$homePath${Platform.pathSeparator}$name'); return file.create(recursive: true); } /// 添加历史记录 HistoryItem addHistory(String name, File file, int requestLength) { var historyItem = HistoryItem(name, file.path, requestLength, 0); _histories.add(historyItem); refresh(); return historyItem; } int getIndex(HistoryItem item) { return _histories.indexOf(item); } //更新 Future updateHistory(int index, HistoryItem item) async { _histories.update(index, item); refresh(); } //获取 HistoryItem getHistory(int index) { return _histories.source[index]; } Future refresh() async { await _storageFile.writeAsString(jsonEncode(_histories.source)); } ///删除 Future removeHistory(int index) async { var history = _histories.removeAt(index); logger.i('删除历史记录 $history'); final homePath = await _homePath(); var file = File('$homePath${Platform.pathSeparator}${Files.getName(history.path)}'); file.delete(); await refresh(); } //获取请求列表 Future> getRequests(HistoryItem history) async { if (history.requests == null) { final homePath = await _homePath(); String path = '$homePath${Platform.pathSeparator}${Files.getName(history.path)}'; var file = File(path); history.requests = await Har.readFile(file); history.requestLength = history.requests!.length; file.length().then((size) => history.fileSize = size); } return history.requests!; } ///刷新requests Future flushRequests(HistoryItem history, List requests) async { logger.i("刷新历史记录 $history"); final homePath = await _homePath(); String path = '$homePath${Platform.pathSeparator}${Files.getName(history.path)}'; var file = File(path); for (int i = 0; i < requests.length; i++) { var request = requests[i]; var har = Har.toHar(request); await file.writeAsString("${jsonEncode(har)},\n", mode: i == 0 ? FileMode.write : FileMode.append); } history.requestLength = requests.length; await file.length().then((size) => history.fileSize = size); await refresh(); } //添加历史 Future addHarFile(XFile file) async { var readAsBytes = await file.readAsString(); var json = jsonDecode(readAsBytes); var log = json['log']; String name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]); List? pages = log['pages'] as List?; if (pages?.isNotEmpty == true) { name = pages?.first['title']; } //解析请求 List entries = log['entries']; var list = entries.map((e) => Har.toRequest(e)).toList(); //保存文件 var historyFile = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch}.txt"); var open = await historyFile.open(mode: FileMode.append); for (var request in list) { await open.writeString(jsonEncode(Har.toHar(request))); await open.writeString(",\n"); } return addHistory(name, historyFile, list.length); } } class HistoryTask extends ListenerListEvent { HistoryItem? history; Timer? timer; final Queue writeList = Queue(); RandomAccessFile? open; bool locked = false; static HistoryTask? _instance; final Configuration configuration; final ListenableList sourceList; HistoryTask(this.configuration, this.sourceList) { logger.d("start history task"); if (configuration.historyCacheTime != 0) { sourceList.addListener(this); Future.delayed(const Duration(seconds: 3), () => cleanHistory()); } } static HistoryTask ensureInstance(Configuration configuration, ListenableList sourceList) { return _instance ??= HistoryTask(configuration, sourceList); } //清理历史数据 Future cleanHistory() async { if (configuration.historyCacheTime == 0) { return; } var overdueTime = DateTime.now().subtract(Duration(days: configuration.historyCacheTime)); var historyStorage = await HistoryStorage.instance; var histories = historyStorage.histories; for (int i = 0; i < histories.length; i++) { if (histories.elementAt(i).createTime.isBefore(overdueTime)) { await historyStorage.removeHistory(i); i--; } } } @override void onAdd(HttpRequest item) { if (history == null) { startTask(); return; } writeList.add(item); } @override void onRemove(HttpRequest item) => resetList(); @override void onBatchRemove(List items) => resetList(); @override clear() => resetList(); Future resetList() async { locked = true; await open?.lock().timeout(Duration(seconds: 3), onTimeout: () => open!.unlock()); open = await open?.truncate(0); await open?.setPosition(0); history?.requestLength = 0; history?.requests = null; writeList.clear(); writeList.addAll(sourceList.source); locked = false; open?.unlock(); } void cancelTask() { timer?.cancel(); timer = null; open?.close(); open = null; history = null; sourceList.removeListener(this); writeList.clear(); } //写入任务 Future startTask() async { if (history != null || locked) return; locked = true; HistoryStorage storage = await HistoryStorage.instance; var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]); File file = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch}.txt"); history = storage.addHistory(name, file, 0); writeList.clear(); writeList.addAll(sourceList.source); locked = false; open = await file.open(mode: FileMode.append); timer = Timer.periodic(const Duration(seconds: 5), (it) => writeTask()); } //写入任务 Future writeTask() async { if (writeList.isEmpty) { return; } bool changed = false; while (writeList.isNotEmpty && !locked) { var request = writeList.removeFirst(); var har = Har.toHar(request); await open?.writeString("${jsonEncode(har)},\n"); history!.requestLength++; changed = true; } if (!changed) return; history!.fileSize = await open!.length(); history!.requests = null; var historyStorage = await HistoryStorage.instance; historyStorage.updateHistory(historyStorage.getIndex(history!), history!); } } /// 历史记录 class HistoryItem { String name; final String path; // 文件路径 int requestLength = 0; // 请求数量 int? fileSize; // 文件大小 DateTime createTime = DateTime.now(); List? requests; HistoryItem(this.name, this.path, this.requestLength, this.fileSize, {DateTime? createTime}) : createTime = createTime ?? DateTime.now(); //json反序列化 factory HistoryItem.formJson(Map map) { return HistoryItem(map['name'], map['path'], map['requestLength'], map['fileSize'], createTime: map['createTime'] == null ? null : DateTime.fromMillisecondsSinceEpoch(map['createTime'])); } //json序列化 Map toJson() { return { 'name': name, 'path': path, 'requestLength': requestLength, 'fileSize': fileSize, 'createTime': createTime.millisecondsSinceEpoch, }; } //获取文件大小 String get size { if (this.fileSize == null) { return ""; } int fileSize = this.fileSize!; if (fileSize > 1024 * 1024) { return "${(fileSize / 1024 / 1024).toStringAsFixed(1)}MB"; } return "${(fileSize / 1024).toStringAsFixed(1)}KB"; } @override String toString() { return "$path $requestLength $fileSize"; } } ================================================ FILE: lib/storage/local_storage.dart ================================================ import 'package:shared_preferences/shared_preferences.dart'; class LocalStorage { static Future getBool(String key, {bool? defaultValue}) async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getBool(key) ?? defaultValue; } static Future setBool(String key, bool value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setBool(key, value); } } ================================================ FILE: lib/storage/path.dart ================================================ import 'dart:io'; import 'package:path_provider/path_provider.dart'; class Paths { static final Map _cache = {}; //获取配置路径 static Future getPath(String fileName) async { if (_cache.containsKey(fileName)) { return _cache[fileName]!; } final directory = await getApplicationSupportDirectory(); var file = File('${directory.path}${Platform.pathSeparator}$fileName'); if (!await file.exists()) { await file.create(recursive: true); } _cache[fileName] = file; return file; } static Future createFile(String dir, String filename) async { final directory = await getApplicationSupportDirectory(); var file = File('${directory.path}${Platform.pathSeparator}$dir${Platform.pathSeparator}$filename'); return file.create(recursive: true); } } ================================================ FILE: lib/storage/shared_preference_keys.dart ================================================ // ignore_for_file: constant_identifier_names class SharedPreferenceKeys { static const String CERT_INSTALL_SKIP = "cert_install_skip"; } ================================================ FILE: lib/ui/app_update/app_update_repository.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:http/http.dart' as http; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/app_update/remote_version_entity.dart'; import 'package:proxypin/ui/component/app_dialog.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'constants.dart'; import 'new_version_dialog.dart'; class AppUpdateRepository { static final HttpClient httpClient = HttpClient(); static Future checkUpdate(BuildContext context, {bool canIgnore = true, bool showToast = false}) async { try { var lastVersion = await getLatestVersion(); if (lastVersion == null) { logger.w("[AppUpdate] failed to fetch latest version info"); return; } if (!context.mounted) return; var availableUpdates = compareVersions(AppConfiguration.version, lastVersion.version); if (availableUpdates) { if (canIgnore) { var ignoreVersion = await SharedPreferencesAsync().getString(Constants.ignoreReleaseVersionKey); if (ignoreVersion == lastVersion.version) { logger.d("ignored release [${lastVersion.version}]"); return; } } logger.d("new version available: $lastVersion"); if (!context.mounted) return; NewVersionDialog( AppConfiguration.version, lastVersion, canIgnore: true, ).show(context); return; } logger.i("already using latest version[${AppConfiguration.version}], last: [${lastVersion.version}]"); if (showToast) { AppLocalizations localizations = AppLocalizations.of(context)!; CustomToast.success(localizations.appUpdateNotAvailableMsg).show(context); } } catch (e) { logger.e("Error checking for updates: $e"); if (showToast) { CustomToast.error(e.toString()).show(context); } } } /// Fetches the latest version information from the GitHub releases API. static Future getLatestVersion({bool includePreReleases = false}) async { final response = await http.get(Uri.parse(Constants.githubReleasesApiUrl)); if (response.statusCode != 200 || response.body.isEmpty) { logger.w("[AppUpdate] failed to fetch latest version info"); return null; } var body = jsonDecode(response.body) as List; final releases = body.map((e) => GithubReleaseParser.parse(e as Map)); late RemoteVersionEntity latest; if (includePreReleases) { latest = releases.first; } else { latest = releases.firstWhere((e) => e.preRelease == false); } logger.d("[AppUpdate] latest version: $latest"); return latest; } static bool compareVersions(String currentVersion, String latestVersion) { String normalizeVersion(String version) { return version.startsWith('v') ? version.substring(1) : version; } List parseVersion(String version) { return normalizeVersion(version).split('.').map(int.parse).toList(); } List current = parseVersion(currentVersion); List latest = parseVersion(latestVersion); for (int i = 0; i < current.length; i++) { if (i >= latest.length || current[i] > latest[i]) { return false; // 当前版本高于最新版本 } else if (current[i] < latest[i]) { return true; // 需要更新 } } return latest.length > current.length; // 最新版本有更多的子版本号 } } ================================================ FILE: lib/ui/app_update/constants.dart ================================================ abstract class Constants { static const githubUrl = "https://github.com/wanghongenpin/proxypin"; static const githubReleasesApiUrl = "https://api.github.com/repos/wanghongenpin/proxypin/releases"; static const githubLatestReleaseUrl = "https://github.com/wanghongenpin/proxypin/releases/latest"; static const String ignoreReleaseVersionKey = "ignored_release_version"; } const kAnimationDuration = Duration(milliseconds: 250); ================================================ FILE: lib/ui/app_update/new_version_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/app_update/remote_version_entity.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'constants.dart'; class NewVersionDialog extends StatelessWidget { NewVersionDialog( this.currentVersion, this.newVersion, { this.canIgnore = true, }) : super(key: _dialogKey); final String currentVersion; final RemoteVersionEntity newVersion; final bool canIgnore; static final _dialogKey = GlobalKey(debugLabel: 'new version dialog'); Future show(BuildContext context) async { if (_dialogKey.currentContext == null) { return showDialog( context: context, useRootNavigator: true, builder: (context) => this, ); } else { logger.d("new version dialog is already open"); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); AppLocalizations localizations = AppLocalizations.of(context)!; return AlertDialog( title: Text(localizations.appUpdateDialogTitle), // scrollable: true, content: Container( constraints: BoxConstraints(maxHeight: 230, maxWidth: 500), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(localizations.appUpdateUpdateMsg), const SizedBox(height: 5), Text.rich( TextSpan( children: [ TextSpan(text: "${localizations.appUpdateCurrentVersionLbl}: ", style: theme.textTheme.bodySmall), TextSpan(text: currentVersion, style: theme.textTheme.labelMedium), ], ), ), Text.rich( TextSpan( children: [ TextSpan(text: "${localizations.appUpdateNewVersionLbl}: ", style: theme.textTheme.bodySmall), TextSpan(text: newVersion.version, style: theme.textTheme.labelMedium), ], ), ), Text(newVersion.content ?? '', style: theme.textTheme.labelMedium), ], ))), actions: [ Wrap(alignment: WrapAlignment.end, children: [ if (canIgnore) TextButton( onPressed: () async { SharedPreferencesAsync().setString(Constants.ignoreReleaseVersionKey, newVersion.version); logger.i("ignored release [${newVersion.version}]"); if (context.mounted) Navigator.pop(context); }, child: Text(localizations.appUpdateIgnoreBtnTxt), ), TextButton( onPressed: () => Navigator.pop(context), child: Text(localizations.appUpdateLaterBtnTxt), ), TextButton( onPressed: () async { await launchUrl(Uri.parse(newVersion.url), mode: LaunchMode.externalApplication); }, child: Text(localizations.appUpdateUpdateNowBtnTxt), ), ]) ], ); } } ================================================ FILE: lib/ui/app_update/remote_version_entity.dart ================================================ import 'package:proxypin/utils/lang.dart'; class RemoteVersionEntity { final String version; final String buildNumber; final String releaseTag; final bool preRelease; final String url; final String? content; final DateTime publishedAt; RemoteVersionEntity({ required this.version, required this.buildNumber, required this.releaseTag, required this.preRelease, required this.url, this.content, required this.publishedAt, }); @override String toString() { return 'RemoteVersionEntity(version: $version, buildNumber: $buildNumber, releaseTag: $releaseTag, preRelease: $preRelease, url: $url, publishedAt: $publishedAt)'; } } abstract class GithubReleaseParser { static RemoteVersionEntity parse(Map json) { final fullTag = json['tag_name'] as String; final fullVersion = fullTag.removePrefix("v").split("-").first.split("+"); var version = fullVersion.first; var buildNumber = fullVersion.elementAtOrElse(1, (index) => ""); final preRelease = json["prerelease"] as bool; final publishedAt = DateTime.parse(json["published_at"] as String); var body = json['body']?.toString().split("English: "); return RemoteVersionEntity( version: version, buildNumber: buildNumber, releaseTag: fullTag, preRelease: preRelease, url: json["html_url"] as String, content: body?.last, publishedAt: publishedAt); } } ================================================ FILE: lib/ui/component/app_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:toastification/toastification.dart'; class AppAlertDialog extends StatelessWidget { const AppAlertDialog({ super.key, this.title, required this.message, }); final String? title; final String message; factory AppAlertDialog.fromErr(({String type, String? message}) err) => AppAlertDialog( title: err.message == null ? null : err.type, message: err.message ?? err.type, ); Future show(BuildContext context) async { await showDialog( context: context, useRootNavigator: true, builder: (context) => this, ); } @override Widget build(BuildContext context) { final localizations = MaterialLocalizations.of(context); return AlertDialog( title: title != null ? Text(title!) : null, content: SingleChildScrollView( child: SizedBox( width: 468, child: Text(message), ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(localizations.okButtonLabel), ), ], ); } } enum AlertType { info, error, success; ToastificationType get _toastificationType => switch (this) { success => ToastificationType.success, error => ToastificationType.error, info => ToastificationType.info, }; } class CustomToast extends StatelessWidget { const CustomToast( this.message, { super.key, this.type = AlertType.info, this.icon, this.duration = const Duration(seconds: 3), }); const CustomToast.error( this.message, { super.key, this.duration = const Duration(seconds: 5), }) : type = AlertType.error, icon = Icons.error; const CustomToast.success( this.message, { super.key, this.duration = const Duration(seconds: 2), }) : type = AlertType.success, icon = Icons.check_circle; final String message; final AlertType type; final IconData? icon; final Duration duration; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)), color: Theme.of(context).colorScheme.surface, ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible(child: Text(message)), ], ), ); } void show(BuildContext context, {Alignment alignment = Alignment.bottomLeft}) { toastification.show( context: context, title: Text(message), icon: icon == null ? null : Icon(icon), type: type._toastificationType, alignment: alignment, autoCloseDuration: duration, style: ToastificationStyle.flat, pauseOnHover: true, showProgressBar: false, dragToClose: true, closeOnClick: true, closeButton: ToastCloseButton(showType: CloseButtonShowType.onHover), ); } } ================================================ FILE: lib/ui/component/buttons.dart ================================================ import 'package:flutter/material.dart'; class Buttons { static ButtonStyle get buttonStyle => ButtonStyle( padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: 15, vertical: 8)), shape: WidgetStateProperty.all( RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))); } ================================================ FILE: lib/ui/component/chinese_font.dart ================================================ import 'package:flutter/material.dart'; class SystemChineseFont { const SystemChineseFont._(); /// Chinese font family fallback, for windows static const List windowsFontFamily = [ 'Microsoft YaHei', ]; static const systemFont = "system-font"; static bool systemFontLoaded = false; /// Chinese font family fallback, for most platforms static List get fontFamilyFallback { return [ systemFont, "sans-serif", ...windowsFontFamily, ]; } /// Text style with updated fontFamilyFallback & fontVariations static TextStyle get textStyle { return const TextStyle().useSystemChineseFont(); } /// Text theme with updated fontFamilyFallback & fontVariations static TextTheme get textTheme { return Typography().dense.apply(fontFamilyFallback: fontFamilyFallback); } } extension TextStyleUseSystemChineseFont on TextStyle { /// Add fontFamilyFallback & fontVariation to original font style TextStyle useSystemChineseFont() { return copyWith( fontFamilyFallback: [ ...?fontFamilyFallback, ...SystemChineseFont.fontFamilyFallback, ], fontVariations: [ ...?fontVariations, if (fontWeight != null) FontVariation('wght', (fontWeight!.index + 1) * 100), ], ); } } extension TextThemeUseSystemChineseFont on TextTheme { /// Add fontFamilyFallback & fontVariation to original text theme TextTheme useSystemChineseFont() { return SystemChineseFont.textTheme.merge(this); } } extension ThemeDataUseSystemChineseFont on ThemeData { /// Add fontFamilyFallback & fontVariation to original theme data ThemeData useSystemChineseFont() { return copyWith(textTheme: textTheme.useSystemChineseFont()); } } ================================================ FILE: lib/ui/component/context_menu_region.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; typedef ContextMenuBuilder = List Function(); /// 根据用户手势显示和隐藏上下文菜单。 /// 默认情况下,在右键单击和长按时显示菜单。 class ContextMenuRegion extends StatefulWidget { const ContextMenuRegion({ super.key, required this.child, required this.contextMenuBuilder, }); /// Builds the context menu. final ContextMenuBuilder contextMenuBuilder; /// The child widget that will be listened to for gestures. final Widget child; @override State createState() => _ContextMenuRegionState(); } class _ContextMenuRegionState extends State { Offset? _longPressOffset; final ContextMenuController _contextMenuController = ContextMenuController(); static bool get _longPressEnabled { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: return true; case TargetPlatform.macOS: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return false; } } void _onSecondaryTapUp(TapUpDetails details) { _show(details.globalPosition); } void _onTap() { if (!_contextMenuController.isShown) { return; } _hide(); } void _onLongPressStart(LongPressStartDetails details) { _longPressOffset = details.globalPosition; } void _onLongPress() { assert(_longPressOffset != null); _show(_longPressOffset!); _longPressOffset = null; } void _show(Offset position) { _contextMenuController.show( context: context, contextMenuBuilder: (context) { return AdaptiveTextSelectionToolbar.buttonItems( buttonItems: widget.contextMenuBuilder.call(), anchors: TextSelectionToolbarAnchors(primaryAnchor: position)); }, ); } void _hide() { _contextMenuController.remove(); } @override void dispose() { _hide(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onSecondaryTapUp: _onSecondaryTapUp, onTap: _onTap, onLongPress: _longPressEnabled ? _onLongPress : null, onLongPressStart: _longPressEnabled ? _onLongPressStart : null, child: widget.child, ); } } ================================================ FILE: lib/ui/component/device.dart ================================================ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:device_info_plus/device_info_plus.dart'; class DeviceUtils { /// Get the device id static Future deviceId() async { var deviceInfoPlugin = DeviceInfoPlugin(); if (Platform.isAndroid) { return deviceInfoPlugin.androidInfo.then((it) => it.id); } else if (Platform.isIOS) { return deviceInfoPlugin.iosInfo.then((it) => it.identifierForVendor); } return await DesktopMultiWindow.invokeMethod(0, "deviceId", null); } /// Get the desktop device id static Future desktopDeviceId() async { var deviceInfoPlugin = DeviceInfoPlugin(); if (Platform.isWindows) { return deviceInfoPlugin.windowsInfo.then((it) => it.deviceId); } else if (Platform.isMacOS) { return deviceInfoPlugin.macOsInfo.then((it) => it.systemGUID); } else if (Platform.isLinux) { return deviceInfoPlugin.linuxInfo.then((it) => it.machineId); } return null; } } ================================================ FILE: lib/ui/component/history_cache_time.dart ================================================ import 'package:flutter/material.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:proxypin/network/bin/configuration.dart'; ///缓存时间菜单 /// @author wanghongen class HistoryCacheTime extends StatefulWidget { final Configuration configuration; final Function(int) onSelected; const HistoryCacheTime(this.configuration, {super.key, required this.onSelected}); @override State createState() => _HistoryCacheTimeState(); } class _HistoryCacheTimeState extends State { AppLocalizations get localizations => AppLocalizations.of(context)!; @override Widget build(BuildContext context) { return PopupMenuButton( tooltip: localizations.historyCacheTime, offset: const Offset(0, 35), icon: const Icon(Icons.av_timer, size: 19), initialValue: widget.configuration.historyCacheTime, constraints: const BoxConstraints(minWidth: 34, minHeight: 34), onSelected: (val) { widget.configuration.historyCacheTime = val; widget.configuration.flushConfig(); setState(() { widget.onSelected.call(val); }); }, itemBuilder: (BuildContext context) { return [ PopupMenuItem(value: 0, height: 35, child: Text(localizations.historyManualSave)), PopupMenuItem(value: 7, height: 35, child: Text(localizations.historyDay(7))), PopupMenuItem(value: 30, height: 35, child: Text(localizations.historyDay(30))), PopupMenuItem(value: 99999, height: 35, child: Text(localizations.historyForever)), ]; }); } } ================================================ FILE: lib/ui/component/http_method_popup.dart ================================================ // Add MethodPopupMenu widget for compact colored method display import 'package:flutter/material.dart'; import '../../network/http/http.dart'; class MethodPopupMenu extends StatelessWidget { final HttpMethod? value; final ValueChanged onChanged; final bool showSeparator; // whether to display the vertical separator to the right const MethodPopupMenu({super.key, required this.value, required this.onChanged, this.showSeparator = true}); Color _methodColor(HttpMethod? m, BuildContext context) { // colors chosen similar to Postman style switch (m) { case HttpMethod.get: return Colors.green.shade700; case HttpMethod.post: return Colors.orange.shade700; case HttpMethod.put: return Colors.blue.shade700; case HttpMethod.patch: return Colors.purple.shade700; case HttpMethod.delete: return Colors.red.shade700; case HttpMethod.options: return Colors.teal.shade700; // OPTIONS colored teal case HttpMethod.head: return Colors.indigo.shade700; // HEAD colored indigo case HttpMethod.trace: case HttpMethod.connect: case HttpMethod.propfind: case HttpMethod.report: return Colors.grey.shade700; default: return Colors.grey.shade700; } } @override Widget build(BuildContext context) { var items = >[]; items.add(DropdownMenuItem(value: null, child: _buildMenuItem(null, context))); for (var m in HttpMethod.methods()) { if (m == HttpMethod.connect || m == HttpMethod.options) continue; items.add(DropdownMenuItem(value: m, child: _buildMenuItem(m, context))); } final dropdown = DropdownButton( padding: const EdgeInsets.only(), alignment: AlignmentDirectional.center, isDense: true, focusColor: Colors.transparent, underline: const SizedBox(), value: value, onChanged: onChanged, items: items, ); // render dropdown and optional separator together so caller doesn't need to add one return Row( mainAxisSize: MainAxisSize.min, children: [ dropdown, if (showSeparator) ...[ const SizedBox(width: 3), Container(width: 1, height: 22, color: Colors.grey.shade300), const SizedBox(width: 3), ] ], ); } Widget _buildMenuItem(HttpMethod? m, BuildContext context) { final name = m == null ? 'ANY' : m.name; final color = _methodColor(m, context); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text(name, style: TextStyle(color: color, fontSize: 13, fontWeight: FontWeight.w600)), ); } } ================================================ FILE: lib/ui/component/json/json_text.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/component/json/theme.dart'; import 'package:proxypin/ui/component/search/search_controller.dart'; import 'package:proxypin/utils/font.dart'; import 'package:scrollable_positioned_list_nic/scrollable_positioned_list_nic.dart'; import '../../../utils/platform.dart'; class JsonText extends StatefulWidget { final ColorTheme colorTheme; final dynamic json; final String indent; final ScrollController? scrollController; final SearchTextController? searchController; const JsonText({ super.key, required this.json, this.indent = ' ', required this.colorTheme, this.scrollController, this.searchController, }); @override State createState() => _JsonTextState(); } class _JsonTextState extends State { ScrollController? trackingScrollController; SearchTextController? searchController; final ItemScrollController itemScrollController = ItemScrollController(); @override void initState() { super.initState(); searchController = widget.searchController; } @override void dispose() { trackingScrollController?.dispose(); trackingScrollController = null; super.dispose(); } @override Widget build(BuildContext context) { if (searchController == null) { return jsonTextWidget(context); } return AnimatedBuilder( animation: searchController!, builder: (context, child) { return jsonTextWidget(context); }, ); } Widget jsonTextWidget(BuildContext context) { var jsonParser = JsonParser(widget.json, widget.colorTheme, widget.indent, searchController); var textList = jsonParser.getJsonTree(); List>? chunks; WidgetsBinding.instance.addPostFrameCallback((_) { searchController?.updateMatchCount(jsonParser.searchMatchTotal); // 自动滚动到当前高亮项 scrollToMatch(jsonParser, chunks); }); if (textList.length < 1000) { return SelectableText.rich(TextSpan(children: textList), showCursor: true); } else { chunks = chunks ?? splitTextSpans(textList, 500); return SizedBox( width: double.infinity, height: MediaQuery.of(context).size.height - 200, child: SelectionArea( child: ScrollablePositionedList.builder( physics: Platforms.isDesktop() ? null : const BouncingScrollPhysics(), scrollController: Platforms.isDesktop() ? null : trackingScroll(), itemCount: chunks.length, minCacheExtent: 1500, itemScrollController: itemScrollController, itemBuilder: (BuildContext context, int index) { return Text.rich( TextSpan(children: chunks![index]), textHeightBehavior: const TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false), strutStyle: const StrutStyle(forceStrutHeight: true, height: 1.393), style: TextStyle(fontFamily: fonts.regular), ); }, ))); } } Future scrollToMatch(JsonParser jsonParser, [List>? chunks]) async { if (searchController == null || jsonParser.matchKeys.isEmpty) return; final index = searchController!.currentMatchIndex.value; if (index < 0 || index >= jsonParser.matchKeys.length) return; final key = jsonParser.matchKeys[index]; if (key.currentContext != null) { await _ensureVisibleCenter(key, const Duration(milliseconds: 260)); return; } // Chunk-first path for large documents if (chunks != null && chunks.isNotEmpty) { final chunkIndex = _findChunkIndexForKey(chunks, key); if (chunkIndex != -1) { /// 滚动到对应 chunk try { await itemScrollController.scrollTo( index: chunkIndex, duration: const Duration(milliseconds: 150), curve: Curves.easeOut, alignment: 0.0, ); } catch (_) { logger.w('Scroll to chunk $chunkIndex failed'); } for (int i = 0; i < 10 && key.currentContext == null; i++) { await Future.delayed(Duration(milliseconds: 40)); } await _ensureVisibleCenter(key, const Duration(milliseconds: 130)); return; } } } Future _ensureVisibleCenter(GlobalKey key, Duration duration) async { final ctx = key.currentContext; if (ctx != null) { await Scrollable.ensureVisible(ctx, duration: duration, alignment: 0.5); } } // 在分块数据中定位包含目标 key 的 chunk 下标 int _findChunkIndexForKey(List> chunks, GlobalKey key) { for (int i = 0; i < chunks.length; i++) { for (final span in chunks[i]) { if (_textSpanContainsKey(span, key)) return i; } } return -1; } // 递归检查 TextSpan 树是否包含对应 key 的 WidgetSpan bool _textSpanContainsKey(TextSpan span, GlobalKey key) { final children = span.children; if (children == null || children.isEmpty) return false; for (final child in children) { if (child is WidgetSpan) { final w = child.child; if (w is Text && w.key == key) return true; } else if (child is TextSpan) { if (_textSpanContainsKey(child, key)) return true; } } return false; } // 优化分块:避免因为 Text 组件分隔导致额外空行 List> splitTextSpans(List spans, int chunkSize) { if (spans.length <= chunkSize) { return [spans]; } List> chunks = []; bool endsWithNewline(TextSpan s) => s.text != null && s.text!.endsWith('\n'); bool startsWithNewline(TextSpan s) => s.text != null && s.text!.startsWith('\n'); for (int i = 0; i < spans.length; i += chunkSize) { final chunk = spans.sublist(i, (i + chunkSize < spans.length) ? i + chunkSize : spans.length); if (chunk.isEmpty) continue; if (i > 0) { // 对非首块:去掉首 span 的一个前导换行抵消组件分隔换行 final first = chunk.first; if (startsWithNewline(first)) { final newText = first.text!.substring(1); chunk[0] = TextSpan(text: newText, style: first.style, children: first.children); } } if (chunk.length > 1 && endsWithNewline(chunk.last)) { // 除最后一块外,块尾不保留以 \n 结尾的 span(把它挪到下一块) final last = chunk.last; final newText = last.text!.substring(0, last.text!.length - 1); chunk[chunk.length - 1] = TextSpan(text: newText, style: last.style, children: last.children); } chunks.add(chunk); } return chunks; } /// 滚动条控制:保证 ListView/SingleChildScrollView 使用同一个控制器,便于动画 ScrollController trackingScroll() { if (trackingScrollController != null) { return trackingScrollController!; } var trackingScroll = TrackingScrollController(); ScrollController? scrollController = widget.scrollController; double prevOffset = 0; trackingScroll.addListener(() { // iOS 回弹或向上轻微滑动时,驱动外部滚动条联动 if (trackingScroll.offset < -10 || (trackingScroll.offset < 30 && trackingScroll.offset < prevOffset)) { if (scrollController != null && scrollController.offset >= 50) { scrollController.jumpTo(scrollController.offset - max((prevOffset - trackingScroll.offset), 10)); } } prevOffset = trackingScroll.offset; }); if (Platform.isIOS && scrollController != null) { scrollController.addListener(() { if (scrollController.offset >= scrollController.position.maxScrollExtent) { scrollController.jumpTo(scrollController.position.maxScrollExtent); trackingScroll .jumpTo(trackingScroll.offset + (scrollController.offset - scrollController.position.maxScrollExtent)); } }); } trackingScrollController = trackingScroll; return trackingScroll; } } class JsonParser { final dynamic json; final ColorTheme colorTheme; final String indent; final SearchTextController? searchController; int searchMatchTotal = 0; final List matchKeys = []; JsonParser(this.json, this.colorTheme, this.indent, this.searchController); int getLength() { if (json is Map) { return json.length; } else if (json is List) { return json.length; } else { return json == null ? 0 : json.toString().length; } } List getJsonTree() { matchKeys.clear(); // 每次渲染前清空 List textList = []; if (json is Map) { textList.add(const TextSpan(text: '{ \n')); textList.addAll(getMapText(json, prefix: indent)); } else if (json is List) { textList.add(const TextSpan(text: '[ \n')); textList.addAll(getArrayText(json)); } else { textList.add(TextSpan(text: json == null ? '' : json.toString())); textList.add(const TextSpan(text: '\n')); } return textList; } /// 获取Map json List getMapText(Map map, {String openPrefix = '', String prefix = '', String suffix = ''}) { var result = []; var entries = map.entries; for (int i = 0; i < entries.length; i++) { var entry = entries.elementAt(i); String postfix = '${i == entries.length - 1 ? '' : ','} '; var textSpan = TextSpan(text: prefix, children: [ ..._highlightMatches('"${entry.key}"', textColor: colorTheme.propertyKey), const TextSpan(text: ': '), getBasicValue(entry.value, postfix), ]); result.add(textSpan); result.add(const TextSpan(text: '\n')); if (entry.value is Map) { result.addAll(getMapText(entry.value, openPrefix: prefix, prefix: '$prefix$indent', suffix: postfix)); } else if (entry.value is List) { result.addAll(getArrayText(entry.value, openPrefix: prefix, prefix: '$prefix$indent', suffix: postfix)); } } result.add(TextSpan(text: '$openPrefix}$suffix \n')); return result; } /// 获取数组json List getArrayText(List list, {String openPrefix = '', String prefix = '', String suffix = ''}) { var result = []; // result.add(TextSpan(text: '$openPrefix[ \n')); for (int i = 0; i < list.length; i++) { var value = list[i]; String postfix = i == list.length - 1 ? '' : ','; result.add(getBasicValue(value, postfix, prefix: prefix)); result.add(const TextSpan(text: '\n')); if (value is Map) { result.addAll(getMapText(value, openPrefix: '$openPrefix ', prefix: '$prefix$indent', suffix: postfix)); } else if (value is List) { result.addAll(getArrayText(value, openPrefix: '$openPrefix ', prefix: '$prefix$indent', suffix: postfix)); } } result.add(TextSpan(text: '$openPrefix]$suffix \n')); return result; } /// 获取基本类型值 复杂类型会忽略 TextSpan getBasicValue(dynamic value, String suffix, {String? prefix}) { if (value == null) { return TextSpan( text: prefix, children: [..._highlightMatches('null', textColor: colorTheme.keyword), TextSpan(text: suffix)]); } if (value is String) { return TextSpan( text: prefix, children: [..._highlightMatches('"$value"', textColor: colorTheme.string), TextSpan(text: suffix)]); } if (value is num) { return TextSpan( text: prefix, children: [..._highlightMatches(value.toString(), textColor: colorTheme.number), TextSpan(text: suffix)]); } if (value is bool) { return TextSpan( text: prefix, children: [..._highlightMatches(value.toString(), textColor: colorTheme.keyword), TextSpan(text: suffix)]); } if (value is List) { return TextSpan(children: _highlightMatches("${prefix ?? ''}[")); } return TextSpan(children: _highlightMatches("${prefix ?? ''}{")); } List _highlightMatches(String text, {Color? textColor}) { if (searchController == null || searchController?.shouldSearch() == false) { return [TextSpan(text: text, style: TextStyle(color: textColor))]; } final pattern = searchController!.value.pattern; final regex = searchController!.value.isRegExp ? RegExp(pattern, caseSensitive: searchController!.value.isCaseSensitive) : RegExp(RegExp.escape(pattern), caseSensitive: searchController!.value.isCaseSensitive); final spans = []; int start = 0; var allMatches = regex.allMatches(text).toList(); final currentIndex = searchController!.currentMatchIndex.value; for (int i = 0; i < allMatches.length; i++) { final match = allMatches[i]; if (match.start > start) { spans.add(TextSpan(text: text.substring(start, match.start), style: TextStyle(color: textColor))); } // 为每个高亮项分配一个 GlobalKey final key = GlobalKey(); matchKeys.add(key); spans.add(WidgetSpan( alignment: PlaceholderAlignment.middle, baseline: TextBaseline.ideographic, child: Text( text.substring(match.start, match.end), key: key, style: TextStyle( color: textColor, backgroundColor: searchMatchTotal == currentIndex ? colorTheme.searchMatchCurrentColor : colorTheme.searchMatchColor, ), ), )); start = match.end; searchMatchTotal += 1; // 统计总匹配数 } if (start < text.length) { spans.add(TextSpan(text: text.substring(start), style: TextStyle(color: textColor))); } return spans; } } ================================================ FILE: lib/ui/component/json/json_viewer.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:proxypin/ui/component/json/theme.dart'; import 'package:proxypin/ui/component/json/toast.dart'; import 'package:proxypin/utils/lang.dart'; import 'package:proxypin/utils/platform.dart'; import '../search/search_controller.dart'; class JsonViewer extends StatelessWidget { final dynamic jsonObj; final ColorTheme colorTheme; final SearchTextController? searchController; const JsonViewer(this.jsonObj, {super.key, required this.colorTheme, this.searchController}); @override Widget build(BuildContext context) { final matchKeys = []; if (searchController == null) { return DefaultTextStyle.merge( style: const TextStyle(fontWeight: FontWeight.w600), child: getContentWidget(jsonObj, matchTotalCount: ValueWrap.of(0), matchKeys: matchKeys)); } return AnimatedBuilder( animation: searchController ?? ValueNotifier(0), builder: (context, child) { final matchTotalCount = ValueWrap.of(0); matchKeys.clear(); final contentWidget = DefaultTextStyle.merge( style: const TextStyle(fontWeight: FontWeight.w600), child: getContentWidget(jsonObj, matchTotalCount: matchTotalCount, matchKeys: matchKeys)); WidgetsBinding.instance.addPostFrameCallback((_) { searchController?.updateMatchCount(matchTotalCount.get()!); scrollToMatch(matchKeys); }); return contentWidget; }); } Widget getContentWidget(dynamic content, {required ValueWrap matchTotalCount, required List matchKeys}) { if (content is List) { return JsonArrayViewer(content, colorTheme: colorTheme, searchController: searchController, matchTotalCount: matchTotalCount, matchKeys: matchKeys); } else if (content is Map) { return JsonObjectViewer(content, colorTheme: colorTheme, searchController: searchController, matchTotalCount: matchTotalCount, matchKeys: matchKeys); } else { return SelectableText(showCursor: true, content?.toString() ?? ''); } } void scrollToMatch(List matchKeys) { if (searchController != null && matchKeys.isNotEmpty) { final currentIndex = searchController!.currentMatchIndex.value; if (currentIndex >= 0 && currentIndex < matchKeys.length) { final key = matchKeys[currentIndex]; final context = key.currentContext; if (context != null) { Scrollable.ensureVisible( context, duration: const Duration(milliseconds: 300), alignment: 0.5, // 高亮项在视图中的位置 ); } } } } } class JsonObjectViewer extends StatefulWidget { final ColorTheme colorTheme; final Map jsonObj; final bool notRoot; final SearchTextController? searchController; final ValueWrap matchTotalCount; final List matchKeys; const JsonObjectViewer(this.jsonObj, {super.key, this.notRoot = false, required this.colorTheme, this.searchController, required this.matchTotalCount, required this.matchKeys}); @override JsonObjectViewerState createState() => JsonObjectViewerState(); } class JsonObjectViewerState extends State { Map openFlag = {}; @override void didUpdateWidget(covariant JsonObjectViewer oldWidget) { super.didUpdateWidget(oldWidget); openFlag = {}; } @override Widget build(BuildContext context) { if (widget.notRoot) { return Container( padding: const EdgeInsets.only(left: 14.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList()), ); } return Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList()); } List _getList() { List list = []; for (MapEntry entry in widget.jsonObj.entries) { if (openFlag[entry.key] == null) { openFlag[entry.key] = widget.notRoot == false && _isExtensible(entry.value); } list.add(Row( children: [ getKeyWidget(entry), Text(':', style: TextStyle(color: widget.colorTheme.colon)), const SizedBox(width: 3), _copyValue( context, _getValueWidget(entry.value, widget.colorTheme, searchController: widget.searchController, matchTotalCount: widget.matchTotalCount, matchKeys: widget.matchKeys), entry.value), ], )); list.add(const SizedBox(height: 4)); if ((openFlag[entry.key] ?? false) && entry.value != null) { list.add(getContentWidget(entry.value, widget.colorTheme, searchController: widget.searchController, matchTotalCount: widget.matchTotalCount, matchKeys: widget.matchKeys)); } } return list; } // key Widget getKeyWidget(MapEntry entry) { final keyText = entry.key; final keyWidget = Container( constraints: BoxConstraints(maxWidth: 350), child: SelectableText.rich( showCursor: true, TextSpan( children: _highlightText(keyText, TextStyle(color: widget.colorTheme.propertyKey), searchController: widget.searchController, colorTheme: widget.colorTheme, matchTotalCount: widget.matchTotalCount, matchKeys: widget.matchKeys)))); //是否有子层级 if (_isExtensible(entry.value)) { return InkWell( onTap: () { setState(() { openFlag[entry.key] = !(openFlag[entry.key] ?? false); }); }, child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ (openFlag[entry.key] ?? false) ? const Icon(Icons.keyboard_arrow_down, size: 18) : const Icon(Icons.keyboard_arrow_right, size: 18), keyWidget, ], )); } return Row(children: [ const Icon(Icons.keyboard_arrow_right, color: Color.fromARGB(0, 0, 0, 0), size: 18), keyWidget, ]); } static Widget getContentWidget(dynamic content, ColorTheme colorTheme, {SearchTextController? searchController, required ValueWrap matchTotalCount, required List matchKeys}) { if (content is List) { return JsonArrayViewer(content, notRoot: true, colorTheme: colorTheme, searchController: searchController, matchTotalCount: matchTotalCount, matchKeys: matchKeys); } else { return JsonObjectViewer(content, notRoot: true, colorTheme: colorTheme, searchController: searchController, matchTotalCount: matchTotalCount, matchKeys: matchKeys); } } } class JsonArrayViewer extends StatefulWidget { final ColorTheme colorTheme; final List jsonArray; final bool notRoot; final SearchTextController? searchController; final ValueWrap matchTotalCount; final List matchKeys; const JsonArrayViewer(this.jsonArray, {super.key, this.notRoot = false, required this.colorTheme, this.searchController, required this.matchTotalCount, required this.matchKeys}); @override State createState() => _JsonArrayViewerState(); } class _JsonArrayViewerState extends State { late List openFlag; @override Widget build(BuildContext context) { if (widget.notRoot) { return Container( padding: const EdgeInsets.only(left: 14.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList())); } return Column(crossAxisAlignment: CrossAxisAlignment.start, children: _getList()); } @override void initState() { super.initState(); openFlag = List.filled(widget.jsonArray.length, false); } List _getList() { List list = []; int i = 0; for (dynamic content in widget.jsonArray) { list.add(Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ getKeyWidget(content, i), Text(':', style: TextStyle(color: widget.colorTheme.colon)), const SizedBox(width: 3), _copyValue( context, _getValueWidget(content, widget.colorTheme, searchController: widget.searchController, matchTotalCount: widget.matchTotalCount, matchKeys: widget.matchKeys), content) ], )); list.add(const SizedBox(height: 4)); if (openFlag[i]) { list.add(JsonObjectViewerState.getContentWidget(content, widget.colorTheme, searchController: widget.searchController, matchTotalCount: widget.matchTotalCount, matchKeys: widget.matchKeys)); } i++; } return list; } // key Widget getKeyWidget(dynamic content, int index) { //是否有子层级 if (_isExtensible(content)) { return InkWell( onTap: () { setState(() { openFlag[index] = !(openFlag[index]); }); }, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ openFlag[index] ? const Icon(Icons.keyboard_arrow_down, size: 18) : const Icon(Icons.keyboard_arrow_right, size: 18), Text('[$index]', style: TextStyle(color: widget.colorTheme.propertyKey)), ], )); } return Row(children: [ const Icon(Icons.arrow_right, color: Color.fromARGB(0, 0, 0, 0), size: 18), Text('[$index]', style: TextStyle(color: widget.colorTheme.propertyKey)), ]); } } Widget _getValueWidget(dynamic value, ColorTheme colorTheme, {SearchTextController? searchController, required ValueWrap matchTotalCount, required List matchKeys}) { String valueStr; TextStyle style; if (value == null) { valueStr = 'null'; style = TextStyle(color: colorTheme.keyword); } else if (value is num) { valueStr = value.toString(); style = TextStyle(color: colorTheme.number); } else if (value is String) { valueStr = '"$value"'; style = TextStyle(color: colorTheme.string); } else if (value is bool) { valueStr = value.toString(); style = TextStyle(color: colorTheme.keyword); } else if (value is List) { if (value.isEmpty) { valueStr = 'Array[0]'; style = const TextStyle(); } else { valueStr = 'Array<${_getTypeName(value[0])}>[${value.length}]'; style = const TextStyle(); } } else { valueStr = 'Object'; style = const TextStyle(fontSize: 13); } if (searchController?.shouldSearch() == true) { return SelectableText.rich( showCursor: true, TextSpan( children: _highlightText(valueStr, style, searchController: searchController, colorTheme: colorTheme, matchTotalCount: matchTotalCount, matchKeys: matchKeys)), ); } return SelectableText(showCursor: true, valueStr, style: style); } List _highlightText(String text, TextStyle textStyle, {SearchTextController? searchController, required ColorTheme colorTheme, required ValueWrap matchTotalCount, required List matchKeys}) { if (searchController == null || searchController.shouldSearch() == false) { return [TextSpan(text: text, style: textStyle)]; } final pattern = searchController.value.pattern; final regex = searchController.value.isRegExp ? RegExp(pattern, caseSensitive: searchController.value.isCaseSensitive) : RegExp(RegExp.escape(pattern), caseSensitive: searchController.value.isCaseSensitive); final spans = []; int start = 0; var allMatches = regex.allMatches(text).toList(); final currentIndex = searchController.currentMatchIndex.value; for (int i = 0; i < allMatches.length; i++) { final match = allMatches[i]; if (match.start > start) { spans.add(TextSpan(text: text.substring(start, match.start), style: textStyle)); } // 为每个高亮项分配一个 GlobalKey final key = GlobalKey(); matchKeys.add(key); spans.add(WidgetSpan( alignment: PlaceholderAlignment.middle, baseline: TextBaseline.ideographic, child: Text( key: key, text.substring(match.start, match.end), style: textStyle.copyWith( backgroundColor: matchTotalCount.get() == currentIndex ? colorTheme.searchMatchCurrentColor : colorTheme.searchMatchColor, )), )); start = match.end; matchTotalCount.set(matchTotalCount.get()! + 1); } if (start < text.length) { spans.add(TextSpan(text: text.substring(start), style: textStyle)); } return spans; } ///获取值的类型 String _getTypeName(dynamic content) { if (content is int) { return 'int'; } else if (content is String) { return 'String'; } else if (content is bool) { return 'bool'; } else if (content is double) { return 'double'; } else if (content is List) { return 'List'; } return 'Object'; } /// 复制值 Widget _copyValue(BuildContext context, Widget child, Object? value) { return Flexible( child: GestureDetector( onSecondaryTapDown: (details) => showJsonCopyMenu(context, details.globalPosition, value), onTapDown: Platforms.isDesktop() ? null : (details) => showJsonCopyMenu(context, details.globalPosition, value), child: child)); } void showJsonCopyMenu(BuildContext context, Offset position, Object? value) { AppLocalizations localizations = AppLocalizations.of(context)!; //显示复制菜单 showMenu( context: context, position: RelativeRect.fromLTRB(position.dx, position.dy, position.dx, position.dy), items: [ PopupMenuItem( height: 30, child: Text(localizations.copy), onTap: () { if (value == null) { return; } Clipboard.setData(ClipboardData(text: value is String ? value : jsonEncode(value))) .then((value) => Toast.show(localizations.copied, context)); }) ]); } /// 是否可展开 bool _isExtensible(dynamic content) { if (content == null) { return false; } else if (content is int) { return false; } else if (content is String) { return false; } else if (content is bool) { return false; } else if (content is double) { return false; } return true; } ================================================ FILE: lib/ui/component/json/theme.dart ================================================ import 'package:flutter/material.dart'; class ColorTheme { static ColorTheme light(ColorScheme colorScheme) => ColorTheme( background: const Color(0xffffffff), propertyKey: const Color(0xff871094), colon: Colors.black, string: const Color(0xff067d17), number: const Color(0xff1750eb), keyword: const Color(0xff0033b3), searchMatchColor: colorScheme.inversePrimary, searchMatchCurrentColor: colorScheme.primary, ); static ColorTheme dark(ColorScheme colorScheme) => ColorTheme( background: const Color(0XFF1E1F22), propertyKey: const Color(0XFFC77DBB), colon: const Color(0XFFBCBEC4), string: const Color(0XFF6AAB73), number: const Color(0XFF2AACB8), keyword: const Color(0XFFCF8E6D), searchMatchColor: colorScheme.inversePrimary, searchMatchCurrentColor: colorScheme.primary, ); final Color background; final Color propertyKey; final Color colon; final Color string; final Color number; final Color keyword; final Color? searchMatchColor; final Color? searchMatchCurrentColor; const ColorTheme({ required this.background, required this.propertyKey, required this.colon, required this.string, required this.number, required this.keyword, required this.searchMatchColor, required this.searchMatchCurrentColor, }); static ColorTheme of(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final brightness = Theme.of(context).brightness; return brightness == Brightness.dark ? ColorTheme.dark(colorScheme) : ColorTheme.light(colorScheme); } } ================================================ FILE: lib/ui/component/json/toast.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; class Toast { static void show(String message, BuildContext context) { FlutterToastr.show(message, context); } } ================================================ FILE: lib/ui/component/memory_cleanup.dart ================================================ /* * Copyright 2024 hongen Wang All rights reserved. * * 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 * * https://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:io'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/configuration.dart'; /// Memory cleanup handle /// @author wanghongen class MemoryCleanupMonitor { static bool _processing = false; static void onMonitor({Function? onCleanup}) { var threshold = AppConfiguration.current?.memoryCleanupThreshold; if (threshold == null || threshold <= 0) { return; } if (_processing) return; _processing = true; Future.delayed(const Duration(seconds: 3), () { _processing = false; _cleanup(threshold, onCleanup); }); } static void _cleanup(int threshold, Function? onCleanup) { final memory = ProcessInfo.currentRss / 1024 / 1024; logger.d('Memory cleanup, current memory: ${memory.toInt()}M, threshold: ${threshold}M'); if (memory > threshold) { onCleanup?.call(); logger.i('Memory cleanup, current memory: ${memory.toInt()}M, threshold: ${threshold}M, cleanup'); } } } ================================================ FILE: lib/ui/component/model/search_model.dart ================================================ /* * Copyright 2023 Hongen Wang All rights reserved. * * 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 * * https://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 'package:get/get.dart'; import 'package:proxypin/network/http/content_type.dart'; import 'package:proxypin/network/http/http.dart'; /// @author wanghongen /// 2023/8/4 class SearchModel { String? keyword; //是否区分大小写 RxBool caseSensitive = RxBool(false); //搜索范围 Set