├── crates ├── lynx-proxy │ ├── src │ │ ├── env.ts │ │ ├── hooks │ │ │ ├── useApiDebug.ts │ │ │ ├── index.ts │ │ │ └── useDebugMode.ts │ │ ├── routes │ │ │ ├── apiDebug │ │ │ │ ├── components │ │ │ │ │ ├── SettingsEditor.tsx │ │ │ │ │ ├── apiDebugReducer.ts │ │ │ │ │ ├── store │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── CollectionTree │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── context │ │ │ │ │ │ │ └── NodeSelectionContext.tsx │ │ │ │ │ │ ├── CollectionPanel.tsx │ │ │ │ │ │ ├── TreeContainer.tsx │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── TreeSearch.tsx │ │ │ │ │ │ │ └── TreeToolbar.tsx │ │ │ │ │ │ └── hooks │ │ │ │ │ │ │ └── useNodeSelection.ts │ │ │ │ │ ├── CollectionPanel.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── Sidebar.tsx │ │ │ │ │ ├── BodyEditor.tsx │ │ │ │ │ └── RequestBuilder.tsx │ │ │ │ └── index.tsx │ │ │ ├── ruleManager │ │ │ │ ├── components │ │ │ │ │ └── InterceptorPage │ │ │ │ │ │ ├── CreateRuleDrawer │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── HandlerBehavior │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── config │ │ │ │ │ │ │ │ │ │ ├── TestHtmlScriptInjector.tsx │ │ │ │ │ │ │ │ │ │ ├── ModifyRequestConfig.tsx │ │ │ │ │ │ │ │ │ │ ├── ModifyResponseConfig.tsx │ │ │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ │ │ ├── ProxyForwardConfig.tsx │ │ │ │ │ │ │ │ │ │ └── BlockHandlerConfig.tsx │ │ │ │ │ │ │ │ │ ├── HandlerList.tsx │ │ │ │ │ │ │ │ │ └── handlerCollapseContext.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── context.tsx │ │ │ │ │ │ └── CreateRuleDrawer.tsx │ │ │ │ │ │ └── ModifyResponseModal │ │ │ │ │ │ └── context.tsx │ │ │ │ └── index.tsx │ │ │ ├── settings │ │ │ │ ├── components │ │ │ │ │ ├── NetworkSetting │ │ │ │ │ │ └── export.ts │ │ │ │ │ ├── CommonCard │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── SettingsMenu │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── ClientProxySettings.tsx │ │ │ │ ├── general.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── network.tsx │ │ │ │ ├── certificates.tsx │ │ │ │ └── client-proxy.tsx │ │ │ ├── index.tsx │ │ │ ├── network │ │ │ │ └── components │ │ │ │ │ ├── Sequence │ │ │ │ │ ├── index.css │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── WebSocketContent │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Toolbar │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Contents │ │ │ │ │ ├── CodeViewer │ │ │ │ │ │ └── prism.worker.ts │ │ │ │ │ ├── JsonPreview │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TextViewer │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Headers │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Reponse │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Request │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── MediaViewer │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TableFilter │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── BackToBottomButton │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FilterTemplate │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── ActiveTemplatesTags.tsx │ │ │ │ │ └── FilterTemplateModal.tsx │ │ │ │ │ ├── store │ │ │ │ │ ├── autoScrollStore.tsx │ │ │ │ │ └── selectRequestStore.tsx │ │ │ │ │ ├── CleanRequestButton │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── RequestDetailDrawer │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ShowTypeSegmented │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Structure │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Detail │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── RecordingStatusButton │ │ │ │ │ └── index.tsx │ │ │ │ │ └── Websocket │ │ │ │ │ └── index.tsx │ │ │ ├── settings.tsx │ │ │ └── __root.tsx │ │ ├── env.d.ts │ │ ├── global.d.ts │ │ ├── utils │ │ │ ├── ifTrue.ts │ │ │ └── curlGenerator.ts │ │ ├── mock │ │ │ └── node.ts │ │ ├── contexts │ │ │ ├── index.ts │ │ │ ├── useAntdLocale.ts │ │ │ ├── useI18n.ts │ │ │ └── LanguageContext.tsx │ │ ├── components │ │ │ ├── MonacoEditor │ │ │ │ ├── export.ts │ │ │ │ └── types.ts │ │ │ ├── PageLoading │ │ │ │ └── index.tsx │ │ │ ├── RequestContextMenu │ │ │ │ ├── types.ts │ │ │ │ ├── context.tsx │ │ │ │ ├── utils.ts │ │ │ │ └── index.tsx │ │ │ └── LanguageSelector │ │ │ │ └── index.tsx │ │ ├── store │ │ │ ├── useInterval.ts │ │ │ └── useGeneralState.tsx │ │ ├── i18n.ts │ │ ├── services │ │ │ ├── generated │ │ │ │ ├── net-request-sse │ │ │ │ │ └── net-request-sse.msw.ts │ │ │ │ ├── default │ │ │ │ │ └── default.msw.ts │ │ │ │ ├── system │ │ │ │ │ └── system.msw.ts │ │ │ │ └── certificate │ │ │ │ │ └── certificate.msw.ts │ │ │ └── customInstance.ts │ │ ├── test │ │ │ └── setup.ts │ │ ├── main.css │ │ └── index.tsx │ ├── public │ │ ├── .gitkeep │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── icon.png │ │ │ ├── icon-192.png │ │ │ └── icon-512.png │ │ └── manifest.json │ ├── .prettierignore │ ├── Readme.md │ ├── .prettierrc │ ├── Readme.zh-CN.md │ ├── postcss.config.js │ ├── tsr.config.json │ ├── .gitignore │ ├── CHANGELOG.md │ ├── tailwind.config.js │ ├── vitest.config.ts │ ├── orval.config.ts │ ├── tsconfig.json │ ├── test-import-rule.json │ ├── eslint.config.mjs │ └── test-import-rules-complete.json ├── lynx-log │ ├── .gitignore │ ├── dist.toml │ ├── Cargo.toml │ └── docker-compose.yml ├── lynx-mock │ ├── .gitignore │ ├── src │ │ ├── lib.rs │ │ └── mark_service.rs │ ├── examples │ │ ├── start_test_server.rs │ │ └── server.rs │ └── Cargo.toml ├── lynx-core │ ├── tests │ │ ├── temp │ │ │ ├── subdir │ │ │ │ └── nested.txt │ │ │ ├── test.json │ │ │ ├── test.txt │ │ │ ├── test.html │ │ │ └── test.css │ │ ├── setup │ │ │ ├── setup_mock_server.rs │ │ │ ├── setup_self_service_test_server.rs │ │ │ ├── mod.rs │ │ │ ├── setup_tracing.rs │ │ │ ├── setup_api_debug_server.rs │ │ │ └── setup_proxy_handler_server.rs │ │ ├── hello_test.rs │ │ ├── html_script_injector_test.rs │ │ └── proxy_test.rs │ ├── .gitignore │ ├── Readme.md │ ├── src │ │ ├── layers │ │ │ ├── connect_req_patch_layer │ │ │ │ ├── mod.rs │ │ │ │ └── service.rs │ │ │ ├── trace_id_layer │ │ │ │ ├── mod.rs │ │ │ │ ├── layout.rs │ │ │ │ └── service.rs │ │ │ ├── req_extension_layer │ │ │ │ ├── mod.rs │ │ │ │ ├── layout.rs │ │ │ │ └── service.rs │ │ │ ├── log_layer │ │ │ │ ├── mod.rs │ │ │ │ ├── layout.rs │ │ │ │ ├── future.rs │ │ │ │ └── service.rs │ │ │ ├── error_handle_layer │ │ │ │ ├── mod.rs │ │ │ │ ├── layout.rs │ │ │ │ ├── service.rs │ │ │ │ └── future.rs │ │ │ ├── mod.rs │ │ │ ├── message_package_layer │ │ │ │ └── mod.rs │ │ │ └── request_processing_layer │ │ │ │ ├── layout.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── future.rs │ │ │ │ └── handler_trait.rs │ │ ├── proxy │ │ │ ├── mod.rs │ │ │ ├── tunnel_proxy_by_stream.rs │ │ │ ├── proxy_tunnel_request.rs │ │ │ └── proxy_http_request.rs │ │ ├── lib.rs │ │ ├── self_service │ │ │ ├── api │ │ │ │ ├── mod.rs │ │ │ │ ├── base_info.rs │ │ │ │ ├── general_setting.rs │ │ │ │ ├── client_proxy.rs │ │ │ │ └── https_capture.rs │ │ │ └── file_service.rs │ │ ├── proxy_server │ │ │ └── server_config.rs │ │ ├── client │ │ │ └── mod.rs │ │ ├── gateway_service.rs │ │ ├── utils.rs │ │ └── common │ │ │ └── mod.rs │ └── examples │ │ ├── temp │ │ ├── root.pem │ │ └── key.pem │ │ └── proxy_server_example.rs ├── lynx-db │ ├── src │ │ ├── lib.rs │ │ ├── dao │ │ │ ├── mod.rs │ │ │ └── request_processing_dao │ │ │ │ ├── handlers │ │ │ │ ├── proxy_forward_handler.rs │ │ │ │ ├── local_file_handler.rs │ │ │ │ ├── modify_request_handler.rs │ │ │ │ ├── modify_response_handler.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── html_script_injector.rs │ │ │ │ └── block_handler.rs │ │ │ │ └── error.rs │ │ ├── entities │ │ │ ├── mod.rs │ │ │ └── prelude.rs │ │ └── migration │ │ │ ├── mod.rs │ │ │ ├── api_debug.rs │ │ │ └── request_processing.rs │ └── Cargo.toml ├── lynx-cli │ ├── Readme.md │ ├── .gitignore │ ├── src │ │ └── daemon │ │ │ ├── mod.rs │ │ │ └── status.rs │ ├── Readme.zh-CN.md │ └── Cargo.toml └── lynx-cert │ └── Cargo.toml ├── examples ├── self_signed_ca │ ├── .gitignore │ ├── dist.toml │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── websocket-client │ ├── dist.toml │ └── Cargo.toml └── websocket-server │ ├── dist.toml │ ├── self_signed_certs │ ├── key.pem │ └── cert.pem │ └── Cargo.toml ├── .vercel └── project.json ├── .task └── checksum │ └── generate-type-defined ├── images ├── newws.png ├── rule.png ├── newhttp.png ├── newtree.png ├── api_debug.png ├── contextmenu.png └── contextmenu2.png ├── rust-toolchain.toml ├── release.toml ├── .gitignore ├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── 功能请求.md │ └── bug-报告.md └── workflows │ └── test.yml ├── SECURITY.md ├── CONTRIBUTING.md ├── dist-workspace.toml ├── LICENSE ├── actions └── build-ui │ └── action.yml └── Cargo.toml /crates/lynx-proxy/src/env.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/lynx-proxy/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/self_signed_ca/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /crates/lynx-log/.gitignore: -------------------------------------------------------------------------------- 1 | examples/temp -------------------------------------------------------------------------------- /crates/lynx-mock/.gitignore: -------------------------------------------------------------------------------- 1 | examples/temp -------------------------------------------------------------------------------- /crates/lynx-proxy/src/hooks/useApiDebug.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vercel/project.json: -------------------------------------------------------------------------------- 1 | {"projectName":"trae_jvu3ilzl"} -------------------------------------------------------------------------------- /crates/lynx-log/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false 3 | -------------------------------------------------------------------------------- /examples/self_signed_ca/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false -------------------------------------------------------------------------------- /examples/websocket-client/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false -------------------------------------------------------------------------------- /examples/websocket-server/dist.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | dist = false -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/SettingsEditor.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/apiDebugReducer.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.task/checksum/generate-type-defined: -------------------------------------------------------------------------------- 1 | 7a6bac8899e841b08613f243fa70dbb8 2 | -------------------------------------------------------------------------------- /crates/lynx-core/tests/temp/subdir/nested.txt: -------------------------------------------------------------------------------- 1 | This is a file in a subdirectory. 2 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /crates/lynx-db/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod dao; 2 | pub mod entities; 3 | pub mod migration; 4 | -------------------------------------------------------------------------------- /images/newws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/newws.png -------------------------------------------------------------------------------- /images/rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/rule.png -------------------------------------------------------------------------------- /images/newhttp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/newhttp.png -------------------------------------------------------------------------------- /images/newtree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/newtree.png -------------------------------------------------------------------------------- /images/api_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/api_debug.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "rustfmt", "clippy" ] -------------------------------------------------------------------------------- /images/contextmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/contextmenu.png -------------------------------------------------------------------------------- /images/contextmenu2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/images/contextmenu2.png -------------------------------------------------------------------------------- /crates/lynx-proxy/.prettierignore: -------------------------------------------------------------------------------- 1 | # Lock files 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDebugMode'; 2 | // Export other hooks as needed 3 | -------------------------------------------------------------------------------- /crates/lynx-cli/Readme.md: -------------------------------------------------------------------------------- 1 | # lynx cli 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | TODO 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/lynx-core/.gitignore: -------------------------------------------------------------------------------- 1 | examples/temp 2 | tests/temp/key.pem 3 | tests/temp/root.pem 4 | tests/temp/lynx.db -------------------------------------------------------------------------------- /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-proxy/Readme.md: -------------------------------------------------------------------------------- 1 | # lynx proxy 2 | 3 | English | [简体中文](./Readme.zh-CN.md) 4 | 5 | TODO 6 | 7 | 8 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | publish = false 2 | push = true 3 | pre-release-commit-message = "chore(release): {{version}}" 4 | -------------------------------------------------------------------------------- /crates/lynx-proxy/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "plugins": ["prettier-plugin-tailwindcss"] 4 | } 5 | -------------------------------------------------------------------------------- /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-proxy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/crates/lynx-proxy/public/favicon.ico -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/connect_req_patch_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod service; 2 | 3 | pub use service::ConnectReqPatchService; 4 | -------------------------------------------------------------------------------- /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/public/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/crates/lynx-proxy/public/icons/icon.png -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apiDebugSlice'; 2 | export * from './useApiDebug'; 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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-proxy/public/icons/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/crates/lynx-proxy/public/icons/icon-192.png -------------------------------------------------------------------------------- /crates/lynx-proxy/public/icons/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suxin2017/lynx-proxy/HEAD/crates/lynx-proxy/public/icons/icon-512.png -------------------------------------------------------------------------------- /crates/lynx-proxy/src/utils/ifTrue.ts: -------------------------------------------------------------------------------- 1 | export function ifTrue(condition: boolean, expr: T) { 2 | return condition ? expr : null; 3 | } 4 | -------------------------------------------------------------------------------- /crates/lynx-cli/src/daemon/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod manager; 2 | pub mod status; 3 | 4 | pub use manager::DaemonManager; 5 | pub use status::DaemonStatus; 6 | -------------------------------------------------------------------------------- /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/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/config/TestHtmlScriptInjector.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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-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-proxy/postcss.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | } 6 | } -------------------------------------------------------------------------------- /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-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/tsr.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routesDirectory": "./src/routes", 3 | "generatedRouteTree": "./src/routeTree.gen.ts", 4 | "routeFileIgnorePrefix": "component" 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-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-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-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-proxy/src/routes/settings/components/NetworkSetting/export.ts: -------------------------------------------------------------------------------- 1 | export { NetworkSetting } from './index'; 2 | export { ClientProxyConfigComponent, type ClientProxyConfig } from './ClientProxyConfig'; 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 utils; 10 | -------------------------------------------------------------------------------- /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-db/src/dao/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_debug_dao; 2 | pub mod api_debug_tree_dao; 3 | pub mod client_proxy_dao; 4 | pub mod general_setting_dao; 5 | pub mod https_capture_dao; 6 | pub mod net_request_dao; 7 | pub mod request_processing_dao; 8 | -------------------------------------------------------------------------------- /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-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-db/src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4 2 | 3 | pub mod prelude; 4 | 5 | pub mod api_debug; 6 | pub mod api_debug_tree; 7 | pub mod app_config; 8 | pub mod capture; 9 | pub mod handler; 10 | pub mod rule; 11 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/MonacoEditor/export.ts: -------------------------------------------------------------------------------- 1 | export { MonacoEditor } from './index'; 2 | export type { MonacoEditorProps, Language } from './index'; 3 | export { SUPPORTED_LANGUAGES, EDITOR_THEMES } from './types'; 4 | export type { EditorTheme } from './types'; 5 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/routes/apiDebug/components/CollectionTree/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CollectionPanel } from './CollectionPanel'; 2 | export { default as TreeContainer } from './TreeContainer'; 3 | export { TreeProvider, useTreeStore } from './store/treeStore'; 4 | export type { TreeNode } from './store/treeStore'; -------------------------------------------------------------------------------- /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/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 request_processing_layer; 9 | pub mod trace_id_layer; 10 | -------------------------------------------------------------------------------- /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-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/src/self_service/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_debug; 2 | pub mod api_debug_executor; 3 | pub mod api_debug_tree; 4 | pub mod base_info; 5 | pub mod certificate; 6 | pub mod client_proxy; 7 | pub mod general_setting; 8 | pub mod https_capture; 9 | pub mod net_request; 10 | pub mod net_request_sse; 11 | pub mod request_processing; 12 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { ApiDebugPage } from './components/ApiDebugPage'; 3 | 4 | export const Route = createFileRoute('/apiDebug/')({ 5 | component: RouteComponent, 6 | }); 7 | 8 | function RouteComponent() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /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/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/routes/apiDebug/components/CollectionTree/context/NodeSelectionContext.tsx: -------------------------------------------------------------------------------- 1 | import constate from 'constate'; 2 | import { useNodeSelection } from '../hooks/useNodeSelection'; 3 | 4 | /** 5 | * 使用 constate 创建节点选择状态的 Context 6 | * 避免 prop drilling,提供统一的节点选择逻辑 7 | */ 8 | export const [NodeSelectionProvider, useNodeSelectionContext] = constate(useNodeSelection); -------------------------------------------------------------------------------- /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/message_package_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod channel; 2 | pub mod compression; 3 | pub mod event_handler; 4 | pub mod message_event_data; 5 | pub mod message_event_store; 6 | pub mod services; 7 | 8 | // 重新导出主要类型 9 | pub use channel::MessageEventChannel; 10 | pub use services::{MessageEventLayerExt, ProxyMessageEventService, RequestMessageEventService}; 11 | -------------------------------------------------------------------------------- /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-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-db/src/entities/prelude.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4 2 | 3 | pub use super::api_debug::Entity as ApiDebug; 4 | pub use super::api_debug_tree::Entity as ApiDebugTree; 5 | pub use super::app_config::Entity as AppConfig; 6 | pub use super::capture::Entity as Capture; 7 | pub use super::handler::Entity as Handler; 8 | pub use super::rule::Entity as Rule; 9 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/settings/client-proxy.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from '@tanstack/react-router'; 2 | import { ClientProxySettings } from './components/ClientProxySettings'; 3 | 4 | export const Route = createFileRoute('/settings/client-proxy')({ 5 | component: RouteComponent, 6 | }); 7 | 8 | function RouteComponent() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/功能请求.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 3 | about: 提出新的功能建议或改进意见 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 功能描述 11 | 12 | 请简要描述您希望添加的功能或改进。 13 | 14 | ## 解决的问题 15 | 16 | 此功能将解决什么问题或带来哪些提升? 17 | 18 | ## 实现建议 19 | 20 | 请提供实现该功能的建议或思路(可选)。 21 | 22 | ## 相关资料 23 | 24 | 如有相关资料(设计图、参考链接等),请在此附上。 25 | 26 | ## 补充说明 27 | 28 | 其它需要补充的信息。 29 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/CollectionPanel.tsx: -------------------------------------------------------------------------------- 1 | import { CollectionPanel as TreeCollectionPanel } from './CollectionTree'; 2 | 3 | interface CollectionPanelProps { 4 | className?: string; 5 | } 6 | 7 | export function CollectionPanel({ className }: CollectionPanelProps) { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /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-proxy/src/routes/network/components/Sequence/index.css: -------------------------------------------------------------------------------- 1 | .gutter { 2 | background-color: #eee; 3 | background-repeat: no-repeat; 4 | background-position: 50%; 5 | } 6 | .gutter.gutter-horizontal { 7 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 8 | cursor: col-resize; 9 | } -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/proxy_forward_handler.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use utoipa::ToSchema; 3 | 4 | #[derive(Debug, Serialize, Deserialize, ToSchema, Default, Clone)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct ProxyForwardConfig { 7 | pub target_scheme: Option, 8 | pub target_authority: Option, 9 | pub target_path: Option, 10 | } 11 | -------------------------------------------------------------------------------- /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-proxy/src/components/RequestContextMenu/types.ts: -------------------------------------------------------------------------------- 1 | import { IViewMessageEventStoreValue } from '@/store/useSortPoll'; 2 | 3 | export interface RequestContextMenuState { 4 | selectedRecord: IViewMessageEventStoreValue | null; 5 | setSelectedRecord: (record: IViewMessageEventStoreValue | null) => void; 6 | handleContextMenu: ( 7 | record: IViewMessageEventStoreValue, 8 | event: React.MouseEvent, 9 | ) => void; 10 | } 11 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiDebugPage } from './ApiDebugPage'; 2 | export { RequestBuilder } from './RequestBuilder'; 3 | export { HeadersEditor } from './HeadersEditor'; 4 | export { BodyEditor } from './BodyEditor'; 5 | export { ResponseViewer } from './ResponseViewer'; 6 | export { CurlImportModal } from './CurlImportModal'; 7 | export { RequestHistory } from './RequestHistory'; 8 | export * from './types'; 9 | -------------------------------------------------------------------------------- /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-proxy/src/routes/apiDebug/components/CollectionTree/CollectionPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeContainer from './TreeContainer'; 3 | import { TreeProvider } from './store/treeStore'; 4 | 5 | const CollectionPanel: React.FC = () => { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default CollectionPanel; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-报告.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 报告 3 | about: 用于报告项目中的缺陷或异常行为 4 | title: "[Bug] 报告" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 描述 11 | 12 | 请简要描述遇到的问题。 13 | 14 | ## 复现步骤 15 | 16 | 详细描述如何复现该问题: 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 22 | ## 预期行为 23 | 24 | 请描述原本期望的行为。 25 | 26 | ## 实际行为 27 | 28 | 实际发生了什么? 29 | 30 | ## 截图或日志(如有) 31 | 32 | 请附上相关截图或日志信息。 33 | 34 | ## 环境信息 35 | 36 | - 操作系统: 37 | - 浏览器/客户端(如适用): 38 | - 相关依赖版本: 39 | 40 | ## 补充说明 41 | 42 | 如有其它补充信息,请在此填写。 43 | -------------------------------------------------------------------------------- /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/src/components/LanguageSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select } from 'antd'; 3 | 4 | const { Option } = Select; 5 | 6 | export const LanguageSelector: React.FC<{ 7 | value?: string, 8 | onChange?: (value: string) => void; 9 | }> = ({ value, onChange }) => { 10 | 11 | 12 | return ( 13 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /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/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/config/ModifyRequestConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModifyConfigBase } from './ModifyConfigBase'; 3 | 4 | interface ModifyRequestConfigProps { 5 | field: { 6 | key: number; 7 | name: number; 8 | }; 9 | } 10 | 11 | export const ModifyRequestConfig: React.FC = ({ field }) => { 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /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/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/config/ModifyResponseConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModifyConfigBase } from './ModifyConfigBase'; 3 | 4 | interface ModifyResponseConfigProps { 5 | field: { 6 | key: number; 7 | name: number; 8 | }; 9 | } 10 | 11 | export const ModifyResponseConfig: React.FC = ({ field }) => { 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /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-core/src/layers/request_processing_layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_handler_trait; 2 | pub mod delay_handler_trait; 3 | pub mod future; 4 | pub mod handler_trait; 5 | pub mod html_script_injector_trait; 6 | pub mod layout; 7 | pub mod local_file_handler_trait; 8 | pub mod modify_request_handler_trait; 9 | pub mod modify_response_handler_trait; 10 | pub mod proxy_forward_handler_trait; 11 | pub mod service; 12 | 13 | pub use future::RequestProcessingFuture; 14 | pub use layout::RequestProcessingLayer; 15 | pub use service::RequestProcessingService; 16 | -------------------------------------------------------------------------------- /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-db/src/migration/mod.rs: -------------------------------------------------------------------------------- 1 | pub use sea_orm_migration::prelude::*; 2 | 3 | pub mod api_debug; 4 | pub mod api_debug_tree; 5 | pub mod app_config; 6 | pub mod request_processing; 7 | 8 | pub struct Migrator; 9 | 10 | #[async_trait::async_trait] 11 | impl MigratorTrait for Migrator { 12 | fn migrations() -> Vec> { 13 | vec![ 14 | Box::new(app_config::Migration), 15 | Box::new(request_processing::Migration), 16 | Box::new(api_debug::Migration), 17 | Box::new(api_debug_tree::Migration), 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/lynx-proxy/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { pluginReact } from '@rsbuild/plugin-react'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | plugins: [pluginReact()], 7 | test: { 8 | environment: 'jsdom', 9 | globals: true, 10 | setupFiles: ['./src/test/setup.ts'], 11 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 12 | exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'], 13 | }, 14 | resolve: { 15 | alias: { 16 | '@': path.resolve(__dirname, './src'), 17 | }, 18 | }, 19 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { SearchRequestUrlInput } from '../TableFilter'; 5 | 6 | export const Toolbar: React.FC = ({ children }) => { 7 | return ( 8 |
9 | 10 | 11 | 12 | {children} 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # 安全策略 2 | 3 | 感谢您关注和使用本项目!我们非常重视项目的安全性,并欢迎社区成员报告潜在的安全漏洞。 4 | 5 | ## 报告安全漏洞 6 | 7 | 如您在本项目中发现任何安全相关的问题,请按照以下流程处理: 8 | 9 | 1. **请勿公开讨论漏洞** 10 | 为了最大程度保护用户安全,请勿通过 GitHub Issues、Pull Requests 或公开渠道报告安全漏洞。 11 | 12 | 2. **私密报告方式** 13 | 请通过电子邮件联系项目维护者: 14 | **suxin2017@outlook.com** 15 | 在邮件中请尽可能详细地描述漏洞,包括重现步骤、影响范围及建议的修复方式(如有)。 16 | 17 | 3. **响应承诺** 18 | 我们收到报告后会尽快回复,并在修复前与报告人保持沟通。我们会在修复后感谢报告人(如愿意署名)。 19 | 20 | ## 关于安全更新 21 | 22 | - 修复安全漏洞后,我们会及时在发布说明中告知用户。 23 | - 我们鼓励所有用户及时升级到最新版本。 24 | 25 | ## 免责声明 26 | 27 | 请注意,虽然我们努力确保项目安全,但不能保证完全无漏洞。请在生产环境中根据实际需求谨慎使用。 28 | 29 | --- 30 | 31 | 感谢您的负责任披露和支持! 32 | -------------------------------------------------------------------------------- /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/routes/apiDebug/components/CollectionTree/TreeContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TreeUIProvider } from './context/TreeContext'; 3 | import TreeToolbar from './components/TreeToolbar'; 4 | import TreeView from './components/TreeView'; 5 | import TreeModals from './components/TreeModals'; 6 | 7 | const TreeContainer: React.FC = () => { 8 | return ( 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default TreeContainer; -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/CollectionTree/components/TreeSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input } from 'antd'; 3 | import { SearchOutlined } from '@ant-design/icons'; 4 | import { useTreeUI } from '../context/TreeContext'; 5 | 6 | const TreeSearch: React.FC = () => { 7 | const { searchValue, setSearchValue } = useTreeUI(); 8 | 9 | return ( 10 | setSearchValue(e.target.value)} 14 | prefix={} 15 | allowClear 16 | /> 17 | ); 18 | }; 19 | 20 | export default TreeSearch; -------------------------------------------------------------------------------- /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/orval.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'orval'; 2 | 3 | export default defineConfig({ 4 | api: { 5 | input: { 6 | target: 'http://127.0.0.1:7788/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-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-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-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-proxy/src/store/useGeneralState.tsx: -------------------------------------------------------------------------------- 1 | import constate from 'constate'; 2 | import { useGetGeneralSetting } from '@/services/generated/general-setting/general-setting'; 3 | 4 | export enum ConnectType { 5 | ShortPoll = '0', 6 | SSE = '1', 7 | } 8 | 9 | export const [GeneralSettingProvider, useGeneralSetting] = constate(() => { 10 | const { data: generalSettingResponse, isLoading } = useGetGeneralSetting(); 11 | 12 | const generalSetting = generalSettingResponse?.data || { 13 | maxLogSize: 1000, 14 | connectType: ConnectType.ShortPoll, 15 | language: 'en' 16 | }; 17 | 18 | 19 | 20 | return { 21 | ...generalSetting, 22 | generalSetting, 23 | isLoading, 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /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-db/src/dao/request_processing_dao/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_handler; 2 | pub mod delay_handler; 3 | pub mod handler_rule; 4 | pub mod html_script_injector; 5 | pub mod local_file_handler; 6 | pub mod modify_request_handler; 7 | pub mod modify_response_handler; 8 | pub mod proxy_forward_handler; 9 | 10 | pub use block_handler::BlockHandlerConfig; 11 | pub use delay_handler::{DelayHandlerConfig, DelayType}; 12 | pub use handler_rule::HandlerRule; 13 | pub use html_script_injector::HtmlScriptInjectorConfig; 14 | pub use local_file_handler::LocalFileConfig; 15 | pub use modify_request_handler::ModifyRequestConfig; 16 | pub use modify_response_handler::ModifyResponseConfig; 17 | pub use proxy_forward_handler::ProxyForwardConfig; 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Contents/CodeViewer/prism.worker.ts: -------------------------------------------------------------------------------- 1 | // prism.worker.ts 2 | // @ts-ignore 3 | global.Prism = { disableWorkerMessageHandler: true }; 4 | const Prism = require('prismjs'); 5 | 6 | self.onmessage = function (e) { 7 | const { code, language } = e.data; 8 | if (typeof Prism !== 'undefined') { 9 | try { 10 | // @ts-ignore 11 | const html = Prism.highlight( 12 | code, 13 | // @ts-ignore 14 | Prism.languages[language] || Prism.languages.plain, 15 | language, 16 | ); 17 | self.postMessage({ html }); 18 | } catch (err) { 19 | self.postMessage({ html: code }); 20 | } 21 | } else { 22 | self.postMessage({ html: code }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /crates/lynx-log/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lynx-log" 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 | anyhow = { workspace = true } 14 | tokio = { workspace = true } 15 | tracing = { workspace = true } 16 | tracing-subscriber = { workspace = true } 17 | tracing-appender = "0.2" 18 | tracing-opentelemetry = "0.31" 19 | opentelemetry = { version = "0.30.0", features = ["trace"] } 20 | opentelemetry_sdk = { version = "0.30", features = ["trace", "rt-tokio"] } 21 | opentelemetry-otlp = { version = "0.30" } 22 | derive_builder = "0.20.2" 23 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/TableFilter/index.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from '@/contexts'; 2 | import { filterUri } from '@/store/requestTableStore'; 3 | import { Input } from 'antd'; 4 | import React from 'react'; 5 | import { useDispatch } from 'react-redux'; 6 | 7 | interface ISearchRequestUrlInputProps {} 8 | 9 | export const SearchRequestUrlInput: React.FC< 10 | ISearchRequestUrlInputProps 11 | > = () => { 12 | const dispatch = useDispatch(); 13 | const { t } = useI18n(); 14 | 15 | return ( 16 | { 21 | dispatch(filterUri(e.target.value)); 22 | }} 23 | /> 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /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 | { 16 | setAutoScroll(val); 17 | }} 18 | title={t('network.toolbar.autoScroll')} 19 | /> 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /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-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/routes/network/components/Contents/JsonPreview/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import ReactJson from 'react18-json-view'; 3 | import 'react18-json-view/src/style.css'; 4 | 5 | interface IJsonPreviewProps { 6 | arrayBuffer?: ArrayBuffer; 7 | } 8 | 9 | export const JsonPreview: React.FC = ({ arrayBuffer }) => { 10 | const json = useMemo(() => { 11 | if (!arrayBuffer) { 12 | return null; 13 | } 14 | try { 15 | return JSON.parse(new TextDecoder().decode(arrayBuffer)); 16 | } catch (e) { 17 | return { 18 | error: `Lynx Server JSON Parse Error`, 19 | reason: e 20 | }; 21 | } 22 | }, [arrayBuffer]); 23 | if (!json) { 24 | return null; 25 | } 26 | return ; 27 | }; 28 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/FilterTemplate/utils.ts: -------------------------------------------------------------------------------- 1 | // 生成唯一 ID 2 | export const generateId = (): string => { 3 | return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 4 | }; 5 | 6 | // 验证模板名称 7 | export const validateTemplateName = (name: string): boolean => { 8 | return name.trim().length > 0 && name.trim().length <= 50; 9 | }; 10 | 11 | // 验证过滤条件值 12 | export const validateConditionValue = (value: string, operator: string): boolean => { 13 | if (!value.trim()) return false; 14 | 15 | if (operator === 'greaterThan' || operator === 'lessThan') { 16 | return !isNaN(Number(value)); 17 | } 18 | 19 | if (operator === 'regex') { 20 | try { 21 | new RegExp(value); 22 | return true; 23 | } catch { 24 | return false; 25 | } 26 | } 27 | 28 | return true; 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-proxy/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lynx Proxy", 3 | "short_name": "Lynx Proxy", 4 | "description": "A powerful proxy tool for development and testing", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#f8fafc", 8 | "theme_color": "#f8fafc", 9 | "orientation": "portrait-primary", 10 | "scope": "/", 11 | "lang": "en", 12 | "icons": [ 13 | { 14 | "src": "/icons/icon-192.png", 15 | "sizes": "192x192", 16 | "type": "image/png", 17 | "purpose": "any maskable" 18 | }, 19 | { 20 | "src": "/icons/icon-512.png", 21 | "sizes": "512x512", 22 | "type": "image/png", 23 | "purpose": "any maskable" 24 | } 25 | ], 26 | "categories": ["development", "utilities"], 27 | "screenshots": [], 28 | "prefer_related_applications": false 29 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Rust Tests 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Cache Rust dependencies 17 | uses: actions/cache@v4 18 | with: 19 | path: | 20 | ~/.cargo/registry 21 | ~/.cargo/git 22 | target 23 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-cargo- 26 | 27 | - name: Build UI 28 | uses: ./actions/build-ui 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Run tests 33 | run: task test 34 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/RequestContextMenu/context.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import constate from 'constate'; 3 | import { IViewMessageEventStoreValue } from '@/store/useSortPoll'; 4 | import { RequestContextMenuState } from './types'; 5 | 6 | export const [RequestContextMenuProvider, useRequestContextMenuContext] = constate( 7 | (): RequestContextMenuState => { 8 | const [selectedRecord, setSelectedRecord] = 9 | useState(null); 10 | 11 | const handleContextMenu = ( 12 | record: IViewMessageEventStoreValue, 13 | event: React.MouseEvent, 14 | ) => { 15 | event.preventDefault(); 16 | setSelectedRecord(record); 17 | }; 18 | 19 | return { 20 | selectedRecord, 21 | setSelectedRecord, 22 | handleContextMenu, 23 | }; 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /crates/lynx-proxy/test-import-rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "exportTime": "2025-01-11T10:00:00.000Z", 4 | "rules": [ 5 | { 6 | "name": "从v", 7 | "description": "的发射点", 8 | "enabled": true, 9 | "priority": 50, 10 | "capture": { 11 | "condition": { 12 | "type": "simple", 13 | "urlPattern": { 14 | "captureType": "glob", 15 | "pattern": "撒地方" 16 | } 17 | } 18 | }, 19 | "handlers": [ 20 | { 21 | "name": "阻止请求", 22 | "description": null, 23 | "enabled": true, 24 | "executionOrder": 0, 25 | "handlerType": { 26 | "type": "block", 27 | "statusCode": 403, 28 | "reason": "访问被阻止" 29 | } 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /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_api_debug_server; 6 | pub mod setup_mock_server; 7 | pub mod setup_proxy_handler_server; 8 | pub mod setup_proxy_server; 9 | pub mod setup_self_service_test_server; 10 | pub mod setup_tracing; 11 | 12 | #[allow(dead_code)] 13 | pub fn base_url(proxy_server: &ProxyServer) -> String { 14 | format!( 15 | "http://{}{}", 16 | proxy_server 17 | .access_addr_list 18 | .first() 19 | .expect("show get access addr"), 20 | SELF_SERVICE_PATH_PREFIX 21 | ) 22 | } 23 | 24 | #[allow(dead_code)] 25 | pub fn mock_base_url(mock_server: &MockServer) -> String { 26 | format!("http://{}", mock_server.addr) 27 | } 28 | -------------------------------------------------------------------------------- /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-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/routes/apiDebug/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface ApiRequest { 2 | url: string; 3 | method: string; 4 | headers: Record; 5 | body: string; 6 | contentType: string; 7 | timeout: number; 8 | } 9 | 10 | export interface ApiResponse { 11 | status: number; 12 | statusText: string; 13 | headers: Record; 14 | body: string; 15 | responseTime: number; 16 | errorMessage?: string; 17 | } 18 | 19 | export interface HeaderItem { 20 | key: string; 21 | value: string; 22 | enabled: boolean; 23 | } 24 | 25 | export interface QueryParamItem { 26 | key: string; 27 | value: string; 28 | enabled: boolean; 29 | } 30 | 31 | export interface FormattedResponse { 32 | status: number; 33 | statusText: string; 34 | headers: Record; 35 | body: string; 36 | responseTime: number; 37 | size: number; 38 | } 39 | -------------------------------------------------------------------------------- /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(7788)); 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-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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/services/generated/net-request-sse/net-request-sse.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 { HttpResponse, delay, http } from 'msw'; 9 | 10 | export const getMessageEventsSseMockHandler = ( 11 | overrideResponse?: 12 | | unknown 13 | | (( 14 | info: Parameters[1]>[0], 15 | ) => Promise | unknown), 16 | ) => { 17 | return http.get('*/net_request/sse/message-events', async (info) => { 18 | await delay(1000); 19 | if (typeof overrideResponse === 'function') { 20 | await overrideResponse(info); 21 | } 22 | return new HttpResponse(null, { status: 200 }); 23 | }); 24 | }; 25 | export const getNetRequestSseMock = () => [getMessageEventsSseMockHandler()]; 26 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { vi } from 'vitest'; 3 | 4 | // Mock window.matchMedia 5 | Object.defineProperty(window, 'matchMedia', { 6 | writable: true, 7 | value: vi.fn().mockImplementation(query => ({ 8 | matches: false, 9 | media: query, 10 | onchange: null, 11 | addListener: vi.fn(), // deprecated 12 | removeListener: vi.fn(), // deprecated 13 | addEventListener: vi.fn(), 14 | removeEventListener: vi.fn(), 15 | dispatchEvent: vi.fn(), 16 | })), 17 | }); 18 | 19 | // Mock ResizeObserver 20 | global.ResizeObserver = vi.fn().mockImplementation(() => ({ 21 | observe: vi.fn(), 22 | unobserve: vi.fn(), 23 | disconnect: vi.fn(), 24 | })); 25 | 26 | // Mock IntersectionObserver 27 | global.IntersectionObserver = vi.fn().mockImplementation(() => ({ 28 | observe: vi.fn(), 29 | unobserve: vi.fn(), 30 | disconnect: vi.fn(), 31 | })); -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Contents/TextViewer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Empty } from 'antd'; 2 | import React, { useMemo } from 'react'; 3 | 4 | interface TextViewProps { 5 | arrayBuffer?: ArrayBuffer; 6 | text?: string; 7 | } 8 | 9 | const TextView: React.FC = ({ arrayBuffer, text }) => { 10 | const data = useMemo(() => { 11 | if (text) return text; 12 | if (!arrayBuffer) return null; 13 | 14 | return new TextDecoder().decode(new Uint8Array(arrayBuffer)); 15 | }, [arrayBuffer, text]); 16 | 17 | if (!data) 18 | return ( 19 |
20 | 21 |
22 | ); 23 | 24 | return ( 25 |
26 |
{data}
27 |
28 | ); 29 | }; 30 | 31 | export default TextView; 32 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 感谢您对本项目的关注!我们欢迎任何形式的贡献,包括但不限于报告 Bug、提交功能请求、改进文档和代码贡献。 4 | 5 | ## 如何参与贡献 6 | 7 | 1. **Fork 仓库** 8 | 点击右上角的 Fork 按钮,将本仓库 Fork 到您的账户下。 9 | 10 | 2. **创建分支** 11 | 在本地克隆您的 Fork,并基于 `main`(或最新开发分支)创建新分支: 12 | ```bash 13 | git checkout -b feature/your-feature-name 14 | ``` 15 | 16 | 3. **提交更改** 17 | 做出您的更改后,请确保代码风格统一、通过所有测试,并编写简明的提交信息: 18 | ```bash 19 | git add . 20 | git commit -m "描述你的修改" 21 | ``` 22 | 23 | 4. **推送到远程仓库** 24 | ```bash 25 | git push origin feature/your-feature-name 26 | ``` 27 | 28 | 5. **创建 Pull Request** 29 | 在 GitHub 上发起 Pull Request,描述您的更改内容及动机。 30 | 31 | ## 贡献代码须知 32 | 33 | - 保持代码整洁、可读。 34 | - 如涉及新功能,请补充或更新相关文档。 35 | - 请保证所有单元测试通过,如有新增功能也请添加测试用例。 36 | - 遵循本项目的代码规范(如有 `eslint`, `prettier` 等工具请先运行)。 37 | 38 | ## Bug 报告与建议 39 | 40 | - 请尽量详细描述问题,包括复现步骤、期望行为、实际行为及相关环境信息。 41 | - 如能提供截图或日志信息,将有助于更快定位问题。 42 | 43 | ## 沟通渠道 44 | 45 | 如有疑问或建议,欢迎通过 Issue 或 Discussion 与我们交流。 46 | 47 | --- 48 | 49 | 感谢您的贡献! 50 | -------------------------------------------------------------------------------- /crates/lynx-core/src/self_service/api/base_info.rs: -------------------------------------------------------------------------------- 1 | use crate::self_service::RouteState; 2 | use axum::{Json, extract::State}; 3 | use http::StatusCode; 4 | use utoipa_axum::{router::OpenApiRouter, routes}; 5 | 6 | #[utoipa::path( 7 | get, 8 | path = "/address", 9 | tags = ["System"], 10 | responses( 11 | (status = 200, description = "Successfully retrieved base info", body = Vec), 12 | (status = 500, description = "Failed to get base info") 13 | ) 14 | )] 15 | async fn get_base_info( 16 | State(RouteState { 17 | access_addr_list, .. 18 | }): State, 19 | ) -> Result>, StatusCode> { 20 | let info = access_addr_list 21 | .iter() 22 | .map(|addr| addr.to_string()) 23 | .collect(); 24 | 25 | Ok(Json(info)) 26 | } 27 | 28 | pub fn router(state: RouteState) -> OpenApiRouter { 29 | OpenApiRouter::new() 30 | .routes(routes!(get_base_info)) 31 | .with_state(state) 32 | } 33 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/store/selectRequestStore.tsx: -------------------------------------------------------------------------------- 1 | import { IViewMessageEventStoreValue } from '@/store/useSortPoll'; 2 | import constate from 'constate'; 3 | import { useState } from 'react'; 4 | import { useImmer } from 'use-immer'; 5 | 6 | export const [UseSelectRequestProvider, useSelectRequest] = constate(() => { 7 | const [selectRequest, setSelectRequest] = 8 | useState(null); 9 | const [isWebsocketRequest, setIsWebsocketRequest] = useState(false); 10 | const [selectedRequest, setSelectedRequest] = useImmer>({}); 11 | 12 | return { 13 | selectRequest, 14 | isWebsocketRequest, 15 | selectedRequest, 16 | setSelectRequest: (request: IViewMessageEventStoreValue | null) => { 17 | setIsWebsocketRequest(!!request?.messages?.message); 18 | setSelectRequest(request); 19 | setSelectedRequest(draft => { draft[request?.traceId || ''] = true }); 20 | }, 21 | }; 22 | }); 23 | -------------------------------------------------------------------------------- /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-proxy/src/routes/apiDebug/components/CollectionTree/components/TreeToolbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Space } from 'antd'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import { useTreeUI } from '../context/TreeContext'; 5 | import TreeSearch from './TreeSearch'; 6 | 7 | const TreeToolbar: React.FC = () => { 8 | const { showCreateFolderModal } = useTreeUI(); 9 | 10 | return ( 11 |
12 | 13 |
14 | 集合 15 | 23 |
24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default TreeToolbar; -------------------------------------------------------------------------------- /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 | x86_64-pc-windows-msvc = "windows-2022" 31 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Contents/Headers/index.tsx: -------------------------------------------------------------------------------- 1 | import { Descriptions, Empty } from 'antd'; 2 | import { keys, map } from 'lodash'; 3 | import React, { useMemo } from 'react'; 4 | 5 | interface IOverviewProps { 6 | data?: Record; 7 | } 8 | 9 | export const Headers: React.FC = ({ data }) => { 10 | const items = useMemo(() => { 11 | if (data) { 12 | return map(keys(data), (key) => { 13 | return { 14 | key, 15 | label: key, 16 | children:

{data[key]}

, 17 | }; 18 | }); 19 | } 20 | }, [data]); 21 | 22 | if (!data) { 23 | return ( 24 |
25 | 26 |
27 | ); 28 | } 29 | return ( 30 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /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, className }) => { 8 | return ( 9 |
12 |
13 |
14 | 15 | {title} 16 | 17 | 21 | {subTitle} 22 | 23 |
24 | {extra} 25 |
26 |
27 | {children} 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /crates/lynx-log/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | jaeger: 5 | image: jaegertracing/all-in-one:latest 6 | container_name: lynx-jaeger 7 | environment: 8 | - COLLECTOR_OTLP_ENABLED=true 9 | - COLLECTOR_ZIPKIN_HOST_PORT=:9411 10 | ports: 11 | - "16686:16686" # Jaeger UI 12 | - "14268:14268" # HTTP collector 13 | - "14250:14250" # gRPC collector 14 | - "4317:4317" # OTLP gRPC receiver 15 | - "4318:4318" # OTLP HTTP receiver 16 | - "6831:6831/udp" # UDP agent (Thrift compact) 17 | - "6832:6832/udp" # UDP agent (Thrift binary) 18 | - "5778:5778" # HTTP config 19 | - "5775:5775/udp" # UDP zipkin 20 | - "9411:9411" # Zipkin compatible endpoint 21 | command: 22 | - "--memory.max-traces=10000" 23 | - "--query.base-path=/jaeger/ui" 24 | - "--log-level=info" 25 | - "--query.max-clock-skew-adjustment=1s" 26 | restart: unless-stopped 27 | networks: 28 | - jaeger-network 29 | 30 | networks: 31 | jaeger-network: 32 | driver: bridge 33 | -------------------------------------------------------------------------------- /crates/lynx-db/src/dao/request_processing_dao/handlers/html_script_injector.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use utoipa::ToSchema; 3 | 4 | /// HTML script injection handler configuration 5 | #[derive(Debug, Serialize, Deserialize, ToSchema, Default, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct HtmlScriptInjectorConfig { 8 | /// Content to inject into HTML pages 9 | pub content: Option, 10 | /// Position to inject the content (head, body-start, body-end) 11 | pub injection_position: Option, 12 | } 13 | 14 | impl HtmlScriptInjectorConfig { 15 | pub fn new() -> Self { 16 | Self { 17 | content: None, 18 | injection_position: Some("body-end".to_string()), 19 | } 20 | } 21 | 22 | pub fn with_content(mut self, content: String) -> Self { 23 | self.content = Some(content); 24 | self 25 | } 26 | 27 | pub fn with_injection_position(mut self, position: String) -> Self { 28 | self.injection_position = Some(position); 29 | self 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/RequestContextMenu/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 下载 JSON 数据为文件 4 | */ 5 | export const downloadJsonFile = (data: unknown, filename: string) => { 6 | const jsonData = JSON.stringify(data, null, 2); 7 | const blob = new Blob([jsonData], { type: 'application/json' }); 8 | const url = URL.createObjectURL(blob); 9 | const a = document.createElement('a'); 10 | a.href = url; 11 | a.download = filename; 12 | document.body.appendChild(a); 13 | a.click(); 14 | document.body.removeChild(a); 15 | URL.revokeObjectURL(url); 16 | }; 17 | 18 | /** 19 | * 复制文本到剪贴板 20 | */ 21 | export const copyToClipboard = async (text: string): Promise => { 22 | try { 23 | await navigator.clipboard.writeText(text); 24 | return true; 25 | } catch (_: unknown) { 26 | return false; 27 | } 28 | }; 29 | 30 | /** 31 | * 生成时间戳文件名 32 | */ 33 | export const generateTimestampFilename = (prefix: string, extension: string = 'json'): string => { 34 | return `${prefix}-${new Date().toISOString()}.${extension}`; 35 | }; 36 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/RequestContextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'antd'; 2 | import { PropsWithChildren } from 'react'; 3 | import { useRequestContextMenuContext } from './hooks'; 4 | import { useContextMenuItems } from './menuItems'; 5 | 6 | // Re-export the context provider and hook for external use 7 | export { 8 | RequestContextMenuProvider, 9 | useRequestContextMenuContext, 10 | } from './context'; 11 | export type { RequestContextMenuState } from './types'; 12 | 13 | export const RequestContextMenu: React.FC = ({ 14 | children, 15 | }) => { 16 | const { selectedRecord, setSelectedRecord } = useRequestContextMenuContext(); 17 | 18 | const contextMenuItems = useContextMenuItems(); 19 | 20 | return ( 21 | { 26 | if (!visible) { 27 | setSelectedRecord(null); 28 | } 29 | }} 30 | > 31 | {children} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /crates/lynx-core/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http_client; 2 | pub mod request_client; 3 | pub mod reqwest_client; 4 | pub mod websocket_client; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum ProxyType { 8 | None, 9 | System, 10 | Custom(String), // URL 11 | } 12 | 13 | impl Default for ProxyType { 14 | fn default() -> Self { 15 | ProxyType::None 16 | } 17 | } 18 | 19 | impl ProxyType { 20 | /// Convert from database proxy config to ProxyType 21 | pub fn from_proxy_config(proxy_type: &str, url: Option<&String>) -> Self { 22 | match proxy_type { 23 | "none" => ProxyType::None, 24 | "system" => ProxyType::System, 25 | "custom" => { 26 | if let Some(url) = url { 27 | ProxyType::Custom(url.clone()) 28 | } else { 29 | ProxyType::None 30 | } 31 | } 32 | _ => ProxyType::None, 33 | } 34 | } 35 | } 36 | 37 | pub use request_client::RequestClient; 38 | pub use reqwest_client::{ReqwestClient, ReqwestClientBuilder}; 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/apiDebug/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Splitter } from 'antd'; 2 | import { RequestHistory } from './RequestHistory'; 3 | import { CollectionPanel } from './CollectionPanel'; 4 | import { ApiDebugResponse } from '../../../services/generated/utoipaAxum.schemas'; 5 | 6 | interface SidebarProps { 7 | onLoadRequest: (request: ApiDebugResponse) => void; 8 | className?: string; 9 | } 10 | 11 | export function Sidebar({ onLoadRequest, className }: SidebarProps) { 12 | return ( 13 |
14 | 18 | 19 | 23 | 24 | 25 | 28 | 29 | 30 |
31 | ); 32 | } -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/CleanRequestButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { clearRequestTable } from '@/store/requestTableStore'; 2 | import { requestTreeSliceAction } 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 | const { clearRequestTree } = requestTreeSliceAction; 11 | 12 | export const CleanRequestButton: React.FC = () => { 13 | const { t } = useTranslation(); 14 | const dispatch = useDispatch(); 15 | 16 | const { setSelectRequest } = useSelectRequest(); 17 | 18 | return ( 19 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /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 | return ( 19 |
23 | setVisible(false)} 34 | > 35 | 36 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /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-proxy/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/index.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from '@/contexts'; 2 | import { Form, Typography } from 'antd'; 3 | import React from 'react'; 4 | import { AddHandlerButton } from './components/AddHandlerButton'; 5 | import { HandlerList } from './components/HandlerList'; 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 | {(fields, { add, remove }) => ( 17 |
18 |
19 | 20 | {t('ruleManager.createRuleDrawer.handlerBehavior.title')} 21 | 22 | 23 |
24 | 25 | {t('ruleManager.createRuleDrawer.handlerBehavior.description')} 26 | 27 | 28 |
29 | )} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /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 | *:focus-visible { 30 | outline: none; 31 | } 32 | 33 | body { 34 | margin: 0; 35 | background-color: #f8fafc; 36 | color: #222; 37 | --ant-color-bg-base: #fff; 38 | } 39 | 40 | html.dark body { 41 | color: #e5e7eb; 42 | background-color: #0d0d0d; 43 | } 44 | 45 | 46 | 47 | body, 48 | #root { 49 | scrollbar-color: #c1c1c1 #f1f1f1; 50 | transition: background-color 0.3s, color 0.3s; 51 | } 52 | 53 | html.dark body, 54 | html.dark #root { 55 | scrollbar-color: #444857 #23272e; 56 | } -------------------------------------------------------------------------------- /crates/lynx-proxy/src/components/MonacoEditor/types.ts: -------------------------------------------------------------------------------- 1 | export type Language = 2 | | 'json' 3 | | 'javascript' 4 | | 'typescript' 5 | | 'html' 6 | | 'css' 7 | | 'xml' 8 | | 'yaml' 9 | | 'text' 10 | | 'plaintext' 11 | | 'markdown' 12 | | 'sql' 13 | | 'python' 14 | | 'java' 15 | | 'cpp' 16 | | 'csharp'; 17 | 18 | export interface EditorTheme { 19 | name: string; 20 | value: 'vs-dark' | 'light' | 'vs'; 21 | isDark: boolean; 22 | } 23 | 24 | export const SUPPORTED_LANGUAGES: Array<{ label: string; value: Language }> = [ 25 | { label: 'JSON', value: 'json' }, 26 | { label: 'JavaScript', value: 'javascript' }, 27 | { label: 'TypeScript', value: 'typescript' }, 28 | { label: 'HTML', value: 'html' }, 29 | { label: 'CSS', value: 'css' }, 30 | { label: 'XML', value: 'xml' }, 31 | { label: 'YAML', value: 'yaml' }, 32 | { label: 'Markdown', value: 'markdown' }, 33 | { label: 'SQL', value: 'sql' }, 34 | { label: 'Python', value: 'python' }, 35 | { label: 'Plain Text', value: 'text' }, 36 | ]; 37 | 38 | export const EDITOR_THEMES: EditorTheme[] = [ 39 | { name: 'Dark', value: 'vs-dark', isDark: true }, 40 | { name: 'Light', value: 'light', isDark: false }, 41 | { name: 'Visual Studio', value: 'vs', isDark: false }, 42 | ]; 43 | -------------------------------------------------------------------------------- /crates/lynx-core/src/gateway_service.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use axum::response::Response; 3 | use tracing::instrument; 4 | // 添加这一行来获取 oneshot 方法 5 | 6 | use crate::common::Req; 7 | use crate::self_service::self_service_router; 8 | use crate::{ 9 | proxy::{ 10 | proxy_connect_request::{is_connect_req, proxy_connect_request}, 11 | proxy_http_request::{is_http_req, proxy_http_request}, 12 | proxy_tunnel_request::proxy_tunnel_proxy, 13 | proxy_ws_request::{is_websocket_req, proxy_ws_request}, 14 | }, 15 | self_service::is_self_service, 16 | }; 17 | 18 | #[instrument(skip_all)] 19 | pub async fn proxy_gateway_service_fn(req: Req) -> Result { 20 | if is_websocket_req(&req) { 21 | return proxy_ws_request(req).await; 22 | } 23 | if is_http_req(&req) { 24 | return proxy_http_request(req).await; 25 | } 26 | proxy_tunnel_proxy(req).await 27 | } 28 | 29 | #[instrument(skip_all)] 30 | pub async fn gateway_service_fn(req: Req) -> Result { 31 | if is_self_service(&req) { 32 | return self_service_router(req).await; 33 | } 34 | if is_connect_req(&req) { 35 | return proxy_connect_request(req).await; 36 | } 37 | proxy_gateway_service_fn(req).await 38 | } 39 | -------------------------------------------------------------------------------- /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-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.4.8", path = "../lynx-core" } 23 | tracing-subscriber = { workspace = true } 24 | tracing = { workspace = true } 25 | tracing-appender = "0.2" 26 | anyhow = { workspace = true } 27 | tokio = { workspace = true } 28 | clap = { version = "4.5.27", features = ["derive"] } 29 | console = { version = "0.15.10", features = ["windows-console-colors"] } 30 | directories = "6.0.0" 31 | include_dir = { workspace = true } 32 | sea-orm = { workspace = true, features = ["sqlx-sqlite"] } 33 | serde = { version = "1.0", features = ["derive"] } 34 | serde_json = "1.0" 35 | rustls = { version = "0.23.26", default-features = false, features = ["ring"] } 36 | reqwest = { version = "0.12.18", features = ["json"] } 37 | 38 | [dev-dependencies] 39 | tempfile = "3.12.0" 40 | -------------------------------------------------------------------------------- /crates/lynx-core/src/layers/log_layer/service.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use axum::response::Response; 7 | use tower::Service; 8 | use tracing::info; 9 | 10 | use crate::common::Req; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct LogService { 14 | pub service: S, 15 | } 16 | 17 | impl Service for LogService 18 | where 19 | S: Service 20 | + Clone 21 | + Send 22 | + Sync 23 | + 'static, 24 | S::Future: Send, 25 | { 26 | type Response = S::Response; 27 | type Error = S::Error; 28 | type Future = Pin> + Send>>; 29 | 30 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 31 | self.service.poll_ready(cx) 32 | } 33 | 34 | fn call(&mut self, request: Req) -> Self::Future { 35 | let mut inner = self.service.clone(); 36 | 37 | Box::pin(async move { 38 | info!("Processing request: {}", request.uri()); 39 | let future = inner.call(request); 40 | 41 | let response = future.await?; 42 | Ok(response) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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-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-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-core/tests/setup/setup_api_debug_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_api_debug_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-proxy/src/routes/network/components/Contents/Reponse/index.tsx: -------------------------------------------------------------------------------- 1 | import { WebSocketDirection } from '@/services/generated/utoipaAxum.schemas'; 2 | import { get } from 'lodash'; 3 | import React from 'react'; 4 | import { useSelectRequest } from '../../store/selectRequestStore'; 5 | import { ContentPreviewTabs } from '../ContentPreviewTabs'; 6 | 7 | interface IContentsProps {} 8 | 9 | export const Response: React.FC = (_props) => { 10 | const { selectRequest, isWebsocketRequest } = useSelectRequest(); 11 | 12 | const responseData = selectRequest?.response; 13 | 14 | const headers = responseData?.headers; 15 | const contentType = !isWebsocketRequest 16 | ? get(headers, 'content-type', '') 17 | : 'websocket'; 18 | 19 | const websocketBody = selectRequest?.messages?.message.filter( 20 | (item) => item.direction === WebSocketDirection.ClientToServer, 21 | ); 22 | return ( 23 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Contents/Request/index.tsx: -------------------------------------------------------------------------------- 1 | import { WebSocketDirection } from '@/services/generated/utoipaAxum.schemas'; 2 | import { get } from 'lodash'; 3 | import React from 'react'; 4 | import { useSelectRequest } from '../../store/selectRequestStore'; 5 | import { ContentPreviewTabs } from '../ContentPreviewTabs'; 6 | 7 | interface IContentsProps {} 8 | 9 | export const Request: React.FC = (_props) => { 10 | const { selectRequest, isWebsocketRequest } = useSelectRequest(); 11 | 12 | const headers = selectRequest?.request?.headers; 13 | const contentType = !isWebsocketRequest 14 | ? get(headers, 'content-type', '') 15 | : 'websocket'; 16 | 17 | const websocketBody = selectRequest?.messages?.message.filter( 18 | (item) => item.direction === WebSocketDirection.ServerToClient, 19 | ); 20 | 21 | return ( 22 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /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-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 | export const getGetBaseInfoResponseMock = (): string[] => 13 | Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, () => 14 | faker.word.sample(), 15 | ); 16 | 17 | export const getGetBaseInfoMockHandler = ( 18 | overrideResponse?: 19 | | string[] 20 | | (( 21 | info: Parameters[1]>[0], 22 | ) => Promise | string[]), 23 | ) => { 24 | return http.get('*/base_info/address', async (info) => { 25 | await delay(1000); 26 | 27 | return new HttpResponse( 28 | JSON.stringify( 29 | overrideResponse !== undefined 30 | ? typeof overrideResponse === 'function' 31 | ? await overrideResponse(info) 32 | : overrideResponse 33 | : getGetBaseInfoResponseMock(), 34 | ), 35 | { status: 200, headers: { 'Content-Type': 'application/json' } }, 36 | ); 37 | }); 38 | }; 39 | export const getSystemMock = () => [getGetBaseInfoMockHandler()]; 40 | -------------------------------------------------------------------------------- /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 | import { initThemeColorObserver } from './utils/themeColor'; 7 | 8 | const theme = localStorage.getItem('theme'); 9 | if ( 10 | theme === 'dark' || 11 | (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches) 12 | ) { 13 | document.documentElement.classList.add('dark'); 14 | } else { 15 | document.documentElement.classList.remove('dark'); 16 | } 17 | 18 | const rootEl = document.getElementById('root'); 19 | if (rootEl) { 20 | const root = ReactDOM.createRoot(rootEl); 21 | root.render( 22 | 23 | 24 | , 25 | ); 26 | } 27 | 28 | // 初始化主题颜色观察器 29 | initThemeColorObserver(); 30 | 31 | // 只在生产环境下注册 Service Worker 32 | if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') { 33 | window.addEventListener('load', () => { 34 | navigator.serviceWorker 35 | .register('/service-worker.js') 36 | .then((registration) => { 37 | console.log('SW registered: ', registration); 38 | }) 39 | .catch((registrationError) => { 40 | console.log('SW registration failed: ', registrationError); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/ShowTypeSegmented/index.tsx: -------------------------------------------------------------------------------- 1 | import { RiListUnordered, RiNodeTree } from '@remixicon/react'; 2 | import { Segmented } from 'antd'; 3 | import constate from 'constate'; 4 | import { useState } from 'react'; 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 | 23 | 24 | return ( 25 | , value: t('network.sequence') }, 29 | { label: , value: t('network.structure') }, 30 | ]} 31 | value={t(`network.${state.toLowerCase()}`)} 32 | onChange={(value) => { 33 | setState(value === t('network.sequence') ? 'Sequence' : 'Structure'); 34 | }} 35 | /> 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /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-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/network/components/FilterTemplate/ActiveTemplatesTags.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tag, Space } from 'antd'; 3 | import { useFilterTemplate } from './context'; 4 | import { FilterTemplate } from './types'; 5 | 6 | export const ActiveTemplatesTags: React.FC = () => { 7 | const { state, toggleTemplateEnabled } = useFilterTemplate(); 8 | 9 | // 获取已开启的模板 10 | const activeTemplates = state.templates.filter(template => template.enabled); 11 | 12 | if (activeTemplates.length === 0) { 13 | return null; 14 | } 15 | 16 | const handleTagClose = (template: FilterTemplate) => { 17 | toggleTemplateEnabled(template.id); 18 | }; 19 | 20 | return ( 21 |
22 | 23 | { 24 | activeTemplates.forEach(template => toggleTemplateEnabled(template.id)); 25 | }}> 26 | 清除全部 27 | 28 | {activeTemplates.map(template => ( 29 | handleTagClose(template)} 33 | color={template.isPreset ? 'blue' : 'green'} 34 | > 35 | {template.name} 36 | 37 | ))} 38 | 39 |
40 | ); 41 | }; -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import { SideBar } from '@/components/SideBar'; 2 | import { store, useSortPoll } from '@/store/useSortPoll'; 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 | import { useSseMonitor } from '@/store/useSse'; 8 | import { FilterTemplateProvider } from './network/components/FilterTemplate/context'; 9 | 10 | export const Route = createRootRoute({ 11 | component: RootComponent, 12 | }); 13 | 14 | function InnerRouteComponent() { 15 | useSortPoll(); 16 | useSseMonitor() 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | function RootComponent() { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /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-proxy/src/routes/network/components/Contents/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Empty, Splitter } from 'antd'; 3 | import { Request } from './Request'; 4 | import { Response } from './Reponse'; 5 | import { useSelectRequest } from '../store/selectRequestStore'; 6 | 7 | export const Contents: React.FC = (_props) => { 8 | const [sizes, setSizes] = React.useState<(number | string)[]>(['50%', '50%']); 9 | const { selectRequest } = useSelectRequest(); 10 | 11 | if (!selectRequest) { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | 19 | return ( 20 | { 23 | if (sizes[0] != null && sizes[1] != null) { 24 | setSizes([Math.max(sizes[0], 40), Math.max(sizes[1], 40)]); 25 | } 26 | }} 27 | > 28 | 35 | 36 | 37 | 44 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /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/src/routes/ruleManager/components/InterceptorPage/CreateRuleDrawer/components/HandlerBehavior/components/HandlerList.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd'; 2 | import React from 'react'; 3 | import { HandlerItem } from './HandlerItem'; 4 | import { useI18n } from '@/contexts'; 5 | import { HandlerCollapseProvider } from './handlerCollapseContext'; 6 | 7 | const { Text } = Typography; 8 | 9 | interface HandlerListProps { 10 | fields: Array<{ 11 | key: number; 12 | name: number; 13 | }>; 14 | remove: (index: number) => void; 15 | } 16 | 17 | export const HandlerList: React.FC = ({ fields, remove }) => { 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 |
33 | {fields.map((field, index) => ( 34 |
35 | remove(field.name)} 39 | /> 40 |
41 | ))} 42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /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 = [ 26 | "gzip", 27 | "brotli", 28 | "deflate", 29 | "tokio", 30 | "zlib", 31 | ] } 32 | tokio-rustls = { workspace = true } 33 | lynx-cert = { path = "../lynx-cert" } 34 | pin-project-lite = { workspace = true } 35 | tower = { workspace = true } 36 | reqwest = { version = "0.12.18", features = [ 37 | "rustls-tls-manual-roots", 38 | "gzip", 39 | "json", 40 | "stream", 41 | "rustls-tls", 42 | ] } 43 | reqwest-websocket = "0.5.0" 44 | rcgen = { workspace = true } 45 | tracing = { workspace = true } 46 | serde_json = "1.0.135" 47 | 48 | [dev-dependencies] 49 | tracing-subscriber = { workspace = true } 50 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/ruleManager/components/InterceptorPage/ModifyResponseModal/context.tsx: -------------------------------------------------------------------------------- 1 | import constate from 'constate'; 2 | import { useState } from 'react'; 3 | 4 | export interface ModifyResponseModalState { 5 | visible: boolean; 6 | responseContent: string; 7 | statusCode?: number; 8 | headers?: Record; 9 | ruleData?: any; // 存储完整的规则数据 10 | } 11 | 12 | export const [ModifyResponseModalProvider, useModifyResponseModal] = constate(() => { 13 | const [state, setState] = useState({ 14 | visible: false, 15 | responseContent: '', 16 | statusCode: undefined, 17 | headers: undefined, 18 | ruleData: undefined, 19 | }); 20 | 21 | const openModal = (initialData?: { 22 | responseContent?: string; 23 | statusCode?: number; 24 | headers?: Record; 25 | ruleData?: any; 26 | }) => { 27 | setState({ 28 | visible: true, 29 | responseContent: initialData?.responseContent || '', 30 | statusCode: initialData?.statusCode, 31 | headers: initialData?.headers, 32 | ruleData: initialData?.ruleData, 33 | }); 34 | }; 35 | 36 | const closeModal = () => { 37 | setState({ 38 | visible: false, 39 | responseContent: '', 40 | statusCode: undefined, 41 | headers: undefined, 42 | ruleData: undefined, 43 | }); 44 | }; 45 | 46 | return { 47 | state, 48 | openModal, 49 | closeModal, 50 | }; 51 | }); -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Structure/index.tsx: -------------------------------------------------------------------------------- 1 | import { Spin, Splitter } from 'antd'; 2 | import React from 'react'; 3 | import { Detail } from '../Detail'; 4 | import { RequestTree } from '../RequestTree'; 5 | import { useSplitSize } from '../Sequence'; 6 | 7 | interface IStructureProps { } 8 | 9 | export const Structure: React.FC = () => { 10 | const { ref, size, splitSize, setSplitSize } = useSplitSize(); 11 | 12 | return ( 13 |
14 |
15 | {!size ? ( 16 |
17 | 18 |
19 | ) : ( 20 | { 22 | setSplitSize([size1, size2]); 23 | }} 24 | className="flex-1 max-h-screen" 25 | layout="horizontal" 26 | > 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | )} 40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /crates/lynx-proxy/src/routes/network/components/Contents/MediaViewer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | interface IMediaViewerProps { 4 | arrayBuffer?: ArrayBuffer; 5 | contentType?: string; 6 | type: [ 7 | boolean, // image 8 | boolean, // video 9 | boolean, // font 10 | ]; 11 | } 12 | 13 | export const MediaViewer: React.FC = ({ 14 | arrayBuffer, 15 | type, 16 | contentType, 17 | }) => { 18 | const mediaUrl = useMemo(() => { 19 | if (!arrayBuffer) { 20 | return null; 21 | } 22 | const blob = new Blob([arrayBuffer], { type: contentType }); 23 | return URL.createObjectURL(blob); 24 | }, [arrayBuffer, contentType]); 25 | 26 | if (!mediaUrl) { 27 | return null; 28 | } 29 | 30 | if (type[2]) { 31 | const fontCode = ` 32 | @font-face { 33 | font-family: 'font'; 34 | src: url(${mediaUrl}); 35 | font-display: block; 36 | } 37 | .custom-font * { 38 | font-family: 'font'; 39 | } 40 | `; 41 | 42 | return ( 43 | <> 44 | 45 |
46 |
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
47 |
48 | 庐山烟雨浙江潮,未至千般恨不消。 到得还来别无事,庐山烟雨浙江潮。 49 |
50 |
51 | 52 | ); 53 | } 54 | 55 | if (type[1]) { 56 | return