├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .task └── checksum │ └── generate-type-defined ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── README.zh-CN.md ├── actions └── build-ui │ └── action.yml ├── cliff.toml ├── crates ├── lynx-cert │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lynx-cli │ ├── .gitignore │ ├── Cargo.toml │ ├── Readme.md │ ├── Readme.zh-CN.md │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── lynx-core │ ├── .gitignore │ ├── Cargo.toml │ ├── Readme.md │ ├── examples │ │ ├── proxy_server_example.rs │ │ └── temp │ │ │ ├── key.pem │ │ │ └── root.pem │ ├── src │ │ ├── client │ │ │ ├── http_client.rs │ │ │ ├── mod.rs │ │ │ ├── request_client.rs │ │ │ └── websocket_client.rs │ │ ├── common │ │ │ └── mod.rs │ │ ├── config │ │ │ └── mod.rs │ │ ├── gateway_service.rs │ │ ├── layers │ │ │ ├── build_proxy_request.rs │ │ │ ├── connect_req_patch_layer │ │ │ │ ├── mod.rs │ │ │ │ └── service.rs │ │ │ ├── error_handle_layer │ │ │ │ ├── future.rs │ │ │ │ ├── layout.rs │ │ │ │ ├── mod.rs │ │ │ │ └── service.rs │ │ │ ├── extend_extension_layer.rs │ │ │ ├── log_layer │ │ │ │ ├── future.rs │ │ │ │ ├── layout.rs │ │ │ │ ├── mod.rs │ │ │ │ └── service.rs │ │ │ ├── message_package_layer │ │ │ │ ├── message_event_data.rs │ │ │ │ ├── message_event_store.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── req_extension_layer │ │ │ │ ├── layout.rs │ │ │ │ ├── mod.rs │ │ │ │ └── service.rs │ │ │ ├── request_processing_layer │ │ │ │ ├── block_handler_trait.rs │ │ │ │ ├── future.rs │ │ │ │ ├── handler_trait.rs │ │ │ │ ├── layout.rs │ │ │ │ ├── local_file_handler_trait.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── modify_request_handler_trait.rs │ │ │ │ ├── modify_response_handler_trait.rs │ │ │ │ ├── proxy_forward_handler_trait.rs │ │ │ │ └── service.rs │ │ │ └── trace_id_layer │ │ │ │ ├── layout.rs │ │ │ │ ├── mod.rs │ │ │ │ └── service.rs │ │ ├── lib.rs │ │ ├── proxy │ │ │ ├── connect_upgraded.rs │ │ │ ├── mod.rs │ │ │ ├── proxy_connect_request.rs │ │ │ ├── proxy_http_request.rs │ │ │ ├── proxy_tunnel_request.rs │ │ │ ├── proxy_ws_request.rs │ │ │ └── tunnel_proxy_by_stream.rs │ │ ├── proxy_server │ │ │ ├── mod.rs │ │ │ ├── server_ca_manage.rs │ │ │ └── server_config.rs │ │ ├── self_service │ │ │ ├── api │ │ │ │ ├── base_info.rs │ │ │ │ ├── certificate.rs │ │ │ │ ├── https_capture.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── net_request.rs │ │ │ │ └── request_processing.rs │ │ │ ├── file_service.rs │ │ │ ├── mod.rs │ │ │ └── utils │ │ │ │ └── mod.rs │ │ ├── server.rs │ │ ├── server_context.rs │ │ └── utils.rs │ └── tests │ │ ├── hello_test.rs │ │ ├── proxy_forward_handler_test.rs │ │ ├── proxy_test.rs │ │ ├── request_processing_handler_test.rs │ │ ├── request_processing_test.rs │ │ ├── setup │ │ ├── mock_rule.rs │ │ ├── mod.rs │ │ ├── setup_mock_server.rs │ │ ├── setup_proxy_handler_server.rs │ │ ├── setup_proxy_server.rs │ │ ├── setup_self_service_test_server.rs │ │ └── setup_tracing.rs │ │ └── temp │ │ ├── subdir │ │ └── nested.txt │ │ ├── test.css │ │ ├── test.html │ │ ├── test.json │ │ └── test.txt ├── lynx-db │ ├── Cargo.toml │ └── src │ │ ├── dao │ │ ├── https_capture_dao.rs │ │ ├── mod.rs │ │ ├── net_request_dao.rs │ │ └── request_processing_dao │ │ │ ├── common.rs │ │ │ ├── error.rs │ │ │ ├── handlers │ │ │ ├── block_handler.rs │ │ │ ├── handler_rule.rs │ │ │ ├── local_file_handler.rs │ │ │ ├── mod.rs │ │ │ ├── modify_request_handler.rs │ │ │ ├── modify_response_handler.rs │ │ │ └── proxy_forward_handler.rs │ │ │ ├── matcher.rs │ │ │ ├── mod.rs │ │ │ ├── types.rs │ │ │ └── validator.rs │ │ ├── entities │ │ ├── app_config.rs │ │ ├── capture.rs │ │ ├── handler.rs │ │ ├── mod.rs │ │ ├── prelude.rs │ │ └── rule.rs │ │ ├── lib.rs │ │ └── migration │ │ ├── app_config.rs │ │ ├── mod.rs │ │ └── request_processing.rs ├── lynx-mock │ ├── .gitignore │ ├── Cargo.toml │ ├── examples │ │ ├── server.rs │ │ └── start_test_server.rs │ └── src │ │ ├── client.rs │ │ ├── lib.rs │ │ ├── mark_service.rs │ │ ├── mock_server_fn.rs │ │ └── server.rs └── lynx-proxy │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── Readme.md │ ├── Readme.zh-CN.md │ ├── eslint.config.mjs │ ├── orval.config.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── public │ ├── .gitkeep │ └── locales │ │ ├── en │ │ └── common.json │ │ └── zh-CN │ │ └── common.json │ ├── rsbuild.config.ts │ ├── src │ ├── App.tsx │ ├── components │ │ ├── LanguageSelector │ │ │ └── index.tsx │ │ ├── MimeTypeIcon │ │ │ └── index.tsx │ │ ├── PageLoading │ │ │ └── index.tsx │ │ ├── RequestContextMenu │ │ │ └── index.tsx │ │ └── SideBar │ │ │ └── index.tsx │ ├── contexts │ │ ├── LanguageContext.tsx │ │ ├── index.ts │ │ ├── useAntdLocale.ts │ │ └── useI18n.ts │ ├── env.d.ts │ ├── env.ts │ ├── global.d.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useDebugMode.ts │ │ └── useRequestContextMenu.ts │ ├── i18n.ts │ ├── index.tsx │ ├── main.css │ ├── mock │ │ ├── handlers.ts │ │ └── node.ts │ ├── routeTree.gen.ts │ ├── routes │ │ ├── __root.tsx │ │ ├── about.tsx │ │ ├── index.tsx │ │ ├── network │ │ │ ├── components │ │ │ │ ├── BackToBottomButton │ │ │ │ │ └── index.tsx │ │ │ │ ├── CleanRequestButton │ │ │ │ │ └── index.tsx │ │ │ │ ├── Contents │ │ │ │ │ ├── CodeViewer │ │ │ │ │ │ ├── hight.theme.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── prism.worker.ts │ │ │ │ │ ├── ContentPreviewTabs │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FormView │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Headers │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── HexViewer │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── JsonPreview │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── MediaViewer │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Reponse │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Request │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TextViewer │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Detail │ │ │ │ │ └── index.tsx │ │ │ │ ├── Overview │ │ │ │ │ └── index.tsx │ │ │ │ ├── RecordingStatusButton │ │ │ │ │ └── index.tsx │ │ │ │ ├── RequestDetailDrawer │ │ │ │ │ └── index.tsx │ │ │ │ ├── RequestTable │ │ │ │ │ └── index.tsx │ │ │ │ ├── RequestTree │ │ │ │ │ └── index.tsx │ │ │ │ ├── Sequence │ │ │ │ │ └── index.tsx │ │ │ │ ├── ShowTypeSegmented │ │ │ │ │ └── index.tsx │ │ │ │ ├── Structure │ │ │ │ │ └── index.tsx │ │ │ │ ├── TableFilter │ │ │ │ │ └── index.tsx │ │ │ │ ├── Toolbar │ │ │ │ │ └── index.tsx │ │ │ │ ├── WebSocketContent │ │ │ │ │ └── index.tsx │ │ │ │ ├── Websocket │ │ │ │ │ └── index.tsx │ │ │ │ └── store │ │ │ │ │ ├── autoScrollStore.tsx │ │ │ │ │ └── selectRequestStore.tsx │ │ │ └── index.tsx │ │ ├── ruleManager │ │ │ ├── components │ │ │ │ ├── InterceptorPage.tsx │ │ │ │ └── InterceptorPage │ │ │ │ │ ├── ActionCell.tsx │ │ │ │ │ ├── ConditionsText.tsx │ │ │ │ │ ├── CopyRuleButton.tsx │ │ │ │ │ └── CreateRuleDrawer │ │ │ │ │ ├── CreateRuleDrawer.tsx │ │ │ │ │ ├── CreateRuleForm.tsx │ │ │ │ │ ├── components │ │ │ │ │ ├── BasicInfo.tsx │ │ │ │ │ ├── CaptureRule.tsx │ │ │ │ │ ├── ComplexCaptureRule.tsx │ │ │ │ │ ├── HandlerBehavior │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── AddHandlerButton.tsx │ │ │ │ │ │ │ ├── HandlerItem.tsx │ │ │ │ │ │ │ ├── HandlerList.tsx │ │ │ │ │ │ │ └── config │ │ │ │ │ │ │ │ ├── BlockHandlerConfig.tsx │ │ │ │ │ │ │ │ ├── LocalFileConfig.tsx │ │ │ │ │ │ │ │ ├── ModifyConfigBase.tsx │ │ │ │ │ │ │ │ ├── ModifyRequestConfig.tsx │ │ │ │ │ │ │ │ ├── ModifyResponseConfig.tsx │ │ │ │ │ │ │ │ ├── ProxyForwardConfig.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── SimpleCaptureCondition.tsx │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ └── index.tsx │ │ ├── settings.tsx │ │ └── settings │ │ │ ├── certificates.tsx │ │ │ ├── components │ │ │ ├── CertificateSetting │ │ │ │ ├── CertInstallDesc.tsx │ │ │ │ └── index.tsx │ │ │ ├── CommonCard │ │ │ │ └── index.tsx │ │ │ ├── GeneralSetting │ │ │ │ └── index.tsx │ │ │ ├── NetworkSetting │ │ │ │ └── index.tsx │ │ │ └── SettingsMenu │ │ │ │ └── index.tsx │ │ │ ├── general.tsx │ │ │ ├── index.tsx │ │ │ └── network.tsx │ ├── services │ │ ├── customInstance.ts │ │ └── generated │ │ │ ├── certificate │ │ │ ├── certificate.msw.ts │ │ │ └── certificate.ts │ │ │ ├── default │ │ │ ├── default.msw.ts │ │ │ └── default.ts │ │ │ ├── https-capture │ │ │ ├── https-capture.msw.ts │ │ │ └── https-capture.ts │ │ │ ├── net-request │ │ │ ├── net-request.msw.ts │ │ │ └── net-request.ts │ │ │ ├── request-processing │ │ │ ├── request-processing.msw.ts │ │ │ └── request-processing.ts │ │ │ ├── system │ │ │ ├── system.msw.ts │ │ │ └── system.ts │ │ │ └── utoipaAxum.schemas.ts │ ├── store │ │ ├── index.tsx │ │ ├── requestTableStore.tsx │ │ ├── requestTreeStore.tsx │ │ ├── useGeneralState.tsx │ │ └── useInterval.ts │ └── utils │ │ ├── curlGenerator.ts │ │ └── ifTrue.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsr.config.json ├── dist-workspace.toml ├── examples ├── self_signed_ca │ ├── .gitignore │ ├── Cargo.toml │ ├── dist.toml │ └── src │ │ └── main.rs ├── websocket-client │ ├── Cargo.toml │ ├── dist.toml │ └── src │ │ └── main.rs └── websocket-server │ ├── Cargo.toml │ ├── dist.toml │ ├── self_signed_certs │ ├── cert.pem │ └── key.pem │ └── src │ └── main.rs ├── images ├── http.png ├── rule.png ├── tree.png └── webscoket.png ├── release.toml ├── rust-toolchain.toml └── taskfile.yml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/rust:1": {}, 5 | "ghcr.io/devcontainers/features/node:1": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | sql.db 3 | crates/lynx-core/assets 4 | # Local 5 | .DS_Store 6 | *.local 7 | *.log* 8 | .idea 9 | .vscode 10 | -------------------------------------------------------------------------------- /.task/checksum/generate-type-defined: -------------------------------------------------------------------------------- 1 | 7a6bac8899e841b08613f243fa70dbb8 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.1] - 2025-06-05 6 | 7 | ### 🚀 Features 8 | 9 | - Proxy Interception Support ([#8](https://github.com/suxin2017/lynx-server/issues/8)) 10 | 11 | ### 🐛 Bug Fixes 12 | 13 | - Test case 14 | - Cli start error 15 | - Dark mode bug 16 | 17 | ## [0.1.6-alpha.1] - 2025-05-25 18 | 19 | ### 🚀 Features 20 | 21 | - Support a more user-friendly experience for rule config and network tree ([#39](https://github.com/suxin2017/lynx-server/issues/39)) 22 | - Support html,css,js,font,video,image,font content preview ([#41](https://github.com/suxin2017/lynx-server/issues/41)) 23 | - Filter support and limit the number of size ([#42](https://github.com/suxin2017/lynx-server/issues/42)) 24 | - Websocket support ([#43](https://github.com/suxin2017/lynx-server/issues/43)) 25 | - Add some layer ([#46](https://github.com/suxin2017/lynx-server/issues/46)) 26 | - Add axum and swagger ([#47](https://github.com/suxin2017/lynx-server/issues/47)) 27 | - Add request session event ([#2](https://github.com/suxin2017/lynx-server/issues/2)) 28 | 29 | ### 🐛 Bug Fixes 30 | 31 | - Ui assert not found 32 | - Window local ip ([#33](https://github.com/suxin2017/lynx-server/issues/33)) 33 | - *(ui)* Clear request log and content ui bug in request tree struce ([#35](https://github.com/suxin2017/lynx-server/issues/35)) 34 | - Unable to create dir on startup ([#36](https://github.com/suxin2017/lynx-server/issues/36)) 35 | - Http1.1, http 1.0 proxy request and lose some header ([#34](https://github.com/suxin2017/lynx-server/issues/34)) 36 | - A lot of bugs 37 | - Table style and websocket log ([#6](https://github.com/suxin2017/lynx-server/issues/6)) 38 | - Record time error ([#7](https://github.com/suxin2017/lynx-server/issues/7)) 39 | - Record time error 40 | 41 | ### 🚜 Refactor 42 | 43 | - Use include dir replace static dir ([#32](https://github.com/suxin2017/lynx-server/issues/32)) 44 | - Refactoring everything ([#44](https://github.com/suxin2017/lynx-server/issues/44)) 45 | 46 | ## [0.1.0] - 2025-02-13 47 | 48 | ### 🚀 Features 49 | 50 | - Rule support 51 | - Add rule group 52 | - Support tariui ([#5](https://github.com/suxin2017/lynx-server/issues/5)) 53 | - *(lynx-core)* Support glob match model 54 | - Support more access ip 55 | - Support certificate download and install doc 56 | - Fetch request log in the app context ([#13](https://github.com/suxin2017/lynx-server/issues/13)) 57 | - Support clear request log ([#16](https://github.com/suxin2017/lynx-server/issues/16)) 58 | - Support ssl capture switch and ssl capture rule ([#18](https://github.com/suxin2017/lynx-server/issues/18)) 59 | - Support better default config dir and support specifying dir ([#21](https://github.com/suxin2017/lynx-server/issues/21)) 60 | 61 | ### 🐛 Bug Fixes 62 | 63 | - Parse request log to json 64 | 65 | 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/lynx-core", 4 | "crates/lynx-cli", 5 | "examples/*", 6 | "crates/lynx-cert", 7 | "crates/lynx-mock", "crates/lynx-db", 8 | ] 9 | exclude = ["third-libs"] 10 | resolver = "2" 11 | 12 | [workspace.package] 13 | version = "0.2.1" 14 | authors = ["suxin2017"] 15 | description = "A proxy service" 16 | edition = "2024" 17 | license = "MIT" 18 | documentation = "https://github.com/suxin2017/lynx-server" 19 | homepage = "https://github.com/suxin2017/lynx-server" 20 | repository = "https://github.com/suxin2017/lynx-server" 21 | 22 | # Add this config to your root Cargo.toml (virtual manifest) 23 | [workspace.metadata.release] 24 | shared-version = true 25 | tag-name = "v{{version}}" 26 | 27 | [workspace.dependencies] 28 | sea-orm = { version = "1.1.0", features = [ 29 | "sqlx-sqlite", 30 | "runtime-tokio-rustls", 31 | "macros", 32 | "with-uuid", 33 | "debug-print", 34 | ] } 35 | tokio = { version = "1.10.0", features = ["full"] } 36 | tracing = "0.1.41" 37 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 38 | anyhow = "1.0.95" 39 | rcgen = { version = "0.13.0", default-features = false, features = [ 40 | "x509-parser", 41 | "pem", 42 | "ring", 43 | ] } 44 | rustls-pemfile = "2.2.0" 45 | include_dir = "0.7.4" 46 | 47 | hyper-tungstenite = "0.17.0" 48 | hyper = { version = "1", features = ["full"] } 49 | hyper-util = { version = "0.1", features = ["full"] } 50 | http-body-util = "0.1" 51 | bytes = "1.9.0" 52 | futures-util = "0.3.31" 53 | tokio-util = { version = "0.7", features = ["io-util", "compat"] } 54 | once_cell = "1.20.2" 55 | tokio-stream = { version = "0.1.14", default-features = false, features = [ 56 | "sync", 57 | ] } 58 | http = "1.0" 59 | tokio-rustls = { version = "0.26.0", default-features = false, features = [ 60 | "ring", 61 | "tls12", 62 | "logging", 63 | ] } 64 | pin-project-lite = "0.2.16" 65 | tower = { version = "0.5.2", features = ["full"] } 66 | tempdir = "0.3.7" 67 | 68 | # The profile that 'dist' will build with 69 | [profile.dist] 70 | inherits = "release" 71 | lto = "thin" 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 suxin2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Lynx Proxy 2 | 3 | [English](README.md) | 简体中文 4 | 5 | [![Crates.io License](https://img.shields.io/crates/l/lynx-core)](./LICENSE) 6 | [![Crates](https://img.shields.io/crates/v/lynx-core.svg)](https://crates.io/crates/lynx-core) 7 | 8 | **Lynx Proxy** 是一款基于 Rust 语言开发的代理抓包工具,项目采用 hyper、axum、tower 等主流 Rust 网络库,以满足不同在开发阶段的需求,比如移动端开发时候查看接口,脚本注入,web 端开发时候将静态资源指向本地服务 9 | 10 | ## 功能特性 11 | 12 | - **常见协议支持**:支持 HTTP(S) 与 WS(S) 13 | - **Web 客户端**:使用流行的现代 web 技术,支持亮色与暗色两种主题 14 | - **Rust 生态**:基于 hyper、axum、tower 等主流库开发。 15 | - **请求面板**: 16 | - 列表视图 17 | ![HTTP 代理示例](./images/http.png) 18 | - 树形视图 19 | ![树形结构视图示例](./images/tree.png) 20 | - **规则捕获与处理** 21 | - 通过添加规则进行请求捕获,同时进行请求处理 22 | - 规则 23 | - 简单规则 (Glob 匹配,正则匹配,HostName,精确匹配) 24 | - 复杂规则 (AND、OR、NOR) 25 | - **安装与升级脚本支持** 26 | - 安装只需要一行脚本,不需要安装任何运行时 27 | - **跨平台支持** 28 | - 支持 Window、Macos、Linux 平台 29 | 30 | ## 功能展示 31 | 32 | ### HTTP/HTTPS 代理 33 | 34 | ![HTTP 代理示例](./images/http.png) 35 | 36 | ### WebSocket 代理 37 | 38 | ![WebSocket 代理示例](./images/webscoket.png) 39 | 40 | ### 树形结构视图 41 | 42 | ![树形结构视图示例](./images/tree.png) 43 | 44 | ### 规则配置 45 | 46 | ![规则配置](./images/rule.png) 47 | 48 | ## 使用 49 | 50 | 通过一键安装脚本快速安装 Lynx Proxy: 51 | 52 | ```bash 53 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/suxin2017/lynx-server/releases/latest/download/lynx-cli-installer.sh | sh 54 | ``` 55 | 56 | ```bash 57 | # 启动服务 58 | lynx-cli 59 | ``` 60 | 61 | ### 命令行参数 62 | 63 | ``` 64 | A proxy service 65 | 66 | Usage: lynx-cli [OPTIONS] 67 | 68 | Options: 69 | -p, --port proxy server port [default: 3000] 70 | --log-level log level [default: silent] [possible values: silent, info, error, debug, trace] 71 | --data-dir data dir if not set, use default data dir 72 | -h, --help Print help 73 | -V, --version Print version 74 | ``` 75 | 76 | ## 贡献指南 77 | 78 | 欢迎社区贡献!请按照以下流程参与开发: 79 | 80 | 1. Fork 本仓库 81 | 2. 创建新分支:`git checkout -b feature-branch` 82 | 3. 安装依赖 83 | - 安装 [taskfile](https://taskfile.dev/) 84 | - 安装 UI 相关依赖 85 | ```bash 86 | task setup-ui 87 | ``` 88 | - 启动开发环境 89 | ```bash 90 | task dev 91 | ``` 92 | 4. 提交更改:`git commit -am 'Add new feature'` 93 | 5. 推送分支:`git push origin feature-branch` 94 | 6. 创建 Pull Request 95 | 96 | ## 许可证 97 | 98 | 本项目采用 MIT 许可证,详情请参阅 [LICENSE](LICENSE) 文件。 99 | 100 | ## 联系我们 101 | 102 | 如有任何问题或建议,请通过 GitHub Issues 提交反馈。 103 | 104 | ## 项目状态 105 | 106 | 项目仍在持续开发中,欢迎关注和参与! 107 | 108 | ## 未来规划 109 | 110 | https://v0-modern-proxy-tool-wq.vercel.app/ 111 | -------------------------------------------------------------------------------- /actions/build-ui/action.yml: -------------------------------------------------------------------------------- 1 | name: Build ui 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Checkout repository 7 | uses: actions/checkout@v4 8 | with: 9 | fetch-depth: 0 10 | - uses: pnpm/action-setup@v4 11 | name: Install pnpm 12 | with: 13 | version: 8 14 | run_install: false 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: "pnpm" 21 | cache-dependency-path: crates/lynx-proxy/pnpm-lock.yaml 22 | 23 | - name: Install Task 24 | uses: arduino/setup-task@v2 25 | with: 26 | version: 3.x 27 | 28 | - name: Pnpm Install 29 | run: task setup-ui 30 | shell: bash 31 | 32 | - name: Pnpm Build 33 | run: task build-ui 34 | shell: bash 35 | -------------------------------------------------------------------------------- /crates/lynx-cert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lynx-cert" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | rcgen = { workspace = true } 14 | anyhow = { workspace = true } 15 | tokio = { workspace = true } 16 | rustls-pemfile = { workspace = true } 17 | rsa = "0.9.7" 18 | time = "0.3.37" 19 | rand = "0.8.5" 20 | tokio-rustls = { version = "0.26.0", default-features = false, features = [ 21 | "ring", 22 | "tls12", 23 | "logging", 24 | ] } 25 | webpki-roots = "0.26.8" 26 | -------------------------------------------------------------------------------- /crates/lynx-cli/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | sql.db 3 | assets/* 4 | # Local 5 | .DS_Store 6 | *.local 7 | *.log* 8 | .idea 9 | .vscode 10 | -------------------------------------------------------------------------------- /crates/lynx-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lynx-cli" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include = ["src", "Cargo.toml", "README.md", "assets"] 12 | 13 | [[bin]] 14 | name = "lynx-cli" 15 | path = "src/main.rs" 16 | 17 | [lib] 18 | name = "lynx_cli" 19 | path = "src/lib.rs" 20 | 21 | [dependencies] 22 | lynx-core = { version = "0.2.1", path = "../lynx-core" } 23 | tracing-subscriber = { workspace = true } 24 | tracing = { workspace = true } 25 | anyhow = { workspace = true } 26 | tokio = { workspace = true } 27 | clap = { version = "4.5.27", features = ["derive"] } 28 | console = { version = "0.15.10", features = ["windows-console-colors"] } 29 | directories = "6.0.0" 30 | include_dir = { workspace = true } 31 | sea-orm = { workspace = true, features = ["sqlx-sqlite"] } 32 | rustls = { version = "0.23.26", features = ["ring"] } 33 | 34 | [dev-dependencies] 35 | tempfile = "3.12.0" 36 | -------------------------------------------------------------------------------- /crates/lynx-cli/Readme.md: -------------------------------------------------------------------------------- 1 | # lynx cli 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | TODO 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/lynx-cli/Readme.zh-CN.md: -------------------------------------------------------------------------------- 1 | # lynx cli 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | 这是一个命令行工具,提供一个web的ui界面 6 | 7 | # 安装 8 | 9 | TODO 10 | 11 | -------------------------------------------------------------------------------- /crates/lynx-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use lynx_cli::{Args, ProxyApp}; 4 | use tokio::signal; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | let args = Args::parse(); 9 | let app = ProxyApp::new(args); 10 | app.start_server().await?; 11 | 12 | signal::ctrl_c() 13 | .await 14 | .expect("Failed to install Ctrl+C signal handler"); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/lynx-core/.gitignore: -------------------------------------------------------------------------------- 1 | examples/temp 2 | tests/temp/key.pem 3 | tests/temp/root.pem 4 | tests/temp/lynx.db -------------------------------------------------------------------------------- /crates/lynx-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lynx-core" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include = ["src", "Cargo.toml", "README.md"] 12 | 13 | [lib] 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | async-trait = "0.1.88" 18 | sea-orm = { workspace = true, features = ["mock"] } 19 | tracing-subscriber = { workspace = true } 20 | tracing = { workspace = true } 21 | anyhow = { workspace = true } 22 | tokio = { workspace = true } 23 | hyper = { workspace = true } 24 | http-body-util = { workspace = true } 25 | hyper-util = { workspace = true } 26 | hyper-tungstenite = "0.17.0" 27 | http = { workspace = true } 28 | tokio-util = { workspace = true } 29 | tokio-rustls = { workspace = true } 30 | tokio-stream = { workspace = true } 31 | tokio-tungstenite = { version = "0.26.1", features = [ 32 | "rustls", 33 | "rustls-tls-webpki-roots", 34 | "connect", 35 | ] } 36 | futures-util = { workspace = true } 37 | bytes = { workspace = true } 38 | hyper-rustls = { version = "0.27.3", default-features = false, features = [ 39 | "webpki-roots", 40 | "webpki-tokio", 41 | "ring", 42 | "http1", 43 | "http2", 44 | "tls12", 45 | ] } 46 | rustls-pemfile = "2.2.0" 47 | moka = { version = "0.12.10", features = ["future"] } 48 | rand = "0.8.5" 49 | rcgen = { version = "0.13.0", default-features = false, features = [ 50 | "x509-parser", 51 | "pem", 52 | "ring", 53 | ] } 54 | time = "0.3.37" 55 | rsa = "0.9.7" 56 | once_cell = { workspace = true } 57 | tower = { workspace = true } 58 | chrono = "0.4.39" 59 | derive_builder = "0.20.2" 60 | local-ip-address = "0.6.3" 61 | serde_json = "1.0.135" 62 | serde = "1.0.217" 63 | nanoid = "0.4.0" 64 | sea-orm-migration = { version = "1.1.0", features = [ 65 | "sqlx-sqlite", 66 | "runtime-tokio-rustls", 67 | ] } 68 | url = "2.5.4" 69 | include_dir = { workspace = true } 70 | mime_guess = "=2.0.5" 71 | glob-match = "0.2.1" 72 | regex = "1.11.1" 73 | base64 = "0.22.1" 74 | lynx-cert = { path = "../lynx-cert" } 75 | lynx-db = { path = "../lynx-db" } 76 | pin-project-lite = { workspace = true } 77 | axum = "0.8.4" 78 | utoipa-swagger-ui = { version = "9.0.1", features = ["axum"] } 79 | utoipa-axum = "0.2.0" 80 | utoipa = { version = "5.3.1", features = ["axum_extras"] } 81 | tower-http = { version = "0.6.4", features = ["cors", "fs"] } 82 | dashmap = "6.1.0" 83 | 84 | 85 | [dev-dependencies] 86 | dotenv = "0.15.0" 87 | async-compression = { version = "0.4.18", features = ["gzip", "tokio"] } 88 | tempdir = "0.3.7" 89 | tempfile = "3.12.0" 90 | lynx-mock = { path = "../lynx-mock" } 91 | async-once-cell = "0.5.4" 92 | tower-test = { version = "0.4.0" } 93 | tokio-test = "0.4.4" 94 | reqwest = { version = "0.12.18", features = [ 95 | "rustls-tls-manual-roots", 96 | "gzip", 97 | "json", 98 | "stream", 99 | "rustls-tls", 100 | ] } 101 | reqwest-websocket = "0.5.0" 102 | -------------------------------------------------------------------------------- /crates/lynx-core/Readme.md: -------------------------------------------------------------------------------- 1 | # lynx core 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | Proxy Core Server 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/lynx-core/examples/proxy_server_example.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf, sync::Arc}; 2 | 3 | use anyhow::Result; 4 | use lynx_core::proxy_server::{ 5 | ProxyServerBuilder, server_ca_manage::ServerCaManagerBuilder, 6 | server_config::ProxyServerConfigBuilder, 7 | }; 8 | use sea_orm::ConnectOptions; 9 | use tokio::signal; 10 | use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | tracing_subscriber::registry() 15 | .with(fmt::layer()) 16 | .with(EnvFilter::from_default_env().add_directive("lynx_core=trace".parse()?)) 17 | .init(); 18 | 19 | let fixed_temp_dir_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/temp"); 20 | 21 | if !fixed_temp_dir_path.exists() { 22 | fs::create_dir_all(&fixed_temp_dir_path)?; 23 | } 24 | 25 | let server_config = ProxyServerConfigBuilder::default() 26 | .root_cert_file_path(fixed_temp_dir_path.join("root.pem")) 27 | .root_key_file_path(fixed_temp_dir_path.join("key.pem")) 28 | .build()?; 29 | 30 | let server_ca_manager = ServerCaManagerBuilder::new( 31 | server_config.root_cert_file_path.clone(), 32 | server_config.root_key_file_path.clone(), 33 | ) 34 | .build()?; 35 | 36 | let mut proxy_server = ProxyServerBuilder::default() 37 | .config(Arc::new(server_config)) 38 | .port(3000) 39 | .server_ca_manager(Arc::new(server_ca_manager)) 40 | .db_config(ConnectOptions::new(format!( 41 | "sqlite://{}/lynx.db?mode=rwc", 42 | fixed_temp_dir_path.to_string_lossy() 43 | ))) 44 | .build() 45 | .await?; 46 | proxy_server.run().await?; 47 | 48 | signal::ctrl_c() 49 | .await 50 | .expect("Failed to install Ctrl+C signal handler"); 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /crates/lynx-core/examples/temp/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCYZOoGu7Xs/u0q 3 | eU+EyKZBgFJIOTkmpmJBgkd8LkFg0Va4jbhGstXc5b77G17sFOG6C/7o0PBWk9+W 4 | kjn4veWJTc7re7+F0pAGGRncQqUo2k+nzxWGVJ9U8GhFvDCZcxMcNvcX8bPFr2Al 5 | D8YQB8J1nZSU7OAIECFdr++PaXJzbmUcNu1hn+nI5pJm2S+Di+4Bx7R1xTxgHnlN 6 | yP4LwZcongJBt024PUeWS2A87nBybd/e4Iqj7wSsLlsYPZdwnMHYmmN4oZpIk59r 7 | AnPEBGW8kq/aK4Wjc5H62tbBI6ujTJFUiLQlYUw/F9jda+rU0CJ9ghewfIEJOWig 8 | p7lsx3RjAgMBAAECggEAEHyKXOQd0F/b5IZvNdxHMDrjq/CU4DuL0a54cVTHueOr 9 | 0Bd04iEixd0NBjl10XCI4wm9MA66kYM54x46q1BP7sS+J/ehRqb68c3xYETNfVfw 10 | 2iSrHXc2LYg8n562W0N5w8mmfa6mVjKc4iWMtdtBDFxZ4KqnqfekxG/uKMOtoBMc 11 | nI8IJcPlvglxsllR8RLcetE59OdZNI5IPT23dK/Ipyj7rHoxpTX4N15ydCWR5vBI 12 | lMwei77KCJNtxU663fdTr9eE16BJLcmaPgBkjdowWKCEBfgXXj8ukv5dM3hSdukH 13 | VgVrF3ZGG6WgNzzL6+pXz3S9JwwDZVMbBaPzUT+I4QKBgQDFFyRWYJJbKHwVG5yU 14 | zHpsMjS244Oe2ecIx8tL9iOHZtnZS/ypvk7Q5HJnk5JvBfYj2cd2b1kmuMW8cilC 15 | zWF7/LyqWlVN71ZuYwOw+3cbQx+yQ9d6oMZBqO3wR+Knho6ZAxrEuGxUgwPyoZZJ 16 | WBhog2OfXRRRkrmf/WdSZ5I1mwKBgQDF8btmzu8+sAstEjkOK/RZPOrY9DRAcYJx 17 | C85MP2KCLsA69fNTC6NPJSZkPVNpCHGvj1z78j6RWrHVJN6rRQ6Vr44eP9ZFV1VP 18 | YCVgITRQzMDi7C/jNUeIto7xPVVPiCSmY5moMMDFsWZNCSz35u98YqpmR6qI0OS6 19 | GH4cc6pM2QKBgQCV0ki6LME08KqadRnrdyEc/HFcEcltSOG6p/5fqSVK+aFi8MOJ 20 | 2XQakX5yRBkNsq9wg02AN5bCu7T80p+Q+4U+dlqI+RBdpTHDyhr1P8NEAxumLLIx 21 | suPi5+KwREUE6mGd6WFA55zaBZpLqBARgxlS4YYqj9wxQmM/Pqd7WeYoPwKBgQCu 22 | JNpjO2Ed/JEIiQSrJB5nuAFA969UlshUnjdTu1v1/h1ege0dPZriUWOyQoW0XRpv 23 | gqgie99xz6GuTC6d7TZEmFtm33CqNog2OfcH2I6HG2wC+Bm1QbV6YGnncLcyLitY 24 | Oz3+y019X3IKCi2Gt6QwATm6nAg7L9RaqJuielv0GQKBgHu7L49fvFNVxcDSB4gs 25 | sP3hnI3pm76kg0XPhAuYgGLS7LidVZBMJfkdqY3sypG6VKDudBo1NO944jY5qohD 26 | JdkWWfSh/UWyD9Roi7ciufcZzjS4Eg9bCNwP4YqdoDJb6mcww/BKI4KSw4teW3nT 27 | PNEJyc/mmMbnX//3RLOl4xql 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /crates/lynx-core/examples/temp/root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDNjCCAh6gAwIBAgIUFK+7Au/HdGWaLeujVJiyxKQgY8AwDQYJKoZIhvcNAQEL 3 | BQAwKDESMBAGA1UEAwwJbHlueFByb3h5MRIwEAYDVQQKDAlseW54UHJveHkwHhcN 4 | MTUwNjAzMTQzNDM0WhcNMzUwNTI5MTQzNDM0WjAoMRIwEAYDVQQDDAlseW54UHJv 5 | eHkxEjAQBgNVBAoMCWx5bnhQcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 6 | AQoCggEBAJhk6ga7tez+7Sp5T4TIpkGAUkg5OSamYkGCR3wuQWDRVriNuEay1dzl 7 | vvsbXuwU4boL/ujQ8FaT35aSOfi95YlNzut7v4XSkAYZGdxCpSjaT6fPFYZUn1Tw 8 | aEW8MJlzExw29xfxs8WvYCUPxhAHwnWdlJTs4AgQIV2v749pcnNuZRw27WGf6cjm 9 | kmbZL4OL7gHHtHXFPGAeeU3I/gvBlyieAkG3Tbg9R5ZLYDzucHJt397giqPvBKwu 10 | Wxg9l3CcwdiaY3ihmkiTn2sCc8QEZbySr9orhaNzkfra1sEjq6NMkVSItCVhTD8X 11 | 2N1r6tTQIn2CF7B8gQk5aKCnuWzHdGMCAwEAAaNYMFYwDwYDVR0PAQH/BAUDAwcG 12 | ADATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUgnxCoYtAotajI71U9M53 13 | ZUzdZKkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAZJBsvm7G 14 | SkbHWcyUmb58+D/J6SGgcC+fu5VMeRg1jTIXFmfFCbBXZk4u3WU94pjhXR+I/B4P 15 | KC+xCO1yh6stLtd+PJCwPEHTBDV4k84VaPsyBRS8U4M/IvFc4zRPRt/VbwQKJsbM 16 | AHIDtKwWNqdB0ARabYmLz6qJMDugPfP3ckEgvaCzilrtU2h2yYgbPzZv6Y3GyH9P 17 | EnibeHPy/UF3bJcl7ZuO0G/d7VwsqEiVX41WfkqESPxOb9QfWzE8m1Gpj8IcGRNm 18 | MKDas/Y5Uz+ZXFa69w/EKJJe9p3DUw5Nf46B6OQ22aFclQHPq4lZ95xIxwMC67L5 19 | lwDbZJkgvc24SQ== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /crates/lynx-core/src/client/http_client.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::{Result, anyhow}; 4 | use bytes::Bytes; 5 | use http_body_util::combinators::BoxBody; 6 | use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; 7 | use hyper_util::{ 8 | client::legacy::{Client, connect::HttpConnector}, 9 | rt::TokioExecutor, 10 | }; 11 | use lynx_cert::gen_client_config_by_cert; 12 | use rcgen::Certificate; 13 | 14 | use crate::common::{HyperRes, Req}; 15 | 16 | pub struct HttpClient { 17 | client: Client, BoxBody>, 18 | } 19 | 20 | #[derive(Default)] 21 | pub struct HttpClientBuilder { 22 | custom_certs: Option>>>, 23 | } 24 | 25 | impl HttpClient { 26 | pub async fn request(&self, req: Req) -> Result { 27 | self.client 28 | .request(req) 29 | .await 30 | .map_err(|e| anyhow!(e).context("http request client error")) 31 | } 32 | } 33 | 34 | impl HttpClientBuilder { 35 | pub fn custom_certs(mut self, custom_certs: Option>>>) -> Self { 36 | self.custom_certs = custom_certs; 37 | self 38 | } 39 | 40 | pub fn build(&self) -> Result { 41 | let cert_chain = self.custom_certs.clone(); 42 | 43 | let client_config = gen_client_config_by_cert(cert_chain.clone())?; 44 | 45 | let connector = HttpsConnectorBuilder::new() 46 | .with_tls_config(client_config) 47 | .https_or_http() 48 | .enable_all_versions() 49 | .build(); 50 | let client_builder = Client::builder(TokioExecutor::new()); 51 | 52 | let client = client_builder.build(connector); 53 | Ok(HttpClient { client }) 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use anyhow::Ok; 60 | 61 | use super::*; 62 | 63 | #[test] 64 | fn build_http_client() -> Result<()> { 65 | let client = HttpClientBuilder::default().custom_certs(None).build(); 66 | assert!(client.is_ok()); 67 | Ok(()) 68 | } 69 | 70 | #[tokio::test] 71 | #[ignore = "need stable network connect"] 72 | async fn test_http_request() -> Result<()> { 73 | let client = HttpClientBuilder::default().custom_certs(None).build()?; 74 | 75 | let url = "https://example.com"; 76 | let response = client.client.get(url.parse()?).await?; 77 | assert_eq!(response.status(), 200); 78 | let url = "http://example.com"; 79 | let response = client.client.get(url.parse()?).await?; 80 | assert_eq!(response.status(), 200); 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/lynx-core/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http_client; 2 | pub mod request_client; 3 | pub mod websocket_client; 4 | 5 | pub use request_client::RequestClient; 6 | -------------------------------------------------------------------------------- /crates/lynx-core/src/client/request_client.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use http::Extensions; 5 | use rcgen::Certificate; 6 | 7 | use super::{ 8 | http_client::{HttpClient, HttpClientBuilder}, 9 | websocket_client::{WebsocketClient, WebsocketClientBuilder}, 10 | }; 11 | 12 | pub struct RequestClient { 13 | http_client: Arc, 14 | websocket_client: Arc, 15 | } 16 | 17 | #[derive(Default)] 18 | pub struct RequestClientBuilder { 19 | custom_certs: Option>>>, 20 | } 21 | 22 | impl RequestClientBuilder { 23 | pub fn custom_certs(mut self, custom_certs: Option>>>) -> Self { 24 | self.custom_certs = custom_certs; 25 | self 26 | } 27 | 28 | pub fn build(&self) -> Result { 29 | let custom_certs = self.custom_certs.clone(); 30 | 31 | let http_client = Arc::new( 32 | HttpClientBuilder::default() 33 | .custom_certs(custom_certs.clone()) 34 | .build()?, 35 | ); 36 | let websocket_client = Arc::new( 37 | WebsocketClientBuilder::default() 38 | .custom_certs(custom_certs) 39 | .build()?, 40 | ); 41 | 42 | Ok(RequestClient { 43 | http_client, 44 | websocket_client, 45 | }) 46 | } 47 | } 48 | 49 | pub type ShareRequestClient = Arc; 50 | 51 | pub trait RequestClientExt { 52 | fn get_request_client(&self) -> Option; 53 | fn get_http_client(&self) -> Arc; 54 | fn get_websocket_client(&self) -> Arc; 55 | } 56 | 57 | impl RequestClientExt for Extensions { 58 | fn get_request_client(&self) -> Option { 59 | self.get::().map(Arc::clone) 60 | } 61 | 62 | fn get_http_client(&self) -> Arc { 63 | self.get::() 64 | .map(|c| Arc::clone(&c.http_client)) 65 | .expect("RequestClient not found") 66 | } 67 | 68 | fn get_websocket_client(&self) -> Arc { 69 | self.get::() 70 | .map(|c| Arc::clone(&c.websocket_client)) 71 | .expect("RequestClient not found") 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | 79 | #[test] 80 | fn build_request_client_test() { 81 | let client = RequestClientBuilder::default().custom_certs(None).build(); 82 | assert!(client.is_ok()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/lynx-core/src/client/websocket_client.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use anyhow::Result; 4 | use lynx_cert::gen_client_config_by_cert; 5 | use rcgen::Certificate; 6 | use tokio_tungstenite::{ 7 | Connector, WebSocketStream, connect_async_tls_with_config, 8 | tungstenite::client::IntoClientRequest, 9 | }; 10 | 11 | pub struct WebsocketClient { 12 | connector: Connector, 13 | } 14 | 15 | #[derive(Default)] 16 | pub struct WebsocketClientBuilder { 17 | custom_certs: Option>>>, 18 | } 19 | 20 | impl WebsocketClient { 21 | pub async fn request( 22 | &self, 23 | req: R, 24 | ) -> Result<( 25 | WebSocketStream>, 26 | http::Response>>, 27 | )> 28 | where 29 | R: IntoClientRequest + Unpin + Debug, 30 | { 31 | println!("WebsocketClient request: {:?}", req); 32 | let websocket_stream = 33 | connect_async_tls_with_config(req, None, false, Some(self.connector.clone())).await?; 34 | 35 | Ok(websocket_stream) 36 | } 37 | } 38 | 39 | impl WebsocketClientBuilder { 40 | pub fn custom_certs(mut self, custom_certs: Option>>>) -> Self { 41 | self.custom_certs = custom_certs; 42 | self 43 | } 44 | pub fn build(&self) -> Result { 45 | let cert_chain = self.custom_certs.clone(); 46 | 47 | let client_config = gen_client_config_by_cert(cert_chain.clone())?; 48 | 49 | let connector = Connector::Rustls(Arc::new(client_config)); 50 | 51 | Ok(WebsocketClient { connector }) 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | use futures_util::{SinkExt, StreamExt}; 59 | 60 | #[tokio::test] 61 | #[ignore = "need stable network connect"] 62 | async fn websocket_test() -> Result<()> { 63 | let client = WebsocketClientBuilder::default() 64 | .custom_certs(None) 65 | .build()?; 66 | let (stream, _) = client.request("wss://echo.websocket.org/").await?; 67 | let (mut sink, stream) = stream.split(); 68 | 69 | sink.send(tokio_tungstenite::tungstenite::Message::Text( 70 | "Hello, World!".into(), 71 | )) 72 | .await?; 73 | sink.close().await?; 74 | 75 | let data: Vec<_> = stream.collect().await; 76 | 77 | assert!(data.len() > 1); 78 | 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/lynx-core/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use http::{Request, Response}; 3 | use http_body_util::{BodyExt, combinators::BoxBody as HttpBoxBody}; 4 | use hyper::body::Incoming; 5 | 6 | pub type HyperReq = hyper::Request; 7 | pub type HyperRes = hyper::Response; 8 | 9 | pub type BoxBody = HttpBoxBody; 10 | pub type Req = Request; 11 | pub type Res = Response; 12 | 13 | pub trait HyperReqExt { 14 | fn into_box_req(self) -> Req; 15 | } 16 | 17 | impl HyperReqExt for HyperReq { 18 | fn into_box_req(self) -> Req { 19 | let (parts, body) = self.into_parts(); 20 | let body = body 21 | .map_err(|e| anyhow!(e).context("http request body box error")) 22 | .boxed(); 23 | Request::from_parts(parts, body) 24 | } 25 | } 26 | 27 | pub trait HyperResExt { 28 | fn into_box_res(self) -> Res; 29 | } 30 | 31 | impl HyperResExt for HyperRes { 32 | fn into_box_res(self) -> Res { 33 | let (parts, body) = self.into_parts(); 34 | let body = body 35 | .map_err(|e| anyhow!(e).context("http response body box error")) 36 | .boxed(); 37 | Response::from_parts(parts, body) 38 | } 39 | } 40 | 41 | pub async fn is_https_tcp_stream(tcp_stream: &tokio::net::TcpStream) -> bool { 42 | let mut buf = [0; 1]; 43 | match tcp_stream.peek(&mut buf).await { 44 | Ok(n) => n == 1 && buf[0] == 0x16, 45 | Err(_) => false, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/lynx-core/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use derive_builder::Builder; 4 | use include_dir::Dir; 5 | use tracing::debug; 6 | 7 | #[derive(Builder, Debug, Default, Clone)] 8 | pub struct AppConfig { 9 | pub assets_ui_root_dir: Option>, 10 | pub assets_root_dir: PathBuf, 11 | pub ca_root_dir: PathBuf, 12 | pub raw_root_dir: PathBuf, 13 | pub db_root_dir: PathBuf, 14 | } 15 | 16 | impl AppConfig { 17 | pub fn get_root_ca_path(&self) -> PathBuf { 18 | self.ca_root_dir.join("root_ca.pem") 19 | } 20 | pub fn get_root_ca_key(&self) -> PathBuf { 21 | self.ca_root_dir.join("root_ca.key") 22 | } 23 | pub fn get_db_path(&self) -> PathBuf { 24 | self.db_root_dir.join("proxy.sqlite") 25 | } 26 | } 27 | 28 | #[derive(Debug, Default)] 29 | pub struct InitAppConfigParams { 30 | pub assets_ui_root_dir: Option>, 31 | pub root_dir: Option, 32 | } 33 | 34 | pub fn set_up_config_dir(init_params: InitAppConfigParams) -> AppConfig { 35 | let default_assets_root_dir = if let Some(root_dir) = init_params.root_dir { 36 | root_dir 37 | } else { 38 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets") 39 | }; 40 | 41 | let default_ca_root_dir = default_assets_root_dir.join("ca"); 42 | let default_raw_root_dir = default_assets_root_dir.join("raw"); 43 | let default_db_root_dir = default_assets_root_dir.join("db"); 44 | 45 | let config = AppConfigBuilder::create_empty() 46 | .assets_root_dir(default_assets_root_dir) 47 | .ca_root_dir(default_ca_root_dir) 48 | .db_root_dir(default_db_root_dir) 49 | .raw_root_dir(default_raw_root_dir) 50 | .assets_ui_root_dir(init_params.assets_ui_root_dir) 51 | .build() 52 | .expect("init assets dir error"); 53 | 54 | create_dir_if_not_exists(&config.assets_root_dir); 55 | create_dir_if_not_exists(&config.ca_root_dir); 56 | create_dir_if_not_exists(&config.db_root_dir); 57 | create_dir_if_not_exists(&config.raw_root_dir); 58 | config 59 | } 60 | 61 | pub fn create_dir_if_not_exists(dir: &PathBuf) { 62 | if !fs::exists(dir) 63 | .unwrap_or_else(|_| panic!("can't check existence of {}", &dir.to_string_lossy())) 64 | { 65 | fs::create_dir_all(dir) 66 | .unwrap_or_else(|e| panic!("can't create {}\nreason: {}", &dir.to_string_lossy(), e)); 67 | debug!("create dir {}", &dir.to_string_lossy()); 68 | } 69 | debug!("dir {} exists", &dir.to_string_lossy()); 70 | } 71 | 72 | pub const REQ_DIR: &str = "req"; 73 | pub const RES_DIR: &str = "res"; 74 | -------------------------------------------------------------------------------- /crates/lynx-core/src/gateway_service.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use axum::response::Response; 3 | // 添加这一行来获取 oneshot 方法 4 | 5 | use crate::common::Req; 6 | use crate::self_service::self_service_router; 7 | use crate::{ 8 | proxy::{ 9 | proxy_connect_request::{is_connect_req, proxy_connect_request}, 10 | proxy_http_request::{is_http_req, proxy_http_request}, 11 | proxy_tunnel_request::proxy_tunnel_proxy, 12 | proxy_ws_request::{is_websocket_req, proxy_ws_request}, 13 | }, 14 | self_service::is_self_service, 15 | }; 16 | 17 | pub async fn proxy_gateway_service_fn(req: Req) -> Result { 18 | if is_websocket_req(&req) { 19 | return proxy_ws_request(req).await; 20 | } 21 | if is_http_req(&req) { 22 | return proxy_http_request(req).await; 23 | } 24 | proxy_tunnel_proxy(req).await 25 | } 26 | 27 | pub async fn gateway_service_fn(req: Req) -> Result { 28 | if is_self_service(&req) { 29 | return self_service_router(req).await; 30 | } 31 | if is_connect_req(&req) { 32 | return proxy_connect_request(req).await; 33 | } 34 | proxy_gateway_service_fn(req).await 35 | } 36 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/build_proxy_request.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | pin::Pin, 4 | str::FromStr, 5 | task::{Context, Poll}, 6 | }; 7 | 8 | use anyhow::Result; 9 | use axum::response::Response; 10 | use http::{ 11 | Request, Uri, 12 | header::{CONNECTION, HOST, PROXY_AUTHORIZATION}, 13 | }; 14 | use tower::Service; 15 | use url::Url; 16 | 17 | use crate::{ 18 | common::Req, 19 | layers::extend_extension_layer::clone_extensions, 20 | }; 21 | 22 | #[derive(Clone)] 23 | pub struct BuildProxyRequestService { 24 | pub service: S, 25 | } 26 | 27 | impl Service for BuildProxyRequestService 28 | where 29 | S: Service 30 | + Clone 31 | + Send 32 | + Sync 33 | + 'static, 34 | S::Future: Send, 35 | { 36 | type Response = S::Response; 37 | type Error = S::Error; 38 | type Future = Pin> + Send>>; 39 | 40 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 41 | self.service.poll_ready(cx) 42 | } 43 | 44 | fn call(&mut self, req: Req) -> Self::Future { 45 | let mut s = self.service.clone(); 46 | Box::pin(async move { 47 | let extensions = clone_extensions(req.extensions())?; 48 | let (parts, body) = req.into_parts(); 49 | 50 | let uri = { 51 | let url = Url::from_str(parts.uri.to_string().as_str())?; 52 | Uri::from_str(url.as_str())? 53 | }; 54 | 55 | let mut req_builder = Request::builder().method(parts.method).uri(uri); 56 | 57 | for (key, value) in parts.headers.iter() { 58 | if matches!(key, &HOST | &CONNECTION | &PROXY_AUTHORIZATION) { 59 | continue; 60 | } 61 | req_builder = req_builder.header(key.clone(), value.clone()); 62 | } 63 | let mut proxy_req = req_builder.body(body)?; 64 | 65 | proxy_req.extensions_mut().extend(extensions); 66 | s.call(proxy_req).await 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/connect_req_patch_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod service; 2 | 3 | pub use service::ConnectReqPatchService; 4 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/connect_req_patch_layer/service.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll}; 2 | 3 | use http::{ 4 | Request, Uri, Version, 5 | uri::{Authority, Scheme}, 6 | }; 7 | use tower::Service; 8 | 9 | use crate::common::Req; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct ConnectReqPatchService { 13 | pub service: S, 14 | pub authority: Authority, 15 | pub schema: Scheme, 16 | } 17 | 18 | impl Service for ConnectReqPatchService 19 | where 20 | S: Service, 21 | { 22 | type Response = S::Response; 23 | type Error = S::Error; 24 | type Future = S::Future; 25 | 26 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 27 | self.service.poll_ready(cx) 28 | } 29 | 30 | fn call(&mut self, request: Req) -> Self::Future { 31 | let req = if matches!(request.version(), Version::HTTP_10 | Version::HTTP_11) { 32 | let (mut parts, body) = request.into_parts(); 33 | parts.uri = { 34 | let mut parts = parts.uri.into_parts(); 35 | parts.scheme = Some(self.schema.clone()); 36 | parts.authority = Some(self.authority.clone()); 37 | Uri::from_parts(parts).expect("Failed to build URI") 38 | }; 39 | Request::from_parts(parts, body) 40 | } else { 41 | request 42 | }; 43 | 44 | self.service.call(req) 45 | } 46 | } 47 | 48 | pub struct ConnectReqPatchLayer { 49 | authority: Authority, 50 | schema: Scheme, 51 | } 52 | 53 | impl ConnectReqPatchLayer { 54 | pub fn new(authority: Authority, schema: Scheme) -> Self { 55 | Self { authority, schema } 56 | } 57 | } 58 | 59 | impl tower::Layer for ConnectReqPatchLayer { 60 | type Service = ConnectReqPatchService; 61 | 62 | fn layer(&self, service: S) -> Self::Service { 63 | ConnectReqPatchService { 64 | service, 65 | authority: self.authority.clone(), 66 | schema: self.schema.clone(), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/error_handle_layer/future.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Write, 3 | task::{Context, Poll, ready}, 4 | }; 5 | 6 | use anyhow::Result; 7 | use axum::response::{IntoResponse, Response}; 8 | use http::StatusCode; 9 | use pin_project_lite::pin_project; 10 | use tracing::error; 11 | 12 | 13 | pin_project! { 14 | pub struct ErrorHandleFuture { 15 | #[pin] 16 | pub f: F, 17 | } 18 | } 19 | 20 | impl Future for ErrorHandleFuture 21 | where 22 | F: Future>, 23 | { 24 | type Output = F::Output; 25 | 26 | fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 27 | let this = self.project(); 28 | let res = ready!(this.f.poll(cx)); 29 | 30 | if let Err(err) = &res { 31 | error!("Error occurred: {:?}", err); 32 | let error_reason = 33 | err.chain() 34 | .enumerate() 35 | .fold(String::new(), |mut output, (i, cause)| { 36 | let _ = if i == 0 { 37 | writeln!(output, "Error: {cause}") 38 | } else { 39 | writeln!(output, "Caused by: {cause}") 40 | }; 41 | output 42 | }); 43 | 44 | let res = Response::builder() 45 | .status(StatusCode::INTERNAL_SERVER_ERROR) 46 | .body(error_reason) 47 | .map(|r| r.into_response()) 48 | .map_err(|e| anyhow::anyhow!(e)); 49 | 50 | return Poll::Ready(res); 51 | } 52 | Poll::Ready(res) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/error_handle_layer/layout.rs: -------------------------------------------------------------------------------- 1 | use tower::Layer; 2 | 3 | use super::ErrorHandlerService; 4 | 5 | pub struct ErrorHandlerLayer; 6 | 7 | impl Layer for ErrorHandlerLayer { 8 | type Service = ErrorHandlerService; 9 | 10 | fn layer(&self, service: S) -> Self::Service { 11 | ErrorHandlerService { service } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/error_handle_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod future; 2 | pub mod layout; 3 | pub mod service; 4 | 5 | pub use future::ErrorHandleFuture; 6 | pub use layout::ErrorHandlerLayer; 7 | pub use service::ErrorHandlerService; 8 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/error_handle_layer/service.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll}; 2 | 3 | use axum::response::Response; 4 | use tower::Service; 5 | 6 | use crate::common::Req; 7 | 8 | use super::ErrorHandleFuture; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct ErrorHandlerService { 12 | pub service: S, 13 | } 14 | 15 | impl Service for ErrorHandlerService 16 | where 17 | S: Service, 18 | { 19 | type Response = S::Response; 20 | type Error = S::Error; 21 | type Future = ErrorHandleFuture; 22 | 23 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 24 | self.service.poll_ready(cx) 25 | } 26 | 27 | fn call(&mut self, request: Req) -> Self::Future { 28 | ErrorHandleFuture { 29 | f: self.service.call(request), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/log_layer/future.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll, ready}; 2 | 3 | use anyhow::Result; 4 | use axum::response::Response; 5 | use pin_project_lite::pin_project; 6 | use tracing::Span; 7 | 8 | pin_project! { 9 | pub struct LogFuture { 10 | #[pin] 11 | pub f: F, 12 | pub span: Span 13 | } 14 | } 15 | 16 | impl Future for LogFuture 17 | where 18 | F: Future>, 19 | { 20 | type Output = F::Output; 21 | 22 | fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 23 | let this = self.project(); 24 | let _enter = this.span.enter(); 25 | let res = ready!(this.f.poll(cx))?; 26 | Poll::Ready(Ok(res)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/log_layer/layout.rs: -------------------------------------------------------------------------------- 1 | use tower::Layer; 2 | 3 | use super::LogService; 4 | 5 | pub struct LogLayer; 6 | 7 | impl Layer for LogLayer { 8 | type Service = LogService; 9 | 10 | fn layer(&self, service: S) -> Self::Service { 11 | LogService { service } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/log_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod future; 2 | pub mod layout; 3 | pub mod service; 4 | 5 | pub use future::LogFuture; 6 | pub use layout::LogLayer; 7 | pub use service::LogService; 8 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/log_layer/service.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll}; 2 | 3 | use axum::response::Response; 4 | use tower::Service; 5 | use tracing::{info, info_span}; 6 | 7 | use crate::{common::Req, layers::log_layer::LogFuture}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct LogService { 11 | pub service: S, 12 | } 13 | 14 | impl Service for LogService 15 | where 16 | S: Service, 17 | { 18 | type Response = S::Response; 19 | type Error = S::Error; 20 | type Future = LogFuture; 21 | 22 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 23 | self.service.poll_ready(cx) 24 | } 25 | 26 | fn call(&mut self, request: Req) -> Self::Future { 27 | // Insert log statement here or other functionality 28 | let span = info_span!("log_service",); 29 | let future = { 30 | info!("handling request for {:?}", request.uri()); 31 | self.service.call(request) 32 | }; 33 | LogFuture { f: future, span } 34 | } 35 | } -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod build_proxy_request; 2 | pub mod connect_req_patch_layer; 3 | pub mod error_handle_layer; 4 | pub mod extend_extension_layer; 5 | pub mod log_layer; 6 | pub mod message_package_layer; 7 | pub mod req_extension_layer; 8 | pub mod trace_id_layer; 9 | pub mod request_processing_layer; -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/req_extension_layer/layout.rs: -------------------------------------------------------------------------------- 1 | use tower::Layer; 2 | 3 | use super::RequestExtensionService; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct RequestExtensionLayer { 7 | value: V, 8 | } 9 | 10 | impl RequestExtensionLayer { 11 | pub fn new(value: V) -> Self { 12 | RequestExtensionLayer { value } 13 | } 14 | } 15 | 16 | impl Layer for RequestExtensionLayer { 17 | type Service = RequestExtensionService; 18 | 19 | fn layer(&self, service: S) -> Self::Service { 20 | RequestExtensionService { 21 | service, 22 | value: self.value.clone(), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/req_extension_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod layout; 2 | pub mod service; 3 | 4 | pub use layout::RequestExtensionLayer; 5 | pub use service::RequestExtensionService; 6 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/req_extension_layer/service.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll}; 2 | 3 | use http::Request; 4 | use tower::Service; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct RequestExtensionService { 8 | pub service: S, 9 | pub value: V, 10 | } 11 | 12 | impl Service> for RequestExtensionService 13 | where 14 | S: Service>, 15 | V: Clone + Sync + Send + 'static, 16 | { 17 | type Response = S::Response; 18 | type Error = S::Error; 19 | type Future = S::Future; 20 | 21 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 22 | self.service.poll_ready(cx) 23 | } 24 | 25 | fn call(&mut self, mut request: Request) -> Self::Future { 26 | request.extensions_mut().insert(self.value.clone()); 27 | self.service.call(request) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/request_processing_layer/future.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use anyhow::Result; 8 | use axum::response::Response; 9 | use pin_project_lite::pin_project; 10 | 11 | pin_project! { 12 | pub struct RequestProcessingFuture { 13 | #[pin] 14 | pub f: F, 15 | } 16 | } 17 | 18 | impl Future for RequestProcessingFuture 19 | where 20 | F: Future>, 21 | { 22 | type Output = F::Output; 23 | 24 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 25 | let this = self.project(); 26 | this.f.poll(cx) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/request_processing_layer/handler_trait.rs: -------------------------------------------------------------------------------- 1 | use crate::common::Req; 2 | use anyhow::Result; 3 | use axum::response::Response; 4 | 5 | /// Represents the type of result returned by request handling operations. 6 | /// This enum allows handlers to return either processed request information 7 | /// or response information based on the handling logic. 8 | pub enum HandleRequestType { 9 | /// Contains processed request information 10 | Request(Req), 11 | /// Contains processed response information 12 | Response(Response), 13 | } 14 | 15 | #[async_trait::async_trait] 16 | pub trait HandlerTrait { 17 | async fn handle_request(&self, request: Req) -> Result; 18 | 19 | async fn handle_response(&self, response: Response) -> Result { 20 | Ok(response) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/request_processing_layer/layout.rs: -------------------------------------------------------------------------------- 1 | use tower::Layer; 2 | 3 | use super::service::RequestProcessingService; 4 | 5 | pub struct RequestProcessingLayer; 6 | 7 | impl Layer for RequestProcessingLayer { 8 | type Service = RequestProcessingService; 9 | 10 | fn layer(&self, service: S) -> Self::Service { 11 | RequestProcessingService::new(service) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/request_processing_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod future; 2 | pub mod layout; 3 | pub mod service; 4 | pub mod handler_trait; 5 | pub mod block_handler_trait; 6 | pub mod modify_request_handler_trait; 7 | pub mod modify_response_handler_trait; 8 | pub mod local_file_handler_trait; 9 | pub mod proxy_forward_handler_trait; 10 | 11 | pub use future::RequestProcessingFuture; 12 | pub use layout::RequestProcessingLayer; 13 | pub use service::RequestProcessingService; 14 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/trace_id_layer/layout.rs: -------------------------------------------------------------------------------- 1 | use tower::Layer; 2 | 3 | use super::TraceIdService; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct TraceIdLayer; 7 | 8 | impl Layer for TraceIdLayer { 9 | type Service = TraceIdService; 10 | 11 | fn layer(&self, service: S) -> Self::Service { 12 | TraceIdService { service } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/trace_id_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod layout; 2 | pub mod service; 3 | 4 | pub use layout::TraceIdLayer; 5 | pub use service::TraceIdService; 6 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/trace_id_layer/service.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::Arc, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use http::{Extensions, Request}; 7 | use nanoid::nanoid; 8 | use tower::Service; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct TraceIdService { 12 | pub service: S, 13 | } 14 | 15 | pub type TraceId = Arc; 16 | 17 | impl Service> for TraceIdService 18 | where 19 | S: Service>, 20 | { 21 | type Response = S::Response; 22 | type Error = S::Error; 23 | type Future = S::Future; 24 | 25 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 26 | self.service.poll_ready(cx) 27 | } 28 | 29 | fn call(&mut self, mut request: Request) -> Self::Future { 30 | request.extensions_mut().insert(Arc::new(nanoid!())); 31 | self.service.call(request) 32 | } 33 | } 34 | 35 | pub trait TraceIdExt { 36 | fn get_trace_id(&self) -> TraceId; 37 | } 38 | 39 | impl TraceIdExt for Extensions { 40 | fn get_trace_id(&self) -> TraceId { 41 | self.get::().expect("expect trace id").clone() 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use anyhow::{Ok, Result}; 48 | 49 | use super::*; 50 | 51 | #[test] 52 | fn get_trace_id_test() -> Result<()> { 53 | let mut req = Request::builder().body(())?; 54 | req.extensions_mut().insert(Arc::new(nanoid!())); 55 | let _trace_id = req.extensions().get_trace_id(); 56 | 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/lynx-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod common; 3 | pub mod config; 4 | pub mod gateway_service; 5 | pub mod layers; 6 | pub mod proxy; 7 | pub mod proxy_server; 8 | pub mod self_service; 9 | pub mod server_context; 10 | pub mod utils; 11 | -------------------------------------------------------------------------------- /crates/lynx-core/src/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod connect_upgraded; 2 | pub mod proxy_connect_request; 3 | pub mod proxy_http_request; 4 | pub mod proxy_tunnel_request; 5 | pub mod proxy_ws_request; 6 | pub mod tunnel_proxy_by_stream; 7 | -------------------------------------------------------------------------------- /crates/lynx-core/src/proxy/proxy_http_request.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use axum::response::{IntoResponse, Response}; 3 | use tower::{ServiceBuilder, ServiceExt, service_fn}; 4 | 5 | use crate::{ 6 | client::request_client::RequestClientExt, 7 | common::Req, 8 | layers::{ 9 | build_proxy_request::BuildProxyRequestService, 10 | message_package_layer::ProxyMessageEventService, 11 | request_processing_layer::RequestProcessingService, trace_id_layer::service::TraceIdExt, 12 | }, 13 | }; 14 | 15 | pub fn is_http_req(req: &Req) -> bool { 16 | req.headers().get("Upgrade").is_none() 17 | } 18 | 19 | async fn proxy_http_request_inner(req: Req) -> Result { 20 | let trace_id = req.extensions().get_trace_id().clone(); 21 | let http_client = req.extensions().get_http_client(); 22 | http_client 23 | .request(req) 24 | .await 25 | .map_err(|e| e.context("http request failed")) 26 | .map(|mut res| { 27 | res.extensions_mut().insert(trace_id); 28 | res 29 | }) 30 | .map(|res| res.into_response()) 31 | } 32 | 33 | pub async fn proxy_http_request(req: Req) -> Result { 34 | let svc = service_fn(proxy_http_request_inner); 35 | 36 | let svc = ServiceBuilder::new() 37 | .layer_fn(|s| BuildProxyRequestService { service: s }) 38 | .layer_fn(|s| ProxyMessageEventService { service: s }) 39 | .layer_fn(|s| RequestProcessingService { service: s }) 40 | .service(svc); 41 | 42 | let res = svc.oneshot(req).await?; 43 | Ok(res.into_response()) 44 | } 45 | -------------------------------------------------------------------------------- /crates/lynx-core/src/proxy/proxy_tunnel_request.rs: -------------------------------------------------------------------------------- 1 | 2 | use anyhow::{Ok, Result}; 3 | use axum::body::Body; 4 | use axum::response::Response; 5 | use hyper::Method; 6 | use hyper_util::rt::TokioIo; 7 | use tracing::error; 8 | 9 | use crate::common::Req; 10 | use crate::layers::message_package_layer::MessageEventLayerExt; 11 | use crate::layers::trace_id_layer::service::TraceIdExt; 12 | use crate::utils::host_addr; 13 | 14 | use super::tunnel_proxy_by_stream::tunnel_proxy_by_stream; 15 | 16 | fn handle_tunnel_error(err: anyhow::Error) { 17 | error!("Error handling tunnel: {}", err); 18 | } 19 | 20 | pub async fn proxy_tunnel_proxy(req: Req) -> anyhow::Result { 21 | assert_eq!(req.method(), Method::CONNECT); 22 | 23 | tokio::task::spawn(async move { 24 | let res = tunnel_proxy_by_req(req).await; 25 | if let Err(err) = res { 26 | handle_tunnel_error(err); 27 | } 28 | }); 29 | 30 | Ok(Response::new(Body::empty())) 31 | } 32 | 33 | pub async fn tunnel_proxy_by_req(req: Req) -> Result<()> { 34 | let trace_id = req.extensions().get_trace_id(); 35 | let event_cannel = req.extensions().get_message_event_cannel(); 36 | let addr = host_addr(req.uri()).ok_or_else(|| anyhow::anyhow!("Invalid URI: {}", req.uri()))?; 37 | 38 | let upgraded = hyper::upgrade::on(req).await?; 39 | 40 | tunnel_proxy_by_stream(TokioIo::new(upgraded), addr, trace_id, event_cannel).await?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /crates/lynx-core/src/proxy/tunnel_proxy_by_stream.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use tokio::{ 5 | io::{AsyncRead, AsyncWrite}, 6 | net::{TcpStream, ToSocketAddrs}, 7 | }; 8 | use tracing::{trace, warn}; 9 | 10 | use crate::layers::{message_package_layer::MessageEventChannel, trace_id_layer::service::TraceId}; 11 | 12 | pub async fn tunnel_proxy_by_stream< 13 | S: AsyncRead + AsyncWrite + Unpin + Send + 'static, 14 | A: ToSocketAddrs, 15 | >( 16 | mut stream: S, 17 | addr: A, 18 | trace_id: TraceId, 19 | event_cannel: Arc, 20 | ) -> Result<()> { 21 | // let mut upgraded = TokioIo::new(stream); 22 | let mut server = TcpStream::connect(addr).await?; 23 | 24 | event_cannel 25 | .dispatch_on_tunnel_start(trace_id.clone()) 26 | .await; 27 | let res = tokio::io::copy_bidirectional(&mut stream, &mut server).await; 28 | 29 | match res { 30 | Ok((from_client, from_server)) => { 31 | trace!( 32 | "client wrote {} bytes and received {} bytes", 33 | from_client, from_server 34 | ); 35 | event_cannel.dispatch_on_tunnel_end(trace_id).await; 36 | } 37 | Err(e) => { 38 | warn!("tunnel error {:?}", e); 39 | event_cannel.dispatch_on_tunnel_end(trace_id).await; 40 | } 41 | } 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /crates/lynx-core/src/proxy_server/server_config.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use derive_builder::Builder; 4 | use http::Extensions; 5 | 6 | #[derive(Builder, Debug, Default, Clone)] 7 | pub struct ProxyServerConfig { 8 | pub root_cert_file_path: PathBuf, 9 | pub root_key_file_path: PathBuf, 10 | } 11 | 12 | pub trait ProxyServerConfigExtensionsExt { 13 | fn get_proxy_server_config(&self) -> Arc; 14 | } 15 | 16 | impl ProxyServerConfigExtensionsExt for Extensions { 17 | fn get_proxy_server_config(&self) -> Arc { 18 | self.get::>() 19 | .expect("proxy server config not found") 20 | .clone() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/lynx-core/src/self_service/api/base_info.rs: -------------------------------------------------------------------------------- 1 | use crate::self_service::{ 2 | RouteState, 3 | utils::{ResponseDataWrapper, ok}, 4 | }; 5 | use axum::{Json, extract::State}; 6 | use http::StatusCode; 7 | use utoipa::ToSchema; 8 | use utoipa_axum::{router::OpenApiRouter, routes}; 9 | 10 | #[derive(ToSchema, serde::Serialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct BaseInfo { 13 | access_addr_list: Vec, 14 | } 15 | 16 | #[utoipa::path( 17 | get, 18 | path = "/base_info", 19 | tags = ["System"], 20 | responses( 21 | (status = 200, description = "Successfully retrieved base info", body = ResponseDataWrapper), 22 | (status = 500, description = "Failed to get base info") 23 | ) 24 | )] 25 | async fn get_base_info( 26 | State(RouteState { 27 | access_addr_list, .. 28 | }): State, 29 | ) -> Result>, StatusCode> { 30 | let info = BaseInfo { 31 | access_addr_list: access_addr_list 32 | .iter() 33 | .map(|addr| addr.to_string()) 34 | .collect(), 35 | }; 36 | Ok(Json(ok(info))) 37 | } 38 | 39 | pub fn router(state: RouteState) -> OpenApiRouter { 40 | OpenApiRouter::new() 41 | .routes(routes!(get_base_info)) 42 | .with_state(state) 43 | } 44 | -------------------------------------------------------------------------------- /crates/lynx-core/src/self_service/api/certificate.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::{Json, extract::State}; 3 | use utoipa_axum::{router::OpenApiRouter, routes}; 4 | 5 | use crate::self_service::RouteState; 6 | use crate::self_service::utils::{ResponseDataWrapper, ok}; 7 | use axum::http::header; 8 | use axum::response::IntoResponse; 9 | use tokio::fs::File; 10 | use tokio::io::AsyncReadExt; 11 | 12 | #[utoipa::path( 13 | get, 14 | path = "/path", 15 | tags = ["Certificate"], 16 | responses( 17 | (status = 200, description = "Successfully retrieved certificate file path", body = ResponseDataWrapper), 18 | (status = 500, description = "Failed to get certificate path") 19 | ) 20 | )] 21 | async fn get_cert_path( 22 | State(state): State, 23 | ) -> Result>, StatusCode> { 24 | Ok(Json(ok(state 25 | .proxy_config 26 | .root_cert_file_path 27 | .to_string_lossy() 28 | .to_string()))) 29 | } 30 | 31 | #[utoipa::path( 32 | get, 33 | path = "/download", 34 | tags = ["Certificate"], 35 | responses( 36 | (status = 200, description = "Successfully downloaded root certificate file", content_type = "application/x-x509-ca-cert"), 37 | (status = 404, description = "Root certificate file not found"), 38 | (status = 500, description = "Failed to read root certificate file") 39 | ) 40 | )] 41 | async fn download_certificate( 42 | State(state): State, 43 | ) -> Result { 44 | let cert_path = &state.proxy_config.root_cert_file_path; 45 | 46 | // Try to open and read the certificate file 47 | let mut file = File::open(&cert_path) 48 | .await 49 | .map_err(|_| StatusCode::NOT_FOUND)?; 50 | 51 | let mut contents = Vec::new(); 52 | file.read_to_end(&mut contents) 53 | .await 54 | .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 55 | 56 | // Create the response with appropriate headers 57 | let headers = [ 58 | (header::CONTENT_TYPE, "application/x-x509-ca-cert"), 59 | ( 60 | header::CONTENT_DISPOSITION, 61 | "attachment; filename=\"certificate.crt\"", 62 | ), 63 | ]; 64 | 65 | Ok((headers, contents)) 66 | } 67 | 68 | pub fn router(state: RouteState) -> OpenApiRouter { 69 | OpenApiRouter::new() 70 | .routes(routes!(get_cert_path)) 71 | .routes(routes!(download_certificate)) 72 | .with_state(state) 73 | } 74 | -------------------------------------------------------------------------------- /crates/lynx-core/src/self_service/api/https_capture.rs: -------------------------------------------------------------------------------- 1 | use crate::self_service::RouteState; 2 | use crate::self_service::utils::{AppError, ErrorResponse, ResponseDataWrapper, ok}; 3 | use axum::Json; 4 | use axum::extract::State; 5 | use lynx_db::dao::https_capture_dao::{CaptureFilter, HttpsCaptureDao}; 6 | use utoipa::TupleUnit; 7 | use utoipa_axum::router::OpenApiRouter; 8 | use utoipa_axum::routes; 9 | 10 | #[utoipa::path( 11 | get, 12 | path = "/https-capture/filter", 13 | tags = ["HTTPS Capture"], 14 | responses( 15 | (status = 200, description = "Successfully retrieved HTTPS capture filter", body = ResponseDataWrapper), 16 | (status = 500, description = "Failed to get HTTPS capture filter",body = ErrorResponse), 17 | ) 18 | )] 19 | async fn get_https_capture_filter( 20 | State(RouteState { db, .. }): State, 21 | ) -> Result>, AppError> { 22 | let dao = HttpsCaptureDao::new(db); 23 | let filter = dao 24 | .get_capture_filter() 25 | .await 26 | .map_err(|e| AppError::DatabaseError(e.to_string()))?; 27 | Ok(Json(ok(filter))) 28 | } 29 | 30 | #[utoipa::path( 31 | post, 32 | path = "/https-capture/filter", 33 | tags = ["HTTPS Capture"], 34 | request_body = CaptureFilter, 35 | responses( 36 | (status = 200, description = "Successfully updated HTTPS capture filter", body = ResponseDataWrapper), 37 | (status = 500, description = "Failed to update HTTPS capture filter",body = ErrorResponse) 38 | ) 39 | )] 40 | async fn update_https_capture_filter( 41 | State(RouteState { db, .. }): State, 42 | Json(filter): Json, 43 | ) -> Result>, AppError> { 44 | let dao = HttpsCaptureDao::new(db); 45 | dao.update_capture_filter(filter) 46 | .await 47 | .map_err(|e| AppError::DatabaseError(e.to_string()))?; 48 | Ok(Json(ok(()))) 49 | } 50 | 51 | pub fn router(state: RouteState) -> OpenApiRouter { 52 | OpenApiRouter::new() 53 | .routes(routes!( 54 | get_https_capture_filter, 55 | update_https_capture_filter 56 | )) 57 | .with_state(state) 58 | } 59 | -------------------------------------------------------------------------------- /crates/lynx-core/src/self_service/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod base_info; 2 | pub mod certificate; 3 | pub mod https_capture; 4 | pub mod net_request; 5 | pub mod request_processing; 6 | -------------------------------------------------------------------------------- /crates/lynx-core/src/self_service/file_service.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::State; 2 | use axum::response::IntoResponse; 3 | use http::header::CONTENT_TYPE; 4 | use http::{HeaderMap, Uri}; 5 | use mime_guess::from_path; 6 | 7 | use super::RouteState; 8 | 9 | pub async fn get_file( 10 | file_path: Uri, 11 | State(RouteState { static_dir, .. }): State, 12 | ) -> impl IntoResponse { 13 | if let Some(static_dir) = static_dir { 14 | let file_path = file_path.path().trim_start_matches('/'); 15 | 16 | let file_path = if file_path.is_empty() { 17 | "index.html" 18 | } else { 19 | file_path 20 | }; 21 | 22 | let res = static_dir.0.get_file(file_path); 23 | 24 | if let Some(res) = res { 25 | let mime_type = from_path(file_path).first_or_octet_stream(); 26 | let content_type = mime_type.to_string(); 27 | let mut header = HeaderMap::new(); 28 | header.insert(CONTENT_TYPE, content_type.parse().unwrap()); 29 | return (http::StatusCode::OK, header, res.contents()); 30 | } 31 | } 32 | let mut header = HeaderMap::new(); 33 | header.insert(CONTENT_TYPE, "text/plain".parse().unwrap()); 34 | (http::StatusCode::OK, header, "not found".as_bytes()) 35 | } 36 | -------------------------------------------------------------------------------- /crates/lynx-core/src/server_context.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use sea_orm::DatabaseConnection; 3 | 4 | use crate::{ 5 | // cert::{CertificateAuthority, set_up_ca_manager}, 6 | config::{AppConfig, InitAppConfigParams}, 7 | proxy_server::server_ca_manage::ServerCaManager, 8 | }; 9 | 10 | pub static APP_CONFIG: OnceCell = OnceCell::new(); 11 | pub static CA_MANAGER: OnceCell = OnceCell::new(); 12 | pub static DB: OnceCell = OnceCell::new(); 13 | 14 | #[derive(Debug, Default)] 15 | pub struct InitContextParams { 16 | pub init_app_config_params: InitAppConfigParams, 17 | } 18 | 19 | pub fn get_db_connect() -> &'static DatabaseConnection { 20 | DB.get().unwrap() 21 | } 22 | 23 | pub fn get_app_config() -> &'static AppConfig { 24 | APP_CONFIG.get().unwrap() 25 | } 26 | 27 | pub fn get_ca_manager() -> &'static ServerCaManager { 28 | CA_MANAGER.get().unwrap() 29 | } 30 | -------------------------------------------------------------------------------- /crates/lynx-core/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | use anyhow::{Error, anyhow}; 5 | use http_body_util::combinators::BoxBody; 6 | use http_body_util::{BodyExt, Empty, Full}; 7 | use hyper::body::Bytes; 8 | 9 | pub fn host_addr(uri: &http::Uri) -> Option { 10 | uri.authority().map(|auth| auth.to_string()) 11 | } 12 | 13 | pub fn empty() -> BoxBody { 14 | Empty::::new() 15 | .map_err(|never| anyhow!(never)) 16 | .boxed() 17 | } 18 | 19 | pub fn full>(chunk: T) -> BoxBody { 20 | Full::new(chunk.into()) 21 | .map_err(|never| anyhow!(never)) 22 | .boxed() 23 | } 24 | 25 | pub fn is_http(uri: &http::Uri) -> bool { 26 | uri.scheme_str().map(|s| s == "http").unwrap_or(false) 27 | } 28 | 29 | pub fn is_https(uri: &http::Uri) -> bool { 30 | matches!(uri.port_u16(), Some(443)) 31 | } 32 | 33 | pub fn get_current_timestamp_millis() -> u128 { 34 | let start = SystemTime::now(); 35 | let since_the_epoch = start 36 | .duration_since(UNIX_EPOCH) 37 | .expect("Time went backwards"); 38 | since_the_epoch.as_millis() 39 | } 40 | 41 | pub async fn read_file(file_path: &PathBuf) -> anyhow::Result> { 42 | let content = tokio::fs::read(file_path).await?; 43 | Ok(content) 44 | } 45 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/hello_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use setup::{base_url, setup_self_service_test_server::setup_self_service_test_server}; 3 | mod setup; 4 | 5 | #[tokio::test] 6 | async fn hello_test() -> Result<()> { 7 | let (server, client) = setup_self_service_test_server().await?; 8 | let base_url = base_url(&server); 9 | let res = client 10 | .get_request_client() 11 | .get(format!("{}/health", base_url)) 12 | .send() 13 | .await?; 14 | assert_eq!("ok", res.text().await?); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/proxy_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Ok, Result}; 2 | use lynx_db::dao::https_capture_dao::{CaptureFilter, HttpsCaptureDao}; 3 | use lynx_mock::client::MockClient; 4 | use setup::{setup_mock_server::setup_mock_server, setup_proxy_server::setup_proxy_server}; 5 | use std::sync::Arc; 6 | mod setup; 7 | 8 | #[tokio::test] 9 | async fn proxy_test() -> Result<()> { 10 | let mock_server = setup_mock_server().await?; 11 | let proxy_server = setup_proxy_server(Some(Arc::new(vec![mock_server.cert.clone()]))).await?; 12 | let proxy_server_root_ca = proxy_server.server_ca_manager.ca_cert.clone(); 13 | 14 | HttpsCaptureDao::new(proxy_server.db_connect.clone()) 15 | .update_capture_filter(CaptureFilter { 16 | enabled: true, 17 | include_domains: vec![], 18 | exclude_domains: vec![], 19 | }) 20 | .await?; 21 | 22 | let proxy_addr = format!("http://{}", proxy_server.access_addr_list.first().unwrap()); 23 | 24 | let client = MockClient::new( 25 | Some(vec![mock_server.cert.clone(), proxy_server_root_ca]), 26 | Some(proxy_addr), 27 | )?; 28 | client.test_request_http_request(&mock_server).await?; 29 | client.test_request_https_request(&mock_server).await?; 30 | client.test_request_websocket(&mock_server).await?; 31 | client.test_request_tls_websocket(&mock_server).await?; 32 | 33 | Ok(()) 34 | } 35 | 36 | #[tokio::test] 37 | async fn test_real_world_request() -> Result<()> { 38 | let mock_server = setup_mock_server().await?; 39 | let proxy_server = setup_proxy_server(Some(Arc::new(vec![mock_server.cert.clone()]))).await?; 40 | let proxy_server_root_ca = proxy_server.server_ca_manager.ca_cert.clone(); 41 | 42 | let proxy_addr = format!("http://{}", proxy_server.access_addr_list.first().unwrap()); 43 | 44 | let client = MockClient::new(Some(vec![proxy_server_root_ca]), Some(proxy_addr))?; 45 | client.test_real_world_http_request().await?; 46 | client.test_real_world_https_request().await?; 47 | // FIXME: The websocket test is not working due to the server not being able to handle the request. 48 | // client.test_real_world_websocket_request().await?; 49 | // client.test_real_world_tls_websocket_request().await?; 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/mock_rule.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use lynx_db::dao::request_processing_dao::{ 5 | CaptureRule, HandlerRule, RequestProcessingDao, RequestRule, 6 | types::{CaptureCondition, SimpleCaptureCondition}, 7 | }; 8 | use lynx_db::entities::capture::CaptureType; 9 | use sea_orm::DatabaseConnection; 10 | 11 | #[allow(dead_code)] 12 | pub async fn create_test_rule( 13 | dao: &RequestProcessingDao, 14 | name: &str, 15 | enabled: bool, 16 | ) -> Result { 17 | let rule = RequestRule { 18 | id: None, 19 | name: name.to_string(), 20 | description: Some("Test rule description".to_string()), 21 | enabled, 22 | priority: 1, 23 | capture: create_basic_capture_rule(), 24 | handlers: vec![], 25 | }; 26 | 27 | dao.create_rule(rule).await 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub fn create_basic_capture_rule() -> CaptureRule { 32 | use lynx_db::dao::request_processing_dao::types::UrlPattern; 33 | 34 | CaptureRule { 35 | id: None, 36 | condition: CaptureCondition::Simple(SimpleCaptureCondition { 37 | url_pattern: Some(UrlPattern { 38 | capture_type: CaptureType::Glob, 39 | pattern: "/api/*".to_string(), 40 | }), 41 | method: Some("GET".to_string()), 42 | host: None, 43 | headers: None, 44 | }), 45 | } 46 | } 47 | 48 | #[allow(dead_code)] 49 | pub async fn mock_test_rule( 50 | db: Arc, 51 | handlers: Vec, 52 | ) -> Result { 53 | let dao = RequestProcessingDao::new(db); 54 | 55 | let rule = RequestRule { 56 | id: None, 57 | name: "Test Rule".to_string(), 58 | description: Some("Test rule description".to_string()), 59 | enabled: true, 60 | priority: 1, 61 | capture: CaptureRule { 62 | id: None, 63 | condition: CaptureCondition::Simple(SimpleCaptureCondition { 64 | url_pattern: Some(lynx_db::dao::request_processing_dao::types::UrlPattern { 65 | capture_type: CaptureType::Glob, 66 | pattern: "*".to_string(), 67 | }), 68 | method: None, 69 | host: None, 70 | headers: None, 71 | }), 72 | }, 73 | handlers, 74 | }; 75 | dao.create_rule(rule).await 76 | } 77 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/mod.rs: -------------------------------------------------------------------------------- 1 | use lynx_core::{proxy_server::ProxyServer, self_service::SELF_SERVICE_PATH_PREFIX}; 2 | use lynx_mock::server::MockServer; 3 | 4 | pub mod mock_rule; 5 | pub mod setup_mock_server; 6 | pub mod setup_proxy_handler_server; 7 | pub mod setup_proxy_server; 8 | pub mod setup_self_service_test_server; 9 | pub mod setup_tracing; 10 | 11 | #[allow(dead_code)] 12 | pub fn base_url(proxy_server: &ProxyServer) -> String { 13 | format!( 14 | "http://{}{}", 15 | proxy_server 16 | .access_addr_list 17 | .first() 18 | .expect("show get access addr"), 19 | SELF_SERVICE_PATH_PREFIX 20 | ) 21 | } 22 | 23 | #[allow(dead_code)] 24 | pub fn mock_base_url(mock_server: &MockServer) -> String { 25 | format!("http://{}", mock_server.addr) 26 | } 27 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/setup_mock_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Ok, Result}; 2 | 3 | use lynx_mock::server::MockServer; 4 | 5 | #[allow(dead_code)] 6 | pub async fn setup_mock_server() -> Result { 7 | let mut mock_server = MockServer::new(None); 8 | mock_server.start_server().await?; 9 | Ok(mock_server) 10 | } 11 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/setup_proxy_handler_server.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::{Ok, Result}; 4 | 5 | use lynx_core::proxy_server::ProxyServer; 6 | use lynx_db::dao::https_capture_dao::{CaptureFilter, HttpsCaptureDao}; 7 | use lynx_mock::{client::MockClient, server::MockServer}; 8 | 9 | use super::{setup_mock_server::setup_mock_server, setup_proxy_server::setup_proxy_server}; 10 | 11 | #[allow(dead_code)] 12 | pub async fn setup_proxy_handler_server() -> Result<(ProxyServer, MockServer, MockClient)> { 13 | let mock_server = setup_mock_server().await?; 14 | let proxy_server = setup_proxy_server(Some(Arc::new(vec![mock_server.cert.clone()]))).await?; 15 | let proxy_server_root_ca = proxy_server.server_ca_manager.ca_cert.clone(); 16 | 17 | HttpsCaptureDao::new(proxy_server.db_connect.clone()) 18 | .update_capture_filter(CaptureFilter { 19 | enabled: true, 20 | include_domains: vec![], 21 | exclude_domains: vec![], 22 | }) 23 | .await?; 24 | 25 | let proxy_addr = format!("http://{}", proxy_server.access_addr_list.first().unwrap()); 26 | 27 | let client = MockClient::new( 28 | Some(vec![mock_server.cert.clone(), proxy_server_root_ca]), 29 | Some(proxy_addr), 30 | )?; 31 | Ok((proxy_server, mock_server, client)) 32 | } 33 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/setup_proxy_server.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf, sync::Arc}; 2 | 3 | use anyhow::{Ok, Result}; 4 | use lynx_core::proxy_server::{ 5 | ProxyServer, ProxyServerBuilder, server_ca_manage::ServerCaManagerBuilder, 6 | server_config::ProxyServerConfigBuilder, 7 | }; 8 | use rcgen::Certificate; 9 | use sea_orm::ConnectOptions; 10 | 11 | pub async fn setup_proxy_server( 12 | custom_certs: Option>>>, 13 | ) -> Result { 14 | let fixed_temp_dir_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/temp"); 15 | 16 | if !fixed_temp_dir_path.exists() { 17 | fs::create_dir_all(&fixed_temp_dir_path)?; 18 | } 19 | 20 | let server_config = ProxyServerConfigBuilder::default() 21 | .root_cert_file_path(fixed_temp_dir_path.join("root.pem")) 22 | .root_key_file_path(fixed_temp_dir_path.join("key.pem")) 23 | .build()?; 24 | 25 | let server_ca_manager = ServerCaManagerBuilder::new( 26 | server_config.root_cert_file_path.clone(), 27 | server_config.root_key_file_path.clone(), 28 | ) 29 | .build()?; 30 | 31 | let mut proxy_server_builder = ProxyServerBuilder::default(); 32 | 33 | proxy_server_builder 34 | .config(Arc::new(server_config)) 35 | .server_ca_manager(Arc::new(server_ca_manager)) 36 | .db_config(ConnectOptions::new("sqlite::memory:")); 37 | if let Some(custom_certs) = custom_certs { 38 | proxy_server_builder.custom_certs(custom_certs); 39 | } 40 | 41 | let mut proxy_server = proxy_server_builder.build().await?; 42 | proxy_server.run().await?; 43 | Ok(proxy_server) 44 | } 45 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/setup_self_service_test_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Ok, Result}; 2 | 3 | use lynx_core::proxy_server::ProxyServer; 4 | use lynx_mock::client::MockClient; 5 | 6 | use super::setup_proxy_server::setup_proxy_server; 7 | 8 | #[allow(dead_code)] 9 | pub async fn setup_self_service_test_server() -> Result<(ProxyServer, MockClient)> { 10 | let proxy_server = setup_proxy_server(None).await?; 11 | let proxy_server_root_ca = proxy_server.server_ca_manager.ca_cert.clone(); 12 | let client = MockClient::new(Some(vec![proxy_server_root_ca]), None)?; 13 | 14 | Ok((proxy_server, client)) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/setup/setup_tracing.rs: -------------------------------------------------------------------------------- 1 | use tracing_subscriber::{ 2 | Layer, filter::FilterFn, fmt, layer::SubscriberExt, util::SubscriberInitExt, 3 | }; 4 | 5 | #[allow(dead_code)] 6 | pub fn setup_tracing() { 7 | let my_filter = FilterFn::new(|metadata| { 8 | // Only enable spans or events with the target "interesting_things" 9 | metadata.target().starts_with("lynx_core") 10 | || metadata.target().starts_with("lynx_db") 11 | || metadata.target().starts_with("lynx_mock") 12 | }); 13 | let _ = tracing_subscriber::registry() 14 | .with( 15 | fmt::layer() 16 | .with_ansi(true) 17 | .with_level(true) 18 | .with_target(true) 19 | .with_file(true) 20 | .with_line_number(true) 21 | .with_filter(my_filter), 22 | ) 23 | .try_init(); 24 | } 25 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/temp/subdir/nested.txt: -------------------------------------------------------------------------------- 1 | This is a file in a subdirectory. 2 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/temp/test.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | background-color: #f0f0f0; 4 | margin: 0; 5 | padding: 20px; 6 | } 7 | 8 | h1 { 9 | color: #333; 10 | text-align: center; 11 | } 12 | 13 | .container { 14 | max-width: 800px; 15 | margin: 0 auto; 16 | background-color: white; 17 | padding: 20px; 18 | border-radius: 10px; 19 | box-shadow: 0 2px 10px rgba(0,0,0,0.1); 20 | } 21 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/temp/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Page 5 | 6 | 7 |

Hello World

8 |

This is a test HTML file for local file handler testing.

9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/temp/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-data", 3 | "version": "1.0.0", 4 | "message": "This is a test JSON file for local file handler testing." 5 | } 6 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/temp/test.txt: -------------------------------------------------------------------------------- 1 | This is a plain text file for testing the local file handler. 2 | It contains multiple lines of text. 3 | Line 3 4 | Line 4 5 | End of test file. 6 | -------------------------------------------------------------------------------- /crates/lynx-db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lynx-db" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | sea-orm = { workspace = true, features = ["mock"] } 14 | tracing-subscriber = { workspace = true } 15 | tracing = { workspace = true } 16 | anyhow = { workspace = true } 17 | tokio = { workspace = true } 18 | sea-orm-migration = { version = "1.1.0", features = [ 19 | "sqlx-sqlite", 20 | "runtime-tokio-rustls", 21 | ] } 22 | serde_json = "1.0.135" 23 | serde = "1.0.217" 24 | utoipa = { version = "5.3.1" } 25 | glob = "0.3" 26 | regex = "1.10" 27 | chrono = { version = "0.4", features = ["serde"] } 28 | async-trait = "0.1.88" 29 | axum = "0.8.4" 30 | http = { workspace = true } 31 | http-body-util = { workspace = true } 32 | thiserror = "1.0" 33 | bytes = { workspace = true } 34 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod https_capture_dao; 2 | pub mod net_request_dao; 3 | pub mod request_processing_dao; 4 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Specific error types for request processing operations 4 | #[derive(Error, Debug)] 5 | pub enum RequestProcessingError { 6 | #[error("Database error: {0}")] 7 | Database(#[from] sea_orm::DbErr), 8 | 9 | #[error("Rule not found with ID: {id}")] 10 | RuleNotFound { id: i32 }, 11 | 12 | #[error("Invalid capture pattern: {pattern}, reason: {reason}")] 13 | InvalidCapturePattern { pattern: String, reason: String }, 14 | 15 | #[error("Invalid handler configuration for type {handler_type}: {reason}")] 16 | InvalidHandlerConfig { 17 | handler_type: String, 18 | reason: String, 19 | }, 20 | 21 | #[error("Rule validation failed: {reason}")] 22 | RuleValidation { reason: String }, 23 | 24 | #[error("Transaction failed: {reason}")] 25 | Transaction { reason: String }, 26 | 27 | #[error("Serialization error: {0}")] 28 | Serialization(#[from] serde_json::Error), 29 | 30 | #[error("Pattern compilation error: {0}")] 31 | PatternCompilation(#[from] glob::PatternError), 32 | 33 | #[error("Regex compilation error: {0}")] 34 | RegexCompilation(#[from] regex::Error), 35 | } 36 | 37 | pub type Result = std::result::Result; 38 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/block_handler.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use utoipa::ToSchema; 3 | 4 | /// Block handler configuration 5 | #[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct BlockHandlerConfig { 8 | pub status_code: Option, 9 | pub reason: Option, 10 | } 11 | 12 | impl Default for BlockHandlerConfig { 13 | fn default() -> Self { 14 | Self { 15 | status_code: Some(403), 16 | reason: Some("Access blocked by proxy".to_string()), 17 | } 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::*; 24 | 25 | #[test] 26 | fn test_block_handler_serialization() { 27 | let handler = BlockHandlerConfig { 28 | status_code: Some(403), 29 | reason: Some("Custom block message".to_string()), 30 | }; 31 | 32 | let json = serde_json::to_string(&handler).unwrap(); 33 | let deserialized: BlockHandlerConfig = serde_json::from_str(&json).unwrap(); 34 | 35 | assert_eq!(handler.status_code, deserialized.status_code); 36 | assert_eq!(handler.reason, deserialized.reason); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/local_file_handler.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use utoipa::ToSchema; 3 | 4 | /// Local file handler configuration 5 | #[derive(Debug, Serialize, Deserialize, ToSchema, Default, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct LocalFileConfig { 8 | pub file_path: String, 9 | pub content_type: Option, 10 | pub status_code: Option, 11 | } 12 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_handler; 2 | pub mod handler_rule; 3 | pub mod local_file_handler; 4 | pub mod modify_request_handler; 5 | pub mod modify_response_handler; 6 | pub mod proxy_forward_handler; 7 | 8 | pub use block_handler::BlockHandlerConfig; 9 | pub use handler_rule::HandlerRule; 10 | pub use local_file_handler::LocalFileConfig; 11 | pub use modify_request_handler::ModifyRequestConfig; -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/modify_request_handler.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use utoipa::ToSchema; 3 | 4 | /// Modify request handler configuration 5 | #[derive(Debug, Serialize, Deserialize,Default, ToSchema, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct ModifyRequestConfig { 8 | pub modify_headers: Option>, 9 | pub modify_body: Option, 10 | pub modify_method: Option, 11 | pub modify_url: Option, 12 | } 13 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/modify_response_handler.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use utoipa::ToSchema; 3 | 4 | /// Modify request handler configuration 5 | #[derive(Debug, Serialize, Deserialize, ToSchema,Default, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct ModifyResponseConfig { 8 | pub modify_headers: Option>, 9 | pub modify_body: Option, 10 | pub modify_method: Option, 11 | pub modify_status_code: Option, 12 | } 13 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/proxy_forward_handler.rs: -------------------------------------------------------------------------------- 1 | 2 | use serde::{Deserialize, Serialize}; 3 | use utoipa::ToSchema; 4 | 5 | #[derive(Debug, Serialize, Deserialize, ToSchema, Default, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct ProxyForwardConfig { 8 | pub target_scheme: Option, 9 | pub target_authority: Option, 10 | pub target_path: Option, 11 | } 12 | -------------------------------------------------------------------------------- /crates/lynx-db/src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4 2 | 3 | 4 | 5 | pub mod prelude; 6 | 7 | pub mod app_config; 8 | pub mod rule; 9 | pub mod capture; 10 | pub mod handler; 11 | -------------------------------------------------------------------------------- /crates/lynx-db/src/entities/prelude.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4 2 | 3 | pub use super::app_config::Entity as AppConfig; 4 | pub use super::rule::Entity as Rule; 5 | pub use super::capture::Entity as Capture; 6 | pub use super::handler::Entity as Handler; 7 | -------------------------------------------------------------------------------- /crates/lynx-db/src/entities/rule.rs: -------------------------------------------------------------------------------- 1 | //! Rule Entity for capturing and processing requests 2 | 3 | use async_trait::async_trait; 4 | use sea_orm::{Set, entity::prelude::*}; 5 | use serde::{Deserialize, Serialize}; 6 | use utoipa::ToSchema; 7 | 8 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)] 9 | #[serde(rename_all = "camelCase")] 10 | #[sea_orm(table_name = "rule")] 11 | pub struct Model { 12 | #[sea_orm(primary_key)] 13 | pub id: i32, 14 | 15 | /// Rule name for identification 16 | #[sea_orm(column_type = "Text")] 17 | pub name: String, 18 | 19 | /// Rule description 20 | pub description: Option, 21 | 22 | /// Whether the rule is enabled 23 | #[sea_orm(default_value = true)] 24 | pub enabled: bool, 25 | 26 | /// Rule priority (higher number = higher priority) 27 | #[sea_orm(default_value = 0)] 28 | pub priority: i32, 29 | 30 | /// Creation timestamp 31 | #[serde(skip)] 32 | #[sea_orm(column_type = "BigInteger")] 33 | pub created_at: i64, 34 | 35 | /// Update timestamp 36 | #[serde(skip)] 37 | #[sea_orm(column_type = "BigInteger")] 38 | pub updated_at: i64, 39 | } 40 | 41 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 42 | pub enum Relation { 43 | #[sea_orm(has_one = "super::capture::Entity")] 44 | Capture, 45 | #[sea_orm(has_many = "super::handler::Entity")] 46 | Handlers, 47 | } 48 | 49 | impl Related for Entity { 50 | fn to() -> RelationDef { 51 | Relation::Capture.def() 52 | } 53 | } 54 | 55 | impl Related for Entity { 56 | fn to() -> RelationDef { 57 | Relation::Handlers.def() 58 | } 59 | } 60 | 61 | #[async_trait] 62 | impl ActiveModelBehavior for ActiveModel { 63 | /// Called before insert and update 64 | fn new() -> Self { 65 | Self { 66 | created_at: Set(chrono::Utc::now().timestamp()), 67 | updated_at: Set(chrono::Utc::now().timestamp()), 68 | ..ActiveModelTrait::default() 69 | } 70 | } 71 | 72 | /// Called before insert 73 | async fn before_save(mut self, _db: &C, insert: bool) -> Result 74 | where 75 | C: ConnectionTrait, 76 | { 77 | let now = chrono::Utc::now(); 78 | 79 | if insert { 80 | // Only set created_at if it's not already set (for new records) 81 | if self.created_at.is_not_set() { 82 | self.created_at = Set(now.timestamp()); 83 | } 84 | } 85 | 86 | // Always update updated_at on both insert and update 87 | self.updated_at = Set(now.timestamp()); 88 | 89 | Ok(self) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/lynx-db/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod dao; 2 | pub mod entities; 3 | pub mod migration; 4 | -------------------------------------------------------------------------------- /crates/lynx-db/src/migration/mod.rs: -------------------------------------------------------------------------------- 1 | pub use sea_orm_migration::prelude::*; 2 | 3 | pub mod app_config; 4 | pub mod request_processing; 5 | 6 | pub struct Migrator; 7 | 8 | #[async_trait::async_trait] 9 | impl MigratorTrait for Migrator { 10 | fn migrations() -> Vec> { 11 | vec![ 12 | Box::new(app_config::Migration), 13 | Box::new(request_processing::Migration), 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/lynx-db/src/migration/request_processing.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::Schema; 2 | use sea_orm_migration::prelude::*; 3 | 4 | use crate::entities::prelude::{Capture, Handler, Rule}; 5 | 6 | #[derive(DeriveMigrationName)] 7 | pub struct Migration; 8 | 9 | #[async_trait::async_trait] 10 | impl MigrationTrait for Migration { 11 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 12 | let builder = manager.get_database_backend(); 13 | let schema = Schema::new(builder); 14 | 15 | // Create rule table 16 | let rule_table = builder.build(schema.create_table_from_entity(Rule).if_not_exists()); 17 | manager.get_connection().execute(rule_table).await?; 18 | 19 | // Create capture table 20 | let capture_table = builder.build(schema.create_table_from_entity(Capture).if_not_exists()); 21 | manager.get_connection().execute(capture_table).await?; 22 | 23 | // Create handler table 24 | let handler_table = builder.build(schema.create_table_from_entity(Handler).if_not_exists()); 25 | manager.get_connection().execute(handler_table).await?; 26 | 27 | Ok(()) 28 | } 29 | 30 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 31 | // Drop tables in reverse order due to foreign key constraints 32 | manager 33 | .drop_table(Table::drop().table(HandlerTable::Table).to_owned()) 34 | .await?; 35 | manager 36 | .drop_table(Table::drop().table(CaptureTable::Table).to_owned()) 37 | .await?; 38 | manager 39 | .drop_table(Table::drop().table(RuleTable::Table).to_owned()) 40 | .await?; 41 | Ok(()) 42 | } 43 | } 44 | 45 | #[derive(DeriveIden)] 46 | enum RuleTable { 47 | Table, 48 | } 49 | 50 | #[derive(DeriveIden)] 51 | enum CaptureTable { 52 | Table, 53 | } 54 | 55 | #[derive(DeriveIden)] 56 | enum HandlerTable { 57 | Table, 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | use crate::migration::Migrator; 64 | use sea_orm::{Database, DatabaseConnection}; 65 | use sea_orm_migration::MigratorTrait; 66 | 67 | async fn setup_test_db() -> DatabaseConnection { 68 | let db = Database::connect("sqlite::memory:").await.unwrap(); 69 | Migrator::up(&db, None).await.unwrap(); 70 | db 71 | } 72 | 73 | #[tokio::test] 74 | async fn test_migration_up() { 75 | let _db = setup_test_db().await; 76 | // Migration successful if no panic occurs 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/lynx-mock/.gitignore: -------------------------------------------------------------------------------- 1 | examples/temp -------------------------------------------------------------------------------- /crates/lynx-mock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lynx-mock" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | hyper = { workspace = true } 14 | hyper-tungstenite = { workspace = true } 15 | tokio = { workspace = true } 16 | anyhow = { workspace = true } 17 | http-body-util = { workspace = true } 18 | bytes = { workspace = true } 19 | futures-util = { workspace = true } 20 | tokio-util = { workspace = true } 21 | once_cell = { workspace = true } 22 | tokio-stream = { workspace = true } 23 | http = { workspace = true } 24 | hyper-util = { workspace = true } 25 | async-compression = { version = "0.4.18", features = ["gzip", "tokio"] } 26 | tokio-rustls = { workspace = true } 27 | lynx-cert = { path = "../lynx-cert" } 28 | pin-project-lite = { workspace = true } 29 | tower = { workspace = true } 30 | reqwest = { version = "0.12.18", features = [ 31 | "rustls-tls-manual-roots", 32 | "gzip", 33 | "json", 34 | "stream", 35 | "rustls-tls", 36 | ] } 37 | reqwest-websocket = "0.5.0" 38 | rcgen = { workspace = true } 39 | tracing = { workspace = true } 40 | 41 | [dev-dependencies] 42 | tracing-subscriber = { workspace = true } 43 | -------------------------------------------------------------------------------- /crates/lynx-mock/examples/server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Ok, Result}; 2 | use lynx_mock::{client::MockClient, server::MockServer}; 3 | use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | tracing_subscriber::registry() 8 | .with(fmt::layer()) 9 | .with(EnvFilter::from_default_env().add_directive("lynx_mock=trace".parse()?)) 10 | .init(); 11 | let mut server = MockServer::new(Some(3000)); 12 | server.write_cert_to_file()?; 13 | server.start_server().await?; 14 | let client = MockClient::new(Some(vec![server.cert.clone()]), None)?; 15 | client.test_request_https_request(&server).await?; 16 | client.test_request_http_request(&server).await?; 17 | client.test_request_websocket(&server).await?; 18 | client.test_request_tls_websocket(&server).await?; 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /crates/lynx-mock/examples/start_test_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Ok, Result}; 2 | use lynx_mock::server::MockServer; 3 | use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | tracing_subscriber::registry() 8 | .with(fmt::layer()) 9 | .with(EnvFilter::from_default_env().add_directive("lynx_mock=trace".parse()?)) 10 | .init(); 11 | let mut server = MockServer::new(Some(3001)); 12 | server.write_cert_to_file()?; 13 | server.start_server().await?; 14 | 15 | tokio::signal::ctrl_c() 16 | .await 17 | .expect("Failed to install Ctrl+C signal handler"); 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /crates/lynx-mock/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | mod mark_service; 3 | mod mock_server_fn; 4 | pub mod server; 5 | -------------------------------------------------------------------------------- /crates/lynx-mock/src/mark_service.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | sync::Arc, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use bytes::Bytes; 8 | use futures_util::ready; 9 | use http::{HeaderValue, Response}; 10 | use http_body_util::combinators::BoxBody; 11 | use hyper::{Request, body::Incoming, service::Service}; 12 | use pin_project_lite::pin_project; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct MarkService { 16 | inner: S, 17 | mark: Arc, 18 | } 19 | impl MarkService { 20 | pub fn new(inner: S, mark: Arc) -> Self { 21 | MarkService { inner, mark } 22 | } 23 | } 24 | type Req = Request; 25 | type Res = Response>; 26 | 27 | impl Service for MarkService 28 | where 29 | S: Service, 30 | { 31 | type Response = S::Response; 32 | type Error = S::Error; 33 | type Future = MarkFuture; 34 | fn call(&self, req: Req) -> Self::Future { 35 | tracing::trace!("handle request: {:?}", req.uri()); 36 | MarkFuture { 37 | future: self.inner.call(req), 38 | mark: self.mark.clone(), 39 | } 40 | } 41 | } 42 | 43 | pin_project! { 44 | /// Response future for [`SetResponseHeader`]. 45 | #[derive(Debug)] 46 | pub struct MarkFuture { 47 | #[pin] 48 | future: F, 49 | mark: Arc, 50 | } 51 | } 52 | 53 | impl Future for MarkFuture 54 | where 55 | F: Future>, 56 | { 57 | type Output = F::Output; 58 | 59 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 60 | let this = self.project(); 61 | let mut res = ready!(this.future.poll(cx)?); 62 | res.headers_mut() 63 | .insert("X-Mark-Addr", HeaderValue::from_str(this.mark).unwrap()); 64 | Poll::Ready(Ok(res)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/lynx-proxy/.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist/ 9 | 10 | # IDE 11 | .vscode/* 12 | !.vscode/extensions.json 13 | .idea 14 | -------------------------------------------------------------------------------- /crates/lynx-proxy/.prettierignore: -------------------------------------------------------------------------------- 1 | # Lock files 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /crates/lynx-proxy/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "plugins": ["prettier-plugin-tailwindcss"] 4 | } 5 | -------------------------------------------------------------------------------- /crates/lynx-proxy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.0.2](https://github.com/suxin2017/lynx-server/compare/lynx-proxy-v0.0.1...lynx-proxy-v0.0.2) - 2025-02-05 11 | 12 | ### Other 13 | 14 | - update Cargo.lock dependencies 15 | -------------------------------------------------------------------------------- /crates/lynx-proxy/Readme.md: -------------------------------------------------------------------------------- 1 | # lynx proxy 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | TODO 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/lynx-proxy/Readme.zh-CN.md: -------------------------------------------------------------------------------- 1 | # lynx proxy 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | 这是一个命令行工具,提供一个 taiui 的桌面端应用 6 | 7 | # 安装 8 | 9 | TODO -------------------------------------------------------------------------------- /crates/lynx-proxy/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import js from '@eslint/js'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | import reactJsx from 'eslint-plugin-react/configs/jsx-runtime.js'; 5 | import react from 'eslint-plugin-react/configs/recommended.js'; 6 | import globals from 'globals'; 7 | import ts from 'typescript-eslint'; 8 | 9 | export default [ 10 | { languageOptions: { globals: globals.browser } }, 11 | js.configs.recommended, 12 | ...ts.configs.recommended, 13 | ...fixupConfigRules([ 14 | { 15 | ...react, 16 | settings: { 17 | react: { version: 'detect' }, 18 | }, 19 | }, 20 | reactJsx, 21 | ]), 22 | { 23 | plugins: { 24 | 'react-hooks': reactHooks, 25 | }, 26 | rules: { 27 | ...reactHooks.configs.recommended.rules, 28 | }, 29 | }, 30 | { 31 | rules: { 32 | '@typescript-eslint/no-empty-object-type': 'off', 33 | 'react/prop-types': 'off', 34 | '@typescript-eslint/no-unused-vars': [ 35 | 'error', 36 | { 37 | args: 'all', 38 | argsIgnorePattern: '^_', 39 | caughtErrors: 'all', 40 | caughtErrorsIgnorePattern: '^_', 41 | destructuredArrayIgnorePattern: '^_', 42 | varsIgnorePattern: '^_', 43 | ignoreRestSiblings: true, 44 | }, 45 | ], 46 | }, 47 | }, 48 | { ignores: ['dist/'] }, 49 | ]; 50 | -------------------------------------------------------------------------------- /crates/lynx-proxy/orval.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'orval'; 2 | 3 | export default defineConfig({ 4 | api: { 5 | input: { 6 | target: 'http://127.0.0.1:3000/api/api-docs/openapi.json', 7 | }, 8 | output: { 9 | mode: 'tags-split', 10 | target: './src/services/generated', 11 | client: 'react-query', 12 | prettier: true, 13 | mock: true, 14 | override: { 15 | mutator: { 16 | path: './src/services/customInstance.ts', 17 | name: 'customInstance', 18 | }, 19 | query: { 20 | useQuery: true, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /crates/lynx-proxy/postcss.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | } 6 | } -------------------------------------------------------------------------------- /crates/lynx-proxy/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-server/30f2d89eb3bf873886fc64d3f1fe5f84d7558434/crates/lynx-proxy/public/.gitkeep -------------------------------------------------------------------------------- /crates/lynx-proxy/rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rsbuild/core'; 2 | import { pluginReact } from '@rsbuild/plugin-react'; 3 | import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'; 4 | import { env } from 'process'; 5 | import { pluginTypeCheck } from '@rsbuild/plugin-type-check'; 6 | 7 | import { server } from './src/mock/node'; 8 | 9 | const useMock = env.MOCK === 'true'; 10 | 11 | if (useMock) { 12 | server.listen(); 13 | } 14 | 15 | export default defineConfig({ 16 | plugins: [ 17 | pluginReact(), 18 | pluginTypeCheck({ 19 | tsCheckerOptions: { 20 | issue: { 21 | // ignore types errors from node_modules 22 | exclude: [ 23 | ({ file = '' }) => /[\\/]services|ruleManager[\\/]/.test(file), 24 | ], 25 | }, 26 | }, 27 | }), 28 | ].filter(Boolean), 29 | html: { 30 | title: 'Lynx Proxy', 31 | }, 32 | source: { 33 | define: { 34 | 'process.platform': '"browser"', 35 | }, 36 | }, 37 | dev: { 38 | client: { 39 | overlay: false, 40 | }, 41 | }, 42 | server: { 43 | port: 8080, 44 | proxy: { 45 | '/api': { 46 | target: 'http://127.0.0.1:3000', 47 | onProxyRes(proxyRes, _req, res) { 48 | res.on('close', () => { 49 | proxyRes.destroy(); 50 | }); 51 | proxyRes.on('data', () => { 52 | if (res.closed) { 53 | proxyRes.destroy(); 54 | } 55 | }); 56 | }, 57 | }, 58 | }, 59 | }, 60 | 61 | tools: { 62 | rspack: { 63 | plugins: [TanStackRouterRspack()], 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.md$/, 68 | type: 'asset/source', 69 | }, 70 | ], 71 | }, 72 | }, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { StyleProvider } from '@ant-design/cssinjs'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { 4 | createHashHistory, 5 | createRouter, 6 | RouterProvider, 7 | } from '@tanstack/react-router'; 8 | import { App as AntdApp, ConfigProvider, theme } from 'antd'; 9 | import { useEffect, useMemo, useState } from 'react'; 10 | import './main.css'; 11 | import { routeTree } from './routeTree.gen'; 12 | import { LanguageProvider } from './contexts/LanguageContext'; 13 | import { useAntdLocale } from './contexts/useAntdLocale'; 14 | 15 | const hashHistory = createHashHistory(); 16 | // Set up a Router instance 17 | const router = createRouter({ 18 | routeTree, 19 | defaultPreload: 'intent', 20 | history: hashHistory, 21 | }); 22 | 23 | // Register things for typesafety 24 | declare module '@tanstack/react-router' { 25 | interface Register { 26 | router: typeof router; 27 | } 28 | } 29 | 30 | const queryClient = new QueryClient({ 31 | defaultOptions: { 32 | queries: { 33 | retry: false, 34 | }, 35 | }, 36 | }); 37 | 38 | const getIsDark = () => { 39 | if (typeof window !== 'undefined') { 40 | return document.documentElement.classList.contains('dark'); 41 | } 42 | return false; 43 | }; 44 | 45 | const AppContent = () => { 46 | const [isDark, setIsDark] = useState(getIsDark()); 47 | const antdLocale = useAntdLocale(); 48 | 49 | useEffect(() => { 50 | const observer = new MutationObserver(() => { 51 | setIsDark(getIsDark()); 52 | }); 53 | observer.observe(document.documentElement, { 54 | attributes: true, 55 | attributeFilter: ['class'], 56 | }); 57 | return () => observer.disconnect(); 58 | }, []); 59 | 60 | const antdAlgorithm = useMemo(() => { 61 | return isDark ? [theme.darkAlgorithm] : []; 62 | }, [isDark]); 63 | 64 | return ( 65 | 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | const App = () => { 87 | return ( 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | export default App; 99 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/LanguageSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select } from 'antd'; 3 | import { useI18n } from '@/contexts'; 4 | 5 | const { Option } = Select; 6 | 7 | export const LanguageSelector: React.FC = () => { 8 | const { language, setLanguage } = useI18n(); 9 | 10 | const handleLanguageChange = (value: string) => { 11 | setLanguage(value as 'en' | 'zh-CN'); 12 | }; 13 | 14 | return ( 15 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from 'antd'; 2 | import React from 'react'; 3 | 4 | export const PageLoading: React.FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/RequestContextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'antd'; 2 | import { useRequestContextMenu } from '@/hooks/useRequestContextMenu'; 3 | import { IViewMessageEventStoreValue } from '@/store'; 4 | 5 | interface RequestContextMenuProps { 6 | children: 7 | | React.ReactNode 8 | | ((props: { 9 | handleContextMenu: ( 10 | record: IViewMessageEventStoreValue, 11 | event: React.MouseEvent, 12 | ) => void; 13 | }) => React.ReactNode); 14 | onSelectRecord?: (record: IViewMessageEventStoreValue) => void; 15 | } 16 | 17 | export const RequestContextMenu: React.FC = ({ 18 | children, 19 | onSelectRecord, 20 | }) => { 21 | const { 22 | selectedRecord, 23 | setSelectedRecord, 24 | contextMenuItems, 25 | handleContextMenu, 26 | } = useRequestContextMenu({ 27 | onSelectRecord, 28 | }); 29 | 30 | return ( 31 | { 36 | if (!visible) { 37 | setSelectedRecord(null); 38 | } 39 | }} 40 | > 41 | {typeof children === 'function' 42 | ? children({ handleContextMenu }) 43 | : children} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/contexts/LanguageContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | useState, 5 | useEffect, 6 | ReactNode, 7 | } from 'react'; 8 | import i18n from 'i18next'; 9 | import dayjs from 'dayjs'; 10 | 11 | // Define language type 12 | export type Language = 'en' | 'zh-CN'; 13 | 14 | // Define context type 15 | interface LanguageContextType { 16 | language: Language; 17 | setLanguage: (lang: Language) => void; 18 | } 19 | 20 | // Create the context with default values 21 | const LanguageContext = createContext({ 22 | language: 'en', 23 | setLanguage: () => {}, 24 | }); 25 | 26 | // Custom hook to use the language context 27 | export const useLanguage = () => useContext(LanguageContext); 28 | 29 | interface LanguageProviderProps { 30 | children: ReactNode; 31 | } 32 | 33 | export const LanguageProvider: React.FC = ({ 34 | children, 35 | }) => { 36 | // Initialize language from localStorage or default to browser language 37 | const [language, setLanguageState] = useState(() => { 38 | const savedLanguage = localStorage.getItem('i18nextLng'); 39 | return savedLanguage && 40 | (savedLanguage === 'en' || savedLanguage === 'zh-CN') 41 | ? (savedLanguage as Language) 42 | : 'en'; 43 | }); 44 | 45 | // Handle language change 46 | const setLanguage = (lang: Language) => { 47 | setLanguageState(lang); 48 | i18n.changeLanguage(lang); 49 | dayjs.locale(lang); 50 | localStorage.setItem('i18nextLng', lang); 51 | }; 52 | 53 | // Effect to sync i18n and context on language changes 54 | useEffect(() => { 55 | const handleLanguageChange = () => { 56 | const currentLang = i18n.language; 57 | if ( 58 | (currentLang === 'en' || currentLang === 'zh-CN') && 59 | currentLang !== language 60 | ) { 61 | setLanguageState(currentLang as Language); 62 | } 63 | }; 64 | 65 | i18n.on('languageChanged', handleLanguageChange); 66 | 67 | return () => { 68 | i18n.off('languageChanged', handleLanguageChange); 69 | }; 70 | }, [language]); 71 | 72 | return ( 73 | 74 | {children} 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | // Export all context-related functionality from a single entry point 2 | export * from './LanguageContext'; 3 | export * from './useI18n'; 4 | export * from './useAntdLocale'; 5 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/contexts/useAntdLocale.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import enUS from 'antd/locale/en_US'; 3 | import zhCN from 'antd/locale/zh_CN'; 4 | import { useLanguage } from './LanguageContext'; 5 | 6 | /** 7 | * Custom hook that returns the appropriate Ant Design locale based on the current language 8 | */ 9 | export const useAntdLocale = () => { 10 | const { language } = useLanguage(); 11 | 12 | return useMemo(() => { 13 | switch (language) { 14 | case 'zh-CN': 15 | return zhCN; 16 | case 'en': 17 | default: 18 | return enUS; 19 | } 20 | }, [language]); 21 | }; 22 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/contexts/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import { useLanguage, Language } from './LanguageContext'; 3 | 4 | // Define our extended hook return type 5 | interface UseI18nResponse { 6 | t: (key: string, options?: Record) => string; 7 | i18n: { 8 | language: string; 9 | changeLanguage: (lng: string) => Promise; 10 | }; 11 | language: Language; 12 | setLanguage: (lang: Language) => void; 13 | } 14 | 15 | /** 16 | * Custom hook that combines useTranslation and useLanguage 17 | * Makes it easier to work with translations and language changes 18 | */ 19 | export const useI18n = (): UseI18nResponse => { 20 | const { t, i18n } = useTranslation(); 21 | const { language, setLanguage } = useLanguage(); 22 | 23 | return { 24 | t, 25 | i18n, 26 | language, 27 | setLanguage, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/env.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-server/30f2d89eb3bf873886fc64d3f1fe5f84d7558434/crates/lynx-proxy/src/env.ts -------------------------------------------------------------------------------- /crates/lynx-proxy/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' { 2 | const content: string; 3 | 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDebugMode'; 2 | // Export other hooks as needed 3 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/hooks/useDebugMode.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from '@tanstack/react-router'; 2 | 3 | /** 4 | * Hook to check if debug mode is enabled via URL parameter (debug=true) 5 | * @returns boolean indicating if debug mode is enabled 6 | */ 7 | export function useDebugMode(): boolean { 8 | // Get the current location from TanStack Router 9 | const location = useLocation(); 10 | 11 | // Extract the search parameters 12 | const searchParams = new URLSearchParams(location.search); 13 | 14 | // Check if debug=true is in the URL 15 | const isDebugMode = searchParams.get('debug') === 'true'; 16 | 17 | return isDebugMode; 18 | } 19 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import HttpBackend from 'i18next-http-backend'; 4 | import LanguageDetector from 'i18next-browser-languagedetector'; 5 | 6 | i18n 7 | .use(HttpBackend) 8 | .use(LanguageDetector) 9 | .use(initReactI18next) 10 | .init({ 11 | fallbackLng: 'en', 12 | supportedLngs: ['en', 'zh-CN'], 13 | defaultNS: 'common', 14 | interpolation: { 15 | escapeValue: false, 16 | }, 17 | detection: { 18 | order: ['localStorage', 'navigator'], 19 | caches: ['localStorage'], 20 | }, 21 | backend: { 22 | loadPath: process.env.ASSET_PREFIX + '/locales/{{lng}}/{{ns}}.json', 23 | }, 24 | }); 25 | 26 | export default i18n; 27 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './i18n'; 4 | import App from './App'; 5 | import '@ant-design/v5-patch-for-react-19'; 6 | 7 | const theme = localStorage.getItem('theme'); 8 | if ( 9 | theme === 'dark' || 10 | (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches) 11 | ) { 12 | document.documentElement.classList.add('dark'); 13 | } else { 14 | document.documentElement.classList.remove('dark'); 15 | } 16 | 17 | const rootEl = document.getElementById('root'); 18 | if (rootEl) { 19 | const root = ReactDOM.createRoot(rootEl); 20 | root.render( 21 | 22 | 23 | , 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/main.css: -------------------------------------------------------------------------------- 1 | @layer antd, theme, base, components, utilities; 2 | 3 | @import 'tailwindcss/theme.css' layer(theme); 4 | @import 'tailwindcss/utilities.css' layer(utilities); 5 | 6 | @config '../tailwind.config.js'; 7 | 8 | #root { 9 | display: flex; 10 | flex-direction: column; 11 | max-height: 100vh; 12 | height: 100vh; 13 | min-height: 600px; 14 | min-width: 800px; 15 | max-width: 100vw; 16 | width: 100vw; 17 | 18 | } 19 | 20 | * { 21 | box-sizing: border-box; 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 23 | 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 24 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 25 | scrollbar-width: thin; 26 | 27 | } 28 | 29 | 30 | body { 31 | margin: 0; 32 | background-color: #f8fafc; 33 | color: #222; 34 | --ant-color-bg-base: #fff; 35 | } 36 | 37 | html.dark body { 38 | color: #e5e7eb; 39 | background-color: #0d0d0d; 40 | } 41 | 42 | 43 | 44 | body, #root { 45 | scrollbar-color: #c1c1c1 #f1f1f1; 46 | transition: background-color 0.3s, color 0.3s; 47 | } 48 | html.dark body, html.dark #root { 49 | scrollbar-color: #444857 #23272e; 50 | } -------------------------------------------------------------------------------- /crates/lynx-proxy/src/mock/handlers.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultMock } from '../services/generated/default/default.msw'; 2 | import { 3 | getGetCachedRequestsMockHandler, 4 | getGetCaptureStatusMockHandler, 5 | getToggleCaptureMockHandler, 6 | } from '../services/generated/net-request/net-request.msw'; 7 | import { getRequestProcessingMock } from '../services/generated/request-processing/request-processing.msw'; 8 | import { ResponseCode } from '../services/generated/utoipaAxum.schemas'; 9 | 10 | export const handlers = [ 11 | getGetCachedRequestsMockHandler({ 12 | code: ResponseCode.ok, 13 | data: { 14 | newRequests: [ 15 | { 16 | status: 'RequestStarted', 17 | traceId: 'u1_xbtl7IGuoJSgzcyPOW', 18 | isNew: true, 19 | request: { 20 | method: 'GET', 21 | url: 'https://demo.piesocket.com/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self', 22 | headers: { 23 | 'sec-websocket-extensions': 24 | 'permessage-deflate; client_max_window_bits', 25 | 'sec-websocket-version': '13', 26 | 'sec-websocket-key': 'WXDkka6bQsJtypxHeR4Vkw==', 27 | connection: 'Upgrade', 28 | upgrade: 'websocket', 29 | host: 'demo.piesocket.com', 30 | }, 31 | version: 'HTTP/1.1', 32 | headerSize: 185, 33 | body: '', 34 | }, 35 | response: null, 36 | messages: { 37 | status: 'Start', 38 | message: [], 39 | }, 40 | tunnel: null, 41 | timings: { 42 | requestStart: 1748095420253, 43 | requestEnd: null, 44 | requestBodyStart: null, 45 | requestBodyEnd: 1748095420253, 46 | proxyStart: null, 47 | proxyEnd: null, 48 | reponseBodyStart: null, 49 | reponseBodyEnd: null, 50 | tunnelStart: null, 51 | tunnelEnd: null, 52 | websocketStart: 1748095420253, 53 | websocketEnd: null, 54 | }, 55 | }, 56 | ], 57 | }, 58 | message: null, 59 | }), 60 | getGetCaptureStatusMockHandler(), 61 | getToggleCaptureMockHandler(), 62 | ...getDefaultMock(), 63 | ...getRequestProcessingMock() 64 | ]; 65 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/mock/node.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | 3 | import { handlers } from './handlers'; 4 | 5 | export const server = setupServer(...handlers); 6 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import { SideBar } from '@/components/SideBar'; 2 | import { store, useUpdateRequestLog } from '@/store'; 3 | import { GeneralSettingProvider } from '@/store/useGeneralState'; 4 | import { Outlet, createRootRoute } from '@tanstack/react-router'; 5 | import { Provider } from 'react-redux'; 6 | import { UseSelectRequestProvider } from './network/components/store/selectRequestStore'; 7 | 8 | export const Route = createRootRoute({ 9 | component: RootComponent, 10 | }); 11 | 12 | function InnerRouteComponent() { 13 | useUpdateRequestLog(); 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | function RootComponent() { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/about.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router' 2 | 3 | export const Route = createFileRoute('/about')({ 4 | component: RouteComponent, 5 | }) 6 | 7 | function RouteComponent() { 8 | return
Hello "/about"!
9 | } 10 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Navigate } from '@tanstack/react-router'; 2 | 3 | export const Route = createFileRoute('/')({ 4 | component: RouteComponent, 5 | }); 6 | 7 | function RouteComponent() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/BackToBottomButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'antd'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useAutoScroll } from '../store/autoScrollStore'; 5 | 6 | export const AutoScrollToBottom: React.FC = () => { 7 | const { t } = useTranslation(); 8 | const { autoScroll, setAutoScroll } = useAutoScroll(); 9 | 10 | return ( 11 |
12 |
13 | {t('network.toolbar.autoScrollLabel')}: 14 |
15 |
16 | { 20 | setAutoScroll(val); 21 | }} 22 | title={t('network.toolbar.autoScroll')} 23 | /> 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/CleanRequestButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { clearRequestTable } from '@/store/requestTableStore'; 2 | import { clearRequestTree } from '@/store/requestTreeStore'; 3 | import { RiDeleteBin7Line } from '@remixicon/react'; 4 | import { Button } from 'antd'; 5 | import React from 'react'; 6 | import { useDispatch } from 'react-redux'; 7 | import { useSelectRequest } from '../store/selectRequestStore'; 8 | import { useTranslation } from 'react-i18next'; 9 | 10 | export const CleanRequestButton: React.FC = () => { 11 | const { t } = useTranslation(); 12 | const dispatch = useDispatch(); 13 | 14 | const { setSelectRequest } = useSelectRequest(); 15 | 16 | return ( 17 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/RequestDetailDrawer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from 'antd'; 2 | import constate from 'constate'; 3 | import React, { useState } from 'react'; 4 | import { Detail } from '../Detail'; 5 | 6 | export const [RequestDetailDrawerStateProvider, useRequestDetailDrawerState] = 7 | constate(() => { 8 | const [visible, setVisible] = useState(true); 9 | 10 | return { 11 | visible, 12 | setVisible, 13 | }; 14 | }); 15 | export const RequestDetailDrawer: React.FC<{}> = () => { 16 | const { visible, setVisible } = useRequestDetailDrawerState(); 17 | 18 | console.log('RequestDetailDrawer', visible); 19 | return ( 20 |
24 | setVisible(false)} 35 | > 36 | 37 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Sequence/index.tsx: -------------------------------------------------------------------------------- 1 | import { usePrevious, useSize } from 'ahooks'; 2 | import { Spin, Splitter } from 'antd'; 3 | import React, { useEffect, useRef, useState } from 'react'; 4 | import { Detail } from '../Detail'; 5 | import { RequestTable } from '../RequestTable'; 6 | import { ShowTypeSegmented } from '../ShowTypeSegmented'; 7 | import { AutoScrollProvider } from '../store/autoScrollStore'; 8 | import { Toolbar } from '../Toolbar'; 9 | import { AutoScrollToBottom } from '../BackToBottomButton'; 10 | 11 | interface ISequenceProps {} 12 | 13 | export const Sequence: React.FC = () => { 14 | const ref = useRef(null); 15 | const size = useSize(ref); 16 | const prevSize = usePrevious(size); 17 | 18 | const [splitSize, setSplitSize] = useState<[number, number]>([0, 0]); 19 | 20 | useEffect(() => { 21 | if (!prevSize && size) { 22 | setSplitSize([size.height / 2, size.height / 2]); 23 | } 24 | }, [size, prevSize]); 25 | 26 | return ( 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | {!size ? ( 37 |
38 | 39 |
40 | ) : ( 41 | { 45 | setSplitSize([size1, size2]); 46 | }} 47 | > 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | )} 57 |
58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/ShowTypeSegmented/index.tsx: -------------------------------------------------------------------------------- 1 | import { Segmented, Tag } from 'antd'; 2 | import { useState } from 'react'; 3 | import constate from 'constate'; 4 | import { useRequestLogCount } from '@/store/requestTableStore'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | export const [ 8 | ShowTypeSegmentedStateContextProvider, 9 | useShowTypeSegmentedStateContext, 10 | ] = constate(() => { 11 | const [state, setState] = useState('Sequence'); 12 | 13 | return { 14 | state, 15 | setState, 16 | }; 17 | }); 18 | 19 | export function ShowTypeSegmented() { 20 | const { t } = useTranslation(); 21 | const { state, setState } = useShowTypeSegmentedStateContext(); 22 | const requestCount = useRequestLogCount(); 23 | 24 | const options = [t('network.sequence'), t('network.structure')]; 25 | 26 | return ( 27 |
28 | { 32 | setState(value === t('network.sequence') ? 'Sequence' : 'Structure'); 33 | }} 34 | /> 35 | {requestCount} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Structure/index.tsx: -------------------------------------------------------------------------------- 1 | import { Splitter } from 'antd'; 2 | import React, { useEffect, useRef } from 'react'; 3 | import { Detail } from '../Detail'; 4 | import { ShowTypeSegmented } from '../ShowTypeSegmented'; 5 | import { RequestTree } from '../RequestTree'; 6 | import { useSize } from 'ahooks'; 7 | import { Toolbar } from '../Toolbar'; 8 | 9 | interface IStructureProps {} 10 | 11 | export const Structure: React.FC = () => { 12 | const ref = useRef(null); 13 | const size = useSize(ref); 14 | const [sizes, setSizes] = React.useState([400, 400]); 15 | 16 | useEffect(() => { 17 | if (size?.width) { 18 | setSizes([400, size.width - 400]); 19 | } 20 | }, [size?.width]); 21 | 22 | return ( 23 |
24 |
25 | {size && ( 26 | { 28 | if (sizes[0] < 400) { 29 | return; 30 | } 31 | setSizes(sizes); 32 | }} 33 | className="h-full max-h-screen" 34 | layout="horizontal" 35 | > 36 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 |
52 | )} 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { RecordingStatusButton } from '../RecordingStatusButton'; 3 | import { CleanRequestButton } from '../CleanRequestButton'; 4 | import { TableFilter } from '../TableFilter'; 5 | 6 | export const Toolbar: React.FC = ({ children }) => { 7 | return ( 8 |
9 | 10 | 11 | 12 | {children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/WebSocketContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelectRequest } from '../store/selectRequestStore'; 3 | import Websocket from '../Websocket'; 4 | 5 | export const WebSocketContent: React.FC = () => { 6 | const { selectRequest } = useSelectRequest(); 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Websocket/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketDirection, 3 | WebSocketLog, 4 | WebSocketMessage, 5 | } from '@/services/generated/utoipaAxum.schemas'; 6 | import { RiArrowDownLine, RiArrowUpLine } from '@remixicon/react'; 7 | import { Table } from 'antd'; 8 | import React from 'react'; 9 | 10 | interface TextViewProps { 11 | websocketLog?: WebSocketLog[]; 12 | } 13 | 14 | function base64ToString(base64: string): string { 15 | return decodeURIComponent(escape(window.atob(base64))); 16 | } 17 | const Websocket: React.FC = ({ websocketLog }) => { 18 | if (!websocketLog) return null; 19 | 20 | return ( 21 |
22 | { 30 | return text === WebSocketDirection.ClientToServer ? ( 31 | 32 | ) : ( 33 | 34 | ); 35 | }, 36 | }, 37 | { 38 | title: 'data', 39 | dataIndex: 'message', 40 | render: (msg: WebSocketMessage) => { 41 | if ('text' in msg && msg.text) { 42 | return {base64ToString(msg.text)}; 43 | } 44 | if ('binary' in msg && msg.binary) { 45 | return {msg.binary}; 46 | } 47 | if ('ping' in msg) { 48 | return ping; 49 | } 50 | if ('pong' in msg) { 51 | return pong; 52 | } 53 | if ('close' in msg) { 54 | return close; 55 | } 56 | }, 57 | }, 58 | ]} 59 | dataSource={websocketLog} 60 | pagination={false} 61 | /> 62 | 63 | ); 64 | }; 65 | 66 | export default Websocket; 67 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/store/autoScrollStore.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from 'react'; 2 | 3 | interface AutoScrollContextType { 4 | autoScroll: boolean; 5 | setAutoScroll: (value: boolean) => void; 6 | } 7 | 8 | const AutoScrollContext = createContext( 9 | undefined, 10 | ); 11 | 12 | export const AutoScrollProvider: React.FC<{ children: React.ReactNode }> = ({ 13 | children, 14 | }) => { 15 | const [autoScroll, setAutoScroll] = useState(true); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export const useAutoScroll = (): AutoScrollContextType => { 25 | const context = useContext(AutoScrollContext); 26 | if (context === undefined) { 27 | throw new Error('useAutoScroll must be used within a AutoScrollProvider'); 28 | } 29 | return context; 30 | }; 31 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/store/selectRequestStore.tsx: -------------------------------------------------------------------------------- 1 | import { IViewMessageEventStoreValue } from '@/store'; 2 | import constate from 'constate'; 3 | import { useState } from 'react'; 4 | 5 | export const [UseSelectRequestProvider, useSelectRequest] = constate(() => { 6 | const [selectRequest, setSelectRequest] = 7 | useState(null); 8 | const [isWebsocketRequest, setIsWebsocketRequest] = useState(false); 9 | 10 | return { 11 | selectRequest, 12 | isWebsocketRequest, 13 | setSelectRequest: (request: IViewMessageEventStoreValue | null) => { 14 | setIsWebsocketRequest(!!request?.messages?.message); 15 | setSelectRequest(request); 16 | }, 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { Sequence } from './components/Sequence'; 3 | import { Structure } from './components/Structure'; 4 | import { 5 | ShowTypeSegmentedStateContextProvider, 6 | useShowTypeSegmentedStateContext, 7 | } from './components/ShowTypeSegmented'; 8 | export const Route = createFileRoute('/network/')({ 9 | component: RouteComponent, 10 | }); 11 | 12 | function InnerComponent() { 13 | const { state } = useShowTypeSegmentedStateContext(); 14 | 15 | return ( 16 |
17 | {state === 'Sequence' && } 18 | {state === 'Structure' && } 19 |
20 | ); 21 | } 22 | 23 | function RouteComponent() { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CopyRuleButton.tsx: -------------------------------------------------------------------------------- 1 | import { RequestRule } from '@/services/generated/utoipaAxum.schemas'; 2 | import { useCreateRule } from '@/services/generated/request-processing/request-processing'; 3 | import { RiFileCopyLine } from '@remixicon/react'; 4 | import { Button, Modal } from 'antd'; 5 | import React from 'react'; 6 | import { useI18n } from '@/contexts'; 7 | 8 | interface CopyRuleButtonProps { 9 | record: RequestRule; 10 | onSuccess?: () => void; 11 | } 12 | 13 | export const CopyRuleButton: React.FC = ({ 14 | record, 15 | onSuccess, 16 | }) => { 17 | const { t } = useI18n(); 18 | 19 | // Create rule 20 | const createRuleMutation = useCreateRule({ 21 | mutation: { 22 | onSuccess: () => { 23 | onSuccess?.(); 24 | }, 25 | }, 26 | }); 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | const clearAllIds = (obj: any): any => { 30 | if (!obj || typeof obj !== 'object') return obj; 31 | 32 | if (Array.isArray(obj)) { 33 | return obj.map((item) => clearAllIds(item)); 34 | } 35 | 36 | const newObj = { ...obj }; 37 | 38 | delete newObj.id; 39 | 40 | for (const key in newObj) { 41 | if (typeof newObj[key] === 'object' && newObj[key] !== null) { 42 | newObj[key] = clearAllIds(newObj[key]); 43 | } 44 | } 45 | 46 | return newObj; 47 | }; 48 | 49 | const handleCopy = () => { 50 | if (!record) return; 51 | 52 | Modal.confirm({ 53 | title: t('ruleManager.copyConfirm.title'), 54 | content: t('ruleManager.copyConfirm.content', { name: record.name }), 55 | okText: t('ruleManager.copyConfirm.okText'), 56 | cancelText: t('ruleManager.copyConfirm.cancelText'), 57 | onOk: async () => { 58 | try { 59 | const ruleCopy = clearAllIds(record); 60 | 61 | ruleCopy.name = t('ruleManager.copyRuleName', { name: record.name }); 62 | 63 | createRuleMutation.mutate({ 64 | data: ruleCopy, 65 | }); 66 | } catch (error) { 67 | console.error(t('ruleManager.copyRuleError'), error); 68 | } 69 | }, 70 | }); 71 | }; 72 | 73 | return ( 74 | 37 | 42 | 43 | 44 | } 45 | width={720} 46 | open={state.visible} 47 | onClose={closeDrawer} 48 | destroyOnClose 49 | closeIcon={false} 50 | footer={null} 51 | > 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/AddHandlerButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import React from 'react'; 3 | import { useI18n } from '@/contexts'; 4 | 5 | interface AddHandlerButtonProps { 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | add: (defaultValue?: any) => void; 8 | } 9 | 10 | export const AddHandlerButton: React.FC = ({ add }) => { 11 | const { t } = useI18n(); 12 | 13 | const quickAddItems = [ 14 | { 15 | key: 'block', 16 | type: 'block' as const, 17 | name: t('ruleManager.quickAdd.blockRequest.name'), 18 | config: { 19 | type: 'block', 20 | statusCode: 403, 21 | reason: t('ruleManager.handlerDescriptions.reason'), 22 | }, 23 | }, 24 | { 25 | key: 'modifyRequest', 26 | type: 'modifyRequest' as const, 27 | name: t('ruleManager.quickAdd.modifyRequest.name'), 28 | config: { 29 | type: 'modifyRequest', 30 | modifyHeaders: null, 31 | modifyBody: null, 32 | modifyMethod: null, 33 | modifyUrl: null, 34 | }, 35 | }, 36 | { 37 | key: 'modifyResponse', 38 | type: 'modifyResponse' as const, 39 | name: t('ruleManager.quickAdd.modifyResponse.name'), 40 | config: { 41 | type: 'modifyResponse', 42 | modifyHeaders: null, 43 | modifyBody: null, 44 | modifyMethod: null, 45 | modifyUrl: null, 46 | }, 47 | }, 48 | { 49 | key: 'localFile', 50 | type: 'localFile' as const, 51 | name: t('ruleManager.quickAdd.localFile.name'), 52 | config: { 53 | type: 'localFile', 54 | filePath: '', 55 | contentType: null, 56 | statusCode: null, 57 | }, 58 | }, 59 | { 60 | key: 'proxyForward', 61 | type: 'proxyForward' as const, 62 | name: t('ruleManager.quickAdd.proxyForward.name'), 63 | config: { 64 | type: 'proxyForward', 65 | targetUrl: '', 66 | headers: null, 67 | statusCode: null, 68 | }, 69 | }, 70 | ]; 71 | 72 | return ( 73 |
74 |
75 | {quickAddItems.map((item) => ( 76 | 90 | ))} 91 |
92 |
93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/HandlerList.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd'; 2 | import React, { useState } from 'react'; 3 | import { HandlerItem } from './HandlerItem'; 4 | import { useI18n } from '@/contexts'; 5 | 6 | const { Text } = Typography; 7 | 8 | interface HandlerListProps { 9 | fields: Array<{ 10 | key: number; 11 | name: number; 12 | }>; 13 | remove: (index: number) => void; 14 | } 15 | 16 | export const HandlerList: React.FC = ({ fields, remove }) => { 17 | const [editingHandler, setEditingHandler] = useState(null); 18 | const { t } = useI18n(); 19 | 20 | if (fields.length === 0) { 21 | return ( 22 |
23 | 24 | {t('ruleManager.createRuleDrawer.handlerBehavior.noHandlers')} 25 | 26 |
27 | ); 28 | } 29 | 30 | return ( 31 |
32 | {fields.map((field, index) => ( 33 |
34 | setEditingHandler(field.name)} 39 | onSave={() => setEditingHandler(null)} 40 | onCancel={() => setEditingHandler(null)} 41 | onDelete={() => remove(field.name)} 42 | /> 43 |
44 | ))} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/config/BlockHandlerConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, InputNumber, Input, Typography } from 'antd'; 3 | import { useI18n } from '@/contexts'; 4 | 5 | const { Text } = Typography; 6 | 7 | interface BlockHandlerConfigProps { 8 | field: { 9 | key: number; 10 | name: number; 11 | }; 12 | } 13 | 14 | export const BlockHandlerConfig: React.FC = ({ 15 | field, 16 | }) => { 17 | const { t } = useI18n(); 18 | 19 | return ( 20 |
21 | 22 | {t('ruleManager.createRuleDrawer.handlerBehavior.blockHandler.title')} 23 | 24 | 25 |
26 | 48 | 54 | 55 | 56 | 63 | 64 | 65 |
66 | 67 |
68 | 69 | {t( 70 | 'ruleManager.createRuleDrawer.handlerBehavior.blockHandler.description', 71 | )} 72 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/config/LocalFileConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input, Select, Button, Typography } from 'antd'; 3 | import { FolderOpenOutlined } from '@ant-design/icons'; 4 | 5 | const { Text } = Typography; 6 | 7 | interface LocalFileConfigProps { 8 | field: { 9 | key: number; 10 | name: number; 11 | }; 12 | } 13 | 14 | export const LocalFileConfig: React.FC = ({ field }) => { 15 | const commonContentTypes = [ 16 | { value: 'text/html', label: 'HTML (text/html)' }, 17 | { value: 'application/json', label: 'JSON (application/json)' }, 18 | { value: 'text/plain', label: '纯文本 (text/plain)' }, 19 | { 20 | value: 'application/javascript', 21 | label: 'JavaScript (application/javascript)', 22 | }, 23 | { value: 'text/css', label: 'CSS (text/css)' }, 24 | { value: 'application/xml', label: 'XML (application/xml)' }, 25 | { value: 'image/png', label: 'PNG 图片 (image/png)' }, 26 | { value: 'image/jpeg', label: 'JPEG 图片 (image/jpeg)' }, 27 | { value: 'image/gif', label: 'GIF 图片 (image/gif)' }, 28 | { value: 'image/svg+xml', label: 'SVG 图片 (image/svg+xml)' }, 29 | { value: 'application/pdf', label: 'PDF (application/pdf)' }, 30 | { 31 | value: 'application/octet-stream', 32 | label: '二进制文件 (application/octet-stream)', 33 | }, 34 | ]; 35 | return ( 36 |
37 | 本地文件配置 38 | 39 |
40 | 46 | } size="small" /> 50 | } 51 | /> 52 | 53 | 54 |
55 | 60 | 30 | 31 | 32 | 46 | 47 | 48 | 49 | 56 | 57 | 58 |
59 | 60 |
61 | 62 | {t( 63 | 'ruleManager.createRuleDrawer.handlerBehavior.proxyForward.description', 64 | )} 65 | 66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/config/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HandlerRuleType } from '@/services/generated/utoipaAxum.schemas'; 3 | import { BlockHandlerConfig } from './BlockHandlerConfig'; 4 | import { ModifyRequestConfig } from './ModifyRequestConfig'; 5 | import { ModifyResponseConfig } from './ModifyResponseConfig'; 6 | import { LocalFileConfig } from './LocalFileConfig'; 7 | import { ProxyForwardConfig } from './ProxyForwardConfig'; 8 | 9 | interface HandlerConfigProps { 10 | field: { 11 | key: number; 12 | name: number; 13 | }; 14 | handler: HandlerRuleType; 15 | } 16 | 17 | export const HandlerConfig: React.FC = ({ 18 | handler, 19 | field, 20 | }) => { 21 | // Type guard to safely access the type property 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const getHandlerType = (handlerType: any): string => { 24 | if ( 25 | handlerType && 26 | typeof handlerType === 'object' && 27 | 'type' in handlerType 28 | ) { 29 | return handlerType.type; 30 | } 31 | return 'unknown'; 32 | }; 33 | 34 | const handlerType = getHandlerType(handler); 35 | 36 | switch (handlerType) { 37 | case 'block': 38 | return ; 39 | case 'modifyRequest': 40 | return ; 41 | case 'modifyResponse': 42 | return ; 43 | case 'localFile': 44 | return ; 45 | case 'proxyForward': 46 | return ; 47 | default: 48 | return ( 49 |
50 | 不支持的处理器类型: {handlerType} 51 |
52 | ); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/index.tsx: -------------------------------------------------------------------------------- 1 | import { Space, Typography, Form } from 'antd'; 2 | import React from 'react'; 3 | import { HandlerList } from './components/HandlerList'; 4 | import { AddHandlerButton } from './components/AddHandlerButton'; 5 | import { useI18n } from '@/contexts'; 6 | 7 | const { Title, Text } = Typography; 8 | 9 | interface HandlerBehaviorProps {} 10 | 11 | export const HandlerBehavior: React.FC = () => { 12 | const { t } = useI18n(); 13 | 14 | return ( 15 | 16 | 17 | {t('ruleManager.createRuleDrawer.handlerBehavior.title')} 18 | 19 | 20 | {t('ruleManager.createRuleDrawer.handlerBehavior.description')} 21 | 22 | 23 | 24 | {(fields, { add, remove }) => ( 25 |
26 | 27 | 28 |
29 | )} 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/index.ts: -------------------------------------------------------------------------------- 1 | export { BasicInfo } from './BasicInfo'; 2 | export { CaptureRule } from './CaptureRule'; 3 | export { HandlerBehavior } from './HandlerBehavior'; 4 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/context.tsx: -------------------------------------------------------------------------------- 1 | import constate from 'constate'; 2 | import { useState } from 'react'; 3 | 4 | export interface CreateRuleDrawerState { 5 | visible: boolean; 6 | editMode: boolean; 7 | editingRuleId?: number; 8 | } 9 | 10 | export const [CreateRuleDrawerProvider, useCreateRuleDrawer] = constate(() => { 11 | const [state, setState] = useState({ 12 | visible: false, 13 | editMode: false, 14 | editingRuleId: undefined, 15 | }); 16 | 17 | const openDrawer = () => { 18 | setState({ 19 | visible: true, 20 | editMode: false, 21 | editingRuleId: undefined, 22 | }); 23 | }; 24 | 25 | const openEditDrawer = (ruleId: number) => { 26 | setState({ 27 | visible: true, 28 | editMode: true, 29 | editingRuleId: ruleId, 30 | }); 31 | }; 32 | 33 | const closeDrawer = () => { 34 | setState({ 35 | visible: false, 36 | editMode: false, 37 | editingRuleId: undefined, 38 | }); 39 | }; 40 | 41 | const updateDrawerState = (updates: Partial) => { 42 | setState(prev => ({ ...prev, ...updates })); 43 | }; 44 | 45 | return { 46 | state, 47 | openDrawer, 48 | openEditDrawer, 49 | closeDrawer, 50 | updateDrawerState, 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/index.ts: -------------------------------------------------------------------------------- 1 | export { CreateRuleDrawer } from './CreateRuleDrawer'; 2 | export { CreateRuleDrawerProvider, useCreateRuleDrawer } from './context'; 3 | export { CreateRuleForm } from './CreateRuleForm'; 4 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { InterceptorPage } from './components/InterceptorPage.tsx'; 3 | 4 | export const Route = createFileRoute('/ruleManager/')({ 5 | component: RouteComponent, 6 | }); 7 | 8 | function RouteComponent() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Outlet } from '@tanstack/react-router'; 2 | import { SettingsMenu } from './settings/components/SettingsMenu'; 3 | 4 | export const Route = createFileRoute('/settings')({ 5 | component: RouteComponent, 6 | }); 7 | 8 | function RouteComponent() { 9 | return ( 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/certificates.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { CertificatesSetting } from './components/CertificateSetting'; 3 | 4 | export const Route = createFileRoute('/settings/certificates')({ 5 | component: RouteComponent, 6 | }); 7 | 8 | function RouteComponent() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/components/CertificateSetting/CertInstallDesc.tsx: -------------------------------------------------------------------------------- 1 | import { RiComputerLine } from '@remixicon/react'; 2 | import { Segmented, Space, Typography, Steps } from 'antd'; 3 | import { useState } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | type Platform = 'windows' | 'macos' | 'mobile'; 7 | 8 | export const CertInstallDesc = () => { 9 | const { t } = useTranslation(); 10 | const [platform, setPlatform] = useState('windows'); 11 | 12 | const getSteps = (platform: Platform) => { 13 | return Array.from({ length: platform === 'mobile' ? 3 : 4 }, (_, i) => ({ 14 | title: t(`settings.certificate.install.${platform}.step${i + 1}.title`), 15 | description: t( 16 | `settings.certificate.install.${platform}.step${i + 1}.description`, 17 | ), 18 | })); 19 | }; 20 | 21 | return ( 22 | 23 | setPlatform(value as Platform)} 27 | options={[ 28 | { 29 | label: ( 30 | 31 | 32 | 33 | {t('settings.certificate.install.platform.windows')} 34 | 35 | 36 | ), 37 | value: 'windows', 38 | }, 39 | { 40 | label: ( 41 | 42 | 43 | {t('settings.certificate.install.platform.macos')} 44 | 45 | ), 46 | value: 'macos', 47 | }, 48 | { 49 | label: ( 50 | 51 | 52 | {t('settings.certificate.install.platform.linux')} 53 | 54 | ), 55 | value: 'linux', 56 | }, 57 | { 58 | label: ( 59 | 60 | 61 | {t('settings.certificate.install.platform.mobile')} 62 | 63 | ), 64 | value: 'mobile', 65 | }, 66 | ]} 67 | /> 68 |
69 | 70 | {t('settings.certificate.install.title')} 71 | 72 | 78 |
79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/components/CommonCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { CardProps, Typography } from 'antd'; 2 | 3 | export const CommonCard: React.FC< 4 | CardProps & { 5 | subTitle?: string; 6 | } 7 | > = ({ title, subTitle, children, extra }) => { 8 | return ( 9 |
10 |
11 |
12 | 13 | {title} 14 | 15 | 19 | {subTitle} 20 | 21 |
22 | {extra} 23 |
24 | {children} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/components/GeneralSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import { LanguageSelector } from '@/components/LanguageSelector'; 2 | import { useGeneralSetting } from '@/store/useGeneralState'; 3 | import { Button, Form, InputNumber, message, Space, Typography } from 'antd'; 4 | import React from 'react'; 5 | import { useI18n } from '@/contexts'; 6 | import { CommonCard } from '../CommonCard'; 7 | 8 | interface IGeneralSettingProps {} 9 | 10 | export const GeneralSetting: React.FC = () => { 11 | const [form] = Form.useForm(); 12 | const { maxLogSize, setMaxLogSize } = useGeneralSetting(); 13 | const [messageApi, contextHolder] = message.useMessage(); 14 | const { t } = useI18n(); 15 | 16 | return ( 17 | 22 | 32 | 40 | 41 | } 42 | > 43 | {contextHolder} 44 |
{ 52 | setMaxLogSize(maxLogSize); 53 | messageApi.success(t('settings.general.actions.save')); 54 | }} 55 | > 56 | 57 | {t('settings.general.language')} 58 | 59 | 60 | 61 | {t('settings.general.maxLogSize.title')} 62 | 63 | 64 | {t('settings.general.maxLogSize.description')} 65 | 66 | 67 | 83 | 84 | 85 | 86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/components/SettingsMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from '@tanstack/react-router'; 2 | import { Segmented } from 'antd'; 3 | import React from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | const menuConfig = [ 7 | { 8 | key: 'general', 9 | translationKey: 'settings.menu.general', 10 | path: '/settings/general', 11 | }, 12 | { 13 | key: 'network', 14 | translationKey: 'settings.menu.network', 15 | path: '/settings/network', 16 | }, 17 | { 18 | key: 'certificates', 19 | translationKey: 'settings.menu.certificates', 20 | path: '/settings/certificates', 21 | }, 22 | ]; 23 | 24 | export const SettingsMenu: React.FC = () => { 25 | const { pathname } = useLocation(); 26 | const navigate = useNavigate(); 27 | const { t } = useTranslation(); 28 | 29 | const currentMenu = menuConfig.find((item) => 30 | pathname.includes(item.path), 31 | )?.translationKey; 32 | 33 | return ( 34 | t(item.translationKey))} 40 | onChange={(value) => { 41 | const selectedMenu = menuConfig.find( 42 | (item) => t(item.translationKey) === value, 43 | ); 44 | if (selectedMenu) { 45 | navigate({ to: selectedMenu.path }); 46 | } 47 | }} 48 | /> 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/general.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { GeneralSetting } from './components/GeneralSetting'; 3 | 4 | export const Route = createFileRoute('/settings/general')({ 5 | component: GeneralSetting, 6 | }); 7 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Navigate } from '@tanstack/react-router'; 2 | 3 | export const Route = createFileRoute('/settings/')({ 4 | component: RouteComponent, 5 | }); 6 | 7 | function RouteComponent() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/network.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { NetworkSetting } from './components/NetworkSetting'; 3 | 4 | export const Route = createFileRoute('/settings/network')({ 5 | component: RouteComponent, 6 | }); 7 | 8 | function RouteComponent() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/services/customInstance.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosRequestConfig } from 'axios'; 2 | 3 | export const AXIOS_INSTANCE = axios.create({ 4 | baseURL: '/api', 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | }); 9 | 10 | AXIOS_INSTANCE.interceptors.request.use( 11 | (config) => { 12 | if (config.url === '/health') { 13 | config.baseURL = 'https://' + location.host + "/api"; 14 | } 15 | return config; 16 | }, 17 | (error) => { 18 | console.error('[Request Error]', error); 19 | return Promise.reject(error); 20 | }, 21 | ); 22 | 23 | AXIOS_INSTANCE.interceptors.response.use( 24 | (response) => { 25 | return response; 26 | }, 27 | (error: AxiosError) => { 28 | console.warn( 29 | `[Response Error] ${error.config?.method?.toUpperCase()} ${error.config?.url}`, 30 | { 31 | status: error.response?.status, 32 | data: error.response?.data, 33 | error: error.message, 34 | }, 35 | ); 36 | return Promise.reject(error); 37 | }, 38 | ); 39 | 40 | export const customInstance = (config: AxiosRequestConfig): Promise => { 41 | return AXIOS_INSTANCE(config) 42 | .then(({ data }) => data) 43 | .catch((error: AxiosError) => { 44 | throw error; 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/services/generated/certificate/certificate.msw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by orval v7.9.0 🍺 3 | * Do not edit manually. 4 | * utoipa-axum 5 | * Utoipa's axum bindings for seamless integration for the two 6 | * OpenAPI spec version: 0.2.0 7 | */ 8 | import { faker } from '@faker-js/faker'; 9 | 10 | import { HttpResponse, delay, http } from 'msw'; 11 | 12 | import { ResponseCode } from '../utoipaAxum.schemas'; 13 | import type { ResponseDataWrapperString } from '../utoipaAxum.schemas'; 14 | 15 | export const getGetCertPathResponseMock = ( 16 | overrideResponse: Partial = {}, 17 | ): ResponseDataWrapperString => ({ 18 | code: faker.helpers.arrayElement(Object.values(ResponseCode)), 19 | data: faker.string.alpha(20), 20 | message: faker.helpers.arrayElement([ 21 | faker.helpers.arrayElement([faker.string.alpha(20), null]), 22 | undefined, 23 | ]), 24 | ...overrideResponse, 25 | }); 26 | 27 | export const getDownloadCertificateMockHandler = ( 28 | overrideResponse?: 29 | | unknown 30 | | (( 31 | info: Parameters[1]>[0], 32 | ) => Promise | unknown), 33 | ) => { 34 | return http.get('*/certificate/download', async (info) => { 35 | await delay(1000); 36 | if (typeof overrideResponse === 'function') { 37 | await overrideResponse(info); 38 | } 39 | return new HttpResponse(null, { status: 200 }); 40 | }); 41 | }; 42 | 43 | export const getGetCertPathMockHandler = ( 44 | overrideResponse?: 45 | | ResponseDataWrapperString 46 | | (( 47 | info: Parameters[1]>[0], 48 | ) => Promise | ResponseDataWrapperString), 49 | ) => { 50 | return http.get('*/certificate/path', async (info) => { 51 | await delay(1000); 52 | 53 | return new HttpResponse( 54 | JSON.stringify( 55 | overrideResponse !== undefined 56 | ? typeof overrideResponse === 'function' 57 | ? await overrideResponse(info) 58 | : overrideResponse 59 | : getGetCertPathResponseMock(), 60 | ), 61 | { status: 200, headers: { 'Content-Type': 'application/json' } }, 62 | ); 63 | }); 64 | }; 65 | export const getCertificateMock = () => [ 66 | getDownloadCertificateMockHandler(), 67 | getGetCertPathMockHandler(), 68 | ]; 69 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/services/generated/default/default.msw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by orval v7.9.0 🍺 3 | * Do not edit manually. 4 | * utoipa-axum 5 | * Utoipa's axum bindings for seamless integration for the two 6 | * OpenAPI spec version: 0.2.0 7 | */ 8 | import { faker } from '@faker-js/faker'; 9 | 10 | import { HttpResponse, delay, http } from 'msw'; 11 | 12 | export const getGetHealthResponseMock = (): string => faker.word.sample(); 13 | 14 | export const getGetHealthMockHandler = ( 15 | overrideResponse?: 16 | | string 17 | | (( 18 | info: Parameters[1]>[0], 19 | ) => Promise | string), 20 | ) => { 21 | return http.get('*/health', async (info) => { 22 | await delay(1000); 23 | 24 | return new HttpResponse(getGetHealthResponseMock(), { 25 | status: 200, 26 | headers: { 'Content-Type': 'text/plain' }, 27 | }); 28 | }); 29 | }; 30 | export const getDefaultMock = () => [getGetHealthMockHandler()]; 31 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/services/generated/system/system.msw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by orval v7.9.0 🍺 3 | * Do not edit manually. 4 | * utoipa-axum 5 | * Utoipa's axum bindings for seamless integration for the two 6 | * OpenAPI spec version: 0.2.0 7 | */ 8 | import { faker } from '@faker-js/faker'; 9 | 10 | import { HttpResponse, delay, http } from 'msw'; 11 | 12 | import { ResponseCode } from '../utoipaAxum.schemas'; 13 | import type { ResponseDataWrapperBaseInfo } from '../utoipaAxum.schemas'; 14 | 15 | export const getGetBaseInfoResponseMock = ( 16 | overrideResponse: Partial = {}, 17 | ): ResponseDataWrapperBaseInfo => ({ 18 | code: faker.helpers.arrayElement(Object.values(ResponseCode)), 19 | data: { 20 | accessAddrList: Array.from( 21 | { length: faker.number.int({ min: 1, max: 10 }) }, 22 | (_, i) => i + 1, 23 | ).map(() => faker.string.alpha(20)), 24 | }, 25 | message: faker.helpers.arrayElement([ 26 | faker.helpers.arrayElement([faker.string.alpha(20), null]), 27 | undefined, 28 | ]), 29 | ...overrideResponse, 30 | }); 31 | 32 | export const getGetBaseInfoMockHandler = ( 33 | overrideResponse?: 34 | | ResponseDataWrapperBaseInfo 35 | | (( 36 | info: Parameters[1]>[0], 37 | ) => Promise | ResponseDataWrapperBaseInfo), 38 | ) => { 39 | return http.get('*/base_info/base_info', async (info) => { 40 | await delay(1000); 41 | 42 | return new HttpResponse( 43 | JSON.stringify( 44 | overrideResponse !== undefined 45 | ? typeof overrideResponse === 'function' 46 | ? await overrideResponse(info) 47 | : overrideResponse 48 | : getGetBaseInfoResponseMock(), 49 | ), 50 | { status: 200, headers: { 'Content-Type': 'application/json' } }, 51 | ); 52 | }); 53 | }; 54 | export const getSystemMock = () => [getGetBaseInfoMockHandler()]; 55 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/store/useGeneralState.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorageState } from 'ahooks'; 2 | import constate from 'constate'; 3 | 4 | export const [GeneralSettingProvider, useGeneralSetting] = constate(() => { 5 | const [maxLogSize, setMaxLogSize] = useLocalStorageState( 6 | 'maxLogSize', 7 | { 8 | defaultValue: 1000, 9 | serializer(value) { 10 | return value.toString(); 11 | }, 12 | deserializer(value) { 13 | return parseInt(value, 10); 14 | }, 15 | }, 16 | ); 17 | 18 | return { 19 | maxLogSize, 20 | setMaxLogSize, 21 | }; 22 | }); 23 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/store/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export function useInterval(callback: () => void, delay: number | null) { 4 | const savedCallback = useRef(callback) 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback 8 | }, [callback]) 9 | 10 | useEffect(() => { 11 | if (delay === null) { 12 | return 13 | } 14 | 15 | const id = setInterval(() => savedCallback.current(), delay) 16 | return () => clearInterval(id) 17 | }, [delay]) 18 | } 19 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/utils/curlGenerator.ts: -------------------------------------------------------------------------------- 1 | import { IViewMessageEventStoreValue } from '@/store'; 2 | 3 | export function generateCurlCommand( 4 | request: IViewMessageEventStoreValue, 5 | ): string { 6 | if (!request.request) { 7 | return ''; 8 | } 9 | 10 | const { 11 | method = 'GET', 12 | headers = {}, 13 | url = '', 14 | bodyArrayBuffer, 15 | } = request.request; 16 | 17 | // Start with curl and URL, add line continuation 18 | let curlCommand = `curl \\\n '${url}'`; 19 | 20 | // Add method if not GET 21 | if (method !== 'GET') { 22 | curlCommand += ` \\\n -X ${method}`; 23 | } 24 | 25 | // Add headers 26 | Object.entries(headers).forEach(([key, value]) => { 27 | // Skip connection headers as they're handled by curl automatically 28 | if (!['connection', 'content-length'].includes(key.toLowerCase())) { 29 | curlCommand += ` \\\n -H '${key}: ${value}'`; 30 | } 31 | }); 32 | 33 | // Add body if exists 34 | if (bodyArrayBuffer) { 35 | try { 36 | const bodyText = new TextDecoder().decode(bodyArrayBuffer); 37 | const contentType = headers['content-type'] || ''; 38 | 39 | if (contentType.includes('application/json')) { 40 | // For JSON data, format it properly 41 | curlCommand += ` \\\n -d '${bodyText}'`; 42 | } else if (contentType.includes('application/x-www-form-urlencoded')) { 43 | // For form data, keep it as is 44 | curlCommand += ` \\\n --data '${bodyText}'`; 45 | } else { 46 | // For other types, add as binary data 47 | curlCommand += ` \\\n --data-binary '${bodyText}'`; 48 | } 49 | } catch (e) { 50 | console.error('Failed to decode request body:', e); 51 | } 52 | } 53 | 54 | return curlCommand; 55 | } 56 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/utils/ifTrue.ts: -------------------------------------------------------------------------------- 1 | export function ifTrue(condition: boolean, expr: T) { 2 | return condition ? expr : null; 3 | } 4 | -------------------------------------------------------------------------------- /crates/lynx-proxy/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | darkMode: 'class', 5 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], 6 | theme: { 7 | extend: { 8 | animation: { 9 | 'fade-in': 'fadeIn 0.2s ease-in-out', 10 | }, 11 | keyframes: { 12 | fadeIn: { 13 | '0%': { opacity: 0 }, 14 | '100%': { opacity: 1 }, 15 | }, 16 | }, 17 | }, 18 | }, 19 | plugins: [], 20 | }; 21 | -------------------------------------------------------------------------------- /crates/lynx-proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2020"], 4 | "jsx": "react-jsx", 5 | "target": "ES2020", 6 | "noEmit": true, 7 | "skipLibCheck": true, 8 | "useDefineForClassFields": true, 9 | /* modules */ 10 | "module": "ESNext", 11 | "isolatedModules": true, 12 | "resolveJsonModule": true, 13 | "moduleResolution": "Bundler", 14 | "allowImportingTsExtensions": true, 15 | /* type checking */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src"], 24 | } 25 | -------------------------------------------------------------------------------- /crates/lynx-proxy/tsr.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routesDirectory": "./src/routes", 3 | "generatedRouteTree": "./src/routeTree.gen.ts", 4 | "routeFileIgnorePrefix": "component" 5 | } 6 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:crates/lynx-cli"] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | 11 | # The installers to generate for each app 12 | installers = ["shell", "powershell"] 13 | # Target platforms to build apps for (Rust target-triple syntax) 14 | targets = [ 15 | "x86_64-apple-darwin", 16 | "x86_64-unknown-linux-gnu", 17 | "x86_64-pc-windows-msvc", 18 | ] 19 | # Path that installers should place binaries in 20 | install-path = "CARGO_HOME" 21 | # Whether to install an updater program 22 | install-updater = true 23 | # Skip checking whether the specified configuration files are up to date 24 | allow-dirty = ["ci", "msi"] 25 | # Which actions to run on pull requests 26 | pr-run-mode = "skip" 27 | 28 | [dist.github-custom-runners] 29 | x86_64-unknown-linux-gnu = "ubuntu-22.04" 30 | -------------------------------------------------------------------------------- /examples/self_signed_ca/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/self_signed_ca/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "self_signed_ca" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | rcgen = { version = "0.13.0", default-features = false, features = [ 14 | "x509-parser", 15 | "pem", 16 | "ring", 17 | ] } 18 | -------------------------------------------------------------------------------- /examples/self_signed_ca/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false -------------------------------------------------------------------------------- /examples/self_signed_ca/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use rcgen::{CertifiedKey, generate_simple_self_signed}; 4 | 5 | fn main() { 6 | let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 7 | fs::create_dir_all(manifest_dir.join("dist")).unwrap(); 8 | 9 | let subject_alt_names = vec!["localhost".to_string(), "127.0.0.1".to_string()]; 10 | 11 | let CertifiedKey { cert, key_pair } = generate_simple_self_signed(subject_alt_names).unwrap(); 12 | fs::write(manifest_dir.join("dist/cert.pem"), cert.pem()).unwrap(); 13 | fs::write(manifest_dir.join("dist/key.pem"), key_pair.serialize_pem()).unwrap(); 14 | } 15 | -------------------------------------------------------------------------------- /examples/websocket-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket-client" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | 14 | tokio-tungstenite = { version = "0.26.1", features = [ 15 | "rustls", 16 | "rustls-tls-webpki-roots", 17 | "connect", 18 | "url", 19 | ] } 20 | tokio = { version = "1.10.0", features = ["full"] } 21 | url = "2.5.4" 22 | http = "1.0" 23 | webpki-roots = "0.26.8" 24 | rustls = { version = "0.23.26", features = ["ring"] } 25 | futures-util = "0.3.31" 26 | rustls-pemfile = "2.2.0" 27 | -------------------------------------------------------------------------------- /examples/websocket-client/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false -------------------------------------------------------------------------------- /examples/websocket-client/src/main.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{SinkExt, StreamExt}; 2 | use rustls::{ClientConfig, RootCertStore, pki_types::CertificateDer}; 3 | use std::{fs, io, path::PathBuf, sync::Arc}; 4 | use tokio::spawn; 5 | use tokio::sync::mpsc::unbounded_channel; 6 | use tokio_tungstenite::{Connector, connect_async_tls_with_config}; 7 | use url::Url; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 12 | 13 | rustls::crypto::ring::default_provider() 14 | .install_default() 15 | .unwrap_or_default(); 16 | 17 | let url = Url::parse("ws://localhost:3000").unwrap(); 18 | 19 | // create root cert store 20 | let mut root_cert_store: RootCertStore = RootCertStore::empty(); 21 | // add webpki roots 22 | root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); 23 | 24 | let cert_file = 25 | fs::File::open(manifest_dir.join("../websocket-server/self_signed_certs/cert.pem")) 26 | .unwrap(); 27 | 28 | let mut reader = io::BufReader::new(cert_file); 29 | 30 | let cert_chain: io::Result>> = 31 | rustls_pemfile::certs(&mut reader).collect(); 32 | 33 | for cert in cert_chain.unwrap() { 34 | root_cert_store.add(cert).unwrap(); 35 | } 36 | 37 | let client_config = ClientConfig::builder() 38 | .with_root_certificates(root_cert_store) 39 | .with_no_client_auth(); 40 | 41 | let connector = Connector::Rustls(Arc::new(client_config)); 42 | 43 | let (ws_stream, _response) = connect_async_tls_with_config(url, None, false, Some(connector)) 44 | .await 45 | .expect("WebSocket handshake failed"); 46 | 47 | let (mut sink, mut stream) = ws_stream.split(); 48 | let (shutdown_send, mut shutdown_recv) = unbounded_channel(); 49 | 50 | spawn(async move { 51 | sink.send(tokio_tungstenite::tungstenite::Message::Text( 52 | "Hello, World!".into(), 53 | )) 54 | .await 55 | .expect("Failed to send message"); 56 | while let Some(msg) = stream.next().await { 57 | match msg { 58 | Ok(msg) => { 59 | println!("Received message: {:?}", msg); 60 | sink.send(tokio_tungstenite::tungstenite::Message::Close(None)) 61 | .await 62 | .unwrap(); 63 | shutdown_send.send(()).unwrap(); 64 | } 65 | Err(e) => { 66 | eprintln!("Error receiving message: {:?}", e); 67 | } 68 | } 69 | } 70 | }); 71 | 72 | tokio::select! { 73 | _ = shutdown_recv.recv() => { 74 | println!("Shutdown signal received"); 75 | } 76 | _ = tokio::signal::ctrl_c() => { 77 | println!("Ctrl-C signal received"); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/websocket-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket-server" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | documentation.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | 12 | [dependencies] 13 | tokio = { version = "1.10.0", features = ["full"] } 14 | hyper = { version = "1", features = ["full"] } 15 | hyper-util = { version = "0.1", features = ["full"] } 16 | hyper-tungstenite = "0.17.0" 17 | tokio-stream = { version = "0.1.14", default-features = false, features = [ 18 | "sync", 19 | ] } 20 | futures-util = "0.3.31" 21 | anyhow = { workspace = true } 22 | tokio-rustls = { version = "0.26.0", default-features = false, features = [ 23 | "ring", 24 | "tls12", 25 | "logging", 26 | ] } 27 | http-body-util = "0.1" 28 | rustls-pemfile = { workspace = true } 29 | -------------------------------------------------------------------------------- /examples/websocket-server/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false -------------------------------------------------------------------------------- /examples/websocket-server/self_signed_certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBZDCCAQqgAwIBAgIULKCLfHlv27kJ4PywNKcIrawq8TowCgYIKoZIzj0EAwIw 3 | ITEfMB0GA1UEAwwWcmNnZW4gc2VsZiBzaWduZWQgY2VydDAgFw03NTAxMDEwMDAw 4 | MDBaGA80MDk2MDEwMTAwMDAwMFowITEfMB0GA1UEAwwWcmNnZW4gc2VsZiBzaWdu 5 | ZWQgY2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCNEXF+LP+hIOweUV29y 6 | Tym3ZS5WBBXYaQcj0GbrwGTgr8gPAR1W4OrajMmgIIHQfYOBdMi1LiESHprfn8x/ 7 | tqSjHjAcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjOPQQDAgNI 8 | ADBFAiEAhUQLBIUise9gdzi5O6xcPKySl2oP0U4h2VZ7x7s+LYkCIHoAhp27yuAN 9 | UEqn372tniYv9E4lzvAirHkgIqs3yY7t 10 | -----END CERTIFICATE----- 11 | -------------------------------------------------------------------------------- /examples/websocket-server/self_signed_certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNPysiQ1TbKZRSktu 3 | ANenVdXVEZ1NJfMJk6oNEixmTsmhRANCAAQjRFxfiz/oSDsHlFdvck8pt2UuVgQV 4 | 2GkHI9Bm68Bk4K/IDwEdVuDq2ozJoCCB0H2DgXTItS4hEh6a35/Mf7ak 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /images/http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-server/30f2d89eb3bf873886fc64d3f1fe5f84d7558434/images/http.png -------------------------------------------------------------------------------- /images/rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-server/30f2d89eb3bf873886fc64d3f1fe5f84d7558434/images/rule.png -------------------------------------------------------------------------------- /images/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-server/30f2d89eb3bf873886fc64d3f1fe5f84d7558434/images/tree.png -------------------------------------------------------------------------------- /images/webscoket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-server/30f2d89eb3bf873886fc64d3f1fe5f84d7558434/images/webscoket.png -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | publish = false 2 | push = false 3 | pre-release-commit-message = "chore(release): {{version}}" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "rustfmt", "clippy" ] -------------------------------------------------------------------------------- /taskfile.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | tasks: 4 | dev: 5 | desc: Run development server 6 | deps: [dev-lynx-server, dev-ui] 7 | 8 | test: 9 | desc: Run tests 10 | dir: crates/lynx-core 11 | cmds: 12 | - cargo test 13 | 14 | lint: 15 | desc: Run clippy linter 16 | cmds: 17 | - cargo clippy --all-targets --all-features -- -D warnings 18 | 19 | fix: 20 | desc: Run cargo fix 21 | cmds: 22 | - cargo fix --allow-dirty 23 | 24 | setup-ui: 25 | desc: Setup UI dependencies 26 | dir: crates/lynx-proxy 27 | cmds: 28 | - pnpm install 29 | 30 | dev-ui: 31 | desc: Run UI in development mode 32 | dir: crates/lynx-proxy 33 | cmds: 34 | - pnpm dev 35 | 36 | dev-mock-ui: 37 | desc: Run UI in mock development mode 38 | dir: crates/lynx-proxy 39 | cmds: 40 | - pnpm dev:mock 41 | 42 | build-ui: 43 | desc: Build UI 44 | cmds: 45 | - cd crates/lynx-proxy && pnpm build 46 | - rm -rf crates/lynx-cli/assets 47 | - cp -r crates/lynx-proxy/dist/ crates/lynx-cli/assets 48 | 49 | build-server: 50 | desc: Build server 51 | cmds: 52 | - cargo build --release 53 | 54 | build: 55 | desc: Build UI and server 56 | cmds: 57 | - task: build-ui 58 | - task: build-server 59 | 60 | dev-lynx-server: 61 | desc: Start test server 62 | dir: crates/lynx-core 63 | cmds: 64 | - cargo run --package lynx-core --example proxy_server_example 65 | 66 | release-minor: 67 | deps: [update-main-branch, build-ui, test] 68 | desc: Release minor version 69 | cmds: 70 | - cargo release minor --execute 71 | # - task: publish 72 | - task: generate-change-log 73 | release-patch: 74 | deps: [update-main-branch, build-ui, test] 75 | desc: Release patch version 76 | cmds: 77 | - cargo release patch --execute 78 | # - task: publish 79 | - task: generate-change-log 80 | release-alpha: 81 | deps: [update-main-branch, build-ui, test] 82 | desc: Release alpha version 83 | cmds: 84 | - cargo release alpha --execute 85 | # - task: publish 86 | - task: generate-change-log 87 | 88 | generate-change-log: 89 | desc: Generate change log 90 | cmds: 91 | - git cliff -o CHANGELOG.md 92 | - git add . 93 | - git commit --amend --no-edit 94 | - git push 95 | - git push --tags 96 | 97 | generate-type-defined: 98 | aliases: [gen-ty] 99 | dir: crates/lynx-core 100 | desc: Generate type defined 101 | cmds: 102 | - cargo test export_bindings 103 | 104 | update-main-branch: 105 | desc: Update remote branch 106 | cmds: 107 | - git pull origin main 108 | 109 | publish: 110 | desc: Publish to crates.io 111 | cmds: 112 | - task: build-ui 113 | - cargo publish --registry crates-io -p lynx-core 114 | - cargo publish --registry crates-io -p lynx-cli --allow-dirty 115 | --------------------------------------------------------------------------------