├── .gitattributes ├── src-tauri ├── static │ └── .keep ├── .gitattributes ├── src │ ├── proxy │ │ ├── mod.rs │ │ ├── system_proxy.rs │ │ └── delay.rs │ ├── main.rs │ ├── apis.rs │ ├── apis │ │ ├── api_traits.rs │ │ ├── hysteria_apis.rs │ │ ├── parse_subscription.rs │ │ ├── common_apis.rs │ │ └── xray_apis.rs │ ├── tauri_event_handler.rs │ ├── logger.rs │ ├── types.rs │ ├── state.rs │ ├── tray.rs │ ├── tauri_apis │ │ ├── hysteria.rs │ │ ├── common.rs │ │ ├── xray.rs │ │ └── utils.rs │ ├── tauri_init.rs │ └── lib.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 32x32.png │ ├── icon.icns │ ├── 128x128.png │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── protocols │ ├── src │ │ ├── types.rs │ │ ├── utils.rs │ │ ├── lib.rs │ │ ├── traits.rs │ │ ├── hysteria.rs │ │ ├── xray.rs │ │ └── kitty_command.rs │ └── Cargo.toml ├── .gitignore ├── migration │ ├── src │ │ ├── main.rs │ │ ├── lib.rs │ │ ├── m20240205_054639_add_rules.rs │ │ ├── m20220101_000001_create_hysteria.rs │ │ ├── m20231223_035153_create_xray.rs │ │ ├── m20231210_094555_create_base_config.rs │ │ └── m20241126_062352_add_lang_col.rs │ ├── Cargo.toml │ └── README.md ├── capabilities │ ├── desktop.json │ └── main.json ├── entity │ ├── src │ │ ├── lib.rs │ │ ├── utils.rs │ │ ├── subscribe.rs │ │ ├── base_config.rs │ │ ├── rules.rs │ │ ├── hysteria.rs │ │ ├── macros.rs │ │ └── types.rs │ └── Cargo.toml ├── tauri.conf.json └── Cargo.toml ├── .env.web ├── .tool-versions ├── postcss.config.cjs ├── .eslintignore ├── src ├── types │ ├── index.ts │ ├── rule │ │ └── index.ts │ ├── setting │ │ └── index.ts │ └── proxy │ │ └── index.ts ├── tools │ ├── index.ts │ ├── logHook.ts │ ├── autoUpdateHook.ts │ └── useTask.ts ├── views │ ├── proxy │ │ ├── store.ts │ │ ├── form │ │ │ ├── HysteriaForm.vue │ │ │ └── XrayForm.vue │ │ └── modal │ │ │ ├── EditProxy.vue │ │ │ ├── ImportProxy.vue │ │ │ └── AddProxy.vue │ ├── setting │ │ ├── store.ts │ │ ├── hook.ts │ │ └── SettingView.vue │ ├── log │ │ ├── store.ts │ │ └── LogView.vue │ ├── menu │ │ └── MenuView.vue │ └── rule │ │ └── RuleView.vue ├── routers │ ├── index.ts │ └── routes.ts ├── global.ts ├── utils │ ├── darkMode.ts │ ├── invoke.ts │ └── theme.ts ├── main.ts ├── components │ ├── Empty.vue │ ├── HeaderBar.vue │ ├── FormItem.vue │ ├── ProxyCardList.vue │ └── ProxyCard.vue ├── assets │ └── vue.svg ├── styles.scss ├── translations │ ├── index.ts │ ├── zh-CN.json │ └── en-US.json ├── vite-env.d.ts ├── apis │ ├── rule │ │ └── index.ts │ └── proxy │ │ └── index.ts ├── icons │ └── LogoIcon.vue ├── composables │ └── useFormItem.ts ├── App.vue └── models │ └── xray.ts ├── .vscode └── extensions.json ├── .cargo └── config.toml ├── tsconfig.node.json ├── index.html ├── .gitignore ├── tailwind.config.ts ├── LICENSE ├── eslint.config.js ├── tsconfig.json ├── vite.config.ts ├── public ├── vite.svg └── tauri.svg ├── README.md ├── package.json └── .github └── workflows └── main.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.web: -------------------------------------------------------------------------------- 1 | KITTY_ENV = 'web' 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.14.0 2 | -------------------------------------------------------------------------------- /src-tauri/src/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delay; 2 | pub mod system_proxy; 3 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .vscode/ 4 | .husky/ 5 | .idea/ 6 | src-tauri/ 7 | eslint.config.js -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/protocols/src/types.rs: -------------------------------------------------------------------------------- 1 | pub enum CheckStatusCommandPipe { 2 | StdErr, 3 | StdOut, 4 | } 5 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface KittyResponse { 2 | code: number 3 | data: T 4 | msg: string 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /binaries/ 5 | /gen/ -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fagao-ai/kitty/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src-tauri/migration/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[async_std::main] 4 | async fn main() { 5 | cli::run_cli(migration::Migrator).await; 6 | } 7 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-gnu] 2 | linker = "x86_64-w64-mingw32-gcc" 3 | ar = "x86_64-w64-mingw32-ar" 4 | 5 | [target.x86_64-pc-windows-msvc] 6 | linker = "rust-lld" -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { useSubscriptionAutoUpdate } from '@/tools/autoUpdateHook' 2 | import { useQueueRef } from '@/tools/logHook' 3 | 4 | export { useSubscriptionAutoUpdate, useQueueRef } 5 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | kitty_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/src/apis.rs: -------------------------------------------------------------------------------- 1 | pub mod api_traits; 2 | #[cfg(feature = "hysteria")] 3 | pub mod hysteria_apis; 4 | 5 | pub mod common_apis; 6 | #[cfg(feature = "hysteria")] 7 | pub mod xray_apis; 8 | 9 | pub mod parse_subscription; 10 | -------------------------------------------------------------------------------- /src/views/proxy/store.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | import { ProxyType } from '@/types/proxy' 3 | 4 | const proxyStore = useLocalStorage('proxy', { 5 | currentProxy: ProxyType.Hysteria, 6 | }) 7 | 8 | export { proxyStore } 9 | -------------------------------------------------------------------------------- /src/views/setting/store.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | 3 | const settingStore = useLocalStorage('setting', { 4 | autoUpdate: 3, 5 | sysproxyFlag: false, 6 | port: 11080, 7 | }) 8 | 9 | export { settingStore } 10 | -------------------------------------------------------------------------------- /src/routers/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import { routes } from '@/routers/routes' 3 | 4 | const router = createRouter({ 5 | history: createWebHashHistory(), 6 | routes, 7 | }) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "autostart:default" 13 | ] 14 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/protocols/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, TcpStream}; 2 | use std::time::Duration; 3 | 4 | pub fn socket_addr_busy(socket_addr: SocketAddr) -> bool { 5 | let timeout_duration = Duration::from_secs(1); 6 | TcpStream::connect_timeout(&socket_addr, timeout_duration).is_ok() 7 | } 8 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from '@vueuse/core' 2 | import type { Subscription } from '@/types/proxy' 3 | 4 | const defaultSubscription: Subscription = { id: 0, url: '' } 5 | 6 | const subscriptionStore = useStorage('subscription', defaultSubscription) 7 | 8 | export { subscriptionStore } 9 | -------------------------------------------------------------------------------- /src/utils/darkMode.ts: -------------------------------------------------------------------------------- 1 | // export const matchBrowserDarkMode = () => { 2 | // if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 3 | // document.documentElement.classList.add("dark") 4 | // } else { 5 | // document.documentElement.classList.remove("dark") 6 | // } 7 | // } 8 | -------------------------------------------------------------------------------- /src/types/rule/index.ts: -------------------------------------------------------------------------------- 1 | export type RuleAction = 'proxy' | 'direct' | 'reject' 2 | 3 | export type RuleType = 'domain_suffix' | 'domain_preffix' | 'full_domain' | 'cidr' 4 | 5 | export interface ProxyRule { 6 | id?: number 7 | ruleAction: RuleAction 8 | ruleType: RuleType 9 | rule: string 10 | } 11 | -------------------------------------------------------------------------------- /src/types/setting/index.ts: -------------------------------------------------------------------------------- 1 | export interface KittyBaseConfig { 2 | id: number 3 | localIp: string 4 | httpPort: number 5 | socksPort: number 6 | delayTestUrl: string 7 | sysproxyFlag: boolean 8 | autoStart: boolean 9 | language: string 10 | allowLan: boolean 11 | mode: 'Global' | 'Rules' | 'Direct' 12 | updateInterval: number 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import naive from 'naive-ui' 3 | import '@/styles.scss' 4 | import App from '@/App.vue' 5 | import router from '@/routers' 6 | import { i18n } from '@/translations' 7 | import 'reflect-metadata' 8 | 9 | const app = createApp(App) 10 | 11 | app.use(router) 12 | app.use(naive) 13 | app.use(i18n) 14 | app.mount('#app') 15 | -------------------------------------------------------------------------------- /src-tauri/entity/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 2 | 3 | pub mod base_config; 4 | pub mod rules; 5 | pub mod types; 6 | pub mod utils; 7 | #[macro_use] 8 | mod macros; 9 | 10 | #[cfg(feature = "hysteria")] 11 | pub mod hysteria; 12 | 13 | #[cfg(feature = "xray")] 14 | pub mod subscribe; 15 | #[cfg(feature = "xray")] 16 | pub mod xray; 17 | -------------------------------------------------------------------------------- /src-tauri/protocols/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod kitty_command; 2 | mod traits; 3 | mod types; 4 | mod utils; 5 | 6 | #[cfg(feature = "hysteria")] 7 | mod hysteria; 8 | #[cfg(feature = "xray")] 9 | mod xray; 10 | 11 | #[cfg(feature = "xray")] 12 | pub use xray::XrayCommandGroup; 13 | 14 | #[cfg(feature = "hysteria")] 15 | pub use hysteria::HysteriaCommandGroup; 16 | 17 | pub use crate::traits::KittyCommandGroupTrait; 18 | -------------------------------------------------------------------------------- /src/components/Empty.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /src/components/HeaderBar.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/views/log/store.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export function useLogQueue(size = 1000) { 4 | const logQueue = ref([]) 5 | 6 | const enqueueLog = (log: string) => { 7 | if (logQueue.value.length >= size) 8 | logQueue.value.shift() 9 | logQueue.value.push(log) 10 | } 11 | 12 | const clearLog = () => { 13 | logQueue.value = [] 14 | } 15 | 16 | return { enqueueLog, clearLog, logQueue } 17 | } 18 | -------------------------------------------------------------------------------- /src-tauri/src/apis/api_traits.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use entity::base_config; 3 | use sea_orm::DatabaseConnection; 4 | 5 | pub trait APIServiceTrait { 6 | async fn get_proxy_ports(db: &DatabaseConnection) -> Result<(u16, u16)> { 7 | let record = base_config::Model::first(db).await?.unwrap(); 8 | let http_port = record.http_port; 9 | let socks_port = record.socks_port; 10 | Ok((http_port, socks_port)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 15 | Kitty 16 | 17 | 18 | 19 |
20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | debug 14 | tmp 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | yarn.lock 29 | package-lock.json 30 | .idea 31 | src-tauri/binaries/ 32 | src-tauri/static/*.dat 33 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | min-width: 1024px; 4 | min-height: 680px; 5 | -webkit-user-select: none !important; /* Safari */ 6 | -moz-user-select: none !important; /* Firefox */ 7 | -ms-user-select: none !important; /* IE10+/Edge */ 8 | user-select: none !important; 9 | @apply overflow-y-hidden; 10 | } 11 | 12 | #app { 13 | @apply h-screen w-full; 14 | } 15 | 16 | @tailwind base; 17 | @tailwind components; 18 | @tailwind utilities; 19 | 20 | .flex-center { 21 | @apply justify-center items-center; 22 | } 23 | -------------------------------------------------------------------------------- /src/translations/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | 3 | // import { zh } from '@/translations/zh' 4 | // import { en } from '@/translations/en' 5 | import zhCN from '@/translations/zh-CN.json' 6 | import enUS from '@/translations/en-US.json' 7 | 8 | type MessageSchema = typeof zhCN 9 | 10 | const messages = { 11 | 'zh-CN': zhCN, 12 | 'en-US': enUS, 13 | } 14 | 15 | export const i18n = createI18n<[MessageSchema], 'zh-CN' | 'en-US'>({ 16 | legacy: false, 17 | locale: 'zh-CN', 18 | messages, 19 | }) 20 | -------------------------------------------------------------------------------- /src/tools/logHook.ts: -------------------------------------------------------------------------------- 1 | import { customRef } from 'vue' 2 | 3 | export function useQueueRef(size: number = 1000) { 4 | return customRef((track, trigger) => { 5 | const queue: T[] = [] 6 | return { 7 | get() { 8 | track() 9 | return queue 10 | }, 11 | set(value) { 12 | if (queue.length >= size) 13 | queue.shift() 14 | 15 | const item = value.at(-1) 16 | if (!item) 17 | return 18 | queue.push(item) 19 | trigger() 20 | }, 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/routers/routes.ts: -------------------------------------------------------------------------------- 1 | export const routes = [ 2 | { 3 | path: '/', 4 | name: 'proxy', 5 | component: () => import('@/views/proxy/ProxyView.vue'), 6 | }, 7 | { 8 | path: '/setting', 9 | name: 'setting', 10 | component: () => import('@/views/setting/SettingView.vue'), 11 | }, 12 | { 13 | path: '/rule', 14 | name: 'rule', 15 | component: () => import('@/views/rule/RuleView.vue'), 16 | }, 17 | { 18 | path: '/log', 19 | name: 'log', 20 | component: () => import('@/views/log/LogView.vue'), 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /src-tauri/capabilities/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "main-capability", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:path:default", 10 | "core:event:default", 11 | "core:window:default", 12 | "core:app:default", 13 | "core:resources:default", 14 | "core:menu:default", 15 | "core:tray:default", 16 | "core:window:allow-start-dragging", 17 | "notification:allow-is-permission-granted" 18 | ] 19 | } -------------------------------------------------------------------------------- /src-tauri/protocols/src/traits.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::Serialize; 3 | use std::collections::HashMap; 4 | use std::net::SocketAddr; 5 | 6 | pub trait KittyCommandGroupTrait { 7 | fn start_commands( 8 | &mut self, 9 | config: HashMap, 10 | env_mapping: Option>, 11 | ) -> Result<()>; 12 | 13 | fn terminate_backends(&mut self) -> Result<()>; 14 | fn name(&self) -> String; 15 | 16 | fn get_socket_addrs(&self, config: &T) -> Result>; 17 | } 18 | -------------------------------------------------------------------------------- /src-tauri/protocols/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "protocols" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | name = "protocols" 10 | path = "src/lib.rs" 11 | 12 | [dependencies] 13 | anyhow = "1.0.79" 14 | serde = { version = "1.0.195", features = [ "derive" ] } 15 | serde_json = "1.0.111" 16 | shared_child = "1.0.0" 17 | uuid = { version = "1.6.1", features = [ "v4" ] } 18 | log = "0.4.20" 19 | 20 | [features] 21 | xray = [] 22 | hysteria = [] 23 | default = [ 24 | "xray", 25 | "hysteria" 26 | ] 27 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import type { Config } from 'tailwindcss' 3 | 4 | const tailwindConfig: Config = { 5 | darkMode: 'class', 6 | content: [ 7 | './index.html', 8 | './src/**/*.{js,ts,jsx,tsx,vue}', 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | primay: '#5352ed', 14 | dark: '#3e4247', 15 | }, 16 | screens: { 17 | xl: '1200px', 18 | xxl: '1400px', 19 | xxxl: '1500px', 20 | tv: '1700px', 21 | }, 22 | }, 23 | }, 24 | plugins: [], 25 | } 26 | export default tailwindConfig 27 | -------------------------------------------------------------------------------- /src-tauri/entity/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "entity" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | name = "entity" 9 | path = "src/lib.rs" 10 | 11 | [dependencies] 12 | serde = { version = "1", features = [ "derive" ] } 13 | serde_json = "1.0" 14 | anyhow = "1" 15 | rand = "0.8.5" 16 | url = { version = "2.5.0", optional = true, features = [ "default" ] } 17 | base64 = { version = "0.21.7", optional = true, features = [ "std" ] } 18 | sea-orm = "0.12.6" 19 | log = "0.4.20" 20 | uuid = "1.8.0" 21 | 22 | [features] 23 | hysteria = [] 24 | default = [ 25 | "xray", 26 | "hysteria" 27 | ] 28 | xray = [ 29 | "dep:url", 30 | "dep:base64" 31 | ] 32 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /// 3 | // import type { MessageApiInjection } from "naive-ui/lib/message/src/MessageProvider" 4 | 5 | declare module '*.vue' { 6 | import type { DefineComponent } from 'vue' 7 | const component: DefineComponent<{}, {}, any> 8 | export default component 9 | } 10 | 11 | declare module '@tauri-apps/api/primitives' { 12 | import { transformCallback, Channel, PluginListener, addPluginListener, invoke, convertFileSrc } from '@tauri-apps/api/types/primitives' 13 | export { transformCallback, Channel, PluginListener, addPluginListener, invoke, convertFileSrc } 14 | } 15 | 16 | declare interface Window { 17 | $message: MessageApiInjection 18 | } -------------------------------------------------------------------------------- /src-tauri/migration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "migration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | name = "migration" 9 | path = "src/lib.rs" 10 | 11 | [dependencies] 12 | async-std = { version = "1", features = ["attributes", "tokio1"] } 13 | 14 | [dependencies.sea-orm-migration] 15 | version = "0.12.0" 16 | features = [ 17 | # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. 18 | # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. 19 | # e.g. 20 | # "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature 21 | # "sqlx-postgres", # `DATABASE_DRIVER` feature 22 | ] 23 | -------------------------------------------------------------------------------- /src/apis/rule/index.ts: -------------------------------------------------------------------------------- 1 | import { camelizeKeys, decamelizeKeys } from 'humps' 2 | import type { ProxyRule } from '@/types/rule' 3 | import { invoke } from '@/utils/invoke' 4 | 5 | export async function updateRule(rule: ProxyRule) { 6 | await invoke('update_rules_item', { records: [decamelizeKeys(rule)] }) 7 | } 8 | 9 | export async function getAllRules() { 10 | const res = await invoke('query_rules') 11 | return camelizeKeys(res.data) as ProxyRule[] 12 | } 13 | 14 | export async function createRule(rule: ProxyRule) { 15 | await invoke('add_rules', { records: [decamelizeKeys(rule)] }) 16 | } 17 | 18 | export async function deleteRule(id: number) { 19 | await invoke('delete_rules', { ids: [id] }) 20 | } 21 | -------------------------------------------------------------------------------- /src-tauri/entity/src/utils.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::*; 2 | use std::collections::HashSet; 3 | use std::net::TcpListener; 4 | 5 | const START_PORT: u16 = 20000; 6 | const END_PORT: u16 = 30000; 7 | 8 | pub fn is_port_available(port: u16) -> bool { 9 | if let Ok(listener) = TcpListener::bind(("127.0.0.1", port)) { 10 | drop(listener); 11 | return true; 12 | } 13 | false 14 | } 15 | 16 | pub fn get_random_port(used_ports: &HashSet) -> Option { 17 | let mut rng = thread_rng(); 18 | const MAX_ATTEMPTS: u16 = 1000; 19 | 20 | for _ in 0..MAX_ATTEMPTS { 21 | let port = rng.gen_range(START_PORT..=END_PORT); 22 | if !used_ports.contains(&port) && is_port_available(port) { 23 | return Some(port); 24 | } 25 | } 26 | 27 | None 28 | } 29 | -------------------------------------------------------------------------------- /src-tauri/migration/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use sea_orm_migration::prelude::*; 2 | 3 | mod m20220101_000001_create_hysteria; 4 | mod m20231210_094555_create_base_config; 5 | mod m20231223_035153_create_xray; 6 | mod m20240205_054639_add_rules; 7 | mod m20241126_062352_add_lang_col; 8 | 9 | pub struct Migrator; 10 | 11 | #[async_trait::async_trait] 12 | impl MigratorTrait for Migrator { 13 | fn migrations() -> Vec> { 14 | vec![ 15 | Box::new(m20220101_000001_create_hysteria::Migration), 16 | Box::new(m20231210_094555_create_base_config::Migration), 17 | Box::new(m20231223_035153_create_xray::Migration), 18 | Box::new(m20240205_054639_add_rules::Migration), 19 | Box::new(m20241126_062352_add_lang_col::Migration), 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "kitty", 3 | "version": "0.0.6", 4 | "identifier": "com.kitty.dev", 5 | "build": { 6 | "beforeDevCommand": "pnpm dev", 7 | "devUrl": "http://localhost:1420", 8 | "beforeBuildCommand": "pnpm build", 9 | "frontendDist": "../dist" 10 | }, 11 | "app": { 12 | "security": { 13 | "csp": null 14 | }, 15 | "windows": [ 16 | { 17 | "fullscreen": false, 18 | "resizable": true, 19 | "title": "", 20 | "minWidth": 1024, 21 | "minHeight": 720, 22 | "titleBarStyle": "Overlay" 23 | } 24 | ] 25 | }, 26 | "bundle": { 27 | "active": true, 28 | "targets": "all", 29 | "icon": [ 30 | "icons/32x32.png", 31 | "icons/128x128.png", 32 | "icons/128x128@2x.png", 33 | "icons/icon.icns", 34 | "icons/icon.ico" 35 | ], 36 | "externalBin": ["binaries/hysteria", "binaries/xray"], 37 | "resources": ["static/*"] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/icons/LogoIcon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src-tauri/migration/README.md: -------------------------------------------------------------------------------- 1 | # Running Migrator CLI 2 | 3 | - Generate a new migration file 4 | ```sh 5 | cargo run -- generate MIGRATION_NAME 6 | ``` 7 | - Apply all pending migrations 8 | ```sh 9 | cargo run 10 | ``` 11 | ```sh 12 | cargo run -- up 13 | ``` 14 | - Apply first 10 pending migrations 15 | ```sh 16 | cargo run -- up -n 10 17 | ``` 18 | - Rollback last applied migrations 19 | ```sh 20 | cargo run -- down 21 | ``` 22 | - Rollback last 10 applied migrations 23 | ```sh 24 | cargo run -- down -n 10 25 | ``` 26 | - Drop all tables from the database, then reapply all migrations 27 | ```sh 28 | cargo run -- fresh 29 | ``` 30 | - Rollback all applied migrations, then reapply all migrations 31 | ```sh 32 | cargo run -- refresh 33 | ``` 34 | - Rollback all applied migrations 35 | ```sh 36 | cargo run -- reset 37 | ``` 38 | - Check the status of all migrations 39 | ```sh 40 | cargo run -- status 41 | ``` 42 | -------------------------------------------------------------------------------- /src/tools/autoUpdateHook.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted, unref, watch } from 'vue' 2 | import { autoUpdateSubscription, batchGetSubscriptions } from '@/apis/proxy' 3 | import { useTask } from '@/tools/useTask' 4 | import { settingStore } from '@/views/setting/store' 5 | 6 | export function useSubscriptionAutoUpdate() { 7 | const hour = unref(settingStore).autoUpdate || 3 8 | const { startTask, stopTask, taskStatus } = useTask(hour, async () => { 9 | const subscriptions = await batchGetSubscriptions() 10 | await autoUpdateSubscription(subscriptions.map(item => item.id)) 11 | }) 12 | 13 | function autoUpdate() { 14 | startTask() 15 | } 16 | 17 | function stopAutoUpdate() { 18 | stopTask() 19 | } 20 | 21 | const unwatch = watch(settingStore, (val, oldVal) => { 22 | if (!oldVal || val.autoUpdate !== oldVal.autoUpdate) 23 | stopAutoUpdate() 24 | autoUpdate() 25 | }, { immediate: true }) 26 | 27 | onUnmounted(() => { 28 | stopAutoUpdate() 29 | unwatch() 30 | }) 31 | 32 | return { autoUpdate, stopAutoUpdate, updateStatus: taskStatus } 33 | } 34 | -------------------------------------------------------------------------------- /src-tauri/entity/src/subscribe.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::{entity::prelude::*, ActiveValue::NotSet}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] 5 | #[sea_orm(table_name = "subscribe")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, auto_increment = true)] 8 | pub id: i32, 9 | pub url: String, 10 | } 11 | 12 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 13 | pub enum Relation { 14 | #[sea_orm(has_many = "super::xray::Entity")] 15 | Xray, 16 | } 17 | 18 | // impl RelationTrait for Relation { 19 | // fn def(&self) -> RelationDef { 20 | // match self { 21 | // Self::Xray => Entity::has_many(super::xray::Entity) 22 | // } 23 | // } 24 | // } 25 | 26 | // `Related` trait has to be implemented by hand 27 | impl Related for Entity { 28 | fn to() -> RelationDef { 29 | Relation::Xray.def() 30 | } 31 | } 32 | 33 | impl ActiveModelBehavior for ActiveModel {} 34 | 35 | impl Model { 36 | generate_model_functions!(); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 fagao-ai 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | // Enable stylistic formatting rules 6 | // stylistic: true, 7 | 8 | // Or customize the stylistic rules 9 | stylistic: { 10 | indent: 2, // 4, or 'tab' 11 | quotes: 'single', // or 'double' 12 | }, 13 | 14 | // TypeScript and Vue are auto-detected, you can also explicitly enable them: 15 | typescript: true, 16 | vue: true, 17 | 18 | // Disable jsonc and yaml support 19 | jsonc: false, 20 | yaml: false, 21 | 22 | // `.eslintignore` is no longer supported in Flat config, use `ignores` instead 23 | ignores: [ 24 | './fixtures', 25 | './node_modules/', 26 | './dist/', 27 | '.vscode/', 28 | '.idea/', 29 | './src-tauri/', 30 | '.cargo', 31 | './public/', 32 | ], 33 | }, 34 | { 35 | files: ['src/*.vue', 'src/**/*.vue', 'src/**/**/*.vue'], 36 | rules: { 37 | 'vue/component-name-in-template-casing': ['error', 'kebab-case', { 38 | registeredComponentsOnly: true, 39 | ignores: [], 40 | }], 41 | }, 42 | }, 43 | { 44 | rules: { 45 | 'no-tabs': ['error', { allowIndentationTabs: true }], 46 | }, 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /src/types/proxy/index.ts: -------------------------------------------------------------------------------- 1 | import type { Xray } from '@/models/xray' 2 | 3 | interface Bandwidth { 4 | up: string 5 | down: string 6 | } 7 | 8 | interface TLS { 9 | sni: string 10 | insecure: boolean 11 | } 12 | 13 | // interface Listener { 14 | // listen: string 15 | // } 16 | 17 | export interface HysteriaProxy { 18 | id?: number 19 | name: string 20 | server: string 21 | auth: string 22 | bandwidth: Bandwidth 23 | tls: TLS 24 | } 25 | 26 | export type XrayProxy = { 27 | [K in keyof Xray]: Xray[K]; 28 | } 29 | 30 | // export interface ProxyAdd { 31 | // showModal: boolean 32 | // hysteriaForm: HysteriaProxy 33 | // xrayForm: XrayProxy 34 | // } 35 | 36 | export enum ProxyType { 37 | Hysteria = 'hysteria', 38 | Xray = 'xray', 39 | } 40 | 41 | export interface ProxyCard { 42 | id: number 43 | type: ProxyType 44 | tag: string 45 | name: string 46 | delay: number 47 | protocol: string 48 | } 49 | 50 | export interface ImportProxy { 51 | id?: number 52 | url: string 53 | } 54 | 55 | export type Subscription = Required 56 | 57 | export interface ProxyDelay { 58 | id: number 59 | delay: number 60 | } 61 | 62 | export interface ProxyDelayInfo { 63 | id: number 64 | address: string 65 | port: number 66 | } -------------------------------------------------------------------------------- /src-tauri/entity/src/base_config.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use sea_orm::{NotSet, Set}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json; 5 | 6 | use crate::generate_model_functions; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] 9 | #[sea_orm(table_name = "base_config")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = true)] 12 | pub id: i32, 13 | pub local_ip: String, 14 | pub http_port: u16, 15 | pub socks_port: u16, 16 | pub delay_test_url: String, 17 | pub sysproxy_flag: bool, 18 | pub auto_start: bool, 19 | pub language: String, 20 | pub update_interval: i32, 21 | pub allow_lan: bool, 22 | pub mode: String, 23 | } 24 | 25 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 26 | pub enum Relation {} 27 | 28 | impl ActiveModelBehavior for ActiveModel {} 29 | 30 | impl Model { 31 | pub async fn update_sysproxy_flag(db: &DatabaseConnection, value: bool) -> Result<(), DbErr> { 32 | let record = self::Model::first(db).await?.unwrap(); 33 | let mut record: self::ActiveModel = record.into(); 34 | record.sysproxy_flag = Set(value); 35 | let _ = record.update(db).await?; 36 | Ok(()) 37 | } 38 | generate_model_functions!(); 39 | } 40 | -------------------------------------------------------------------------------- /src-tauri/src/tauri_event_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{proxy::system_proxy::clear_system_proxy, state::ProcessManagerState}; 2 | use log::trace; 3 | use protocols::KittyCommandGroupTrait; 4 | use tauri::{AppHandle, Manager, State}; 5 | 6 | async fn clear_command(app_handle: &AppHandle) { 7 | let state: State = app_handle.state(); 8 | #[cfg(feature = "hysteria")] 9 | { 10 | let mut process_manager = state.hy_process_manager.lock().await; 11 | let process_manager = process_manager.as_mut(); 12 | if let Some(process_manager) = process_manager { 13 | trace!("terminate_backends call"); 14 | process_manager.terminate_backends().unwrap(); 15 | } 16 | } 17 | 18 | #[cfg(feature = "xray")] 19 | { 20 | let mut process_manager = state.xray_process_manager.lock().await; 21 | let process_manager = process_manager.as_mut(); 22 | if let Some(process_manager) = process_manager { 23 | process_manager.terminate_backends().unwrap(); 24 | } 25 | } 26 | println!("clear_system_proxy called"); 27 | clear_system_proxy(); 28 | } 29 | 30 | pub fn on_exit_clear_commands(app_handle: &AppHandle) { 31 | trace!("on_exit_clear_commands call"); 32 | tauri::async_runtime::block_on(clear_command(app_handle)) 33 | } 34 | -------------------------------------------------------------------------------- /src/composables/useFormItem.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, reactive } from 'vue' 2 | 3 | export type FormItemType = 'input' | 'select' | 'textarea' | 'checkbox' | 'radio' 4 | 5 | export interface FormItem { 6 | type: FormItemType 7 | payload: T 8 | next: (current: FormItem, acients: FormItem[]) => FormItem | null 9 | parent: FormItem | null 10 | } 11 | 12 | export function useFormItem() { 13 | function createFormItem(formItemType: FormItem['type'], payload: FormItem['payload'], next?: FormItem['next'], parent?: FormItem['parent']): FormItem { 14 | if (!next) 15 | next = () => null 16 | 17 | if (!parent) 18 | parent = null 19 | 20 | function nextFuncion(current: FormItem, acients: FormItem[]) { 21 | let nextItem = next!(current, acients) 22 | if (!nextItem) 23 | return null 24 | 25 | nextItem.parent = current 26 | if (!isReactive(nextItem)) 27 | nextItem = reactive(nextItem) as FormItem 28 | 29 | return nextItem 30 | } 31 | 32 | const formItem: FormItem = reactive({ 33 | type: formItemType, 34 | payload, 35 | next: nextFuncion, 36 | parent, 37 | }) as FormItem 38 | 39 | return formItem 40 | } 41 | 42 | return { createFormItem } 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "experimentalDecorators": true, 14 | "outDir": "debug", 15 | "lib": [ 16 | "ESNext", 17 | "DOM" 18 | ], 19 | "skipLibCheck": true, 20 | "types": [ 21 | "naive-ui/volar" 22 | ], 23 | "baseUrl": "./", 24 | "paths": { 25 | "@/*": [ 26 | "src/*" 27 | ] 28 | } 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "postcss.config.cjs", 34 | "*.json", 35 | "src-tauri/", 36 | ".cargo", 37 | ".vscode", 38 | "public" 39 | ], 40 | // "include": [ 41 | // "src/**/*.ts", 42 | // "src/**/*.d.ts", 43 | // "src/**/*.tsx", 44 | // "src/**/*.vue", 45 | // "src/**/**/*.vue", 46 | // "src/**/**/*.ts", 47 | // "src/**/**/**/*.ts", 48 | // "src/**/**/**/*.vue", 49 | // "src/*.vue", 50 | // "src/*.ts", 51 | // "tailwind.config.ts", 52 | // "./vite.config.ts" 53 | // ], 54 | "references": [ 55 | { 56 | "path": "./tsconfig.node.json" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { internalIpV4 } from 'internal-ip' 5 | 6 | // eslint-disable-next-line node/prefer-global/process 7 | const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM as string) 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(async () => ({ 11 | plugins: [vue()], 12 | define: { 13 | // eslint-disable-next-line node/prefer-global/process 14 | __APP_VERSION__: JSON.stringify(process.env.npm_package_version), 15 | }, 16 | 17 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 18 | // 19 | // 1. prevent vite from obscuring rust errors 20 | clearScreen: false, 21 | // 2. tauri expects a fixed port, fail if that port is not available 22 | server: { 23 | port: 1420, 24 | strictPort: true, 25 | host: '0.0.0.0', 26 | hmr: mobile 27 | ? { 28 | protocol: 'ws', 29 | host: await internalIpV4(), 30 | port: 1420, 31 | } 32 | : undefined, 33 | }, 34 | css: { 35 | preprocessorOptions: { 36 | scss: { 37 | api: 'modern-compiler', 38 | }, 39 | } 40 | }, 41 | resolve: { 42 | alias: { 43 | '@': resolve(__dirname, './src'), 44 | }, 45 | }, 46 | })) 47 | -------------------------------------------------------------------------------- /src/tools/useTask.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | function getTimeUntilNextRun(hour: number): number { 4 | const now = new Date() 5 | const nextRun = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour) 6 | 7 | if (nextRun < now) 8 | nextRun.setDate(nextRun.getDate() + 1) 9 | 10 | return nextRun.getTime() - now.getTime() 11 | } 12 | 13 | export function useTask(hour: number, taskFn: (...args: any[]) => Promise) { 14 | const taskStatus = ref<'stop' | 'running'>('stop') 15 | let timeoutId: NodeJS.Timeout | undefined 16 | 17 | async function runTask() { 18 | try { 19 | await taskFn() 20 | } 21 | catch { 22 | clearTimeout(timeoutId) 23 | taskStatus.value = 'stop' 24 | return 25 | } 26 | 27 | timeoutId = setTimeout(runTask, getTimeUntilNextRun(hour)) 28 | } 29 | 30 | async function startTask() { 31 | clearTimeout(timeoutId) 32 | taskStatus.value = 'running' 33 | 34 | try { 35 | await taskFn() 36 | } 37 | catch { 38 | clearTimeout(timeoutId) 39 | taskStatus.value = 'stop' 40 | return 41 | } 42 | 43 | timeoutId = setTimeout(runTask, getTimeUntilNextRun(hour)) 44 | } 45 | 46 | function stopTask() { 47 | clearTimeout(timeoutId) 48 | taskStatus.value = 'stop' 49 | } 50 | 51 | return { 52 | startTask, 53 | stopTask, 54 | taskStatus, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src-tauri/src/apis/hysteria_apis.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use entity::hysteria; 3 | use sea_orm::DatabaseConnection; 4 | 5 | use crate::apis::api_traits::APIServiceTrait; 6 | 7 | pub struct HysteriaAPI; 8 | 9 | impl APIServiceTrait for HysteriaAPI {} 10 | 11 | impl HysteriaAPI { 12 | pub async fn get_all(&self, db: &DatabaseConnection) -> Result> { 13 | let hy_proxies = hysteria::Model::fetch_all(db).await?; 14 | Ok(hy_proxies) 15 | } 16 | 17 | pub async fn get_hysteria_by_id( 18 | &self, 19 | db: &DatabaseConnection, 20 | id: i32, 21 | ) -> Result> { 22 | let res = hysteria::Model::get_by_id(db, id).await?; 23 | Ok(res) 24 | } 25 | 26 | pub async fn add_hysteria_item( 27 | &self, 28 | db: &DatabaseConnection, 29 | record: hysteria::Model, 30 | ) -> Result<()> { 31 | record.insert_one(db).await?; 32 | Ok(()) 33 | } 34 | 35 | pub async fn delete_hysteria_item(&self, db: &DatabaseConnection, id: i32) -> Result<()> { 36 | let _ = hysteria::Model::delete_by_id(db, id).await?; 37 | Ok(()) 38 | } 39 | 40 | pub async fn update_hysteria_item( 41 | &self, 42 | db: &DatabaseConnection, 43 | record: hysteria::Model, 44 | ) -> Result<()> { 45 | let _ = record.update(db).await?; 46 | Ok(()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/invoke.ts: -------------------------------------------------------------------------------- 1 | import { invoke as tauriInvoke } from '@tauri-apps/api/core' 2 | import { camelizeKeys } from 'humps' 3 | import type { InvokeArgs, InvokeOptions } from '@tauri-apps/api/core' 4 | import type { KittyResponse } from '@/types' 5 | 6 | export async function invoke(cmd: string, args?: InvokeArgs, options?: InvokeOptions): Promise> { 7 | try { 8 | if (import.meta.env.KITTY_ENV !== 'web') { 9 | const resp = await tauriInvoke>(cmd, args, options) 10 | return camelizeKeys>(resp) as KittyResponse 11 | } 12 | 13 | const fetchOptions = { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify(args ?? {}), 19 | } 20 | const resp = await fetch(`/api/${cmd}`, fetchOptions) 21 | return camelizeKeys(resp.json()) as unknown as KittyResponse 22 | } 23 | catch (e) { 24 | window.$message.error(`${e}`, { duration: 3000 }) 25 | console.error('kitty error', e) 26 | 27 | throw e 28 | } 29 | finally { 30 | // let permissionGranted = await isPermissionGranted() 31 | // if (!permissionGranted) { 32 | // const permission = await requestPermission() 33 | // permissionGranted = permission === 'granted' 34 | // } 35 | // if (permissionGranted) { 36 | // sendNotification('Tauri is awesome!') 37 | // sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' }) 38 | // } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/FormItem.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 51 | -------------------------------------------------------------------------------- /src-tauri/migration/src/m20240205_054639_add_rules.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[derive(DeriveMigrationName)] 4 | pub struct Migration; 5 | 6 | #[async_trait::async_trait] 7 | impl MigrationTrait for Migration { 8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 9 | // Replace the sample below with your own migration scripts 10 | manager 11 | .create_table( 12 | Table::create() 13 | .table(Rules::Table) 14 | .if_not_exists() 15 | .col( 16 | ColumnDef::new(Rules::Id) 17 | .integer() 18 | .not_null() 19 | .auto_increment() 20 | .primary_key(), 21 | ) 22 | .col(ColumnDef::new(Rules::RuleAction).string().not_null()) 23 | .col(ColumnDef::new(Rules::RuleType).string().not_null()) 24 | .col(ColumnDef::new(Rules::Rule).string().not_null()) 25 | .to_owned(), 26 | ) 27 | .await 28 | } 29 | 30 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 31 | // Replace the sample below with your own migration scripts 32 | manager 33 | .drop_table(Table::drop().table(Rules::Table).to_owned()) 34 | .await 35 | } 36 | } 37 | 38 | #[derive(DeriveIden)] 39 | enum Rules { 40 | Table, 41 | Id, 42 | RuleAction, 43 | RuleType, 44 | Rule, 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tauri + Vue 3 + TypeScript 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 20 | 21 | 49 | 50 | 60 | -------------------------------------------------------------------------------- /src/components/ProxyCard.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 66 | -------------------------------------------------------------------------------- /src-tauri/src/logger.rs: -------------------------------------------------------------------------------- 1 | use log::{set_boxed_logger, set_max_level, LevelFilter, Log, Metadata, Record, SetLoggerError}; 2 | use simplelog::{Config, SharedLogger}; 3 | use std::sync::{mpsc, Mutex}; 4 | 5 | pub struct KittyLogger { 6 | level: LevelFilter, 7 | config: Config, 8 | sender: Mutex>, 9 | } 10 | 11 | impl KittyLogger { 12 | pub fn init( 13 | log_level: LevelFilter, 14 | config: Config, 15 | sender: mpsc::Sender, 16 | ) -> Result<(), SetLoggerError> { 17 | set_max_level(log_level); 18 | set_boxed_logger(KittyLogger::new(log_level, config, sender)) 19 | } 20 | 21 | pub fn new( 22 | log_level: LevelFilter, 23 | config: Config, 24 | sender: mpsc::Sender, 25 | ) -> Box { 26 | Box::new(KittyLogger { 27 | level: log_level, 28 | config, 29 | sender: Mutex::new(sender), 30 | }) 31 | } 32 | } 33 | 34 | impl Log for KittyLogger { 35 | fn enabled(&self, metadata: &Metadata) -> bool { 36 | metadata.level() <= log::Level::Info && metadata.target().contains("kitty_proxy::") 37 | } 38 | 39 | fn log(&self, record: &Record) { 40 | if self.enabled(record.metadata()) { 41 | let sender = self.sender.lock().unwrap(); 42 | sender.send(record.args().to_string()).unwrap(); 43 | } 44 | } 45 | 46 | fn flush(&self) {} 47 | } 48 | 49 | impl SharedLogger for KittyLogger { 50 | fn level(&self) -> LevelFilter { 51 | self.level 52 | } 53 | 54 | fn config(&self) -> Option<&Config> { 55 | Some(&self.config) 56 | } 57 | 58 | fn as_log(self: Box) -> Box { 59 | Box::new(*self) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/migration/src/m20220101_000001_create_hysteria.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[derive(DeriveMigrationName)] 4 | pub struct Migration; 5 | 6 | #[async_trait::async_trait] 7 | impl MigrationTrait for Migration { 8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 9 | // Replace the sample below with your own migration scripts 10 | manager 11 | .create_table( 12 | Table::create() 13 | .table(Hysteria::Table) 14 | .if_not_exists() 15 | .col( 16 | ColumnDef::new(Hysteria::Id) 17 | .integer() 18 | .not_null() 19 | .auto_increment() 20 | .primary_key(), 21 | ) 22 | .col(ColumnDef::new(Hysteria::Name).string().not_null()) 23 | .col(ColumnDef::new(Hysteria::Server).string().not_null()) 24 | .col(ColumnDef::new(Hysteria::Auth).string().not_null()) 25 | .col(ColumnDef::new(Hysteria::Tls).json().not_null()) 26 | .col(ColumnDef::new(Hysteria::Bandwidth).json().not_null()) 27 | .to_owned(), 28 | ) 29 | .await 30 | } 31 | 32 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 33 | // Replace the sample below with your own migration scripts 34 | manager 35 | .drop_table(Table::drop().table(Hysteria::Table).to_owned()) 36 | .await 37 | } 38 | } 39 | 40 | #[derive(DeriveIden)] 41 | enum Hysteria { 42 | Table, 43 | Id, 44 | Name, 45 | Server, 46 | Auth, 47 | Tls, 48 | Bandwidth, 49 | } 50 | -------------------------------------------------------------------------------- /src-tauri/src/proxy/system_proxy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | #[cfg(target_os = "windows")] 4 | static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;"; 5 | #[cfg(target_os = "linux")] 6 | static DEFAULT_BYPASS: &str = 7 | "192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.1,localhost,*.local,::1"; 8 | #[cfg(target_os = "macos")] 9 | static DEFAULT_BYPASS: &str = "192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.1,localhost,*.local,timestamp.apple.com,sequoia.apple.com,seed-sequoia.siri.apple.com"; 10 | 11 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 12 | pub fn set_system_proxy(host: &str, _socks_port: u16, http_port: Option) { 13 | use rustem_proxy::SystemProxy; 14 | // SystemProxy::set(SystemProxy { 15 | // is_enabled: true, 16 | // host: host.to_string(), 17 | // port: _socks_port, 18 | // bypass: DEFAULT_BYPASS.to_string(), 19 | // protocol: rustem_proxy::Protocol::SOCKS, 20 | // }); 21 | if http_port.is_some() { 22 | SystemProxy::set(SystemProxy { 23 | is_enabled: true, 24 | host: host.to_string(), 25 | port: http_port.unwrap(), 26 | bypass: DEFAULT_BYPASS.to_string(), 27 | protocol: rustem_proxy::Protocol::HTTP, 28 | }); 29 | SystemProxy::set(SystemProxy { 30 | is_enabled: true, 31 | host: host.to_string(), 32 | port: http_port.unwrap(), 33 | bypass: DEFAULT_BYPASS.to_string(), 34 | protocol: rustem_proxy::Protocol::HTTPS, 35 | }); 36 | } 37 | } 38 | 39 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 40 | pub fn clear_system_proxy() { 41 | use rustem_proxy::SystemProxy; 42 | SystemProxy::unset(); 43 | } 44 | -------------------------------------------------------------------------------- /src/views/log/LogView.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /src-tauri/src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{ser::Serializer, Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 4 | pub struct KittyResponse { 5 | pub data: Option, 6 | pub code: i8, 7 | pub msg: Option, 8 | } 9 | 10 | impl KittyResponse { 11 | pub fn from_msg(code: i8, msg: &str) -> Self { 12 | Self { 13 | data: None, 14 | code, 15 | msg: Some(msg.to_string()), 16 | } 17 | } 18 | 19 | pub fn from_data(data: T) -> Self { 20 | Self { 21 | code: 0, 22 | data: Some(data), 23 | msg: Some("success".to_string()), 24 | } 25 | } 26 | } 27 | 28 | impl Default for KittyResponse { 29 | fn default() -> Self { 30 | Self { 31 | data: None, 32 | code: Default::default(), 33 | msg: None, 34 | } 35 | } 36 | } 37 | 38 | // create the error type that represents all errors possible in our program 39 | #[derive(Debug, thiserror::Error)] 40 | pub enum KittyCommandError { 41 | #[error(transparent)] 42 | RusqliteError(#[from] rusqlite::Error), 43 | 44 | #[error(transparent)] 45 | DBError(#[from] sea_orm::DbErr), 46 | 47 | #[error(transparent)] 48 | TauriError(#[from] tauri::Error), 49 | 50 | #[error(transparent)] 51 | AnyHowError(#[from] anyhow::Error), 52 | 53 | #[error(transparent)] 54 | StdError(#[from] std::io::Error), 55 | } 56 | 57 | #[derive(Debug, Serialize)] 58 | struct ErrorMessage { 59 | error: String, 60 | } 61 | 62 | // we must manually implement serde::Serialize 63 | impl Serialize for KittyCommandError { 64 | fn serialize(&self, serializer: S) -> Result 65 | where 66 | S: Serializer, 67 | { 68 | serializer.serialize_str(self.to_string().as_ref()) 69 | } 70 | } 71 | 72 | pub type CommandResult = anyhow::Result; 73 | -------------------------------------------------------------------------------- /src/translations/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "global": "全局", 4 | "rules": "规则", 5 | "direct": "直连", 6 | "cancel": "取消", 7 | "add": "添加", 8 | "update": "更新", 9 | "createSuccess": "添加成功", 10 | "updateSuccess": "更新成功", 11 | "deleteSuccess": "删除成功", 12 | "import": "导入" 13 | }, 14 | "menubar": { 15 | "proxies": "代理", 16 | "settings": "设置", 17 | "version": "版本", 18 | "rules": "规则", 19 | "logs": "日志" 20 | }, 21 | "setting": { 22 | "language": "语言", 23 | "autoStart": "开机启动", 24 | "title": "设置", 25 | "systemProxy": "系统代理", 26 | "allowLan": "允许局域网连接", 27 | "mode": "代理模式", 28 | "httpPort": "HTTP 代理端口", 29 | "socks5Port": "Socks5 代理端口", 30 | "delayTestUrl": "延迟测试地址", 31 | "subscriptionAutoUpdate": "订阅自动更新(h)" 32 | }, 33 | "proxy": { 34 | "hysteria": { 35 | "proxyName": "代理名称", 36 | "server": "节点地址", 37 | "auth": "认证", 38 | "authPlaceholder": "认证密码", 39 | "bandwidth": { 40 | "uplink": "上行", 41 | "downlink": "下行" 42 | }, 43 | "tls": { 44 | "insecure": "安全连接" 45 | } 46 | }, 47 | "xray": { 48 | "proxyName": "代理名称", 49 | "protocol": "协议", 50 | "address": "节点地址", 51 | "port": "端口", 52 | "network": "网络类型", 53 | "streamSetting": { 54 | "security": "加密类型", 55 | "tlsSettings": { 56 | "allowInsecure": "允许不安全", 57 | "serverName": "服务地址" 58 | }, 59 | "http2Settings": { 60 | "headers": { 61 | "host": "主机地址" 62 | } 63 | } 64 | } 65 | }, 66 | "streamSetting": { 67 | "wsSettings": { 68 | "path": "路径", 69 | "host": "主机地址" 70 | }, 71 | "http2Settings": { 72 | "path": "路径" 73 | } 74 | }, 75 | "addProxy": { 76 | "title": "添加代理" 77 | }, 78 | "editProxy": "编辑代理" 79 | }, 80 | "rule": { 81 | "invalidCIDR": "无效的子网地址" 82 | } 83 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 59 | 60 | 71 | -------------------------------------------------------------------------------- /src/views/setting/hook.ts: -------------------------------------------------------------------------------- 1 | import { reactive, ref, toRaw } from 'vue' 2 | import { decamelizeKeys } from 'humps' 3 | import { disable, enable } from '@tauri-apps/plugin-autostart' 4 | import { invoke } from '@/utils/invoke' 5 | import type { KittyBaseConfig } from '@/types/setting' 6 | import { setProxy } from '@/apis/proxy' 7 | 8 | export function useConfig() { 9 | const loading = ref(true) 10 | const proxyLoading = ref(false) 11 | const baseConfig = reactive({ 12 | id: 0, 13 | localIp: '127.0.0.1', 14 | httpPort: 10086, 15 | socksPort: 10087, 16 | delayTestUrl: 'https://gstatic.com/generate_204', 17 | sysproxyFlag: false, 18 | autoStart: false, 19 | language: 'zh-CN', 20 | allowLan: false, 21 | mode: 'Rules', 22 | updateInterval: 3, 23 | }) 24 | 25 | async function getBaseConfig() { 26 | const config = await invoke('query_base_config') 27 | return config 28 | } 29 | 30 | async function initConfig() { 31 | const config = await getBaseConfig() 32 | 33 | Object.assign(baseConfig, config.data) 34 | 35 | loading.value = false 36 | } 37 | 38 | async function handleSwitchProxy(value: boolean) { 39 | proxyLoading.value = true 40 | try { 41 | await setProxy(value) 42 | } 43 | // eslint-disable-next-line unused-imports/no-unused-vars 44 | catch (_) { 45 | baseConfig.sysproxyFlag = false 46 | } 47 | finally { 48 | proxyLoading.value = false 49 | } 50 | } 51 | 52 | async function handleSwitchAutoStart(value: boolean) { 53 | if (value) { 54 | await enable() 55 | } 56 | else { 57 | await disable() 58 | } 59 | } 60 | 61 | async function handleBaseConfigUpdate() { 62 | await invoke('update_base_config', { record: decamelizeKeys(toRaw(baseConfig)) }) 63 | } 64 | 65 | return { 66 | loading, 67 | proxyLoading, 68 | baseConfig, 69 | handleSwitchProxy, 70 | handleSwitchAutoStart, 71 | handleBaseConfigUpdate, 72 | initConfig, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import { type GlobalThemeOverrides, darkTheme, lightTheme, useOsTheme } from 'naive-ui' 3 | import type { BuiltInGlobalTheme } from 'naive-ui/es/themes/interface' 4 | 5 | function useTheme() { 6 | const osThemeRef = useOsTheme() 7 | const theme = ref(null) 8 | const primaryColor = '#5352ed' 9 | const lightThemeOverrides: GlobalThemeOverrides = { 10 | common: { 11 | primaryColor, 12 | primaryColorHover: primaryColor, 13 | }, 14 | Button: { 15 | textColorPrimary: 'whitesmoke', 16 | textColorHoverPrimary: 'whitesmoke', 17 | textColor: primaryColor, 18 | }, 19 | Menu: { 20 | // itemColorHover: 'red', 21 | itemColorActive: primaryColor, 22 | itemColorActiveHover: primaryColor, 23 | itemTextColorActive: 'whitesmoke', 24 | itemTextColorActiveHover: 'white', 25 | borderRadius: '999px', 26 | }, 27 | Switch: { 28 | railColorActive: primaryColor, 29 | }, 30 | } 31 | 32 | const darkThemeOverrides: GlobalThemeOverrides = { 33 | common: { 34 | primaryColor, 35 | primaryColorHover: primaryColor, 36 | }, 37 | Button: { 38 | textColorPrimary: 'whitesmoke', 39 | textColorHoverPrimary: 'whitesmoke', 40 | textColor: primaryColor, 41 | }, 42 | Menu: { 43 | itemColorActive: primaryColor, 44 | itemColorActiveHover: primaryColor, 45 | itemColorHover: '#3b3c55', 46 | itemTextColor: '#5b7497', 47 | itemTextColorActive: 'whitesmoke', 48 | itemTextColorActiveHover: 'white', 49 | borderRadius: '999px', 50 | }, 51 | Switch: { 52 | railColorActive: primaryColor, 53 | }, 54 | } 55 | 56 | watch(osThemeRef, (value) => { 57 | if (value === 'dark') { 58 | document.documentElement.classList.add('dark') 59 | theme.value = darkTheme 60 | return 61 | } 62 | document.documentElement.classList.remove('dark') 63 | theme.value = lightTheme 64 | }, { immediate: true }) 65 | 66 | return { 67 | theme, 68 | lightThemeOverrides, 69 | darkThemeOverrides, 70 | } 71 | } 72 | 73 | export { useTheme } 74 | -------------------------------------------------------------------------------- /src/views/proxy/form/HysteriaForm.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 78 | -------------------------------------------------------------------------------- /src/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "global": "Global", 4 | "rules": "Rules", 5 | "direct": "Direct", 6 | "cancel": "Cancel", 7 | "add": "Add", 8 | "update": "Update", 9 | "createSuccess": "Create Success", 10 | "updateSuccess": "Update Success", 11 | "deleteSuccess": "Delete Success", 12 | "import": "Import" 13 | }, 14 | "menubar": { 15 | "proxies": "Proxies", 16 | "settings": "Settings", 17 | "version": "version", 18 | "rules": "Rules", 19 | "logs": "Logs" 20 | }, 21 | "setting": { 22 | "language": "Language", 23 | "autoStart": "Start at login", 24 | "title": "Settings", 25 | "systemProxy": "Set as system proxy", 26 | "allowLan": "Allow connect from Lan", 27 | "mode": "Mode", 28 | "socks5Port": "Socks5 port", 29 | "httpPort": "HTTP port", 30 | "delayTestUrl": "Delay test url", 31 | "subscriptionAutoUpdate": "Subscription Auto Update (h)" 32 | }, 33 | "proxy": { 34 | "hysteria": { 35 | "proxyName": "Proxy name", 36 | "server": "Node address", 37 | "auth": "Auth", 38 | "authPlaceholder": "Auth password", 39 | "bandwidth": { 40 | "uplink": "Uplink", 41 | "downlink": "Downlink" 42 | }, 43 | "tls": { 44 | "insecure": "Insecure" 45 | } 46 | }, 47 | "xray": { 48 | "proxyName": "Proxy name", 49 | "protocol": "protocol", 50 | "address": "Node address", 51 | "port": "port", 52 | "network": "network", 53 | "streamSetting": { 54 | "security": "security", 55 | "tlsSettings": { 56 | "allowInsecure": "Allow insecure", 57 | "serverName": "Server name" 58 | }, 59 | "http2Settings": { 60 | "headers": { 61 | "host": "Host" 62 | } 63 | } 64 | } 65 | }, 66 | "streamSetting": { 67 | "wsSettings": { 68 | "path": "Path", 69 | "host": "Host" 70 | }, 71 | "http2Settings": { 72 | "path": "Path" 73 | } 74 | }, 75 | "addProxy": { 76 | "title": "Add Proxy" 77 | }, 78 | "editProxy": "Edit Proxy" 79 | }, 80 | "rule": { 81 | "invalidCIDR": "Invalid CIDR" 82 | } 83 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kitty", 3 | "private": true, 4 | "version": "0.0.6", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build:web": "vite build --mode web", 10 | "preview": "vite preview", 11 | "tauri": "tauri", 12 | "type-check": "vue-tsc --noEmit -p tsconfig.json --composite false", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint . --fix" 15 | }, 16 | "dependencies": { 17 | "@tauri-apps/api": "^2.0.0", 18 | "@tauri-apps/plugin-autostart": "~2", 19 | "@tauri-apps/plugin-clipboard-manager": "^2.0.0", 20 | "@tauri-apps/plugin-notification": "^2.0.0", 21 | "@tauri-apps/plugin-shell": "^2.0.0", 22 | "@vueuse/core": "^10.6.1", 23 | "class-transformer": "^0.5.1", 24 | "highlight.js": "^11.9.0", 25 | "humps": "^2.0.1", 26 | "ip-cidr": "^4.0.0", 27 | "reflect-metadata": "^0.2.1", 28 | "vue": "^3.3.4", 29 | "vue-i18n": "9", 30 | "vue-router": "^4.2.5" 31 | }, 32 | "devDependencies": { 33 | "@antfu/eslint-config": "^2.4.2", 34 | "@tauri-apps/api": "^2.0.0", 35 | "@tauri-apps/cli": "^2.1.0", 36 | "@types/humps": "^2.0.6", 37 | "@types/node": "18.14.0", 38 | "@typescript-eslint/eslint-plugin": "^6.4.0", 39 | "@typescript-eslint/parser": "^6.13.2", 40 | "@vicons/ionicons5": "^0.13.0", 41 | "@vitejs/plugin-vue": "^5.2.1", 42 | "@vue/eslint-config-typescript": "^12.0.0", 43 | "eslint": "^8.55.0", 44 | "eslint-config-standard-with-typescript": "^40.0.0", 45 | "eslint-plugin-import": "^2.25.2", 46 | "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", 47 | "eslint-plugin-promise": "^6.0.0", 48 | "eslint-plugin-vue": "^9.19.2", 49 | "eslint-staged": "^1.0.1", 50 | "internal-ip": "^7.0.0", 51 | "lint-staged": "^15.2.0", 52 | "naive-ui": "^2.38.2", 53 | "pnpm": "^8.11.0", 54 | "sass": "^1.69.5", 55 | "simple-git-hooks": "^2.9.0", 56 | "tailwindcss": "^3.4.3", 57 | "ts-node": "^10.9.2", 58 | "typescript": "^5.3.2", 59 | "vfonts": "^0.0.3", 60 | "vite": "^6.0.1", 61 | "vue-tsc": "^1.8.5" 62 | }, 63 | "simple-git-hooks": { 64 | "pre-commit": "pnpm lint-staged" 65 | }, 66 | "lint-staged": { 67 | "*": "eslint --fix" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kitty" 3 | version = "0.0.5" 4 | description = "A Tauri App" 5 | authors = [ "you" ] 6 | repository = "" 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [lib] 12 | name = "kitty_lib" 13 | crate-type = [ 14 | "staticlib", 15 | "cdylib", 16 | "rlib" 17 | ] 18 | 19 | [build-dependencies] 20 | tauri-build = { version = "2.0.0-rc.0", features = [] } 21 | anyhow = { version = "1", features = [ "backtrace" ] } 22 | reqwest = { version = "0.11.22", features = [ "blocking" ] } 23 | build-target = "0.4.0" 24 | zip = "0.5" 25 | 26 | [dependencies] 27 | tauri = { version = "2.0.0-rc.0", features = [ 28 | "tray-icon", 29 | # "icon-ico", 30 | "image-ico", 31 | "image-png" 32 | # "icon-png", 33 | # "devtools" 34 | ] } 35 | serde_json = "1.0" 36 | serde = "1.0.193" 37 | rusqlite = "0.30.0" 38 | sea-orm = { version = "0.12", features = [ 39 | "sqlx-sqlite", 40 | "runtime-tokio-rustls", 41 | "macros", 42 | ] } 43 | entity = { path = "./entity" } 44 | protocols = { path = "./protocols" } 45 | migration = { path = "./migration" } 46 | tokio = { version = "1.34.0", features = [ "macros" ] } 47 | tauri-plugin-process = "2.0.0-rc.0" 48 | thiserror = "1.0.50" 49 | anyhow = "1" 50 | tauri-plugin-autostart = "2.0.0-rc.0" 51 | tauri-plugin-notification = "2.0.0-rc.0" 52 | reqwest = { version = "0.11.22", features = [ "json" ] } 53 | kitty_proxy = { git = "https://github.com/hezhaozhao-git/kitty_proxy.git", version = "0.1.0" } 54 | base64 = { version = "0.21.7", optional = true, features = [ "std" ] } 55 | tauri-plugin-clipboard-manager = "2.0.0-rc.0" 56 | log = "0.4.20" 57 | simplelog = "0.12.1" 58 | rustem_proxy = "0.1.5" 59 | 60 | [workspace] 61 | members = [ 62 | ".", 63 | "migration", 64 | "entity", 65 | "protocols" 66 | ] 67 | 68 | [features] 69 | # this feature is used for production builds or when `devPath` points to the filesystem 70 | # DO NOT REMOVE!! 71 | custom-protocol = [ "tauri/custom-protocol" ] 72 | default = [ 73 | "xray", 74 | "hysteria" 75 | ] 76 | xray = [ "dep:base64" ] 77 | hysteria = [] 78 | 79 | [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] 80 | tauri-plugin-autostart = "2" 81 | tauri-plugin-global-shortcut = "2.0.0" 82 | -------------------------------------------------------------------------------- /src-tauri/entity/src/rules.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use sea_orm::ActiveValue::NotSet; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json; 5 | 6 | use crate::generate_model_functions; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] 9 | #[sea_orm(table_name = "rules")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = true)] 12 | pub id: i32, 13 | pub rule_action: RuleAction, 14 | pub rule_type: RuleType, 15 | pub rule: String, 16 | } 17 | 18 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)] 19 | #[sea_orm(rs_type = "String", db_type = "String(Some(1))")] 20 | pub enum RuleAction { 21 | #[serde(rename = "proxy")] 22 | #[sea_orm(string_value = "proxy")] 23 | Proxy, 24 | #[serde(rename = "direct")] 25 | #[sea_orm(string_value = "direct")] 26 | Direct, 27 | #[serde(rename = "reject")] 28 | #[sea_orm(string_value = "reject")] 29 | Reject, 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)] 33 | #[sea_orm(rs_type = "String", db_type = "String(Some(1))")] 34 | pub enum RuleType { 35 | #[serde(rename = "domain_suffix")] 36 | #[sea_orm(string_value = "domain_suffix")] 37 | DomainSuffix, 38 | #[serde(rename = "domain_preffix")] 39 | #[sea_orm(string_value = "domain_preffix")] 40 | DomainPreffix, 41 | #[serde(rename = "full_domain")] 42 | #[sea_orm(string_value = "full_domain")] 43 | FullDomain, 44 | #[serde(rename = "cidr")] 45 | #[sea_orm(string_value = "cidr")] 46 | Cidr, 47 | #[serde(rename = "domain_root")] 48 | #[sea_orm(string_value = "domain_root")] 49 | DomainRoot, 50 | } 51 | 52 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 53 | pub enum Relation {} 54 | 55 | impl ActiveModelBehavior for ActiveModel {} 56 | 57 | impl Model { 58 | generate_model_functions!(); 59 | 60 | pub async fn fetch_by_rule_type(db: &C, rule_type: RuleType) -> Result, DbErr> 61 | where 62 | C: ConnectionTrait, 63 | { 64 | let results = self::Entity::find() 65 | .filter(self::Column::RuleType.eq(rule_type)) 66 | .all(db) 67 | .await?; 68 | Ok(results) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src-tauri/src/state.rs: -------------------------------------------------------------------------------- 1 | use kitty_proxy::MatchProxy; 2 | #[cfg(feature = "hysteria")] 3 | use protocols::HysteriaCommandGroup; 4 | #[cfg(feature = "xray")] 5 | use protocols::XrayCommandGroup; 6 | use sea_orm::DatabaseConnection; 7 | use std::collections::HashSet; 8 | use std::sync::mpsc::Receiver; 9 | use std::sync::Arc; 10 | use tokio::sync::watch::Sender; 11 | use tokio::sync::{Mutex, RwLock}; 12 | 13 | pub struct DatabaseState { 14 | pub db: std::sync::Mutex>, 15 | } 16 | 17 | impl DatabaseState { 18 | pub fn get_db(&self) -> DatabaseConnection { 19 | let db = self.db.lock().unwrap(); 20 | let db_clone = db.clone().unwrap(); 21 | db_clone 22 | } 23 | } 24 | 25 | pub struct ProcessManagerState { 26 | #[cfg(feature = "hysteria")] 27 | pub hy_process_manager: Mutex>, 28 | #[cfg(feature = "xray")] 29 | pub xray_process_manager: Mutex>, 30 | } 31 | 32 | impl<'a> Default for ProcessManagerState { 33 | fn default() -> Self { 34 | Self { 35 | #[cfg(feature = "hysteria")] 36 | hy_process_manager: Mutex::new(None), 37 | 38 | #[cfg(feature = "xray")] 39 | xray_process_manager: Mutex::new(None), 40 | } 41 | } 42 | } 43 | 44 | pub struct KittyProxyState { 45 | // pub http_proxy: Mutex>, 46 | // pub socks_proxy: Mutex>, 47 | pub match_proxy: Mutex>>>, 48 | pub http_proxy_sx: Mutex>>, 49 | pub socks_proxy_sx: Mutex>>, 50 | pub used_ports: Mutex>, 51 | } 52 | 53 | impl Default for KittyProxyState { 54 | fn default() -> Self { 55 | Self { 56 | // http_proxy: Mutex::new(None), 57 | // socks_proxy: Mutex::new(None), 58 | match_proxy: Mutex::new(None), 59 | http_proxy_sx: Mutex::new(None), 60 | socks_proxy_sx: Mutex::new(None), 61 | used_ports: Mutex::new(HashSet::new()), 62 | } 63 | } 64 | } 65 | 66 | pub struct KittyLoggerState { 67 | pub logger_reciver: Mutex>>, 68 | } 69 | 70 | impl Default for KittyLoggerState { 71 | fn default() -> Self { 72 | Self { 73 | logger_reciver: Mutex::new(None), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/views/proxy/modal/EditProxy.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 84 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src-tauri/src/apis/parse_subscription.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, DecodeError, Engine}; 2 | use entity::types::ProtocolLine; 3 | use reqwest; 4 | use anyhow::anyhow; 5 | 6 | fn safe_decode_base64(text: &str, no_pad: bool) -> String { 7 | // let trimmed_text = text.trim(); 8 | let bytes_data = if !no_pad { 9 | general_purpose::STANDARD.decode(text) 10 | } else { 11 | general_purpose::STANDARD_NO_PAD.decode(text) 12 | }; 13 | 14 | match bytes_data { 15 | Ok(decode_bytes) => String::from_utf8(decode_bytes).expect("Invalid UTF-8 sequence"), 16 | Err(_e) => { 17 | match _e { 18 | DecodeError::InvalidPadding => safe_decode_base64(text, true), 19 | _ => text.to_string(), 20 | } 21 | }, 22 | } 23 | } 24 | 25 | pub async fn download_subcriptions(url: &str) -> anyhow::Result> { 26 | let client = reqwest::Client::builder() 27 | .user_agent("OKZTWO-Mac-Client-1.5.6") 28 | .build()?; 29 | 30 | let resp = client.get(url).send().await?; 31 | let mut resp_text = String::new(); 32 | if resp.status().is_success() { 33 | resp_text = resp.text().await?; 34 | }else{ 35 | return Err(anyhow!("download subscriptions failed.").into()); 36 | }; 37 | 38 | let decoded_text = safe_decode_base64(&resp_text, false); 39 | let mut results = Vec::new(); 40 | for line in decoded_text.lines() { 41 | let line = line.trim(); 42 | if line.trim().starts_with("#") { 43 | continue; 44 | } 45 | if line.contains("://") { 46 | let trimed_line = if let Some(pos) = line.rfind('#') { 47 | &line[..pos] 48 | } else { 49 | line 50 | }; 51 | let protocol = trimed_line.split("://").next().unwrap(); 52 | // let protocol_str = trimed_line.split("://").last().unwrap(); 53 | results.push(ProtocolLine::new(trimed_line.to_string(), protocol.into())) 54 | // if protocol_str.starts_with("eyJ") { 55 | // // let decode_bytes = general_purpose::STANDARD.decode(new_protocol_str)?; 56 | // // let protocol_line =String::from_utf8(decode_bytes).expect("Invalid UTF-8 sequence"); 57 | // let new_line = format!("{protocol}://{protocol_str}"); 58 | // println!("new_line: {}", new_line); 59 | // results.push(ProtocolLine::new(new_line, protocol.into())) 60 | // } 61 | } 62 | } 63 | // println!("{:?}", results.len()); 64 | anyhow::Ok(results) 65 | } 66 | -------------------------------------------------------------------------------- /src-tauri/migration/src/m20231223_035153_create_xray.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[derive(DeriveMigrationName)] 4 | pub struct Migration; 5 | 6 | #[async_trait::async_trait] 7 | impl MigrationTrait for Migration { 8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 9 | let _ = manager 10 | .create_table( 11 | Table::create() 12 | .table(Xray::Table) 13 | .if_not_exists() 14 | .col( 15 | ColumnDef::new(Xray::Id) 16 | .integer() 17 | .not_null() 18 | .auto_increment() 19 | .primary_key(), 20 | ) 21 | .col(ColumnDef::new(Xray::Name).string().not_null()) 22 | .col(ColumnDef::new(Xray::Protocol).string().not_null()) 23 | .col(ColumnDef::new(Xray::Uuid).string().not_null()) 24 | .col(ColumnDef::new(Xray::Address).string().not_null()) 25 | .col(ColumnDef::new(Xray::Port).integer().not_null()) 26 | .col(ColumnDef::new(Xray::StreamSettings).json().not_null()) 27 | .col(ColumnDef::new(Xray::SubscribeId).integer().null()) 28 | .to_owned(), 29 | ) 30 | .await; 31 | 32 | manager 33 | .create_table( 34 | Table::create() 35 | .table(Subscribe::Table) 36 | .if_not_exists() 37 | .col( 38 | ColumnDef::new(Subscribe::Id) 39 | .integer() 40 | .not_null() 41 | .auto_increment() 42 | .primary_key(), 43 | ) 44 | .col(ColumnDef::new(Subscribe::Url).string().not_null()) 45 | .to_owned(), 46 | ) 47 | .await 48 | } 49 | 50 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 51 | let _ = manager 52 | .drop_table(Table::drop().table(Xray::Table).to_owned()) 53 | .await; 54 | manager 55 | .drop_table(Table::drop().table(Subscribe::Table).to_owned()) 56 | .await 57 | } 58 | } 59 | 60 | #[derive(DeriveIden)] 61 | enum Xray { 62 | Table, 63 | Id, 64 | Name, 65 | Protocol, 66 | Uuid, 67 | Address, 68 | Port, 69 | StreamSettings, 70 | SubscribeId, 71 | } 72 | 73 | #[derive(DeriveIden)] 74 | enum Subscribe { 75 | Table, 76 | Id, 77 | Url, 78 | } 79 | -------------------------------------------------------------------------------- /src-tauri/migration/src/m20231210_094555_create_base_config.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[derive(DeriveMigrationName)] 4 | pub struct Migration; 5 | 6 | #[async_trait::async_trait] 7 | impl MigrationTrait for Migration { 8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 9 | let _ = manager 10 | .create_table( 11 | Table::create() 12 | .table(BaseConfig::Table) 13 | .if_not_exists() 14 | .col( 15 | ColumnDef::new(BaseConfig::Id) 16 | .integer() 17 | .not_null() 18 | .auto_increment() 19 | .primary_key(), 20 | ) 21 | .col(ColumnDef::new(BaseConfig::LocalIp).string().not_null()) 22 | .col(ColumnDef::new(BaseConfig::HttpPort).integer().not_null()) 23 | .col(ColumnDef::new(BaseConfig::SocksPort).integer().not_null()) 24 | .col(ColumnDef::new(BaseConfig::DelayTestUrl).string().not_null()) 25 | .col( 26 | ColumnDef::new(BaseConfig::SysproxyFlag) 27 | .boolean() 28 | .not_null(), 29 | ) 30 | .col( 31 | ColumnDef::new(BaseConfig::AutoStart) 32 | .boolean() 33 | .default(false), 34 | ) 35 | .to_owned(), 36 | ) 37 | .await; 38 | let insert = Query::insert() 39 | .into_table(BaseConfig::Table) 40 | .columns([ 41 | BaseConfig::LocalIp, 42 | BaseConfig::SocksPort, 43 | BaseConfig::HttpPort, 44 | BaseConfig::DelayTestUrl, 45 | BaseConfig::SysproxyFlag, 46 | ]) 47 | .values_panic([ 48 | "127.0.0.1".into(), 49 | 10086.into(), 50 | 10087.into(), 51 | "https://gstatic.com/generate_204".into(), 52 | false.into(), 53 | ]) 54 | .to_owned(); 55 | 56 | manager.exec_stmt(insert).await?; 57 | 58 | Ok(()) 59 | } 60 | 61 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 62 | manager 63 | .drop_table(Table::drop().table(BaseConfig::Table).to_owned()) 64 | .await 65 | } 66 | } 67 | 68 | #[derive(DeriveIden)] 69 | enum BaseConfig { 70 | Table, 71 | Id, 72 | LocalIp, 73 | HttpPort, 74 | SocksPort, 75 | DelayTestUrl, 76 | SysproxyFlag, 77 | AutoStart, 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release App 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | publish-tauri: 10 | permissions: 11 | contents: write 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - target: aarch64-apple-darwin 17 | platform: macos-latest 18 | - target: x86_64-apple-darwin 19 | platform: macos-latest 20 | - target: x86_64-unknown-linux-gnu 21 | platform: ubuntu-22.04 22 | - target: x86_64-pc-windows-msvc 23 | platform: windows-latest 24 | 25 | runs-on: ${{ matrix.platform }} 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Get version 30 | id: get_version 31 | uses: battila7/get-version-action@v2 32 | 33 | - name: setup node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | - name: Install PNPM 38 | run: npm i -g pnpm 39 | 40 | - name: install Rust stable 41 | uses: dtolnay/rust-toolchain@stable 42 | 43 | - name: Rust cache 44 | uses: swatinem/rust-cache@988c164c3d0e93c4dbab36aaf5bbeb77425b2894 45 | with: 46 | workspaces: packages/desktop 47 | shared-key: ${{ matrix.tauri-target }}-${{ hashFiles('packages/desktop/Cargo.lock') }} 48 | 49 | 50 | - name: install dependencies (ubuntu only) 51 | if: matrix.platform == 'ubuntu-22.04' 52 | run: | 53 | sudo apt-get update 54 | sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libayatana-appindicator3-dev librsvg2-dev 55 | - name: install dependencies (mac only) 56 | if: matrix.platform == 'macos-latest' 57 | run: | 58 | rustup target add ${{matrix.target}} 59 | 60 | - name: install frontend dependencies 61 | run: pnpm install # change this to npm or pnpm depending on which one you use 62 | 63 | - name: Build Tauri App 64 | uses: tauri-apps/tauri-action@dev 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | with: 68 | tagName: kitty___VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 69 | releaseName: 'kitty __VERSION__' 70 | releaseBody: 'upgrade tauri to 2.0' 71 | releaseDraft: false 72 | prerelease: false 73 | args: --target ${{matrix.target}} -------------------------------------------------------------------------------- /src/views/menu/MenuView.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 105 | 106 | 122 | -------------------------------------------------------------------------------- /src-tauri/src/apis/common_apis.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{CommandResult, KittyResponse}; 2 | use anyhow::Result; 3 | use entity::{base_config, rules}; 4 | use sea_orm::{ConnectionTrait, DatabaseConnection}; 5 | 6 | pub struct CommonAPI; 7 | 8 | impl CommonAPI { 9 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 10 | pub async fn copy_proxy_env(db: &C) -> Result 11 | where 12 | C: ConnectionTrait, 13 | { 14 | let record = base_config::Model::first(db).await?.unwrap(); 15 | let http_port = record.http_port; 16 | let socks_port = record.socks_port; 17 | #[cfg(target_os = "windows")] 18 | let env_expr = format!("set https_proxy=http://127.0.0.1:{http_port} http_proxy=http://127.0.0.1:{http_port} all_proxy=socks5://127.0.0.1:{socks_port}"); 19 | 20 | #[cfg(any(target_os = "macos", target_os = "linux"))] 21 | let env_expr = format!("export https_proxy=http://127.0.0.1:{http_port} http_proxy=http://127.0.0.1:{http_port} all_proxy=socks5://127.0.0.1:{socks_port}"); 22 | 23 | Ok(env_expr) 24 | } 25 | 26 | pub async fn query_base_config(db: &C) -> CommandResult> 27 | where 28 | C: ConnectionTrait, 29 | { 30 | let record = base_config::Model::first(db).await?; 31 | let response = match record { 32 | Some(record) => KittyResponse::::from_data(record), 33 | None => KittyResponse::from_msg(101, "base_config not exists"), 34 | }; 35 | Ok(response) 36 | } 37 | 38 | pub async fn update_base_config( 39 | db: &C, 40 | record: base_config::Model, 41 | ) -> CommandResult> 42 | where 43 | C: ConnectionTrait, 44 | { 45 | let updated_record = record.update(db).await?; 46 | Ok(KittyResponse::::from_data( 47 | updated_record, 48 | )) 49 | } 50 | 51 | pub async fn add_rules( 52 | db: &C, 53 | records: Vec, 54 | ) -> CommandResult> 55 | where 56 | C: ConnectionTrait, 57 | { 58 | let _ = rules::Model::insert_many(db, records).await?; 59 | Ok(KittyResponse::default()) 60 | } 61 | 62 | pub async fn query_rules(db: &C) -> CommandResult>> 63 | where 64 | C: ConnectionTrait, 65 | { 66 | let res = rules::Model::fetch_all(db).await?; 67 | Ok(KittyResponse::from_data(res)) 68 | } 69 | 70 | pub async fn delete_rules(db: &C, ids: Vec) -> CommandResult> 71 | where 72 | C: ConnectionTrait, 73 | { 74 | let _ = rules::Model::delete_by_ids(db, ids).await?; 75 | Ok(KittyResponse::default()) 76 | } 77 | 78 | pub async fn update_rules( 79 | db: &C, 80 | record: rules::Model, 81 | ) -> CommandResult> 82 | where 83 | C: ConnectionTrait, 84 | { 85 | let updated_record = record.update(db).await?; 86 | Ok(KittyResponse::::from_data(updated_record)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src-tauri/entity/src/hysteria.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sea_orm::NotSet; 3 | use sea_orm::{entity::prelude::*, FromJsonQueryResult}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] 7 | #[sea_orm(table_name = "hysteria")] 8 | pub struct Model { 9 | #[sea_orm(primary_key, auto_increment = true)] 10 | pub id: i32, 11 | pub name: String, 12 | pub server: String, 13 | pub auth: String, 14 | #[sea_orm(column_type = "Text")] 15 | tls: Tls, 16 | #[sea_orm(column_type = "Text")] 17 | bandwidth: Bandwidth, 18 | } 19 | 20 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] 21 | pub struct Tls { 22 | sni: String, 23 | insecure: bool, 24 | #[serde(rename = "pinSHA256")] 25 | pin_sha256: Option, 26 | ca: Option, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] 30 | pub struct Bandwidth { 31 | up: String, 32 | down: String, 33 | } 34 | 35 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 36 | pub enum Relation {} 37 | 38 | impl ActiveModelBehavior for ActiveModel {} 39 | 40 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 41 | pub struct HysteriaModelWithoutName { 42 | #[serde(skip)] 43 | pub name: String, 44 | pub server: String, 45 | pub auth: String, 46 | tls: Tls, 47 | bandwidth: Bandwidth, 48 | } 49 | 50 | impl<'a> From<&'a Model> for HysteriaModelWithoutName { 51 | fn from(source: &'a Model) -> Self { 52 | HysteriaModelWithoutName { 53 | name: source.name.clone(), 54 | server: source.server.clone(), 55 | auth: source.auth.clone(), 56 | tls: source.tls.clone(), 57 | bandwidth: source.bandwidth.clone(), 58 | } 59 | } 60 | } 61 | 62 | impl Model { 63 | generate_model_functions!(); 64 | } 65 | 66 | #[derive(Serialize, Deserialize)] 67 | pub struct ListenAddr { 68 | pub listen: String, 69 | } 70 | 71 | impl ListenAddr { 72 | fn new(port: u16) -> Self { 73 | Self { 74 | listen: format!("127.0.0.1:{port}"), 75 | } 76 | } 77 | } 78 | 79 | #[derive(Serialize, Deserialize)] 80 | pub struct HysteriaConfig { 81 | pub server: String, 82 | pub auth: String, 83 | pub bandwidth: Bandwidth, 84 | pub tls: Tls, 85 | pub socks5: ListenAddr, 86 | pub http: ListenAddr, 87 | } 88 | 89 | impl HysteriaConfig { 90 | pub fn new(http_port: u16, socks_port: u16, record: Model) -> Self { 91 | Self { 92 | server: record.server, 93 | auth: record.auth, 94 | bandwidth: record.bandwidth, 95 | tls: record.tls, 96 | socks5: ListenAddr::new(socks_port), 97 | http: ListenAddr::new(http_port), 98 | } 99 | } 100 | pub fn get_http_port(&self) -> u16 { 101 | let http_addr = &self.http.listen; 102 | http_addr.split(":").nth(1).unwrap().parse::().unwrap() 103 | } 104 | 105 | pub fn get_socks_port(&self) -> u16 { 106 | let http_addr = &self.socks5.listen; 107 | http_addr.split(":").nth(1).unwrap().parse::().unwrap() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src-tauri/protocols/src/hysteria.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde::Serialize; 3 | use std::collections::HashMap; 4 | use std::net::SocketAddr; 5 | use std::path::PathBuf; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | use crate::kitty_command::KittyCommand; 10 | use crate::traits::KittyCommandGroupTrait; 11 | use crate::types::CheckStatusCommandPipe; 12 | 13 | #[derive(Debug)] 14 | pub struct HysteriaCommandGroup { 15 | bin_path: PathBuf, 16 | kitty_commands: HashMap, 17 | config_dir: PathBuf, 18 | } 19 | 20 | impl HysteriaCommandGroup { 21 | pub fn new(bin_path: PathBuf, config_dir: PathBuf) -> Self { 22 | Self { 23 | kitty_commands: HashMap::new(), 24 | bin_path, 25 | config_dir, 26 | } 27 | } 28 | } 29 | 30 | impl Drop for HysteriaCommandGroup { 31 | fn drop(&mut self) { 32 | println!("drop hysteria command!!!"); 33 | for (_, child) in self.kitty_commands.iter_mut() { 34 | if child.is_running() { 35 | child.terminate_backend().ok(); 36 | } 37 | } 38 | self.kitty_commands.clear(); 39 | } 40 | } 41 | 42 | impl KittyCommandGroupTrait for HysteriaCommandGroup { 43 | fn start_commands( 44 | &mut self, 45 | config: HashMap, 46 | env_mapping: Option>, 47 | ) -> Result<()> 48 | where 49 | T: Serialize, 50 | { 51 | for (node_server, config) in config.iter() { 52 | let kitty_command = KittyCommand::spawn( 53 | &self.bin_path, 54 | ["client", "-c"], 55 | config, 56 | &self.config_dir, 57 | env_mapping.clone().unwrap_or(HashMap::new()), 58 | )?; 59 | thread::sleep(Duration::from_secs(1)); 60 | let socket_addrs = self.get_socket_addrs(&config)?; 61 | kitty_command.check_status( 62 | "server listening", 63 | CheckStatusCommandPipe::StdErr, 64 | socket_addrs, 65 | )?; 66 | self.kitty_commands 67 | .insert(node_server.clone(), kitty_command); 68 | } 69 | Ok(()) 70 | } 71 | 72 | fn get_socket_addrs(&self, config: &T) -> Result> 73 | where 74 | T: Serialize, 75 | { 76 | let config_value = serde_json::to_value(config)?; 77 | 78 | let socks_listen = config_value["socks5"]["listen"].as_str(); 79 | let http_listen = config_value["http"]["listen"].as_str(); 80 | let mut res = Vec::with_capacity(2); 81 | for listen in [socks_listen, http_listen] { 82 | if let Some(address_str) = listen { 83 | let server: SocketAddr = address_str.parse()?; 84 | res.push(server); 85 | } 86 | } 87 | if res.len() != 2 { 88 | Err(anyhow!("get_socket_addrs failed.")) 89 | } else { 90 | Ok(res) 91 | } 92 | } 93 | 94 | fn terminate_backends(&mut self) -> Result<()> { 95 | for (_, child) in self.kitty_commands.iter_mut() { 96 | child.terminate_backend()?; 97 | } 98 | self.kitty_commands.clear(); 99 | Ok(()) 100 | } 101 | 102 | fn name(&self) -> String { 103 | "hysteria".into() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src-tauri/entity/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! generate_model_functions { 3 | () => { 4 | pub async fn insert_one(&self, db: &C) -> Result 5 | where 6 | C: ConnectionTrait, 7 | { 8 | let json_value = serde_json::to_value(self).unwrap().into(); 9 | let mut record = ActiveModel::from_json(json_value)?; 10 | record.id = NotSet; 11 | let res = record.insert(db).await; 12 | res 13 | } 14 | 15 | pub async fn insert_many(db: &C, records: Vec) -> Result<(), DbErr> 16 | where 17 | C: ConnectionTrait, 18 | { 19 | let mut active_models = Vec::with_capacity(records.len()); 20 | for record in records { 21 | let json_value = serde_json::to_value(record).unwrap().into(); 22 | let mut record = ActiveModel::from_json(json_value)?; 23 | record.id = NotSet; 24 | active_models.push(record) 25 | } 26 | let _ = self::Entity::insert_many(active_models).exec(db).await?; 27 | Ok(()) 28 | } 29 | 30 | pub async fn first(db: &C) -> Result, DbErr> 31 | where 32 | C: ConnectionTrait, 33 | { 34 | let record = self::Entity::find().one(db).await?; 35 | Ok(record) 36 | } 37 | 38 | pub async fn update(&self, db: &C) -> Result 39 | where 40 | C: ConnectionTrait, 41 | { 42 | let origin_id = self.id; 43 | let json_value = serde_json::to_value(self).unwrap(); 44 | let record = self::Entity::find_by_id(origin_id).one(db).await?; 45 | let mut record: self::ActiveModel = record.unwrap().into(); 46 | let _ = record.set_from_json(json_value); 47 | let res = record.update(db).await?; 48 | Ok(res) 49 | } 50 | 51 | pub async fn fetch_all(db: &C) -> Result, DbErr> 52 | where 53 | C: ConnectionTrait, 54 | { 55 | let results = self::Entity::find().all(db).await?; 56 | Ok(results) 57 | } 58 | 59 | pub async fn fetch_by_ids(db: &C, ids: Vec) -> Result, DbErr> 60 | where 61 | C: ConnectionTrait, 62 | { 63 | let results = self::Entity::find() 64 | .filter(self::Column::Id.is_in(ids)) 65 | .all(db) 66 | .await?; 67 | Ok(results) 68 | } 69 | 70 | pub async fn get_by_id(db: &C, id: i32) -> Result, DbErr> 71 | where 72 | C: ConnectionTrait, 73 | { 74 | let model = self::Entity::find_by_id(id).one(db).await?; 75 | Ok(model) 76 | } 77 | 78 | pub async fn delete_by_id(db: &C, id: i32) -> Result<(), DbErr> 79 | where 80 | C: ConnectionTrait, 81 | { 82 | let _ = Entity::delete_by_id(id).exec(db).await?; 83 | Ok(()) 84 | } 85 | 86 | pub async fn delete_by_ids(db: &C, ids: Vec) -> Result<(), DbErr> 87 | where 88 | C: ConnectionTrait, 89 | { 90 | let _ = Entity::delete_many() 91 | .filter(self::Column::Id.is_in(ids)) 92 | .exec(db) 93 | .await?; 94 | Ok(()) 95 | } 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /src-tauri/protocols/src/xray.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde::Serialize; 3 | use std::collections::HashMap; 4 | use std::net::{IpAddr, SocketAddr}; 5 | use std::path::PathBuf; 6 | use std::str::FromStr; 7 | 8 | use crate::kitty_command::KittyCommand; 9 | use crate::traits::KittyCommandGroupTrait; 10 | use crate::types::CheckStatusCommandPipe; 11 | 12 | #[derive(Debug)] 13 | pub struct XrayCommandGroup { 14 | bin_path: PathBuf, 15 | kitty_commands: HashMap, 16 | config_dir: PathBuf, 17 | } 18 | impl XrayCommandGroup { 19 | pub fn new(bin_path: PathBuf, config_dir: PathBuf) -> Self { 20 | Self { 21 | kitty_commands: HashMap::new(), 22 | bin_path, 23 | config_dir, 24 | } 25 | } 26 | } 27 | 28 | impl Drop for XrayCommandGroup { 29 | fn drop(&mut self) { 30 | for (_, child) in self.kitty_commands.iter_mut() { 31 | if child.is_running() { 32 | child.terminate_backend().ok(); 33 | } 34 | } 35 | self.kitty_commands.clear(); 36 | } 37 | } 38 | 39 | impl KittyCommandGroupTrait for XrayCommandGroup { 40 | fn start_commands( 41 | &mut self, 42 | config: HashMap, 43 | env_mapping: Option>, 44 | ) -> Result<()> 45 | where 46 | T: Serialize, 47 | { 48 | for (node_server, config) in config.iter() { 49 | let kitty_command = KittyCommand::spawn( 50 | &self.bin_path, 51 | ["run", "-c"], 52 | config, 53 | &self.config_dir, 54 | env_mapping.clone().unwrap_or(HashMap::new()), 55 | )?; 56 | println!("xray runed"); 57 | let socket_addrs = self.get_socket_addrs(&config)?; 58 | kitty_command.check_status( 59 | "Reading config:", 60 | CheckStatusCommandPipe::StdOut, 61 | socket_addrs, 62 | )?; 63 | self.kitty_commands 64 | .insert(node_server.clone(), kitty_command); 65 | } 66 | Ok(()) 67 | } 68 | 69 | fn get_socket_addrs(&self, config: &T) -> Result> 70 | where 71 | T: Serialize, 72 | { 73 | let config_value = serde_json::to_value(config)?; 74 | let mut res = Vec::new(); 75 | if let Some(inbounds) = config_value["inbounds"].as_array() { 76 | for inbound in inbounds { 77 | let mut listen = inbound["listen"].as_str().unwrap(); 78 | if listen == "0.0.0.0" { 79 | listen = "127.0.0.1"; 80 | } 81 | let port = inbound["port"].as_i64().unwrap(); 82 | let ip_addr: IpAddr = IpAddr::from_str(listen)?; 83 | let socket_addr = SocketAddr::new(ip_addr, port.to_owned() as u16); 84 | res.push(socket_addr); 85 | } 86 | Ok(res) 87 | } else { 88 | Err(anyhow!("get_socket_addrs failed.")) 89 | } 90 | } 91 | 92 | fn terminate_backends(&mut self) -> Result<()> { 93 | for (_, child) in self.kitty_commands.iter_mut() { 94 | child.terminate_backend()?; 95 | } 96 | self.kitty_commands.clear(); 97 | Ok(()) 98 | } 99 | 100 | fn name(&self) -> String { 101 | "xray".into() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/views/proxy/modal/ImportProxy.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 116 | 117 | 130 | -------------------------------------------------------------------------------- /src/models/xray.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { Expose, Type } from 'class-transformer' 3 | 4 | class TLSSetting { 5 | @Expose({ name: 'allowInsecure', toPlainOnly: true }) 6 | allowInsecure!: boolean 7 | 8 | @Expose({ name: 'serverName', toPlainOnly: true }) 9 | serverName!: string 10 | } 11 | 12 | class WebSocketHeader { 13 | @Expose({ name: 'Host', toPlainOnly: true }) 14 | host!: string 15 | } 16 | 17 | export class WebSocketProtocolSetting { 18 | @Expose() 19 | path!: string 20 | 21 | @Type(() => WebSocketHeader) 22 | @Expose() 23 | headers!: WebSocketHeader 24 | } 25 | 26 | export class Http2ProtocolSetting { 27 | @Expose() 28 | host!: string[] 29 | 30 | @Expose() 31 | path!: string 32 | } 33 | 34 | class StreamSettings { 35 | @Expose() 36 | network!: 'ws' | 'tcp' | 'http2' | 'grpc' | 'kcp' 37 | 38 | @Expose() 39 | security?: 'tls' | 'none' | 'reality' | undefined 40 | 41 | @Expose({ name: 'tlsSettings', toPlainOnly: true }) 42 | tlsSettings?: TLSSetting 43 | 44 | @Type(() => WebSocketProtocolSetting) 45 | @Expose({ name: 'wsSettings', toPlainOnly: true, groups: ['ws'] }) 46 | wsSettings!: WebSocketProtocolSetting 47 | 48 | @Expose({ name: 'tcpSettings', toPlainOnly: true, groups: ['tcp'] }) 49 | tcpSettings!: Record 50 | 51 | @Type(() => Http2ProtocolSetting) 52 | @Expose({ name: 'http2Settings', toPlainOnly: true, groups: ['http2'] }) 53 | http2Settings!: Http2ProtocolSetting 54 | 55 | @Expose({ name: 'grpcSettings', toPlainOnly: true, groups: ['grpc'] }) 56 | grpcSettings!: Record 57 | 58 | @Expose({ name: 'kcpSettings', toPlainOnly: true, groups: ['kcp'] }) 59 | kcpSettings!: Record 60 | } 61 | 62 | export class Xray { 63 | // @Exclude({ toPlainOnly: true }) 64 | @Expose() 65 | id!: number 66 | 67 | @Expose() 68 | name!: string 69 | 70 | @Expose() 71 | protocol!: 'vless' | 'vmess' | 'trojan' 72 | 73 | @Expose() 74 | uuid!: string 75 | 76 | @Expose() 77 | address!: string 78 | 79 | @Expose() 80 | port!: number 81 | 82 | @Type(() => StreamSettings) 83 | @Expose({ name: 'stream_settings', toPlainOnly: true }) 84 | streamSettings!: StreamSettings 85 | } 86 | 87 | // export class XrayController { 88 | // static getForm() { 89 | // const tlsSettings = new TLSSetting() 90 | // tlsSettings.allowInsecure = true 91 | // tlsSettings.serverName = '' 92 | 93 | // const wsHeader = new WebSocketHeader() 94 | // wsHeader.host = '' 95 | 96 | // const wsSettings = new WebSocketProtocolSetting() 97 | // wsSettings.path = '' 98 | // wsSettings.headers = wsHeader 99 | 100 | // const http2Setting = new Http2ProtocolSetting() 101 | // http2Setting.host = [''] 102 | // http2Setting.path = '' 103 | 104 | // const streamSettings = new StreamSettings() 105 | // streamSettings.network = 'ws' 106 | // streamSettings.security = 'none' 107 | // streamSettings.tlsSettings = tlsSettings 108 | // streamSettings.wsSettings = wsSettings 109 | // streamSettings.grpcSettings = {} 110 | // streamSettings.http2Settings = http2Setting 111 | // streamSettings.kcpSettings = {} 112 | // streamSettings.tcpSettings = {} 113 | 114 | // const xray = new Xray() 115 | // xray.id = 0 116 | // xray.name = '' 117 | // xray.protocol = 'vmess' 118 | // xray.uuid = '' 119 | // xray.address = '' 120 | // xray.port = 443 121 | // xray.streamSettings = streamSettings 122 | 123 | // return xray 124 | // } 125 | // } 126 | -------------------------------------------------------------------------------- /src-tauri/migration/src/m20241126_062352_add_lang_col.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[derive(DeriveMigrationName)] 4 | pub struct Migration; 5 | 6 | #[async_trait::async_trait] 7 | impl MigrationTrait for Migration { 8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 9 | let _ = manager 10 | .alter_table( 11 | Table::alter() 12 | .table(BaseConfig::Table) 13 | .add_column_if_not_exists( 14 | ColumnDef::new(Alias::new("language")) 15 | .string() 16 | .not_null() 17 | .default("zh-CN"), 18 | ) 19 | .to_owned(), 20 | ) 21 | .await; 22 | let _ = manager 23 | .alter_table( 24 | Table::alter() 25 | .table(BaseConfig::Table) 26 | .add_column_if_not_exists( 27 | ColumnDef::new(Alias::new("update_interval")) 28 | .integer() 29 | .not_null() 30 | .default(3), 31 | ) 32 | .to_owned(), 33 | ) 34 | .await; 35 | let _ = manager 36 | .alter_table( 37 | Table::alter() 38 | .table(BaseConfig::Table) 39 | .add_column_if_not_exists( 40 | ColumnDef::new(Alias::new("allow_lan")) 41 | .boolean() 42 | .not_null() 43 | .default(false), 44 | ) 45 | .to_owned(), 46 | ) 47 | .await; 48 | manager 49 | .alter_table( 50 | Table::alter() 51 | .table(BaseConfig::Table) 52 | .add_column_if_not_exists( 53 | ColumnDef::new(Alias::new("mode")) 54 | .string() 55 | .not_null() 56 | .default("Rules"), 57 | ) 58 | .to_owned(), 59 | ) 60 | .await 61 | } 62 | 63 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 64 | let _ = manager 65 | .alter_table( 66 | Table::alter() 67 | .table(BaseConfig::Table) 68 | .drop_column(Alias::new("language")) 69 | .to_owned(), 70 | ) 71 | .await; 72 | 73 | let _ = manager 74 | .alter_table( 75 | Table::alter() 76 | .table(BaseConfig::Table) 77 | .drop_column(Alias::new("update_interval")) 78 | .to_owned(), 79 | ) 80 | .await; 81 | let _ = manager 82 | .alter_table( 83 | Table::alter() 84 | .table(BaseConfig::Table) 85 | .drop_column(Alias::new("allow_lan")) 86 | .to_owned(), 87 | ) 88 | .await; 89 | manager 90 | .alter_table( 91 | Table::alter() 92 | .table(BaseConfig::Table) 93 | .drop_column(Alias::new("mode")) 94 | .to_owned(), 95 | ) 96 | .await 97 | } 98 | } 99 | 100 | #[derive(DeriveIden)] 101 | enum BaseConfig { 102 | Table, 103 | } 104 | -------------------------------------------------------------------------------- /src-tauri/src/tray.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::state::DatabaseState; 4 | use crate::tauri_event_handler::on_exit_clear_commands; 5 | use tauri::menu::{Menu, MenuEvent}; 6 | use tauri::tray::TrayIconEvent; 7 | use tauri::{ 8 | menu::{MenuBuilder, MenuItemBuilder}, 9 | tray::TrayIconBuilder, 10 | }; 11 | 12 | use tauri::image::Image; 13 | use tauri::{AppHandle, Manager, State, Wry}; 14 | 15 | use crate::tauri_apis::common as common_api; 16 | 17 | pub struct Tray {} 18 | 19 | impl Tray { 20 | fn tray_menu(app_handle: &AppHandle) -> Result, Box> { 21 | let quit = MenuItemBuilder::with_id("quit", "Quit") 22 | .accelerator("CmdOrControl+Q") 23 | .build(app_handle)?; 24 | let hide = MenuItemBuilder::with_id("hide", "Hide") 25 | .accelerator("CmdOrControl+W") 26 | .build(app_handle)?; 27 | let system_proxy = MenuItemBuilder::with_id("system_proxy", "System Proxy") 28 | .accelerator("CmdOrControl+Shift+Y") 29 | .build(app_handle)?; 30 | let copy_env = MenuItemBuilder::with_id("copy_env", "Copy ENV") 31 | .accelerator("CmdOrControl+Shift+C") 32 | .build(app_handle)?; 33 | let menu = MenuBuilder::new(app_handle) 34 | .items(&[&quit, &hide, &system_proxy, ©_env]) 35 | .build() 36 | .unwrap(); 37 | Ok(menu) 38 | } 39 | 40 | pub fn init_tray(app_handle: &AppHandle) -> Result<(), Box> { 41 | let menu = Tray::tray_menu(app_handle)?; 42 | // let icon = Tray::icon()?; 43 | let tray = TrayIconBuilder::new() 44 | .menu(&menu) 45 | // .icon(icon) 46 | .on_menu_event(move |app, event: tauri::menu::MenuEvent| { 47 | Tray::on_menu_event(app, &event) 48 | }) 49 | .on_tray_icon_event(|tray, event| match event { 50 | TrayIconEvent::Click { 51 | id, 52 | position, 53 | rect, 54 | button, 55 | button_state, 56 | } => { 57 | let app = tray.app_handle(); 58 | if let Some(window) = app.get_webview_window("main") { 59 | let _ = window.show(); 60 | let _ = window.set_focus(); 61 | } 62 | } 63 | _ => {} 64 | }) 65 | .build(app_handle)?; 66 | // app_handle.path().resource_dir() 67 | let _ = tray.set_icon(Some(Image::from_bytes(include_bytes!( 68 | "../icons/icon.png" 69 | ))?)); 70 | let _ = tray.set_icon_as_template(false); 71 | Ok(()) 72 | } 73 | 74 | fn on_menu_event(app_handle: &AppHandle, event: &MenuEvent) -> () { 75 | match event.id().as_ref() { 76 | "hide" => { 77 | let window = app_handle.get_webview_window("main").unwrap(); 78 | window.hide().unwrap(); 79 | } 80 | "quit" => { 81 | on_exit_clear_commands(app_handle); 82 | println!("on_exit_clear_commands called"); 83 | app_handle.exit(0); 84 | std::process::exit(0); 85 | } 86 | "system_proxy" => (), 87 | "copy_env" => { 88 | let db_state: State = app_handle.state(); 89 | let db = db_state.get_db(); 90 | tauri::async_runtime::block_on(async move { 91 | let _ = common_api::copy_proxy_env(app_handle, &db).await; 92 | }); 93 | } 94 | // 95 | _ => (), 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src-tauri/entity/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de::{self, Deserializer, Visitor}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use uuid::Uuid; 7 | 8 | const XRAY_SCHEMAS: [&str; 3] = ["vmess", "vless", "trojan"]; 9 | const HYSTERIA_SCHEMAS: [&str; 1] = ["hy2"]; 10 | 11 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 12 | pub struct ShareJsonStruct { 13 | pub ps: String, 14 | pub add: String, 15 | #[serde(deserialize_with = "port_as_u16")] 16 | pub port: u16, 17 | #[serde(deserialize_with = "uuid_as_string")] 18 | pub id: String, 19 | pub net: String, 20 | pub r#type: String, 21 | pub host: String, 22 | pub path: String, 23 | pub tls: String, 24 | } 25 | 26 | fn uuid_as_string<'de, D>(deserializer: D) -> Result 27 | where 28 | D: Deserializer<'de>, 29 | { 30 | struct PortVisitor; 31 | 32 | impl<'de> Visitor<'de> for PortVisitor { 33 | type Value = String; 34 | 35 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 36 | formatter.write_str("a string or integer representing a port") 37 | } 38 | 39 | fn visit_str(self, value: &str) -> Result 40 | where 41 | E: de::Error, 42 | { 43 | Uuid::parse_str(value) 44 | .map_err(|_| E::custom(format!("invalid port: {}", value))) 45 | .map(|x| x.to_string()) 46 | } 47 | } 48 | 49 | deserializer.deserialize_any(PortVisitor) 50 | } 51 | 52 | fn port_as_u16<'de, D>(deserializer: D) -> Result 53 | where 54 | D: Deserializer<'de>, 55 | { 56 | struct PortVisitor; 57 | 58 | impl<'de> Visitor<'de> for PortVisitor { 59 | type Value = u16; 60 | 61 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 62 | formatter.write_str("a string or integer representing a port") 63 | } 64 | 65 | fn visit_u64(self, value: u64) -> Result 66 | where 67 | E: de::Error, 68 | { 69 | if value <= u16::MAX as u64 { 70 | Ok(value as u16) 71 | } else { 72 | Err(E::custom(format!("port out of range: {}", value))) 73 | } 74 | } 75 | 76 | fn visit_str(self, value: &str) -> Result 77 | where 78 | E: de::Error, 79 | { 80 | value 81 | .parse::() 82 | .map_err(|_| E::custom(format!("invalid port: {}", value))) 83 | } 84 | } 85 | 86 | deserializer.deserialize_any(PortVisitor) 87 | } 88 | 89 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 90 | pub struct ShareWithProtocol { 91 | pub share: ShareJsonStruct, 92 | pub protocol: String, 93 | } 94 | 95 | impl ShareWithProtocol { 96 | pub fn new(protocol: String, share: ShareJsonStruct) -> Self { 97 | Self { share, protocol } 98 | } 99 | } 100 | 101 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 102 | pub struct ProtocolLine { 103 | pub line: String, 104 | pub protocol: String, 105 | } 106 | 107 | impl ProtocolLine { 108 | pub fn new(line: String, protocol: String) -> Self { 109 | Self { line, protocol } 110 | } 111 | 112 | pub fn is_xray(&self) -> bool { 113 | XRAY_SCHEMAS.contains(&&self.protocol.as_str()) 114 | } 115 | 116 | pub fn is_hy2(&self) -> bool { 117 | HYSTERIA_SCHEMAS.contains(&&self.protocol.as_str()) 118 | } 119 | } 120 | 121 | impl TryFrom for ShareWithProtocol { 122 | type Error = anyhow::Error; 123 | fn try_from(value: ProtocolLine) -> Result { 124 | let share: ShareJsonStruct = serde_json::from_str(&value.line)?; 125 | Ok(ShareWithProtocol::new(value.protocol, share)) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/apis/proxy/index.ts: -------------------------------------------------------------------------------- 1 | import type { HumpsProcessorParameter } from 'humps' 2 | import { camelizeKeys, decamelizeKeys } from 'humps' 3 | import { instanceToPlain, plainToInstance } from 'class-transformer' 4 | import { Xray } from '@/models/xray' 5 | import { invoke } from '@/utils/invoke' 6 | import type { HysteriaProxy, ImportProxy, ProxyDelay, ProxyDelayInfo, Subscription, XrayProxy } from '@/types/proxy' 7 | import { ProxyType } from '@/types/proxy' 8 | 9 | export async function getAllHysterias() { 10 | const res = await invoke('get_all_hysterias') 11 | return camelizeKeys(res.data) as HysteriaProxy[] 12 | } 13 | 14 | export async function getHysteriaById(id: number) { 15 | const res = await invoke('get_hysteria_by_id', { id }) 16 | return camelizeKeys(res.data) as HysteriaProxy | null 17 | } 18 | 19 | export async function getXrayById(id: number) { 20 | const res = await invoke('get_xray_by_id', { id }) 21 | if (!res.data) 22 | return null 23 | const data = camelizeKeys(res.data, (key: string, _: HumpsProcessorParameter): string => { 24 | if (key === 'Host') 25 | return 'host' 26 | return key 27 | }) 28 | return data 29 | } 30 | 31 | export async function getProxyByIdAndType(id: number, proxyType: ProxyType) { 32 | switch (proxyType) { 33 | case ProxyType.Hysteria: 34 | return await getHysteriaById(id) 35 | case ProxyType.Xray: 36 | return await getXrayById(id) 37 | } 38 | } 39 | 40 | export async function createXrayProxy(xrayForm: XrayProxy) { 41 | const groupName = xrayForm.streamSettings.network 42 | const formCopy = { ...xrayForm } 43 | const record = instanceToPlain(plainToInstance(Xray, formCopy, { groups: [groupName] }), { groups: [groupName] }) 44 | await invoke('add_xray_item', { record }) 45 | } 46 | 47 | export async function createHysteriaProxy(hysteriaForm: HysteriaProxy) { 48 | await invoke('add_hysteria_item', { record: decamelizeKeys(hysteriaForm) }) 49 | } 50 | 51 | export async function getAllXraies() { 52 | const res = await invoke('get_all_xrays') 53 | return camelizeKeys(res.data) as XrayProxy[] 54 | } 55 | 56 | export async function createImportProxy(importProxyForm: ImportProxy) { 57 | await invoke('import_xray_subscribe', { url: importProxyForm.url }) 58 | } 59 | 60 | export async function updateXrayProxy(xrayForm: XrayProxy) { 61 | const groupName = xrayForm.streamSettings.network 62 | const formCopy = { ...xrayForm } 63 | const record = instanceToPlain(plainToInstance(Xray, formCopy, { groups: [groupName] }), { groups: [groupName] }) 64 | await invoke('update_xray_item', { record }) 65 | } 66 | 67 | export async function updateHysteriaProxy(hysteriaForm: HysteriaProxy) { 68 | await invoke('update_hysteria_item', { record: decamelizeKeys(hysteriaForm) }) 69 | } 70 | 71 | export async function autoUpdateSubscription(subscriptionIds: number[]) { 72 | await invoke('refresh_xray_subscription', { record_ids: subscriptionIds }) 73 | } 74 | 75 | export async function batchGetSubscriptions(): Promise { 76 | const res = await invoke('batch_get_subscriptions') 77 | return res.data 78 | } 79 | 80 | export async function xrayProxiedDelay(proxies: ProxyDelayInfo[]) { 81 | const res = await invoke('proxies_delay_test', { proxies }) 82 | 83 | return res.data.reduce((acc, item) => { 84 | acc[item.id] = item.delay 85 | return acc 86 | }, {} as Record) 87 | } 88 | 89 | export async function currentProxyDelay(proxy: string, targetUrl: string) { 90 | const res = await invoke('test_current_proxy', { proxy, target_url: targetUrl }) 91 | 92 | return res.data 93 | } 94 | 95 | export async function setProxy(enable: boolean, id: number | null = null) { 96 | if (enable) { 97 | await invoke('start_system_proxy', { xray_id: id }) 98 | } 99 | else { 100 | await invoke('stop_system_proxy') 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/views/proxy/modal/AddProxy.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 153 | 154 | 167 | -------------------------------------------------------------------------------- /src-tauri/src/proxy/delay.rs: -------------------------------------------------------------------------------- 1 | use entity::xray; 2 | use reqwest::{Client, Proxy}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::sync::Arc; 5 | use std::time::{Duration, Instant}; 6 | use tokio::net::TcpStream; 7 | use tokio::sync::Semaphore; 8 | 9 | #[derive(Debug, Serialize, Deserialize, Clone)] 10 | pub struct ProxyInfo { 11 | pub address: String, 12 | pub port: u16, 13 | pub id: u32, 14 | } 15 | 16 | impl From for ProxyInfo { 17 | fn from(source: xray::Model) -> Self { 18 | return ProxyInfo { 19 | id: source.id as u32, 20 | address: source.address, 21 | port: source.port, 22 | }; 23 | } 24 | } 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | pub struct ProxyDelay { 28 | pub id: u32, 29 | pub delay: u128, 30 | } 31 | 32 | async fn measure_tcp_latency(proxy_info: &ProxyInfo) -> ProxyDelay { 33 | let address = format!("{}:{}", proxy_info.address, proxy_info.port); 34 | 35 | // 记录开始时间 36 | let start_time = Instant::now(); 37 | 38 | // 尝试连接到目标 IP 和端口 39 | match tokio::time::timeout( 40 | std::time::Duration::from_secs(3), 41 | TcpStream::connect(address), 42 | ) 43 | .await 44 | { 45 | Ok(_) => { 46 | // 计算往返时间 47 | let round_trip_time = start_time.elapsed(); 48 | let proxy_delay = ProxyDelay { 49 | id: proxy_info.id, 50 | delay: round_trip_time.as_millis(), 51 | }; 52 | return proxy_delay; 53 | } 54 | Err(_) => { 55 | let proxy_delay = ProxyDelay { 56 | id: proxy_info.id, 57 | delay: 9999, 58 | }; 59 | return proxy_delay; 60 | } 61 | } 62 | } 63 | 64 | pub async fn kitty_proxies_delay(proxies: Vec) -> Vec { 65 | let mut result = Vec::new(); 66 | let max_concurrent_connections = 10; 67 | 68 | let seamphore = Arc::new(Semaphore::new(max_concurrent_connections)); 69 | 70 | let mut handles = vec![]; 71 | for proxy in proxies.into_iter() { 72 | let permit = seamphore.clone().acquire_owned().await.unwrap(); 73 | handles.push(tokio::spawn(async move { 74 | let _permit = permit; 75 | measure_tcp_latency(&proxy).await 76 | })); 77 | } 78 | 79 | for handle in handles { 80 | let res = handle.await.unwrap(); 81 | result.push(res); 82 | } 83 | 84 | // sory result by delay 85 | result.sort_by(|a, b| a.delay.cmp(&b.delay)); 86 | result 87 | } 88 | 89 | pub async fn kitty_current_proxy_delay(proxy: String, target_url: String) -> u128 { 90 | let request = Client::builder() 91 | .proxy(Proxy::all(proxy).unwrap()) 92 | .timeout(Duration::from_secs(3)) 93 | .build() 94 | .unwrap(); 95 | let start_time = Instant::now(); 96 | match request.get(target_url).send().await { 97 | Ok(_) => start_time.elapsed().as_millis(), 98 | Err(_) => 9999_u128, 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[tokio::test] 107 | async fn test_proxies() { 108 | let proxies = vec![ 109 | ProxyInfo { 110 | id: 1, 111 | address: "xj0211.alibabaokz.com".to_string(), 112 | port: 40001, 113 | }, 114 | ProxyInfo { 115 | id: 2, 116 | address: "hk0106.alibabaokz.com".to_string(), 117 | port: 60126, 118 | }, 119 | ]; 120 | 121 | let mut aa = Vec::new(); 122 | 123 | for _ in 0..100 { 124 | aa.extend(proxies.clone().into_iter()); 125 | } 126 | 127 | let results = kitty_proxies_delay(aa).await; 128 | println!("{:?}", results); 129 | assert!(results.len() > 0); 130 | assert!(results[0].delay > 0); 131 | } 132 | 133 | #[tokio::test] 134 | async fn test_current_proxy() { 135 | let delay = kitty_current_proxy_delay( 136 | "http://127.0.0.1:7890".to_string(), 137 | "https://www.google.com".to_string(), 138 | ) 139 | .await; 140 | 141 | println!("delay {}", delay); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src-tauri/src/tauri_apis/hysteria.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use entity::base_config; 4 | use entity::hysteria::{self, HysteriaConfig}; 5 | use protocols::KittyCommandGroupTrait; 6 | use protocols::XrayCommandGroup; 7 | use tauri::{AppHandle, Manager, State}; 8 | 9 | use crate::apis::hysteria_apis::HysteriaAPI; 10 | use crate::state::{DatabaseState, KittyProxyState}; 11 | use crate::types::{CommandResult, KittyResponse}; 12 | 13 | use super::utils::{get_http_socks_ports, relative_command_path, speed_delay}; 14 | 15 | #[tauri::command(rename_all = "snake_case")] 16 | pub async fn add_hysteria_item<'a>( 17 | state: State<'a, DatabaseState>, 18 | record: hysteria::Model, 19 | ) -> CommandResult<()> { 20 | let db = state.get_db(); 21 | HysteriaAPI.add_hysteria_item(&db, record).await?; 22 | Ok(()) 23 | } 24 | 25 | #[tauri::command(rename_all = "snake_case")] 26 | pub async fn get_hysteria_by_id<'a>( 27 | state: State<'a, DatabaseState>, 28 | id: i32, 29 | ) -> CommandResult>> { 30 | let db = state.get_db(); 31 | let hysteria = HysteriaAPI.get_hysteria_by_id(&db, id).await?; 32 | Ok(KittyResponse::from_data(hysteria)) 33 | } 34 | 35 | #[tauri::command(rename_all = "snake_case")] 36 | pub async fn get_all_hysterias<'a>( 37 | state: State<'a, DatabaseState>, 38 | ) -> CommandResult>> { 39 | let db = state.get_db(); 40 | let hy_proxies = HysteriaAPI.get_all(&db).await?; 41 | Ok(KittyResponse::from_data(hy_proxies)) 42 | } 43 | 44 | #[tauri::command(rename_all = "snake_case")] 45 | pub async fn delete_hysteria_item<'a>( 46 | state: State<'a, DatabaseState>, 47 | id: i32, 48 | ) -> CommandResult<()> { 49 | let db = state.get_db(); 50 | HysteriaAPI.delete_hysteria_item(&db, id).await?; 51 | Ok(()) 52 | } 53 | 54 | #[tauri::command(rename_all = "snake_case")] 55 | pub async fn update_hysteria_item<'a>( 56 | state: State<'a, DatabaseState>, 57 | record: hysteria::Model, 58 | ) -> CommandResult<()> { 59 | let db = state.get_db(); 60 | HysteriaAPI.update_hysteria_item(&db, record).await?; 61 | Ok(()) 62 | } 63 | 64 | #[tauri::command(rename_all = "snake_case")] 65 | pub async fn speed_hysteria_delay<'a>( 66 | app_handle: AppHandle, 67 | state: State<'a, DatabaseState>, 68 | proxy_state: State<'a, KittyProxyState>, 69 | record_ids: Option>, 70 | ) -> CommandResult> { 71 | let db = state.get_db(); 72 | let hysteria_records = if record_ids.is_none() { 73 | hysteria::Model::fetch_all(&db).await? 74 | } else { 75 | hysteria::Model::fetch_by_ids(&db, record_ids.unwrap()).await? 76 | }; 77 | let base_config_record = base_config::Model::first(&db).await.unwrap(); 78 | let delay_test_url = base_config_record.unwrap().delay_test_url; 79 | drop(db); 80 | let config_dir = app_handle.path().config_dir()?; 81 | let hysteria_bin_path = relative_command_path("hysteria".as_ref())?; 82 | let mut hysteria_command_group = XrayCommandGroup::new(hysteria_bin_path, config_dir.clone()); 83 | let mut config_hash_map: HashMap = HashMap::new(); 84 | 85 | let mut port_model_dict = HashMap::new(); 86 | let mut used_ports = proxy_state.used_ports.lock().await; 87 | for record in hysteria_records.into_iter() { 88 | let (http_port, socks_port) = get_http_socks_ports(&mut used_ports); 89 | let record_id = record.id; 90 | let hysteria_config = HysteriaConfig::new(http_port, socks_port, record); 91 | config_hash_map.insert(hysteria_config.server.clone(), hysteria_config); 92 | port_model_dict.insert(http_port, record_id); 93 | } 94 | drop(used_ports); 95 | let _ = hysteria_command_group.start_commands(config_hash_map, None); 96 | let ports: Vec = port_model_dict.keys().map(|x| x.to_owned()).collect(); 97 | let total = ports.len(); 98 | let result: HashMap = 99 | speed_delay(ports, Some(delay_test_url.as_str())).await?; 100 | let mut new_result: HashMap = HashMap::with_capacity(total); 101 | for (k, v) in result.iter() { 102 | new_result.insert(port_model_dict.get(k).unwrap().to_owned(), v.as_millis()); 103 | } 104 | 105 | Ok(new_result) 106 | } 107 | -------------------------------------------------------------------------------- /src-tauri/src/apis/xray_apis.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use base64::{engine::general_purpose, Engine as _}; 3 | use entity::subscribe; 4 | use entity::xray; 5 | use sea_orm::ActiveModelTrait; 6 | use sea_orm::DatabaseConnection; 7 | use sea_orm::ModelTrait; 8 | use sea_orm::Set; 9 | use sea_orm::TransactionTrait; 10 | use std::str::FromStr; 11 | 12 | use crate::apis::api_traits::APIServiceTrait; 13 | 14 | use super::parse_subscription::download_subcriptions; 15 | 16 | pub struct XrayAPI; 17 | 18 | impl APIServiceTrait for XrayAPI {} 19 | impl XrayAPI { 20 | pub async fn get_all(&self, db: &DatabaseConnection) -> Result> { 21 | let xray_proxies = xray::Model::fetch_all(db).await?; 22 | let xray_proxies: Vec = 23 | xray_proxies.into_iter().map(|model| model.into()).collect(); 24 | Ok(xray_proxies) 25 | } 26 | 27 | pub async fn get_xray_by_id( 28 | &self, 29 | db: &DatabaseConnection, 30 | id: i32, 31 | ) -> Result> { 32 | Ok(xray::Model::get_by_id(db, id).await?) 33 | } 34 | 35 | pub async fn add_xray_item(&self, db: &DatabaseConnection, record: xray::Model) -> Result<()> { 36 | record.insert_one(db).await?; 37 | Ok(()) 38 | } 39 | 40 | pub async fn delete_xray_item(&self, db: &DatabaseConnection, id: i32) -> Result<()> { 41 | let _ = xray::Model::delete_by_id(db, id).await?; 42 | Ok(()) 43 | } 44 | 45 | pub async fn update_xray_item( 46 | &self, 47 | db: &DatabaseConnection, 48 | record: xray::Model, 49 | ) -> Result<()> { 50 | let _ = record.update(db).await?; 51 | Ok(()) 52 | } 53 | 54 | pub async fn import_xray_from_subscribe( 55 | &self, 56 | db: &DatabaseConnection, 57 | url: &str, 58 | ) -> Result<()> { 59 | let mut xray_models = Vec::new(); 60 | let txn = db.begin().await?; 61 | if url.starts_with("http") { 62 | let subscriptions = download_subcriptions(url).await?; 63 | let subscribe = subscribe::ActiveModel { 64 | url: Set(url.to_owned()), 65 | ..Default::default() 66 | }; 67 | let exec_subscribe_res = subscribe.insert(&txn).await?; 68 | for line in subscriptions { 69 | if !line.is_xray() { 70 | continue; 71 | } 72 | if let Ok(mut xray_model) = xray::Model::from_str(&line.line.trim()) { 73 | xray_model.subscribe_id = Some(exec_subscribe_res.id); 74 | xray_models.push(xray_model); 75 | } 76 | } 77 | } else { 78 | let trimed_line = if let Some(pos) = url.rfind('#') { 79 | &url[..pos] 80 | } else { 81 | url 82 | }; 83 | if let Ok(xray_model) = xray::Model::from_str(&trimed_line.trim()) { 84 | xray_models.push(xray_model); 85 | } 86 | } 87 | println!("xray_models: {}", xray_models.len()); 88 | xray::Model::insert_many(&txn, xray_models).await?; 89 | txn.commit().await?; 90 | Ok(()) 91 | } 92 | 93 | pub async fn refresh_subscribe(db: &DatabaseConnection, ids: Option>) -> Result<()> { 94 | let res = if let Some(subscribe_ids) = ids { 95 | subscribe::Model::fetch_by_ids(db, subscribe_ids).await? 96 | } else { 97 | subscribe::Model::fetch_all(db).await? 98 | }; 99 | if res.len() > 0 { 100 | for subscribe_item in res { 101 | let subscriptions = download_subcriptions(&subscribe_item.url).await?; 102 | let xray_records = subscribe_item 103 | .find_related(xray::Entity) 104 | .all(db) 105 | .await 106 | .unwrap(); 107 | let txn = db.begin().await?; 108 | let xray_ids: Vec = xray_records.iter().map(|x| x.id).collect(); 109 | let _ = xray::Model::delete_by_ids(&txn, xray_ids).await?; 110 | let mut xray_models = Vec::new(); 111 | for line in subscriptions { 112 | if !line.is_xray() { 113 | continue; 114 | } 115 | let mut xray_model = xray::Model::from_str(&line.line.trim())?; 116 | xray_model.subscribe_id = Some(subscribe_item.id); 117 | xray_models.push(xray_model) 118 | } 119 | xray::Model::insert_many(&txn, xray_models).await?; 120 | txn.commit().await?; 121 | } 122 | } 123 | Ok(()) 124 | } 125 | 126 | pub async fn batch_get_subscriptions(db: &DatabaseConnection) -> Result> { 127 | Ok(subscribe::Model::fetch_all(db).await?) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src-tauri/protocols/src/kitty_command.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use log::debug; 3 | use serde::Serialize; 4 | use shared_child::SharedChild; 5 | use std::collections::HashMap; 6 | use std::fs::File; 7 | use std::io::{self, BufRead, Write}; 8 | use std::net::SocketAddr; 9 | #[cfg(target_os = "windows")] 10 | use std::os::windows::process::CommandExt; 11 | use std::path::PathBuf; 12 | use std::process::{Command, Stdio}; 13 | use std::sync::Arc; 14 | use std::{thread, time}; 15 | use uuid::Uuid; 16 | 17 | use crate::types::CheckStatusCommandPipe; 18 | use crate::utils::socket_addr_busy; 19 | 20 | #[derive(Debug)] 21 | pub struct KittyCommand { 22 | bin_path: PathBuf, 23 | child: Arc, 24 | config_path: PathBuf, 25 | env_mapping: HashMap, 26 | } 27 | 28 | impl KittyCommand { 29 | pub fn spawn( 30 | bin_path: &PathBuf, 31 | command_args: [&str; 2], 32 | config: T, 33 | config_dir: &PathBuf, 34 | env_mapping: HashMap, 35 | ) -> Result 36 | where 37 | T: Serialize, 38 | { 39 | let config_content = serde_json::to_string(&config)?; 40 | let binary_name = bin_path.file_name().unwrap().to_str().unwrap(); 41 | let config_path = config_dir.join(format!("{binary_name}_{}.json", Uuid::new_v4())); 42 | let mut file = File::create(&config_path)?; 43 | file.write_all(config_content.as_bytes())?; 44 | let mut command = Command::new(bin_path); 45 | let command = command.args(command_args); 46 | let command = command 47 | .arg(config_path.as_os_str()) 48 | .stdout(Stdio::piped()) 49 | .stderr(Stdio::piped()); 50 | #[cfg(target_os = "windows")] 51 | let command = command.creation_flags(0x08000000); 52 | for (env_key, env_value) in env_mapping.iter() { 53 | std::env::set_var(env_key, env_value); 54 | } 55 | 56 | let share_child = SharedChild::spawn(command)?; 57 | let child_arc = Arc::new(share_child); 58 | Ok(Self { 59 | bin_path: bin_path.to_owned(), 60 | child: child_arc, 61 | config_path, 62 | env_mapping, 63 | }) 64 | } 65 | 66 | pub fn check_socket_addrs(&self, socket_addrs: Vec) -> Result<()> { 67 | for socket_addr in socket_addrs { 68 | let res = socket_addr_busy(socket_addr); 69 | if !res { 70 | return Err(anyhow!(anyhow!( 71 | "check_socket_addrs failed, process start failed!" 72 | ))); 73 | } 74 | } 75 | Ok(()) 76 | } 77 | 78 | pub fn check_status( 79 | &self, 80 | started_str: &str, 81 | std_pipe: CheckStatusCommandPipe, 82 | socket_addrs: Vec, 83 | ) -> Result<()> { 84 | let child_clone = self.child.clone(); 85 | if let Ok(None) = child_clone.try_wait() { 86 | match std_pipe { 87 | CheckStatusCommandPipe::StdErr => { 88 | let pipe_out = &mut child_clone.take_stderr(); 89 | if let Some(pipe_out) = pipe_out { 90 | let reader = io::BufReader::new(pipe_out); 91 | for line in reader.lines() { 92 | if let Ok(line) = line { 93 | debug!("stderr: {}", line); 94 | if line.to_lowercase().contains(&started_str.to_lowercase()) { 95 | thread::sleep(time::Duration::from_millis(500)); 96 | self.check_socket_addrs(socket_addrs)?; 97 | return Ok(()); 98 | } 99 | } 100 | } 101 | return Ok(()); 102 | } 103 | } 104 | CheckStatusCommandPipe::StdOut => { 105 | let pipe_out = &mut child_clone.take_stdout(); 106 | if let Some(pipe_out) = pipe_out { 107 | let reader = io::BufReader::new(pipe_out); 108 | for line in reader.lines() { 109 | if let Ok(line) = line { 110 | debug!("stdout: {}", line); 111 | if line.to_lowercase().contains(&started_str.to_lowercase()) { 112 | thread::sleep(time::Duration::from_millis(500)); 113 | self.check_socket_addrs(socket_addrs)?; 114 | return Ok(()); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | Err(anyhow!("process start failed!")) 123 | } 124 | 125 | pub fn terminate_backend(&mut self) -> Result<()> { 126 | self.child.kill()?; 127 | Ok(()) 128 | } 129 | 130 | pub fn is_running(&self) -> bool { 131 | if let Ok(None) = self.child.try_wait() { 132 | return true; 133 | } 134 | false 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/views/rule/RuleView.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 154 | -------------------------------------------------------------------------------- /src-tauri/src/tauri_apis/common.rs: -------------------------------------------------------------------------------- 1 | use crate::apis::common_apis::CommonAPI; 2 | use crate::proxy::delay::kitty_current_proxy_delay; 3 | use crate::state::{DatabaseState, KittyProxyState}; 4 | use crate::types::{CommandResult, KittyResponse}; 5 | use anyhow::anyhow; 6 | use entity::base_config; 7 | use entity::rules; 8 | use sea_orm::{DatabaseConnection, TransactionTrait}; 9 | use tauri::State; 10 | use tauri_plugin_autostart::AutoLaunchManager; 11 | use tauri_plugin_clipboard_manager::ClipboardExt; 12 | 13 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 14 | use tauri::AppHandle; 15 | use tauri::Runtime; 16 | 17 | use super::utils::{add_rule2match_proxy, delete_rule2match_proxy}; 18 | 19 | pub async fn copy_proxy_env( 20 | app_handle: &AppHandle, 21 | db: &DatabaseConnection, 22 | ) -> CommandResult> { 23 | let proxy_string = CommonAPI::copy_proxy_env(db).await?; 24 | app_handle.clipboard().write_text(proxy_string).unwrap(); 25 | Ok(KittyResponse::default()) 26 | } 27 | 28 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 29 | #[tauri::command(rename_all = "snake_case")] 30 | pub async fn copy_proxy_env_cmd<'a, R: Runtime>( 31 | app_handle: AppHandle, 32 | state: State<'a, DatabaseState>, 33 | ) -> CommandResult> { 34 | let db = state.get_db(); 35 | Ok(copy_proxy_env(&app_handle, &db).await?) 36 | } 37 | 38 | #[tauri::command(rename_all = "snake_case")] 39 | pub async fn query_base_config<'a>( 40 | state: State<'a, DatabaseState>, 41 | ) -> CommandResult> { 42 | let db = state.get_db(); 43 | let res = CommonAPI::query_base_config(&db).await?; 44 | Ok(res) 45 | } 46 | 47 | #[tauri::command(rename_all = "snake_case")] 48 | pub async fn update_base_config<'a>( 49 | state: State<'a, DatabaseState>, 50 | record: base_config::Model, 51 | ) -> CommandResult> { 52 | let db = state.get_db(); 53 | let res = CommonAPI::update_base_config(&db, record).await?; 54 | Ok(res) 55 | } 56 | 57 | #[tauri::command(rename_all = "snake_case")] 58 | pub async fn add_rules<'a>( 59 | state: State<'a, DatabaseState>, 60 | proxy_state: State<'a, KittyProxyState>, 61 | records: Vec, 62 | ) -> CommandResult> { 63 | let db = state.get_db(); 64 | let match_proxy = proxy_state.match_proxy.lock().await.clone().unwrap(); 65 | let mut match_proxy_write_share = match_proxy.write().await; 66 | for rule_record in records.iter() { 67 | add_rule2match_proxy(&mut match_proxy_write_share, rule_record).await; 68 | } 69 | drop(match_proxy_write_share); 70 | let _ = CommonAPI::add_rules(&db, records).await?; 71 | Ok(KittyResponse::default()) 72 | } 73 | 74 | #[tauri::command(rename_all = "snake_case")] 75 | pub async fn query_rules<'a>( 76 | state: State<'a, DatabaseState>, 77 | ) -> CommandResult>> { 78 | let db = state.get_db(); 79 | let res = CommonAPI::query_rules(&db).await?; 80 | Ok(res) 81 | } 82 | 83 | #[tauri::command(rename_all = "snake_case")] 84 | pub async fn delete_rules<'a>( 85 | state: State<'a, DatabaseState>, 86 | proxy_state: State<'a, KittyProxyState>, 87 | ids: Vec, 88 | ) -> CommandResult> { 89 | let db = state.get_db(); 90 | let delete_rules = rules::Model::fetch_by_ids(&db, ids.clone()).await?; 91 | let txn = db.begin().await?; 92 | let match_proxy = proxy_state.match_proxy.lock().await.clone().unwrap(); 93 | let mut match_proxy_write_share = match_proxy.write().await; 94 | let _ = delete_rule2match_proxy(&txn, &mut match_proxy_write_share, delete_rules); 95 | drop(match_proxy_write_share); 96 | let _ = CommonAPI::delete_rules(&txn, ids).await?; 97 | txn.commit().await?; 98 | Ok(KittyResponse::default()) 99 | } 100 | 101 | #[tauri::command(rename_all = "snake_case")] 102 | pub async fn update_rules_item<'a>( 103 | state: State<'a, DatabaseState>, 104 | proxy_state: State<'a, KittyProxyState>, 105 | records: Vec, 106 | ) -> CommandResult> { 107 | let db = state.get_db(); 108 | let delete_record_ids: Vec = records.iter().map(|x| x.id).collect(); 109 | let origin_records = rules::Model::fetch_by_ids(&db, delete_record_ids.clone()).await?; 110 | if !origin_records.is_empty() { 111 | let txn = db.begin().await?; 112 | let match_proxy = proxy_state.match_proxy.lock().await.clone().unwrap(); 113 | let mut match_proxy_write_share = match_proxy.write().await; 114 | let _ = delete_rule2match_proxy(&txn, &mut match_proxy_write_share, origin_records); 115 | for rule_record in records.iter() { 116 | add_rule2match_proxy(&mut match_proxy_write_share, rule_record).await; 117 | } 118 | drop(match_proxy_write_share); 119 | let _ = CommonAPI::delete_rules(&txn, delete_record_ids).await?; 120 | let res = CommonAPI::add_rules(&txn, records).await?; 121 | txn.commit().await?; 122 | Ok(res) 123 | } else { 124 | Err(anyhow!("records not exists!").into()) 125 | } 126 | } 127 | 128 | #[tauri::command(rename_all = "snake_case")] 129 | pub async fn test_current_proxy<'a>( 130 | proxy: String, 131 | target_url: String, 132 | ) -> CommandResult> { 133 | println!("proxy: {}", proxy); 134 | let res = kitty_current_proxy_delay(proxy, target_url).await; 135 | Ok(KittyResponse::from_data(res)) 136 | } 137 | -------------------------------------------------------------------------------- /src-tauri/src/tauri_apis/xray.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use entity::xray::{self, XrayConfig}; 4 | use entity::{base_config, subscribe}; 5 | use protocols::XrayCommandGroup; 6 | use tauri::{AppHandle, Manager, State}; 7 | 8 | use crate::apis::xray_apis::XrayAPI; 9 | use crate::proxy::delay::{kitty_proxies_delay, ProxyDelay, ProxyInfo}; 10 | use crate::state::{DatabaseState, KittyProxyState}; 11 | use crate::types::{CommandResult, KittyResponse}; 12 | 13 | use protocols::KittyCommandGroupTrait; 14 | 15 | use super::utils::{relative_command_path, speed_delay}; 16 | 17 | #[tauri::command(rename_all = "snake_case")] 18 | pub async fn get_xray_by_id<'a>( 19 | state: State<'a, DatabaseState>, 20 | id: i32, 21 | ) -> CommandResult>> { 22 | let db = state.get_db(); 23 | Ok(KittyResponse::from_data( 24 | XrayAPI.get_xray_by_id(&db, id).await?, 25 | )) 26 | } 27 | 28 | #[tauri::command(rename_all = "snake_case")] 29 | pub async fn add_xray_item<'a>( 30 | state: State<'a, DatabaseState>, 31 | record: xray::Model, 32 | ) -> CommandResult<()> { 33 | let db = state.get_db(); 34 | XrayAPI.add_xray_item(&db, record).await?; 35 | Ok(()) 36 | } 37 | 38 | #[tauri::command(rename_all = "snake_case")] 39 | pub async fn get_all_xrays<'a>( 40 | state: State<'a, DatabaseState>, 41 | ) -> CommandResult>> { 42 | let db = state.get_db(); 43 | let xraies = XrayAPI.get_all(&db).await?; 44 | Ok(KittyResponse::from_data(xraies)) 45 | } 46 | 47 | #[tauri::command(rename_all = "snake_case")] 48 | pub async fn import_xray_subscribe<'a>( 49 | state: State<'a, DatabaseState>, 50 | url: &str, 51 | ) -> CommandResult<()> { 52 | let db = state.get_db(); 53 | let _res = XrayAPI.import_xray_from_subscribe(&db, url).await?; 54 | Ok(()) 55 | } 56 | 57 | #[tauri::command(rename_all = "snake_case")] 58 | pub async fn delete_xray_item<'a>(state: State<'a, DatabaseState>, id: i32) -> CommandResult<()> { 59 | let db = state.get_db(); 60 | XrayAPI.delete_xray_item(&db, id).await?; 61 | Ok(()) 62 | } 63 | 64 | #[tauri::command(rename_all = "snake_case")] 65 | pub async fn update_xray_item<'a>( 66 | state: State<'a, DatabaseState>, 67 | record: xray::Model, 68 | ) -> CommandResult<()> { 69 | let db = state.get_db(); 70 | XrayAPI.update_xray_item(&db, record).await?; 71 | Ok(()) 72 | } 73 | 74 | #[tauri::command(rename_all = "snake_case")] 75 | pub async fn speed_xray_delay<'a>( 76 | app_handle: AppHandle, 77 | state: State<'a, DatabaseState>, 78 | proxy_state: State<'a, KittyProxyState>, 79 | record_ids: Option>, 80 | ) -> CommandResult> { 81 | let db = state.get_db(); 82 | let xray_records: Vec = if record_ids.is_none() { 83 | xray::Model::fetch_all(&db).await? 84 | } else { 85 | xray::Model::fetch_by_ids(&db, record_ids.unwrap()).await? 86 | }; 87 | let base_config_record = base_config::Model::first(&db).await.unwrap(); 88 | let delay_test_url = base_config_record.unwrap().delay_test_url; 89 | drop(db); 90 | let config_dir = app_handle.path().config_dir()?; 91 | let mut used_ports = proxy_state.used_ports.lock().await; 92 | let hysteria_bin_path = relative_command_path("xray".as_ref())?; 93 | let mut hysteria_command_group = XrayCommandGroup::new(hysteria_bin_path, config_dir.clone()); 94 | let mut config_hash_map: HashMap = HashMap::new(); 95 | 96 | let server_key: String = xray_records 97 | .iter() 98 | .map(|x| x.get_server()) 99 | .collect::>() 100 | .join("_"); 101 | let (xray_config, port_model_dict) = 102 | XrayConfig::from_models4http_delay(xray_records, &mut used_ports); 103 | drop(used_ports); 104 | config_hash_map.insert(server_key, xray_config); 105 | let _ = hysteria_command_group.start_commands(config_hash_map, None); 106 | let ports: Vec = port_model_dict.keys().map(|x| x.to_owned()).collect(); 107 | let total = ports.len(); 108 | let result: HashMap = 109 | speed_delay(ports, Some(delay_test_url.as_str())).await?; 110 | let mut new_result: HashMap = HashMap::with_capacity(total); 111 | for (k, v) in result.iter() { 112 | new_result.insert(port_model_dict.get(k).unwrap().to_owned(), v.as_millis()); 113 | } 114 | 115 | Ok(new_result) 116 | } 117 | 118 | #[tauri::command(rename_all = "snake_case")] 119 | pub async fn refresh_xray_subscription<'a>( 120 | state: State<'a, DatabaseState>, 121 | record_ids: Option>, 122 | ) -> CommandResult<()> { 123 | println!("refresh_xray_subscription: {:?}", record_ids); 124 | let db = state.get_db(); 125 | let _ = XrayAPI::refresh_subscribe(&db, record_ids).await?; 126 | Ok(()) 127 | } 128 | 129 | #[tauri::command(rename_all = "snake_case")] 130 | pub async fn batch_get_subscriptions<'a>( 131 | state: State<'a, DatabaseState>, 132 | ) -> CommandResult>> { 133 | let db = state.get_db(); 134 | let subscriptions = XrayAPI::batch_get_subscriptions(&db).await?; 135 | Ok(KittyResponse::from_data(subscriptions)) 136 | } 137 | 138 | #[tauri::command(rename_all = "snake_case")] 139 | pub async fn proxies_delay_test<'a>( 140 | proxies: Vec, 141 | ) -> CommandResult>> { 142 | // let db = state.get_db(); 143 | // let records = xray::Model::fetch_all(&db).await.unwrap(); 144 | // let proxies = records 145 | // .into_iter() 146 | // .map(|x| x.into()) 147 | // .collect::>(); 148 | let res = kitty_proxies_delay(proxies).await; 149 | Ok(KittyResponse::from_data(res)) 150 | } 151 | -------------------------------------------------------------------------------- /src/views/proxy/form/XrayForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 163 | -------------------------------------------------------------------------------- /src-tauri/src/tauri_apis/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use entity::rules::{self, RuleAction, RuleType}; 3 | use entity::utils::get_random_port; 4 | use kitty_proxy::TrafficStreamRule; 5 | use reqwest; 6 | use sea_orm::ConnectionTrait; 7 | use std::collections::{HashMap, HashSet}; 8 | use std::path::{Path, PathBuf}; 9 | use std::time::{self, Duration}; 10 | use tauri::utils::platform; 11 | use tokio::task::JoinSet; 12 | 13 | pub fn get_http_socks_ports(used_ports: &mut HashSet) -> (u16, u16) { 14 | let http_port = get_random_port(&used_ports).unwrap(); 15 | let socks_port = get_random_port(&used_ports).unwrap(); 16 | (http_port, socks_port) 17 | } 18 | 19 | pub fn relative_command_path(command: &Path) -> Result { 20 | match platform::current_exe()?.parent() { 21 | #[cfg(windows)] 22 | Some(exe_dir) => Ok(exe_dir.join(command).with_extension("exe")), 23 | #[cfg(not(windows))] 24 | Some(exe_dir) => Ok(exe_dir.join(command)), 25 | None => Err(anyhow!("current exe not has parent.")), 26 | } 27 | } 28 | 29 | async fn request_test_url(port: u16, url: String) -> Result<(u16, Duration)> { 30 | let start_time = time::Instant::now(); 31 | let proxy = reqwest::Proxy::http(format!("http://127.0.0.1:{port}"))?; 32 | let client = reqwest::Client::builder() 33 | .timeout(Duration::from_secs(3)) 34 | .proxy(proxy) 35 | .build()?; 36 | let response = client.get(url).send().await; 37 | if response.is_err() { 38 | Ok((port, Duration::from_secs(3))) 39 | } else { 40 | let end_time = time::Instant::now(); 41 | let delay = end_time - start_time; 42 | Ok((port, delay)) 43 | } 44 | } 45 | 46 | pub async fn speed_delay( 47 | ports: Vec, 48 | test_url: Option<&str>, 49 | ) -> Result> { 50 | let mut set = JoinSet::new(); 51 | let url = test_url.unwrap_or("https://gstatic.com/generate_204"); 52 | for port in ports.clone() { 53 | let url_clone = url.to_string().clone(); 54 | set.spawn(async move { request_test_url(port, url_clone) }); 55 | } 56 | let mut delay_dict: HashMap = ports 57 | .into_iter() 58 | .map(|x| (x, Duration::from_secs(3))) 59 | .collect(); 60 | while let Some(res) = set.join_next().await { 61 | let aa = res?.await; 62 | if let Ok((port, delay)) = aa { 63 | delay_dict.insert(port, delay); 64 | } 65 | } 66 | Ok(delay_dict) 67 | } 68 | 69 | pub async fn add_rule2match_proxy( 70 | rwlock_share: &mut tokio::sync::RwLockWriteGuard<'_, kitty_proxy::MatchProxy>, 71 | rule_record: &rules::Model, 72 | ) { 73 | let traffic_stream_rule = match rule_record.rule_action { 74 | RuleAction::Reject => TrafficStreamRule::Reject, 75 | RuleAction::Direct => TrafficStreamRule::Direct, 76 | RuleAction::Proxy => TrafficStreamRule::Proxy, 77 | }; 78 | match rule_record.rule_type { 79 | RuleType::Cidr => rwlock_share 80 | .add_cidr(rule_record.rule.as_str(), traffic_stream_rule) 81 | .unwrap(), 82 | RuleType::DomainPreffix => { 83 | rwlock_share.add_domain_preffix(rule_record.rule.clone(), traffic_stream_rule) 84 | } 85 | RuleType::DomainSuffix => { 86 | rwlock_share.add_domain_suffix(rule_record.rule.clone(), traffic_stream_rule) 87 | } 88 | RuleType::FullDomain => { 89 | rwlock_share.add_full_domain(rule_record.rule.clone(), traffic_stream_rule) 90 | } 91 | RuleType::DomainRoot => { 92 | rwlock_share.add_root_domain(rule_record.rule.as_str(), traffic_stream_rule) 93 | } 94 | } 95 | } 96 | 97 | pub async fn delete_rule2match_proxy( 98 | db: &C, 99 | rwlock_share: &mut tokio::sync::RwLockWriteGuard<'_, kitty_proxy::MatchProxy>, 100 | rule_records: Vec, 101 | ) -> Result<()> 102 | where 103 | C: ConnectionTrait, 104 | { 105 | if rule_records.is_empty() { 106 | return Ok(()); 107 | } 108 | let has_direct = rule_records 109 | .iter() 110 | .any(|x| x.rule_action == RuleAction::Direct && x.rule_type == RuleType::Cidr); 111 | let has_not_direct = rule_records.iter().any(|x| { 112 | (x.rule_action == RuleAction::Proxy || x.rule_action == RuleAction::Reject) 113 | && x.rule_type == RuleType::Cidr 114 | }); 115 | let cidr_rules = rules::Model::fetch_by_rule_type(db, RuleType::Cidr).await?; 116 | if has_direct { 117 | rwlock_share.reset_direct_cidr(); 118 | } 119 | if has_not_direct { 120 | rwlock_share.clear_not_direct_cidr(); 121 | } 122 | if has_not_direct || has_direct { 123 | for rule_record in cidr_rules { 124 | let traffic_stream_rule = match rule_record.rule_action { 125 | RuleAction::Reject => TrafficStreamRule::Reject, 126 | RuleAction::Direct => TrafficStreamRule::Direct, 127 | RuleAction::Proxy => TrafficStreamRule::Proxy, 128 | }; 129 | rwlock_share.add_cidr(rule_record.rule.as_str(), traffic_stream_rule)?; 130 | } 131 | } 132 | let remain_delete_rule_records: Vec = rule_records 133 | .iter() 134 | .filter(|&x| x.rule_type != RuleType::Cidr) 135 | .cloned() 136 | .collect(); 137 | for rule_record in remain_delete_rule_records { 138 | match rule_record.rule_type { 139 | RuleType::DomainPreffix => { 140 | rwlock_share.delete_domain_preffix(rule_record.rule.as_str()) 141 | } 142 | RuleType::DomainSuffix => rwlock_share.delete_domain_suffix(rule_record.rule.as_str()), 143 | RuleType::FullDomain => rwlock_share.delete_full_domain(rule_record.rule.as_str()), 144 | RuleType::DomainRoot => rwlock_share.delete_root_domain(rule_record.rule.as_str()), 145 | _ => (), 146 | } 147 | } 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /src-tauri/src/tauri_init.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, trace, LevelFilter}; 2 | use migration::{Migrator, MigratorTrait}; 3 | use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr}; 4 | use simplelog::{ColorChoice, CombinedLogger, Config, TermLogger, TerminalMode}; 5 | use std::{path::PathBuf, sync::mpsc}; 6 | use tauri::Emitter; 7 | use tauri_plugin_autostart::AutoLaunchManager; 8 | use tokio::sync::RwLock; 9 | 10 | use crate::{logger::KittyLogger, state::DatabaseState, tray::Tray}; 11 | use anyhow::Result; 12 | use entity::base_config; 13 | use kitty_proxy::MatchProxy; 14 | use std::fs; 15 | use std::sync::Arc; 16 | use tauri::{Manager, State}; 17 | 18 | use crate::state::KittyProxyState; 19 | 20 | pub async fn init_db(app_dir: PathBuf) -> Result { 21 | let sqlite_path = app_dir.join("MyApp.sqlite"); 22 | trace!("{:?}", sqlite_path); 23 | println!("{:?}", sqlite_path); 24 | let sqlite_url = format!("sqlite://{}?mode=rwc", sqlite_path.to_string_lossy()); 25 | let connect_options = ConnectOptions::new(sqlite_url) 26 | .sqlx_logging(false) 27 | .to_owned(); 28 | let db: DatabaseConnection = Database::connect(connect_options).await?; 29 | Migrator::up(&db, None).await?; 30 | base_config::Model::update_sysproxy_flag(&db, false).await?; 31 | trace!("Migrator"); 32 | Ok(db) 33 | } 34 | 35 | fn setup_db<'a>(handle: &tauri::AppHandle) -> Result<(), Box> { 36 | let app_dir = handle 37 | .path() 38 | .app_local_data_dir() 39 | .expect("The app data directory should exist."); 40 | if !app_dir.exists() { 41 | fs::create_dir_all(&app_dir)?; 42 | } 43 | trace!("app_dir: {:?}", app_dir); 44 | let app_state: State = handle.state(); 45 | let db = tauri::async_runtime::block_on(async move { 46 | let db = init_db(app_dir).await; 47 | match db { 48 | Ok(db) => db, 49 | Err(err) => { 50 | panic!("Error: {}", err); 51 | } 52 | } 53 | }); 54 | *app_state.db.lock().unwrap() = Some(db); 55 | Ok(()) 56 | } 57 | 58 | fn setup_kitty_proxy<'a>(handle: &tauri::AppHandle) -> Result<(), Box> { 59 | let resource_dir = handle.path().resource_dir()?.join("static"); 60 | let app_state: State = handle.state(); 61 | // tauri::async_runtime::spawn(task) 62 | tauri::async_runtime::block_on(async move { 63 | trace!( 64 | "resource_dir: {:?}, exists: {}", 65 | resource_dir, 66 | resource_dir.exists() 67 | ); 68 | let geoip_file = resource_dir.join("kitty_geoip.dat"); 69 | let geosite_file = resource_dir.join("kitty_geosite.dat"); 70 | let match_proxy = MatchProxy::from_geo_dat(Some(&geoip_file), Some(&geosite_file)).unwrap(); 71 | *app_state.match_proxy.lock().await = Some(Arc::new(RwLock::new(match_proxy))); 72 | }); 73 | 74 | Ok(()) 75 | } 76 | 77 | fn setup_auto_start<'a>(handle: &tauri::AppHandle) -> Result<(), Box> { 78 | let db_state: State = handle.state(); 79 | let auto_start_state: State = handle.state(); 80 | let db = db_state.get_db(); 81 | tauri::async_runtime::block_on(async move { 82 | let record = base_config::Model::first(&db).await; 83 | if let Ok(record) = record { 84 | if let Some(auto_start) = record { 85 | if auto_start.auto_start { 86 | if let Ok(is_enable) = auto_start_state.is_enabled() { 87 | if !is_enable { 88 | let _ = auto_start_state.enable(); 89 | } 90 | } 91 | } else { 92 | if let Ok(is_enable) = auto_start_state.is_enabled() { 93 | if is_enable { 94 | let _ = auto_start_state.disable(); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | }); 101 | 102 | Ok(()) 103 | } 104 | 105 | fn setup_kitty_logger(app: &tauri::AppHandle) -> Result<(), Box> { 106 | let (sender, receiver) = mpsc::channel(); 107 | CombinedLogger::init(vec![ 108 | TermLogger::new( 109 | LevelFilter::Debug, 110 | Config::default(), 111 | TerminalMode::Mixed, 112 | ColorChoice::Auto, 113 | ), 114 | KittyLogger::new(LevelFilter::Info, Config::default(), sender), 115 | ]) 116 | .unwrap(); 117 | let app_clone = app.clone(); 118 | tauri::async_runtime::spawn(async move { 119 | loop { 120 | match receiver.recv() { 121 | Ok(message) => app_clone.emit("kitty_logger", message).unwrap(), 122 | Err(_) => { 123 | debug!("Channel closed"); 124 | break; 125 | } 126 | } 127 | } 128 | }); 129 | 130 | Ok(()) 131 | } 132 | 133 | // fn setup_global_shortcut<'a>(handle: &tauri::AppHandle) -> Result<(), Box> { 134 | // use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut}; 135 | 136 | // let command_w_shortcut = Shortcut::new(Some(Modifiers::META), Code::KeyW); 137 | // // let command_w_shortcut = Shortcut::new(Some(Modifiers::META), Code::KeyW); 138 | // let app_handle = handle.clone(); 139 | // handle.plugin( 140 | // tauri_plugin_global_shortcut::Builder::with_handler(move |_app, shortcut| { 141 | // if shortcut == &command_w_shortcut { 142 | // let window = app_handle.get_webview_window("main").unwrap(); 143 | // window.hide().unwrap(); 144 | // } 145 | // }) 146 | // .build(), 147 | // )?; 148 | 149 | // handle.global_shortcut().register(command_w_shortcut)?; 150 | // Ok(()) 151 | // } 152 | 153 | pub fn init_setup<'a>(app: &'a mut tauri::App) -> Result<(), Box> { 154 | let handle = app.handle(); 155 | let _ = setup_kitty_logger(handle)?; 156 | let _ = setup_db(handle)?; 157 | let _ = setup_db(handle)?; 158 | let _ = setup_auto_start(handle)?; 159 | let _ = setup_kitty_proxy(handle)?; 160 | let _ = Tray::init_tray(handle)?; 161 | // let _ = setup_global_shortcut(handle)?; 162 | Ok(()) 163 | } 164 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | use state::{DatabaseState, KittyLoggerState, ProcessManagerState}; 2 | use std::env; 3 | use tauri::RunEvent; 4 | use tauri::{generate_handler, ipc::Invoke}; 5 | #[cfg(feature = "hysteria")] 6 | use tauri_apis::hysteria as hysteria_api; 7 | #[cfg(feature = "xray")] 8 | use tauri_apis::xray as xray_api; 9 | use tauri_init::init_setup; 10 | 11 | use tauri_apis::common as common_api; 12 | use tauri_plugin_autostart::MacosLauncher; 13 | 14 | use crate::state::KittyProxyState; 15 | use crate::tauri_apis::{start_system_proxy, stop_system_proxy}; 16 | use crate::tauri_event_handler::on_exit_clear_commands; 17 | 18 | mod apis; 19 | mod logger; 20 | mod proxy; 21 | mod state; 22 | mod tauri_apis; 23 | mod tauri_event_handler; 24 | mod tauri_init; 25 | mod tray; 26 | mod types; 27 | 28 | // async fn on_window_exit(event: tauri::GlobalWindowEvent) { 29 | // println!("on_window_exit call!!!"); 30 | // println!("{:?}", event.event()); 31 | // match event.event() { 32 | // WindowEvent::Destroyed => { 33 | // println!("exit!!!"); 34 | // let state: State = event.window().state(); 35 | // #[cfg(feature = "hysteria")] 36 | // { 37 | // let mut process_manager = state.hy_process_manager.lock().await; 38 | // let process_manager = process_manager.as_mut(); 39 | // if let Some(process_manager) = process_manager { 40 | // println!("terminate_backends call"); 41 | // if process_manager.terminate_backends().is_err() { 42 | // let app = event.window(); 43 | // if let Ok(PermissionState::Granted) = app.notification().permission_state() 44 | // { 45 | // app.notification() 46 | // .builder() 47 | // .body(format!("{} terminate failed.", process_manager.name())) 48 | // .show() 49 | // .unwrap(); 50 | // } 51 | // } 52 | // } 53 | // } 54 | 55 | // #[cfg(feature = "xray")] 56 | // { 57 | // let mut process_manager = state.xray_process_manager.lock().await; 58 | // let process_manager = process_manager.as_mut(); 59 | // if let Some(process_manager) = process_manager { 60 | // if process_manager.terminate_backends().is_err() { 61 | // let app = event.window(); 62 | // if let Ok(PermissionState::Granted) = app.notification().permission_state() 63 | // { 64 | // app.notification() 65 | // .builder() 66 | // .body(format!("{} terminate failed.", process_manager.name())) 67 | // .show() 68 | // .unwrap(); 69 | // } 70 | // } 71 | // } 72 | // } 73 | // } 74 | // _ => {} 75 | // } 76 | // } 77 | 78 | // fn on_window_exit_func(event: tauri::GlobalWindowEvent) { 79 | // tauri::async_runtime::block_on(on_window_exit(event)) 80 | // } 81 | 82 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 83 | pub fn run() { 84 | let builder = tauri::Builder::default() 85 | .plugin(tauri_plugin_autostart::init( 86 | MacosLauncher::LaunchAgent, 87 | Some(vec![]), 88 | )) 89 | .manage(DatabaseState { 90 | db: Default::default(), 91 | }); 92 | let builder = builder.manage(ProcessManagerState::default()); 93 | let builder = builder.manage(KittyLoggerState::default()); 94 | let builder = builder 95 | .manage(KittyProxyState::default()) 96 | // .plugin(tauri_plugin_window::init()) 97 | .plugin(tauri_plugin_notification::init()) 98 | .plugin(tauri_plugin_clipboard_manager::init()) 99 | .setup(init_setup); 100 | // .on_window_event(on_window_exit_func); 101 | #[cfg(feature = "xray")] 102 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 103 | let handler: fn(Invoke) -> bool = generate_handler![ 104 | xray_api::add_xray_item, 105 | xray_api::get_all_xrays, 106 | xray_api::import_xray_subscribe, 107 | xray_api::update_xray_item, 108 | xray_api::delete_xray_item, 109 | xray_api::speed_xray_delay, 110 | xray_api::get_xray_by_id, 111 | xray_api::proxies_delay_test, 112 | common_api::copy_proxy_env_cmd, 113 | common_api::query_base_config, 114 | common_api::update_base_config, 115 | common_api::query_rules, 116 | common_api::delete_rules, 117 | common_api::add_rules, 118 | common_api::update_rules_item, 119 | common_api::test_current_proxy, 120 | start_system_proxy, 121 | stop_system_proxy, 122 | ]; 123 | 124 | #[cfg(feature = "xray")] 125 | let handler: fn(Invoke) -> bool = generate_handler![ 126 | xray_api::add_xray_item, 127 | xray_api::get_all_xrays, 128 | xray_api::import_xray_subscribe, 129 | xray_api::update_xray_item, 130 | xray_api::delete_xray_item, 131 | xray_api::speed_xray_delay, 132 | xray_api::get_xray_by_id, 133 | xray_api::proxies_delay_test, 134 | common_api::query_base_config, 135 | common_api::update_base_config, 136 | common_api::query_rules, 137 | common_api::delete_rules, 138 | common_api::add_rules, 139 | common_api::update_rules_item, 140 | common_api::test_current_proxy, 141 | start_system_proxy, 142 | stop_system_proxy, 143 | ]; 144 | 145 | #[cfg(feature = "hysteria")] 146 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 147 | let handler: fn(Invoke) -> bool = generate_handler![ 148 | hysteria_api::add_hysteria_item, 149 | hysteria_api::get_all_hysterias, 150 | hysteria_api::update_hysteria_item, 151 | hysteria_api::delete_hysteria_item, 152 | hysteria_api::speed_hysteria_delay, 153 | hysteria_api::get_hysteria_by_id, 154 | common_api::copy_proxy_env_cmd, 155 | common_api::query_base_config, 156 | common_api::update_base_config, 157 | common_api::query_rules, 158 | common_api::delete_rules, 159 | common_api::add_rules, 160 | common_api::update_rules_item, 161 | common_api::test_current_proxy, 162 | start_system_proxy, 163 | stop_system_proxy, 164 | ]; 165 | 166 | #[cfg(all(feature = "xray", feature = "hysteria"))] 167 | #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] 168 | let handler: fn(Invoke) -> bool = generate_handler![ 169 | hysteria_api::add_hysteria_item, 170 | hysteria_api::get_all_hysterias, 171 | hysteria_api::update_hysteria_item, 172 | hysteria_api::delete_hysteria_item, 173 | hysteria_api::speed_hysteria_delay, 174 | hysteria_api::get_hysteria_by_id, 175 | xray_api::refresh_xray_subscription, 176 | xray_api::batch_get_subscriptions, 177 | xray_api::add_xray_item, 178 | xray_api::get_all_xrays, 179 | xray_api::import_xray_subscribe, 180 | xray_api::update_xray_item, 181 | xray_api::delete_xray_item, 182 | xray_api::speed_xray_delay, 183 | xray_api::get_xray_by_id, 184 | xray_api::proxies_delay_test, 185 | common_api::query_base_config, 186 | common_api::update_base_config, 187 | common_api::copy_proxy_env_cmd, 188 | common_api::query_base_config, 189 | common_api::update_base_config, 190 | common_api::query_rules, 191 | common_api::delete_rules, 192 | common_api::add_rules, 193 | common_api::update_rules_item, 194 | common_api::test_current_proxy, 195 | start_system_proxy, 196 | stop_system_proxy, 197 | ]; 198 | 199 | let builder = builder.invoke_handler(handler); 200 | builder 201 | .build(tauri::generate_context!()) 202 | .expect("error while running tauri application") 203 | .run(|app, event| { 204 | if let RunEvent::Exit = event { 205 | // clear_command(app); 206 | on_exit_clear_commands(app); 207 | } 208 | }); 209 | } 210 | -------------------------------------------------------------------------------- /src/views/setting/SettingView.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 271 | --------------------------------------------------------------------------------