diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-02-28 23:13:39 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-02-28 23:13:39 +0800 |
commit | a0f2f7e29035553562941f4046db88d707daf199 (patch) | |
tree | b6b56019f89a2f3b1a6cffccba2849687ee61241 /store | |
parent | 346b5256b99e28cd5c67c03dca4be84e1c75f749 (diff) | |
download | crupest-a0f2f7e29035553562941f4046db88d707daf199.tar.gz crupest-a0f2f7e29035553562941f4046db88d707daf199.tar.bz2 crupest-a0f2f7e29035553562941f4046db88d707daf199.zip |
chore(store): move everything to store.
Diffstat (limited to 'store')
74 files changed, 3429 insertions, 0 deletions
diff --git a/store/assets/crupest-transparent.png b/store/assets/crupest-transparent.png Binary files differnew file mode 100755 index 0000000..d890d8d --- /dev/null +++ b/store/assets/crupest-transparent.png diff --git a/store/home/bash_profile b/store/home/bash_profile new file mode 100644 index 0000000..b65f405 --- /dev/null +++ b/store/home/bash_profile @@ -0,0 +1 @@ +export PATH="$HOME/.local/bin:$PATH" diff --git a/store/home/bashrc b/store/home/bashrc new file mode 100644 index 0000000..7d31f23 --- /dev/null +++ b/store/home/bashrc @@ -0,0 +1,8 @@ +set-proxy() { + export http_proxy="http://127.0.0.1:7897" + export https_proxy="http://127.0.0.1:7897" +} + +unset-proxy() { + unset http_proxy https_proxy +} diff --git a/store/home/bin/neovide-listen b/store/home/bin/neovide-listen new file mode 100755 index 0000000..2591842 --- /dev/null +++ b/store/home/bin/neovide-listen @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +if [[ -z "$NVIM_SOCKET" ]]; then + NVIM_SOCKET="/tmp/nvimsocket" +fi + +args=() +MY_NEOVIM_PATH="$HOME/codes/neovim/build/bin/nvim" +if [[ -e "$MY_NEOVIM_PATH" ]]; then + echo "Found my neovim at $MY_NEOVIM_PATH" + export VIMRUNTIME="$HOME/codes/neovim/runtime" + args=("${args[@]}" "--neovim-bin" "$MY_NEOVIM_PATH") +fi + +listen_added=0 +for arg in "$@"; do + args=("${args[@]}" "$arg") + if [ "$arg" = '--' ]; then + args=("${args[@]}" "--listen" "$NVIM_SOCKET") + listen_added=1 + fi +done + +if [[ $listen_added = 0 ]]; then + args=("${args[@]}" "--" "--listen" "$NVIM_SOCKET") +fi + +NEOVIDE_BIN=neovide +MY_NEOVIDE_PATH="$HOME/codes/neovide/target/release/neovide" +if [ -e "$MY_NEOVIDE_PATH" ]; then + echo "Found my neovide at $MY_NEOVIDE_PATH" + NEOVIDE_BIN="$MY_NEOVIDE_PATH" +fi + +if which nvr > /dev/null; then + echo "Detected nvr, set git editor env" + export GIT_EDITOR='nvr -cc split --remote-wait' +fi + +args=("$NEOVIDE_BIN" "${args[@]}") +echo "Command is ${args[@]}" +exec "${args[@]}" + diff --git a/store/home/config/halloy/config.toml b/store/home/config/halloy/config.toml new file mode 100644 index 0000000..5d39f52 --- /dev/null +++ b/store/home/config/halloy/config.toml @@ -0,0 +1,20 @@ +tooltips = true + +[notifications] +direct_message = { sound = "peck", show_toast = true } +connected = { sound = "ring", show_toast = true } +disconnected = { sound = "ring", show_toast = true } + +[notifications.highlight] +sound = "dong" +exclude = [ "NickServ" ] + +[servers.liberachat] +nickname = "crupest" +server = "irc.libera.chat" +channels = [ "#hurd" ] +use_tls = true + +[servers.liberachat.sasl.plain] +username = "crupest" +password = ... diff --git a/store/home/config/nvim/.gitignore b/store/home/config/nvim/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/store/home/config/nvim/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/store/home/config/nvim/.luarc.json b/store/home/config/nvim/.luarc.json new file mode 100644 index 0000000..f704d01 --- /dev/null +++ b/store/home/config/nvim/.luarc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "runtime.version": "LuaJIT" +}
\ No newline at end of file diff --git a/store/home/config/nvim/cspell.yaml b/store/home/config/nvim/cspell.yaml new file mode 100644 index 0000000..2a716e2 --- /dev/null +++ b/store/home/config/nvim/cspell.yaml @@ -0,0 +1,13 @@ +dictionaryDefinitions: + - name: nvim-words + path: './nvim-words.txt' + addWords: true + +dictionaries: + - nvim-words + +words: + - crupest + +ignorePaths: + - lazy-lock.json diff --git a/store/home/config/nvim/init.lua b/store/home/config/nvim/init.lua new file mode 100644 index 0000000..9de0b2c --- /dev/null +++ b/store/home/config/nvim/init.lua @@ -0,0 +1,63 @@ +if vim.g.neovide then + -- spellchecker: disable-next-line + vim.opt.guifont = "FiraCode Nerd Font"; + vim.g.neovide_window_blurred = true; + vim.g.neovide_transparency = 0.9; + vim.g.neovide_input_ime = false; + vim.g.neovide_cursor_animate_in_insert_mode = false + vim.g.neovide_cursor_vfx_mode = "pixiedust"; + vim.g.neovide_input_macos_option_key_is_meta = 'only_left' +end + +local is_win = vim.fn.has("win32") ~= 0 + +-- spellchecker: disable +if is_win then + vim.cmd([[ + let &shell = executable('pwsh') ? 'pwsh' : 'powershell' + let &shellcmdflag = '-NoLogo -ExecutionPolicy RemoteSigned -Command [Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();$PSDefaultParameterValues[''Out-File:Encoding'']=''utf8'';Remove-Alias -Force -ErrorAction SilentlyContinue tee;' + let &shellredir = '2>&1 | %%{ "$_" } | Out-File %s; exit $LastExitCode' + let &shellpipe = '2>&1 | %%{ "$_" } | tee %s; exit $LastExitCode' + set shellquote= shellxquote= + ]]) + vim.opt.completeslash = 'slash' +end +-- spellchecker: enable + +-- spellchecker: disable +vim.opt.termguicolors = true; +vim.opt.fileformats = "unix,dos"; +vim.opt.softtabstop = 4; +vim.opt.shiftwidth = 4; +vim.opt.expandtab = true; +vim.opt.wrap = false; +vim.opt.number = true; +-- spellchecker: enable + +vim.g.load_doxygen_syntax = true; +vim.g.doxygen_javadoc_autobrief = false; + +-- Init lazy.nvim +local lazy_path = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not vim.uv.fs_stat(lazy_path) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", -- latest stable release + lazy_path, + }) +end +vim.opt.rtp:prepend(lazy_path) + +-- Use lazy.nvim +require("lazy").setup("plugins") + +vim.cmd("colorscheme catppuccin-macchiato") + +require("crupest.nvim.lsp").setup() +require("crupest.nvim.plugins").setup() +require("crupest.nvim.keymap").setup() + +vim.cmd("autocmd FileType gitcommit,gitrebase,gitconfig set bufhidden=delete") diff --git a/store/home/config/nvim/lazy-lock.json b/store/home/config/nvim/lazy-lock.json new file mode 100644 index 0000000..3d08239 --- /dev/null +++ b/store/home/config/nvim/lazy-lock.json @@ -0,0 +1,20 @@ +{ + "LuaSnip": { "branch": "master", "commit": "0f7bbce41ea152a94d12aea286f2ce98e63c0f58" }, + "catppuccin": { "branch": "main", "commit": "faf15ab0201b564b6368ffa47b56feefc92ce3f4" }, + "cmp-buffer": { "branch": "main", "commit": "3022dbc9166796b644a841a02de8dd1cc1d311fa" }, + "cmp-cmdline": { "branch": "main", "commit": "d250c63aa13ead745e3a40f61fdd3470efde3923" }, + "cmp-nvim-lsp": { "branch": "main", "commit": "39e2eda76828d88b773cc27a3f61d2ad782c922d" }, + "cmp-path": { "branch": "main", "commit": "91ff86cd9c29299a64f968ebb45846c485725f23" }, + "cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" }, + "gitsigns.nvim": { "branch": "main", "commit": "5f808b5e4fef30bd8aca1b803b4e555da07fc412" }, + "lazy.nvim": { "branch": "main", "commit": "56ead98e05bb37a4ec28930a54d836d033cf00f2" }, + "lualine.nvim": { "branch": "master", "commit": "2a5bae925481f999263d6f5ed8361baef8df4f83" }, + "nvim-autopairs": { "branch": "master", "commit": "b464658e9b880f463b9f7e6ccddd93fb0013f559" }, + "nvim-cmp": { "branch": "main", "commit": "ed31156aa2cc14e3bc066c59357cc91536a2bc01" }, + "nvim-lint": { "branch": "master", "commit": "6b46370d02cd001509a765591a3ffc481b538794" }, + "nvim-lspconfig": { "branch": "master", "commit": "4ae9796c4e95ca84ec77946a9f9089b8f1a3eec9" }, + "nvim-tree.lua": { "branch": "master", "commit": "ca7c4c33cac2ad66ec69d45e465379716ef0cc97" }, + "nvim-web-devicons": { "branch": "master", "commit": "edbe0a65cfacbbfff6a4a1e98ddd60c28c560509" }, + "plenary.nvim": { "branch": "master", "commit": "2d9b06177a975543726ce5c73fca176cedbffe9d" }, + "telescope.nvim": { "branch": "master", "commit": "85922dde3767e01d42a08e750a773effbffaea3e" } +} diff --git a/store/home/config/nvim/lua/crupest/nvim/keymap.lua b/store/home/config/nvim/lua/crupest/nvim/keymap.lua new file mode 100644 index 0000000..624c04c --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/keymap.lua @@ -0,0 +1,9 @@ +local function setup() + vim.keymap.set("n", "<c-tab>", "<cmd>bnext<cr>") + vim.keymap.set("n", "<c-s-tab>", "<cmd>bNext<cr>") + vim.keymap.set("n", "<esc>", require("crupest.utils.nvim").close_float) +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/lsp/c.lua b/store/home/config/nvim/lua/crupest/nvim/lsp/c.lua new file mode 100644 index 0000000..bb1f6f7 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/lsp/c.lua @@ -0,0 +1,25 @@ +local lspconfig = require("lspconfig") + +local brew_clangd_path = "/usr/local/opt/llvm/bin/clangd" + +local function setup() + local clangd = "clangd" + + if vim.uv.fs_stat(brew_clangd_path) ~= nil then + clangd = brew_clangd_path + end + + -- setup lsp clangd + lspconfig.clangd.setup { + cmd = { clangd }, + on_attach = function(_, bufnr) + vim.keymap.set('n', '<space>s', "<cmd>ClangdSwitchSourceHeader<cr>", { + buffer = bufnr + }) + end + } +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/lsp/init.lua b/store/home/config/nvim/lua/crupest/nvim/lsp/init.lua new file mode 100644 index 0000000..0fd29a3 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/lsp/init.lua @@ -0,0 +1,50 @@ +local lspconfig = require("lspconfig") +local cmp_nvim_lsp = require("cmp_nvim_lsp") +local cmp_default_caps = cmp_nvim_lsp.default_capabilities() + +local lspconfig_default_caps = lspconfig.util.default_config.capabilities + +lspconfig.util.default_config = vim.tbl_extend( + "force", + lspconfig.util.default_config, + { + capabilities = vim.tbl_extend("force", lspconfig_default_caps, cmp_default_caps), + autostart = false, + }) + +local function setup() + lspconfig.cmake.setup {} + lspconfig.bashls.setup {} + require("crupest.nvim.lsp.c").setup() + require("crupest.nvim.lsp.lua").setup() + + -- Use LspAttach auto command to only map the following keys + -- after the language server attaches to the current buffer + vim.api.nvim_create_autocmd('LspAttach', { + group = vim.api.nvim_create_augroup('UserLspConfig', {}), + callback = function(ev) + -- Buffer local mappings. + -- See `:help vim.lsp.*` for documentation on any of the below functions + local opts = { buffer = ev.buf } + vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts) + vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts) + vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts) + vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts) + vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, opts) + vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, opts) + vim.keymap.set('n', '<space>wl', function() + print(vim.inspect(vim.lsp.buf.list_workspace_folders())) + end, opts) + vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, opts) + vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, opts) + vim.keymap.set({ 'n', 'v' }, '<space>ca', vim.lsp.buf.code_action, opts) + vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts) + vim.keymap.set('n', '<space>f', vim.lsp.buf.format, opts) + end, + }) +end + + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/lsp/lua.lua b/store/home/config/nvim/lua/crupest/nvim/lsp/lua.lua new file mode 100644 index 0000000..93aa503 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/lsp/lua.lua @@ -0,0 +1,29 @@ +local lspconfig = require("lspconfig") + +local function setup() + lspconfig.lua_ls.setup { + settings = { + Lua = { + runtime = { + version = "LuaJIT" + }, + diagnostics = { + globals = { "vim" }, + }, + workspace = { + library = { + [vim.fn.expand "$VIMRUNTIME/lua"] = true, + [vim.fn.expand "$VIMRUNTIME/lua/vim/lsp"] = true, + [vim.fn.stdpath "data" .. "/lazy/lazy.nvim/lua/lazy"] = true, + }, + maxPreload = 100000, + preloadFileSize = 10000, + }, + }, + }, + } +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/cmp.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/cmp.lua new file mode 100644 index 0000000..9b1d876 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/cmp.lua @@ -0,0 +1,31 @@ +local function setup() + local cmp = require("cmp") + local luasnip = require("luasnip") + + cmp.setup { + snippet = { + expand = function(args) + luasnip.lsp_expand(args.body) + end, + }, + window = { + }, + mapping = cmp.mapping.preset.insert({ + ['<C-b>'] = cmp.mapping.scroll_docs(-4), + ['<C-f>'] = cmp.mapping.scroll_docs(4), + ['<C-Space>'] = cmp.mapping.complete(), + ['<C-e>'] = cmp.mapping.abort(), + ['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. + }), + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + { name = 'luasnip' }, + }, { + { name = 'buffer' }, + }) + } +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/gitsigns.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/gitsigns.lua new file mode 100644 index 0000000..220c91a --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/gitsigns.lua @@ -0,0 +1,51 @@ +local function setup() + local gitsigns = require('gitsigns') + gitsigns.setup { + on_attach = function(bufnr) + local function map(mode, l, r, opts) + opts = opts or {} + opts.buffer = bufnr + vim.keymap.set(mode, l, r, opts) + end + + -- Navigation + map('n', ']c', function() + if vim.wo.diff then + vim.cmd.normal({ ']c', bang = true }) + else + gitsigns.nav_hunk('next') + end + end) + + map('n', '[c', function() + if vim.wo.diff then + vim.cmd.normal({ '[c', bang = true }) + else + gitsigns.nav_hunk('prev') + end + end) + + -- Actions + map('n', '<leader>hs', gitsigns.stage_hunk) + map('n', '<leader>hr', gitsigns.reset_hunk) + map('v', '<leader>hs', function() gitsigns.stage_hunk { vim.fn.line('.'), vim.fn.line('v') } end) + map('v', '<leader>hr', function() gitsigns.reset_hunk { vim.fn.line('.'), vim.fn.line('v') } end) + map('n', '<leader>hS', gitsigns.stage_buffer) + map('n', '<leader>hu', gitsigns.undo_stage_hunk) + map('n', '<leader>hR', gitsigns.reset_buffer) + map('n', '<leader>hp', gitsigns.preview_hunk) + map('n', '<leader>hb', function() gitsigns.blame_line { full = true } end) + map('n', '<leader>tb', gitsigns.toggle_current_line_blame) + map('n', '<leader>hd', gitsigns.diffthis) + map('n', '<leader>hD', function() gitsigns.diffthis('~') end) + map('n', '<leader>td', gitsigns.toggle_deleted) + + -- Text object + map({ 'o', 'x' }, 'ih', ':<C-U>Gitsigns select_hunk<CR>') + end + } +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/init.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/init.lua new file mode 100644 index 0000000..24e0c2e --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/init.lua @@ -0,0 +1,12 @@ +local function setup() + require("crupest.nvim.plugins.lint").setup() + require("crupest.nvim.plugins.snip").setup() + require("crupest.nvim.plugins.cmp").setup() + require("crupest.nvim.plugins.telescope").setup() + require("crupest.nvim.plugins.gitsigns").setup() + require("crupest.nvim.plugins.others").setup() +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/lint.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/lint.lua new file mode 100644 index 0000000..5e348d6 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/lint.lua @@ -0,0 +1,82 @@ +local lint = require("lint") + +local find = require('crupest.utils.find') +local is_win = vim.fn.has("win32") ~= 0 + +local cspell_config_patterns = { + ".cspell.json", + "cspell.json", + ".cSpell.json", + "cSpell.json", + "cspell.config.js", + "cspell.config.cjs", + "cspell.config.json", + "cspell.config.yaml", + "cspell.config.yml", + "cspell.yaml", + "cspell.yml", +} + +--- @type FindExeForBufOpts[] +local my_linters = { + { + name = "cspell", + places = { "node_modules", "global" }, + config_files = cspell_config_patterns, + }, +} + +local function run(opt) + if not opt then + opt = {} + end + + if not opt.buf then + opt.buf = 0 + end + + local linters = {} + + for _, l in ipairs(my_linters) do + local linter = find.find_exe_for_buf(opt.buf, l) + if linter then table.insert(linters, linter) end + end + + + local linter_names = {} + + for _, linter in ipairs(linters) do + table.insert(linter_names, linter.name) + require('lint.linters.' .. linter.name).cmd = linter.exe_path + end + + lint.try_lint(linter_names) +end + +local function setup() + if is_win then + for _, l in ipairs(my_linters) do + local name = l.name + local linter = require('lint.linters.' .. name) + if linter.cmd == 'cmd.exe' then + linter.cmd = linter.args[2] + end + table.remove(linter.args, 1) + table.remove(linter.args, 1) + end + end + + vim.api.nvim_create_autocmd({ "BufWritePost" }, { + callback = function(opt) + run({ + buf = opt.buffer + }) + end, + }) + + vim.keymap.set('n', '<leader>lr', run) +end + +return { + setup = setup, +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/others.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/others.lua new file mode 100644 index 0000000..2ef0d75 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/others.lua @@ -0,0 +1,9 @@ +local function setup() + require('lualine').setup {} + require("nvim-tree").setup {} + require("nvim-autopairs").setup {} +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/snip.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/snip.lua new file mode 100644 index 0000000..78ed2eb --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/snip.lua @@ -0,0 +1,75 @@ +--- spellchecker: disable + +local luasnip = require("luasnip") + +local ls = luasnip +-- some shorthands... +local s = ls.snippet +local sn = ls.snippet_node +local t = ls.text_node +local i = ls.insert_node +local f = ls.function_node +local c = ls.choice_node +local d = ls.dynamic_node +local r = ls.restore_node +local l = require("luasnip.extras").lambda +local rep = require("luasnip.extras").rep +local p = require("luasnip.extras").partial +local m = require("luasnip.extras").match +local n = require("luasnip.extras").nonempty +local dl = require("luasnip.extras").dynamic_lambda +local fmt = require("luasnip.extras.fmt").fmt +local fmta = require("luasnip.extras.fmt").fmta +local types = require("luasnip.util.types") +local conds = require("luasnip.extras.conditions") +local conds_expand = require("luasnip.extras.conditions.expand") + +local function copy(args) + return args[1] +end + +local function setup() + vim.keymap.set({ "i", "s" }, "<C-L>", function() luasnip.jump(1) end, { silent = true }) + vim.keymap.set({ "i", "s" }, "<C-J>", function() luasnip.jump(-1) end, { silent = true }) + + vim.keymap.set({ "i", "s" }, "<C-E>", function() + if luasnip.choice_active() then + luasnip.change_choice(1) + end + end, { silent = true }) + + luasnip.add_snippets("cpp", { + s("cs", { + i(1, "classname"), + t("::"), + f(copy, 1), + t("("), + i(0), + t(") { }") + }), + + s("ds", { + i(1, "classname"), + t("::~"), + f(copy, 1), + t("() { }") + }), + + s("csds", { + i(1, "classname"), + t("::"), + f(copy, 1), + t("("), + i(0), + t({ ") { }", "", "" }), + f(copy, 1), + t("::~"), + f(copy, 1), + t("() { }") + }), + }) +end + +return { + setup = setup, +} diff --git a/store/home/config/nvim/lua/crupest/nvim/plugins/telescope.lua b/store/home/config/nvim/lua/crupest/nvim/plugins/telescope.lua new file mode 100644 index 0000000..d68b7f2 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/nvim/plugins/telescope.lua @@ -0,0 +1,11 @@ +local function setup() + local builtin = require('telescope.builtin') + vim.keymap.set('n', '<leader>ff', builtin.find_files, {}) + vim.keymap.set('n', '<leader>fg', builtin.live_grep, {}) + vim.keymap.set('n', '<leader>fb', builtin.buffers, {}) + vim.keymap.set('n', '<leader>fh', builtin.help_tags, {}) +end + +return { + setup = setup +} diff --git a/store/home/config/nvim/lua/crupest/utils/find.lua b/store/home/config/nvim/lua/crupest/utils/find.lua new file mode 100644 index 0000000..dd1f663 --- /dev/null +++ b/store/home/config/nvim/lua/crupest/utils/find.lua @@ -0,0 +1,101 @@ +local is_win = vim.fn.has("win32") ~= 0 + +local M = {} + +local windows_exe_ext = { "exe", "bat", "cmd", "ps1" } + +--- Find real path (with ext) for an executable. +--- @param dir string +--- @param name string | string[] +--- @return string | nil +function M.find_exe_file(dir, name) + if type(name) == "string" then + name = { name } + end + for _, n in ipairs(name) do + if vim.uv.fs_stat(vim.fs.joinpath(dir, n)) ~= nil then + return n + end + if is_win then + for _, ext in ipairs(windows_exe_ext) do + if vim.uv.fs_stat(vim.fs.joinpath(dir, n .. "." .. ext)) ~= nil then + return n .. "." .. ext + end + end + end + end + return nil +end + +--- Walk up until found an executable in node_modules. +--- @param path string +--- @param name string +--- @return string | nil exe_path Path to the executable. +function M.find_node_modules_exe(path, name) + local bin_dirs = vim.fs.find("node_modules/.bin", { path = path, upward = true, type = "directory" }) + if #bin_dirs == 0 then return nil end + local exe = M.find_exe_file(bin_dirs[1], name) + return exe and vim.fs.joinpath(bin_dirs[1], exe) +end + +--- Find executable in PATH. +--- @param name string +--- @return string | nil +function M.find_global_exe(name) + local exe = vim.fn.exepath(name) + if exe == "" then return nil end + return exe +end + +--- @alias ExePlace "node_modules" | "global" +--- @param path string +--- @param name string +--- @param places ExePlace[] +--- @return string | nil, ExePlace? +function M.find_exe(path, name, places) + for _, place in ipairs(places) do + if place == "node_modules" then + local r = M.find_node_modules_exe(path, name) + if r then return r, "node_modules" end + end + if place == "global" then + local r = M.find_global_exe(name) + if r then return r, "global" end + end + end + return nil, nil +end + +--- @alias FindExeForBufOpts { name: string, exe: string?, places: ExePlace[], config_files: string[]?, filetypes: string[]? } +--- @alias FindExeForBufResult { name: string, file: string, exe: string, exe_path: string, place: ExePlace, config_file: string?, filetype: string? } +--- @param buf number +--- @param opts FindExeForBufOpts +--- @return FindExeForBufResult | nil +function M.find_exe_for_buf(buf, opts) + local r = {} --- @type FindExeForBufResult + r.name = opts.name + r.file = vim.api.nvim_buf_get_name(buf) + r.exe = opts.exe or opts.name + + if opts.filetypes then + r.filetype = vim.api.nvim_get_option_value("filetype", { scope = "buffer", buf = buf }) + if not vim.tbl_contains(opts.filetypes, r.filetype) then return nil end + end + + if opts.config_files then + local config_file_list = vim.fs.find(opts.config_files, { path = r.file, upward = true }) + if #config_file_list == 0 then return nil end + r.config_file = config_file_list[1] + end + + local exe_path, place = M.find_exe(r.file, r.exe, opts.places) + if exe_path == nil then return nil end + r.exe_path = exe_path + + --- @cast place ExePlace + r.place = place + + return r +end + +return M diff --git a/store/home/config/nvim/lua/crupest/utils/nvim.lua b/store/home/config/nvim/lua/crupest/utils/nvim.lua new file mode 100644 index 0000000..4477ecc --- /dev/null +++ b/store/home/config/nvim/lua/crupest/utils/nvim.lua @@ -0,0 +1,12 @@ +local M = {} + +function M.close_float() + local wins = vim.api.nvim_list_wins() + for _, v in ipairs(wins) do + if vim.api.nvim_win_get_config(v).relative ~= '' then + vim.api.nvim_win_close(v, false) + end + end +end + +return M diff --git a/store/home/config/nvim/lua/plugins.lua b/store/home/config/nvim/lua/plugins.lua new file mode 100644 index 0000000..b689559 --- /dev/null +++ b/store/home/config/nvim/lua/plugins.lua @@ -0,0 +1,30 @@ +-- spellchecker: disable +return { + { "catppuccin/nvim", name = "catppuccin", priority = 1000 }, + "neovim/nvim-lspconfig", + "L3MON4D3/LuaSnip", + "hrsh7th/nvim-cmp", + "hrsh7th/cmp-nvim-lsp", + "hrsh7th/cmp-buffer", + "hrsh7th/cmp-path", + "hrsh7th/cmp-cmdline", + "saadparwaiz1/cmp_luasnip", + { + "nvim-tree/nvim-tree.lua", + lazy = false, + dependencies = { + "nvim-tree/nvim-web-devicons", + }, + }, + { + "nvim-lualine/lualine.nvim", + dependencies = { 'nvim-tree/nvim-web-devicons' } + }, + { + "nvim-telescope/telescope.nvim", + dependencies = { 'nvim-lua/plenary.nvim' } + }, + "windwp/nvim-autopairs", + "mfussenegger/nvim-lint", + "lewis6991/gitsigns.nvim", +} diff --git a/store/home/config/nvim/nvim-words.txt b/store/home/config/nvim/nvim-words.txt new file mode 100644 index 0000000..564904f --- /dev/null +++ b/store/home/config/nvim/nvim-words.txt @@ -0,0 +1,27 @@ +nvim +vimruntime +bnext +bufhidden +exepath +stdpath +augroup +autocmd +autobrief +autopairs +keymap +joinpath +bufnr + +neovide +pixiedust + +lspconfig +clangd +bashls + +catppuccin +macchiato +diffthis +gitsigns +lualine +luasnip diff --git a/store/home/gitconfig b/store/home/gitconfig new file mode 100644 index 0000000..b343ab3 --- /dev/null +++ b/store/home/gitconfig @@ -0,0 +1,10 @@ +[user] + name = Yuqian Yang + email = crupest@crupest.life +[core] + autocrlf = false + editor = vim +[credential] + helper = /usr/lib/git-core/git-credential-libsecret +[alias] + lg = log --decorate --oneline --graph diff --git a/store/misc/bruno/ComfyUI/Get Object Info.bru b/store/misc/bruno/ComfyUI/Get Object Info.bru new file mode 100644 index 0000000..d1a833c --- /dev/null +++ b/store/misc/bruno/ComfyUI/Get Object Info.bru @@ -0,0 +1,11 @@ +meta { + name: Get Object Info + type: http + seq: 4 +} + +get { + url: {{BASE_URL}}/object_info + body: none + auth: none +} diff --git a/store/misc/bruno/ComfyUI/Get Prompt History.bru b/store/misc/bruno/ComfyUI/Get Prompt History.bru new file mode 100644 index 0000000..2e26888 --- /dev/null +++ b/store/misc/bruno/ComfyUI/Get Prompt History.bru @@ -0,0 +1,15 @@ +meta { + name: Get Prompt History + type: http + seq: 6 +} + +get { + url: {{BASE_URL}}/history/{{prompt_id}} + body: none + auth: none +} + +vars:pre-request { + prompt_id: 7e345a55-21c4-4bdc-9b34-add561775144 +} diff --git a/store/misc/bruno/ComfyUI/Post Prompt.bru b/store/misc/bruno/ComfyUI/Post Prompt.bru new file mode 100644 index 0000000..09bf89a --- /dev/null +++ b/store/misc/bruno/ComfyUI/Post Prompt.bru @@ -0,0 +1,124 @@ +meta { + name: Post Prompt + type: http + seq: 5 +} + +post { + url: {{BASE_URL}}/prompt + body: json + auth: none +} + +body:json { + { + "client_id": "crupest", + "prompt": { + "3": { + "inputs": { + "seed": 156680208700286, + "steps": 20, + "cfg": 8, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1, + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "4": { + "inputs": { + "ckpt_name": "SUPIR/SUPIR-v0Q.ckpt" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "5": { + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage", + "_meta": { + "title": "Empty Latent Image" + } + }, + "6": { + "inputs": { + "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "text, watermark", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + } + } + } +} diff --git a/store/misc/bruno/ComfyUI/Upload Image.bru b/store/misc/bruno/ComfyUI/Upload Image.bru new file mode 100644 index 0000000..92b4aeb --- /dev/null +++ b/store/misc/bruno/ComfyUI/Upload Image.bru @@ -0,0 +1,18 @@ +meta { + name: Upload Image + type: http + seq: 2 +} + +post { + url: {{BASE_URL}}/upload/image + body: multipartForm + auth: none +} + +body:multipart-form { + overwrite: true + type: input + subfolder: crupest-test + image: @file(/Users/crupest/codes/crupest/assets/crupest-transparent.png) +} diff --git a/store/misc/bruno/ComfyUI/View Image.bru b/store/misc/bruno/ComfyUI/View Image.bru new file mode 100644 index 0000000..395eccd --- /dev/null +++ b/store/misc/bruno/ComfyUI/View Image.bru @@ -0,0 +1,19 @@ +meta { + name: View Image + type: http + seq: 1 +} + +get { + url: {{BASE_URL}}/view?filename=crupest-transparent.png&type=input&subfolder=crupest-test&preview=jpeg;90&channel=rgb + body: none + auth: none +} + +query { + filename: crupest-transparent.png + type: input + subfolder: crupest-test + preview: jpeg;90 + channel: rgb +} diff --git a/store/misc/bruno/ComfyUI/bruno.json b/store/misc/bruno/ComfyUI/bruno.json new file mode 100644 index 0000000..ee35540 --- /dev/null +++ b/store/misc/bruno/ComfyUI/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "ComfyUI", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +}
\ No newline at end of file diff --git a/store/misc/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru b/store/misc/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru new file mode 100644 index 0000000..480c8da --- /dev/null +++ b/store/misc/bruno/ComfyUI/environments/ChimerAI ComfyUI Server.bru @@ -0,0 +1,3 @@ +vars:secret [ + BASE_URL +] diff --git a/store/misc/magic/extend-script.js b/store/misc/magic/extend-script.js new file mode 100644 index 0000000..519cee9 --- /dev/null +++ b/store/misc/magic/extend-script.js @@ -0,0 +1,21 @@ +// Define main function (script entry) + +function main(config, profileName) { + delete config.dns; + delete config.tun; + delete config.hosts; + + delete config["cfw-latency-timeout"] + delete config["cfw-latency-url"] + delete config["cfw-conn-break-strategy"] + + config["proxies"] = [config["crupest-proxy"], ...config["proxies"]] + delete config["crupest-proxy"] + + select_proxy = { name: "node-select", type: "select", proxies: ["auto-select", ...config.proxies.map(p => p.name)] } + auto_select_proxy = config["crupest-auto-select"] + config["proxy-groups"] = [ select_proxy, auto_select_proxy ] + delete config["crupest-auto-select"] + + return config; +} diff --git a/store/misc/magic/extend.yaml b/store/misc/magic/extend.yaml new file mode 100644 index 0000000..3006f08 --- /dev/null +++ b/store/misc/magic/extend.yaml @@ -0,0 +1,65 @@ +# Profile Enhancement Merge Template for Clash Verge + +profile: + store-selected: true + +rules: + - "GEOSITE,github,node-select" + - "GEOSITE,google,node-select" + - "GEOSITE,youtube,node-select" + - "GEOSITE,twitter,node-select" + - "GEOSITE,facebook,node-select" + - "GEOSITE,discord,node-select" + - "GEOSITE,reddit,node-select" + - "GEOSITE,twitch,node-select" + - "GEOSITE,quora,node-select" + - "GEOSITE,telegram,node-select" + - "GEOSITE,imgur,node-select" + - "GEOSITE,stackexchange,node-select" + - "GEOSITE,onedrive,node-select" + + - "GEOSITE,duckduckgo,node-select" + - "GEOSITE,wikimedia,node-select" + - "GEOSITE,gitbook,node-select" + - "GEOSITE,gitlab,node-select" + - "GEOSITE,creativecommons,node-select" + - "GEOSITE,archive,node-select" + - "GEOSITE,matrix,node-select" + - "GEOSITE,tor,node-select" + + - "GEOSITE,python,node-select" + - "GEOSITE,ruby,node-select" + - "GEOSITE,rust,node-select" + - "GEOSITE,nodejs,node-select" + - "GEOSITE,npmjs,node-select" + - "GEOSITE,qt,node-select" + - "GEOSITE,docker,node-select" + - "GEOSITE,v2ray,node-select" + - "GEOSITE,homebrew,node-select" + - "GEOSITE,bootstrap,node-select" + + - "GEOSITE,heroku,node-select" + - "GEOSITE,vercel,node-select" + + - "GEOSITE,ieee,node-select" + - "GEOSITE,sci-hub,node-select" + - "GEOSITE,libgen,node-select" + + - "DOMAIN-SUFFIX,gnu.org,node-select" + - "DOMAIN-SUFFIX,nongnu.org,node-select" + - "DOMAIN-SUFFIX,ietf.org,node-select" + - "DOMAIN-SUFFIX,packagist.org,node-select" + - "DOMAIN-SUFFIX,metacubex.one,node-select" + - "MATCH,DIRECT" + +crupest-proxy: + ... + +crupest-auto-select: + name: "auto-select" + type: url-test + interval: 1800 + include-all-proxies: true + url: 'https://www.gstatic.com/generate_204' + filter: "日本|新加坡|香港|台湾|美国" + expected-status: 204 diff --git a/store/win/Microsoft.PowerShell_profile.ps1 b/store/win/Microsoft.PowerShell_profile.ps1 new file mode 100644 index 0000000..aeced5f --- /dev/null +++ b/store/win/Microsoft.PowerShell_profile.ps1 @@ -0,0 +1,35 @@ +function Use-VC { + param( + [Parameter()] + [ValidateSet('x64', 'x86')] + $Arch = 'x64' + ) + + if ($Arch -eq 'x86') { + $p = 'x86'; + } + else { + $p = 'amd64' + } + + cmd /c "`"$(vswhere.exe -format value -property installationPath)\VC\Auxiliary\Build\vcvars64.bat`" $p & set" | + ForEach-Object { + if ($_ -match '=') { + $v = $_ -split '=' + Set-Item -Force -Path "ENV:\$($v[0])" -Value "$($v[1])" + } + } + Pop-Location + Write-Host "Visual Studio Command Prompt variables set." -ForegroundColor Yellow +} + + +function Set-Proxy { + $env:http_proxy = "http://127.0.0.1:2080" + $env:https_proxy = "http://127.0.0.1:2080" +} + +function Reset-Proxy { + Remove-Item env:http_proxy + Remove-Item env:https_proxy +} diff --git a/store/win/crupest-winget.json b/store/win/crupest-winget.json new file mode 100644 index 0000000..df2e7d9 --- /dev/null +++ b/store/win/crupest-winget.json @@ -0,0 +1,104 @@ +{
+ "$schema" : "https://aka.ms/winget-packages.schema.2.0.json",
+ "CreationDate" : "2024-10-31T18:34:15.174-00:00",
+ "Sources" :
+ [
+ {
+ "Packages" :
+ [
+ {
+ "PackageIdentifier" : "7zip.7zip"
+ },
+ {
+ "PackageIdentifier" : "Docker.DockerDesktop"
+ },
+ {
+ "PackageIdentifier" : "Git.Git"
+ },
+ {
+ "PackageIdentifier" : "Mozilla.Firefox"
+ },
+ {
+ "PackageIdentifier" : "Mozilla.Thunderbird"
+ },
+ {
+ "PackageIdentifier" : "VideoLAN.VLC"
+ },
+ {
+ "PackageIdentifier" : "vim.vim"
+ },
+ {
+ "PackageIdentifier" : "Neovim.Neovim"
+ },
+ {
+ "PackageIdentifier" : "OpenJS.NodeJS"
+ },
+ {
+ "PackageIdentifier" : "voidtools.Everything"
+ },
+ {
+ "PackageIdentifier" : "Neovide.Neovide"
+ },
+ {
+ "PackageIdentifier" : "Microsoft.PowerShell"
+ },
+ {
+ "PackageIdentifier" : "Kitware.CMake"
+ },
+ {
+ "PackageIdentifier" : "JetBrains.PyCharm.Community"
+ },
+ {
+ "PackageIdentifier" : "Tencent.QQ.NT"
+ },
+ {
+ "PackageIdentifier" : "Tencent.WeChat"
+ },
+ {
+ "PackageIdentifier" : "Python.Launcher"
+ },
+ {
+ "PackageIdentifier" : "NetEase.CloudMusic"
+ },
+ {
+ "PackageIdentifier" : "agalwood.Motrix"
+ },
+ {
+ "PackageIdentifier" : "BurntSushi.ripgrep.MSVC"
+ },
+ {
+ "PackageIdentifier" : "Microsoft.VisualStudio.Locator"
+ },
+ {
+ "PackageIdentifier" : "Ninja-build.Ninja"
+ },
+ {
+ "PackageIdentifier" : "Rufus.Rufus"
+ },
+ {
+ "PackageIdentifier" : "Rustlang.Rustup"
+ },
+ {
+ "PackageIdentifier" : "Python.Python.3.13"
+ },
+ {
+ "PackageIdentifier" : "Microsoft.PowerToys"
+ },
+ {
+ "PackageIdentifier" : "Microsoft.VisualStudioCode"
+ },
+ {
+ "PackageIdentifier" : "Microsoft.WinDbg"
+ }
+ ],
+ "SourceDetails" :
+ {
+ "Argument" : "https://cdn.winget.microsoft.com/cache",
+ "Identifier" : "Microsoft.Winget.Source_8wekyb3d8bbwe",
+ "Name" : "winget",
+ "Type" : "Microsoft.PreIndexed.Package"
+ }
+ }
+ ],
+ "WinGetVersion" : "1.9.2411-preview"
+}
\ No newline at end of file diff --git a/store/win/neovide-listen.ps1 b/store/win/neovide-listen.ps1 new file mode 100644 index 0000000..e84f3a2 --- /dev/null +++ b/store/win/neovide-listen.ps1 @@ -0,0 +1,38 @@ +$env:NVIM_LISTEN_ADDRESS ??= "\\.\pipe\nvimsocket" + +$neovide_args = @() + +$MY_NEOVIM_PATH="$HOME/codes/neovim/build/bin/nvim.exe" +if (Get-Item $MY_NEOVIM_PATH -ErrorAction Ignore) { + Write-Output "Found my neovim at $MY_NEOVIM_PATH." + $env:VIMRUNTIME="$HOME/codes/neovim/runtime" + $neovide_args += "--neovim-bin", "$MY_NEOVIM_PATH" +} + +$listen_added = $false +foreach ($arg in $args) { + $neovide_args += $arg + if ( $arg -eq '--') { + $neovide_args += "--listen", $env:NVIM_LISTEN_ADDRESS + $listen_added=$true + } +} + +if (-not $listen_added) { + $neovide_args += "--", "--listen", $env:NVIM_LISTEN_ADDRESS +} + +$neovide_bin = "neovide" +$my_neovide_path = "$HOME/codes/neovide/target/release/neovide.exe" +if (Get-Item $my_neovide_path -ErrorAction Ignore) { + Write-Output "Found my neovide at $my_neovide_path." + $neovide_bin = "$my_neovide_path" +} + +if (Get-Command nvr -ErrorAction Ignore) { + Write-Output "Detected nvr, set git editor env." + $env:GIT_EDITOR = "nvr -cc split --remote-wait" +} + +Write-Output "Command is $($neovide_args -join ' ')." +Start-Process $neovide_bin -ArgumentList $neovide_args -Wait diff --git a/store/works/Crupest.SecretTool/.gitignore b/store/works/Crupest.SecretTool/.gitignore new file mode 100644 index 0000000..ac4d8a4 --- /dev/null +++ b/store/works/Crupest.SecretTool/.gitignore @@ -0,0 +1,7 @@ +.vs +bin +obj +*.pubxml.user +*.csproj.user + +publish diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool.sln b/store/works/Crupest.SecretTool/Crupest.SecretTool.sln new file mode 100644 index 0000000..fde4347 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34024.191 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4C2CE80-CDF8-4B08-8912-D1F0F14196AD}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crupest.SecretTool", "Crupest.SecretTool\Crupest.SecretTool.csproj", "{D6335AE4-FD22-49CD-9624-37371F3B4F82}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1E8FD9C-9157-4F4E-8265-4B37F30EEC5E} + EndGlobalSection +EndGlobal diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore b/store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore new file mode 100644 index 0000000..c936492 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/.gitignore @@ -0,0 +1 @@ +vmess.txt diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs new file mode 100644 index 0000000..ff58551 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Config.cs @@ -0,0 +1,95 @@ +namespace Crupest.SecretTool; + +public record ConfigItem(string Value, int LineNumber); + +public class DictionaryConfig(string configString, List<string>? requiredKeys = null) +{ + private static Dictionary<string, ConfigItem> Parse(string configString, List<string>? requiredKeys = null) + { + var config = new Dictionary<string, ConfigItem>(); + var lines = configString.Split('\n'); + int lineNumber = 1; + + foreach (var line in lines) + { + var l = line; + var beginOfComment = l.IndexOf('#'); + if (beginOfComment >= 0) + { + l = line[..beginOfComment]; + } + l = l.Trim(); + if (!string.IsNullOrEmpty(l)) + { + var equalIndex = l.IndexOf('='); + if (equalIndex == -1) + { + throw new FormatException($"No '=' found in line {lineNumber}."); + } + + config.Add(l[..equalIndex].Trim(), new ConfigItem(l[(equalIndex + 1)..].Trim(), lineNumber)); + } + + lineNumber++; + } + + if (requiredKeys is not null) + { + foreach (var key in requiredKeys) + { + if (!config.ContainsKey(key)) + { + throw new FormatException($"Required key '{key}' not found in config."); + } + } + } + + return config; + } + + public string ConfigString { get; } = configString; + public List<string>? RequiredKeys { get; } = requiredKeys; + public Dictionary<string, ConfigItem> Config { get; } = Parse(configString); + public ConfigItem GetItemCaseInsensitive(string key) + { + foreach (var (originalKey, value) in Config) + { + if (string.Equals(originalKey, key, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + throw new KeyNotFoundException($"Key '{key}' not found in config case-insensitively."); + } +} + +public class ListConfig(string configString) +{ + private static List<ConfigItem> Parse(string configString) + { + var config = new List<ConfigItem>(); + var lines = configString.Split('\n'); + int lineNumber = 1; + + foreach (var line in lines) + { + var l = line; + var beginOfComment = l.IndexOf('#'); + if (beginOfComment >= 0) + { + l = line[..beginOfComment]; + } + l = l.Trim(); + if (!string.IsNullOrEmpty(l)) + { + config.Add(new ConfigItem(l, lineNumber)); + } + lineNumber++; + } + + return config; + } + + public string ConfigString { get; } = configString; + public List<ConfigItem> Config { get; } = Parse(configString); +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs new file mode 100644 index 0000000..0803b01 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Controller.cs @@ -0,0 +1,113 @@ +using System.Diagnostics; + +namespace Crupest.SecretTool; + +public class Controller(string executablePath, string configPath, string? assetPath) +{ + public const string ToolAssetEnvironmentVariableName = "v2ray.location.asset"; + + public static string? FindExecutable(string contentDir, out bool isLocal, string? executableName = null) + { + isLocal = false; + executableName ??= "v2ray"; + + if (OperatingSystem.IsWindows()) + { + executableName += ".exe"; + } + + var localToolPath = Path.Combine(contentDir, executableName); + if (File.Exists(localToolPath)) + { + isLocal = true; + return localToolPath; + } + + var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator); + if (paths is not null) + { + foreach (var p in paths) + { + var toolPath = Path.Combine(p, executableName); + if (File.Exists(toolPath)) + { + return toolPath; + } + } + } + + return null; + } + + public string ExecutablePath { get; } = executablePath; + public string ConfigPath { get; } = configPath; + public string? AssetPath { get; } = assetPath; + public Process? CurrentProcess { get; private set; } + + private Process CreateProcess() + { + var process = new Process(); + + var startInfo = new ProcessStartInfo + { + FileName = ExecutablePath, + }; + startInfo.ArgumentList.Add("run"); + startInfo.ArgumentList.Add("-c"); + startInfo.ArgumentList.Add(ConfigPath); + if (AssetPath is not null) + { + startInfo.EnvironmentVariables[ToolAssetEnvironmentVariableName] = AssetPath; + } + + process.StartInfo = startInfo; + process.OutputDataReceived += (_, args) => + { + Console.Out.Write(args.Data); + }; + process.ErrorDataReceived += (_, args) => + { + Console.Error.WriteLine(args.Data); + }; + + return process; + } + + public void Stop() + { + if (CurrentProcess is not null) + { + CurrentProcess.Kill(); + CurrentProcess.Dispose(); + CurrentProcess = null; + Console.WriteLine("V2ray stopped."); + } + } + + public void Start(bool stopOld = false) + { + if (stopOld) Stop(); + + if (CurrentProcess is null) + { + CurrentProcess = CreateProcess(); + CurrentProcess.EnableRaisingEvents = true; + CurrentProcess.Exited += (_, _) => + { + if (CurrentProcess.ExitCode != 0) + { + const string message = "V2ray exited with error."; + Console.Error.WriteLine(message); + throw new Exception(message); + } + }; + CurrentProcess.Start(); + Console.WriteLine("V2ray started."); + } + } + + public void Restart() + { + Start(true); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj b/store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj new file mode 100644 index 0000000..2502e74 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj @@ -0,0 +1,34 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <None Update="config.json.template"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="proxy.txt"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="vmess.txt"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="hosts.txt"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="sing-config.json.template"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="sing-inbounds-mobile.json"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="sing-inbounds-pc.json"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs new file mode 100644 index 0000000..26e9231 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs @@ -0,0 +1,26 @@ +namespace Crupest.SecretTool; + +public class FileWatcher(string directory, List<string> fileNames) +{ + public string Directory { get; set; } = directory; + public List<string> FileNames { get; set; } = fileNames; + + public delegate void OnChangedHandler(); + public event OnChangedHandler? OnChanged; + + public void Run() + { + var sourceWatcher = new FileSystemWatcher(Directory); + foreach (var fileName in FileNames) + { + sourceWatcher.Filters.Add(fileName); + } + sourceWatcher.NotifyFilter = NotifyFilters.LastWrite; + + while (true) + { + var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed | WatcherChangeTypes.Created); + OnChanged?.Invoke(); + } + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs new file mode 100644 index 0000000..8f4c171 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs @@ -0,0 +1,324 @@ +using System.IO.Compression; + +namespace Crupest.SecretTool; + +public interface IGeoSiteEntry +{ + bool IsInclude { get; } + string Value { get; } +} + +public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSiteEntry +{ + public bool IsInclude => true; +} + +public record GeoSiteRuleEntry(HostMatchKind Kind, string Value, List<string> Attributes, string ContainingSite) : IGeoSiteEntry +{ + public bool IsInclude => false; + + public RoutingRuleMatcher GetRoutingRuleMatcher() => new(Kind, Value); +} + +public record GeoSite(string Name, List<IGeoSiteEntry> Entries) +{ + public static GeoSite Parse(string name, string str) + { + List<IGeoSiteEntry> entries = []; + var listConfig = new ListConfig(str); + foreach (var item in listConfig.Config) + { + var (value, line) = item; + + if (value.StartsWith("include:")) + { + var include = value["include:".Length..].Trim(); + if (include.Length == 0 || include.Contains(' ')) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid include value."); + } + entries.Add(new GeoSiteIncludeEntry(include, name)); + continue; + } + + var segments = value.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 2) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. More than one ':'."); + } + + HostMatchKind kind; + if (segments.Length == 2) + { + kind = segments[0] switch + { + "domain" => kind = HostMatchKind.DomainSuffix, + "full" => kind = HostMatchKind.DomainFull, + "keyword" => kind = HostMatchKind.DomainKeyword, + "regexp" => kind = HostMatchKind.DomainRegex, + _ => throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Unknown matcher.") + }; + } + else + { + kind = HostMatchKind.DomainSuffix; + } + + var domainSegments = segments[^1].Split('@', StringSplitOptions.TrimEntries); + var domain = domainSegments[0]; + if (kind != HostMatchKind.DomainRegex && Uri.CheckHostName(domain) != UriHostNameType.Dns) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid domain."); + } + + List<string> attributes = []; + foreach (var s in domainSegments[1..]) + { + if (s.Length == 0) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Empty attribute value."); + } + attributes.Add(s); + } + + entries.Add(new GeoSiteRuleEntry(kind, domain, attributes, name)); + } + return new GeoSite(name, entries); + } +} + +public class GeoSiteData(string directory) +{ + private static List<GeoSite> Parse(string directory) + { + var sites = new List<GeoSite>(); + foreach (var path in Directory.GetFileSystemEntries(directory)) + { + var content = File.ReadAllText(path); + sites.Add(GeoSite.Parse(Path.GetFileName(path), content)); + } + return sites; + } + + public string DataDirectory { get; } = directory; + + public List<GeoSite> Sites { get; } = Parse(directory); + + public GeoSite? GetSite(string name) + { + return Sites.Where(s => s.Name == name).FirstOrDefault(); + } + + public List<GeoSiteRuleEntry> GetEntriesRecursive(List<string> sites, + List<HostMatchKind>? onlyMatcherKinds = null, List<string>? onlyAttributes = null) + { + List<GeoSiteRuleEntry> entries = []; + HashSet<string> visited = []; + HashSet<HostMatchKind>? kinds = onlyMatcherKinds?.ToHashSet(); + + void Visit(string site) + { + if (visited.Contains(site)) + { + return; + } + + visited.Add(site); + var siteData = GetSite(site); + if (siteData == null) + { + return; + } + foreach (var entry in siteData.Entries) + { + if (entry is GeoSiteIncludeEntry includeEntry) + { + Visit(includeEntry.Value); + } + else if (entry is GeoSiteRuleEntry geoSiteRuleEntry) + { + if (kinds != null && !kinds.Contains(geoSiteRuleEntry.Kind)) + { + continue; + } + + if (onlyAttributes != null && !geoSiteRuleEntry.Attributes.Intersect(onlyAttributes).Any()) + { + continue; + } + + entries.Add(geoSiteRuleEntry); + } + } + } + + foreach (var s in sites) + { + Visit(s); + } + + return entries; + } +} + +public class GeoDataManager +{ + public const string GeoSiteFileName = "geosite.dat"; + public const string GeoIpFileName = "geoip.dat"; + public const string GeoIpCnFileName = "geoip-only-cn-private.dat"; + + public static class ToolGithub + { + public const string Organization = "v2fly"; + public const string GeoSiteRepository = "domain-list-community"; + public const string GeoIpRepository = "geoip"; + public const string GeoSiteReleaseFilename = "dlc.dat"; + public const string GeoIpReleaseFilename = "geoip.dat"; + public const string GeoIpCnReleaseFilename = "geoip-only-cn-private.dat"; + } + + public static GeoDataManager Instance { get; } = new GeoDataManager(); + + public record GeoDataAsset(string Name, string FileName, string GithubUser, string GithubRepo, string GithubReleaseFileName); + + public GeoDataManager() + { + Assets = + [ + new("geosite", GeoSiteFileName, ToolGithub.Organization, ToolGithub.GeoSiteRepository, ToolGithub.GeoSiteReleaseFilename), + new("geoip", GeoIpFileName, ToolGithub.Organization, ToolGithub.GeoIpRepository, ToolGithub.GeoIpReleaseFilename), + new("geoip-cn", GeoIpCnFileName, ToolGithub.Organization, ToolGithub.GeoIpRepository, ToolGithub.GeoIpCnReleaseFilename), + ]; + } + + public List<GeoDataAsset> Assets { get; set; } + + public GeoSiteData? GeoSiteData { get; set; } + + public GeoSiteData GetOrCreateGeoSiteData(bool clean, bool silent) + { + if (GeoSiteData is not null) { return GeoSiteData; } + GeoSiteData = DownloadAndGenerateGeoSiteData(clean, silent); + return GeoSiteData; + } + + private static string GetReleaseFileUrl(string user, string repo, string fileName) + { + return $"https://github.com/{user}/{repo}/releases/latest/download/{fileName}"; + } + + private static void GithubDownloadRelease(HttpClient httpClient, string user, string repo, string fileName, string outputPath, bool silent) + { + var url = GetReleaseFileUrl(user, repo, fileName); + if (!silent) Console.WriteLine($"Downloading {url} to {outputPath}"); + using var responseStream = httpClient.GetStreamAsync(url).Result; + using var outputFileStream = File.OpenWrite(outputPath); + responseStream.CopyTo(outputFileStream); + } + + public bool HasAllAssets(string directory, out List<string> missing) + { + missing = []; + foreach (var asset in Assets) + { + var assetPath = Path.Combine(directory, asset.FileName); + if (!File.Exists(assetPath)) + { + missing.Add(asset.Name); + } + } + return missing.Count == 0; + } + + public void Download(string outputDir, bool silent) + { + using var httpClient = new HttpClient(); + + foreach (var asset in Assets) + { + if (!silent) + { + Console.WriteLine($"Downloading {asset.Name}..."); + } + GithubDownloadRelease(httpClient, asset.GithubUser, asset.GithubRepo, asset.GithubReleaseFileName, Path.Combine(outputDir, asset.FileName), silent); + if (!silent) + { + Console.WriteLine($"Downloaded {asset.Name}!"); + } + } + + if (!File.Exists(Program.RestartLabelFilePath)) + { + File.Create(Program.RestartLabelFilePath); + } + else + { + File.SetLastWriteTime(Program.RestartLabelFilePath, DateTime.Now); + } + } + + private static string GetGithubRepositoryArchiveUrl(string user, string repo) + { + return $"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"; + } + + private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath, bool silent) + { + var url = GetGithubRepositoryArchiveUrl(user, repo); + if (!silent) { Console.WriteLine($"Begin to download data from {url} to {outputPath}."); } + using var responseStream = httpClient.GetStreamAsync(url).Result; + using var outputFileStream = File.OpenWrite(outputPath); + responseStream.CopyTo(outputFileStream); + if (!silent) { Console.WriteLine("Succeeded to download."); } + } + + private static void Unzip(string zipPath, string outputPath) + { + using var zip = ZipFile.OpenRead(zipPath) ?? throw new Exception($"Failed to open zip file {zipPath}"); + zip.ExtractToDirectory(outputPath); + } + + private static string DownloadAndExtractGeoDataRepository(bool cleanTempDirIfFailed, bool silent, out string tempDirectoryPath) + { + tempDirectoryPath = ""; + const string zipFileName = "v2ray-geosite-master.zip"; + using var httpClient = new HttpClient(); + var tempDirectory = Directory.CreateTempSubdirectory(Program.Name); + tempDirectoryPath = tempDirectory.FullName; + try + { + var archivePath = Path.Combine(tempDirectoryPath, zipFileName); + var extractPath = Path.Combine(tempDirectoryPath, "repo"); + GithubDownloadRepository(httpClient, ToolGithub.Organization, ToolGithub.GeoSiteRepository, archivePath, silent); + if (!silent) { Console.WriteLine($"Extract geo data to {extractPath}."); } + Directory.CreateDirectory(extractPath); + Unzip(archivePath, extractPath); + if (!silent) { Console.WriteLine($"Extraction done."); } + return Path.Join(extractPath, "domain-list-community-master"); + } + catch (Exception) + { + if (cleanTempDirIfFailed) + { + Directory.Delete(tempDirectoryPath, true); + } + throw; + } + } + + private static GeoSiteData DownloadAndGenerateGeoSiteData(bool clean, bool silent) + { + var repoDirectory = DownloadAndExtractGeoDataRepository(clean, silent, out var tempDirectoryPath); + try + { + return new GeoSiteData(Path.Join(repoDirectory, "data")); + } + finally + { + if (clean) + { + Directory.Delete(tempDirectoryPath, true); + } + } + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs new file mode 100644 index 0000000..858333d --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs @@ -0,0 +1,123 @@ +namespace Crupest.SecretTool; + +public enum HostMatchKind +{ + DomainFull, + DomainSuffix, + DomainKeyword, + DomainRegex, + Ip, + GeoSite, + GeoIp, +} + +public static class HostMatchKindExtensions +{ + public static bool IsDomain(this HostMatchKind kind) + { + return kind.IsNonRegexDomain() || kind == HostMatchKind.DomainRegex; + } + + public static bool IsNonRegexDomain(this HostMatchKind kind) + { + return kind is HostMatchKind.DomainFull or HostMatchKind.DomainSuffix or HostMatchKind.DomainKeyword; + } + + + public static List<HostMatchKind> DomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword, HostMatchKind.DomainRegex]; + + public static List<HostMatchKind> NonRegexDomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword]; + + public static List<HostMatchKind> SupportedInSingRouteMatchKinds { get; } = [..DomainMatchKinds, HostMatchKind.Ip]; + + public static bool IsSupportedInSingRoute(this HostMatchKind kind) => SupportedInSingRouteMatchKinds.Contains(kind); +} + +public record HostMatchConfigItem(HostMatchKind Kind, string MatchString, List<string> Values); + +public class HostMatchConfig(string configString, List<HostMatchKind> allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1) +{ + private static List<HostMatchConfigItem> Parse(string configString, List<HostMatchKind> allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1) + { + var items = new ListConfig(configString).Config; + var result = new List<HostMatchConfigItem>(); + + foreach (var item in items) + { + var lineNumber = item.LineNumber; + var line = item.Value; + var hasExplicitMatchKind = false; + var segments = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); + + foreach (var matchKind in Enum.GetValues<HostMatchKind>()) + { + var matchKindName = Enum.GetName(matchKind) ?? throw new Exception("No such match kind."); + if (segments[0] == matchKindName) + { + hasExplicitMatchKind = true; + + if (segments.Count < 2) + { + throw new FormatException($"Explicit match item needs a value in line {lineNumber}."); + } + if (allowedMatchKinds.Contains(matchKind)) + { + if (matchKind.IsNonRegexDomain() && Uri.CheckHostName(matchKindName) != UriHostNameType.Dns) + { + throw new FormatException($"Invalid domain format in line {lineNumber}."); + } + + var components = segments[2..].ToList(); + if (minComponentCount > 0 && components.Count < minComponentCount) + { + throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required."); + } + if (maxComponentCount >= 0 && components.Count > maxComponentCount) + { + throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed."); + } + result.Add(new HostMatchConfigItem(matchKind, segments[1], components)); + } + else + { + throw new FormatException($"Match kind {matchKindName} is not allowed at line {lineNumber}."); + } + } + } + + if (!hasExplicitMatchKind) + { + if (minComponentCount > 0 && segments.Count - 1 < minComponentCount) + { + throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required."); + } + if (maxComponentCount >= 0 && segments.Count - 1 > maxComponentCount) + { + throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed."); + } + result.Add(new HostMatchConfigItem(HostMatchKind.DomainSuffix, segments[0], segments.Count == 1 ? [] : segments[1..])); + } + } + return result; + } + + public string ConfigString { get; } = configString; + public List<HostMatchKind> AllowedMatchKinds { get; } = allowedMatchKinds; + public int MinComponentCount { get; } = minComponentCount; + public int MaxComponentCount { get; } = maxComponentCount; + public List<HostMatchConfigItem> Items { get; } = Parse(configString, allowedMatchKinds, minComponentCount, maxComponentCount); +} + +public class HostMatchConfigFile +{ + public HostMatchConfigFile(string path, List<HostMatchKind> allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1) + { + Path = path; + FileContent = File.ReadAllText(path); + Config = new HostMatchConfig(FileContent, allowedMatchKinds, minComponentCount, maxComponentCount); ; + } + + public string Path { get; } + public string FileContent { get; } + public HostMatchConfig Config { get; } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs new file mode 100644 index 0000000..18b1ac0 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Program.cs @@ -0,0 +1,113 @@ +using System.Reflection; + +namespace Crupest.SecretTool; + +public static class Program +{ + public static string Name { get; } = typeof(Program).Namespace ?? throw new Exception("Can't get the name of Crupest.SecretTool."); + + public static string CrupestSecretToolDirectory { get; } = + Environment.GetEnvironmentVariable("CRUPEST_V2RAY_DIR") ?? + Path.GetFullPath(Path.GetDirectoryName( + Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of Crupest.SecretTool.")); + + private const string ConfigOutputFileName = "config.json"; + private const string SurgeRuleSetChinaOutputFileName = "ChinaRuleSet.txt"; + private const string SurgeRuleSetGlobalOutputFileName = "GlobalRuleSet.txt"; + + public const string RestartLabelFileName = "restart.label"; + public static string RestartLabelFilePath { get; } = Path.Combine(CrupestSecretToolDirectory, RestartLabelFileName); + + public static void RunToolAndWatchConfigChange() + { + var executablePath = Controller.FindExecutable(CrupestSecretToolDirectory, out var isLocal) ?? + throw new Exception("Can't find v2ray executable either in Crupest.SecretTool directory or in PATH."); + + string? assetsPath; + if (isLocal) + { + assetsPath = CrupestSecretToolDirectory; + var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestSecretToolDirectory, out var missing); + if (!assetsComplete) + { + throw new Exception($"Missing assets: {string.Join(", ", missing)} in {CrupestSecretToolDirectory}. This v2ray is local. So only use assets in Crupest.SecretTool directory."); + } + } + else + { + assetsPath = CrupestSecretToolDirectory; + var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestSecretToolDirectory, out var missing); + if (!assetsComplete) + { + Console.WriteLine($"Missing assets: {string.Join(", ", missing)} in {CrupestSecretToolDirectory}. This v2ray is global. So fallback to its own assets."); + assetsPath = null; + } + } + + var controller = new Controller(executablePath, Path.Combine(CrupestSecretToolDirectory, ConfigOutputFileName), assetsPath); + var configFileWatcher = new FileWatcher(CrupestSecretToolDirectory, + [.. ToolConfig.ConfigFileNames, RestartLabelFileName]); + + ToolConfig.FromDirectoryAndWriteToFile(CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, ConfigOutputFileName)); + controller.Start(); + + configFileWatcher.OnChanged += () => + { + ToolConfig.FromDirectoryAndWriteToFile(CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, ConfigOutputFileName)); + controller.Restart(); + }; + + configFileWatcher.Run(); + } + + public static void Main(string[] args) + { + if (args.Length != 0) + { + var verb = args[0].ToLower(); + if (verb == "download-geodata" || verb == "dg") + { + if (args.Length != 1) + { + throw new Exception("Invalid command line arguments. download-geodata requires no arguments."); + } + GeoDataManager.Instance.Download(CrupestSecretToolDirectory, false); + return; + } + else if (verb == "generate-surge-rule-set" || verb == "gsr") + { + if (args.Length != 1) + { + throw new Exception("Invalid command line arguments. download-geodata requires no arguments."); + } + SurgeConfigGenerator.GenerateTo( + CrupestSecretToolDirectory, + Path.Join(CrupestSecretToolDirectory, SurgeRuleSetChinaOutputFileName), + Path.Join(CrupestSecretToolDirectory, SurgeRuleSetGlobalOutputFileName), + true, true + ); + return; + } + else if (verb == "generate-sing-config" || verb == "gs") + { + if (args.Length != 2 || args[1].ToLower() is not ("pc" or "mobile")) + { + throw new Exception("Invalid command line arguments. generate-sing-config requires 1 argument. The argument must be either 'pc' or 'mobile'."); + } + + var config = SingToolConfig.FromDirectory(CrupestSecretToolDirectory, args[1].ToLower() == "mobile", true, true); + Console.Out.WriteLine(config.ToSingConfigString()); + return; + } + else if (verb == "generate" || verb == "g") + { + var config = ToolConfig.FromDirectory(CrupestSecretToolDirectory); + Console.Out.WriteLine(config.ToJsonStringV4()); + return; + } + throw new Exception("Invalid command line arguments."); + } + + RunToolAndWatchConfigChange(); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml b/store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..5fca454 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +https://go.microsoft.com/fwlink/?LinkID=208121. +--> +<Project> + <PropertyGroup> + <Configuration>Release</Configuration> + <Platform>Any CPU</Platform> + <PublishDir>bin\Release\net8.0\publish\</PublishDir> + <PublishProtocol>FileSystem</PublishProtocol> + <_TargetId>Folder</_TargetId> + </PropertyGroup> +</Project>
\ No newline at end of file diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs new file mode 100644 index 0000000..d2703ba --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs @@ -0,0 +1,76 @@ +namespace Crupest.SecretTool; + +public abstract class Proxy(string tag) : IV4ConfigObject, ISingConfigObject +{ + public string Tag { get; set; } = tag; + + public abstract V4ConfigJsonObjects.Outbound ToJsonObjectV4(); + public abstract SingConfigJsonObjects.OutboundBase ToJsonObjectSing(); + + object IV4ConfigObject.ToJsonObjectV4() + { + return ToJsonObjectV4(); + } + + object ISingConfigObject.ToJsonObjectSing() + { + return ToJsonObjectSing(); + } +} + +public class HttpProxy(string host, int port, string tag) : Proxy(tag) +{ + public string Host { get; set; } = host; + public int Port { get; set; } = port; + + public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing() + { + throw new NotImplementedException("Http proxy is not supported in sing now."); + } + + public override V4ConfigJsonObjects.Outbound ToJsonObjectV4() + { + return new V4ConfigJsonObjects.Outbound(Tag, "http", + new V4ConfigJsonObjects.HttpOutboundSettings([new V4ConfigJsonObjects.HttpOutboundServer(Host, Port, [])]), + null + ); + } +} + + +public class VmessProxy(string host, int port, string userId, string path, string tag) : Proxy(tag) +{ + public string Host { get; set; } = host; + public int Port { get; set; } = port; + public string Path { get; set; } = path; + public string UserId { get; set; } = userId; + + public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing() + { + return new SingConfigJsonObjects.VmessOutbound(Tag, Host, Port, UserId, + Transport: new SingConfigJsonObjects.V2rayWebsocketTransport(Path, new Dictionary<string, string> { { "Host", Host } }), + Tls: new SingConfigJsonObjects.OutboundTls(true)); + } + + public override V4ConfigJsonObjects.Outbound ToJsonObjectV4() + { + return new V4ConfigJsonObjects.Outbound(Tag, "vmess", + new V4ConfigJsonObjects.VmessOutboundSettings( + [new V4ConfigJsonObjects.VnextServer(Host, Port, [new V4ConfigJsonObjects.VnextServerUser(UserId, 0, "auto", 0)])]), + new V4ConfigJsonObjects.WsStreamSettings("ws", "tls", new V4ConfigJsonObjects.WsSettings(Path, new() { ["Host"] = Host })) + ); + } + + public static VmessProxy CreateFromConfigString(string configString, string tag) + { + var config = new DictionaryConfig(configString, ["host", "port", "userid", "path"]); + var portString = config.GetItemCaseInsensitive("port").Value; + if (!int.TryParse(portString, out var port) || port <= 0) + { + throw new FormatException($"Invalid port number: {portString}: not an integer or is a invalid number."); + } + return new VmessProxy(config.GetItemCaseInsensitive("host").Value, port, + config.GetItemCaseInsensitive("userid").Value, config.GetItemCaseInsensitive("path").Value, tag + ); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs new file mode 100644 index 0000000..81698a3 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs @@ -0,0 +1,31 @@ +namespace Crupest.SecretTool;
+
+public class ProxyFile : HostMatchConfigFile
+{
+ public ProxyFile(string path) : base(path, [.. Enum.GetValues<HostMatchKind>()], maxComponentCount: 0)
+ {
+ RoutingRuleMatchers = Config.Items.Select(i => new RoutingRuleMatcher(i.Kind, i.MatchString)).ToList();
+ }
+
+ public List<RoutingRuleMatcher> RoutingRuleMatchers { get; }
+
+ public List<RoutingRuleMatcher> GetChinaRulesByGeoSite(GeoSiteData geoSiteData)
+ {
+ var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
+ return geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds, ["cn"]).Select(e => e.GetRoutingRuleMatcher()).ToList();
+ }
+
+ public List<RoutingRuleMatcher> GetRulesFlattenGeoSite(GeoSiteData geoSiteData, bool noCn = false)
+ {
+ var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
+ var flattenGeoSiteRules = geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds)
+ .Where(e => !noCn || !e.Attributes.Contains("cn"))
+ .Select(e => e.GetRoutingRuleMatcher())
+ .ToList();
+ var otherRules = RoutingRuleMatchers.Where(m => m.MatchKind != HostMatchKind.GeoSite).ToList();
+ return [
+ ..flattenGeoSiteRules,
+ ..otherRules
+ ];
+ }
+}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs new file mode 100644 index 0000000..fdf1b93 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Routing.cs @@ -0,0 +1,155 @@ +namespace Crupest.SecretTool; + +public record RoutingRuleMatcher(HostMatchKind MatchKind, string MatchString) +{ + public RoutingRule ToRoutingRule(string OutboundTag) => new(MatchKind, MatchString, OutboundTag); +} + +public record RoutingRule(HostMatchKind MatchKind, string MatchString, string OutboundTag) : IV4ConfigObject +{ + public string ToolConfigString => MatchKind switch + { + HostMatchKind.DomainFull => $"full:{MatchString}", + HostMatchKind.DomainSuffix => $"domain:{MatchString}", + HostMatchKind.DomainKeyword => MatchString, + HostMatchKind.DomainRegex => $"regexp:{MatchString}", + HostMatchKind.Ip => MatchString, + HostMatchKind.GeoSite => $"geosite:{MatchString}", + HostMatchKind.GeoIp => $"geoip:{MatchString}", + _ => throw new ArgumentException("Invalid matcher kind.") + }; + + public string ToolConfigStringSing => MatchKind.IsSupportedInSingRoute() ? MatchString : throw new ArgumentException("Unsupported matcher kind for sing."); + + public static Dictionary<string, List<RoutingRule>> GroupByOutboundTag(List<RoutingRule> rules) + => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary(); + + public static Dictionary<HostMatchKind, List<RoutingRule>> GroupByMatchKind(List<RoutingRule> rules) + => rules.GroupBy(r => r.MatchKind).Select(g => (g.Key, g.ToList())).ToDictionary(); + + public static List<List<RoutingRule>> GroupByOutboundTagAndMatcherKind(List<RoutingRule> rules) + => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatchKind(groupByTag).Values).ToList(); + + public static SingConfigJsonObjects.RouteRule ListToJsonObjectSing(List<RoutingRule> rules) + { + if (rules.Count == 0) + { + throw new ArgumentException("Rule list is empty."); + } + + var outboundTag = rules[0].OutboundTag; + + if (rules.Any(r => !r.MatchKind.IsSupportedInSingRoute())) + { + throw new ArgumentException("Rules must have matcher kinds supported in sing."); + } + + if (rules.Any(r => r.OutboundTag != outboundTag)) + { + throw new ArgumentException("Rules must have the same outbound tag."); + } + + return new SingConfigJsonObjects.RouteRule(Outbound: outboundTag, + Domain: rules.Where(r => r.MatchKind == HostMatchKind.DomainFull).Select(r => r.ToolConfigStringSing).ToList(), + DomainSuffix: rules.Where(r => r.MatchKind == HostMatchKind.DomainSuffix).Select(r => r.ToolConfigStringSing).ToList(), + DomainKeyword: rules.Where(r => r.MatchKind == HostMatchKind.DomainKeyword).Select(r => r.ToolConfigStringSing).ToList(), + DomainRegex: rules.Where(r => r.MatchKind == HostMatchKind.DomainRegex).Select(r => r.ToolConfigStringSing).ToList(), + IpCidr: rules.Where(r => r.MatchKind == HostMatchKind.Ip).Select(r => r.ToolConfigStringSing).ToList() + ); + } + + public static V4ConfigJsonObjects.RoutingRule ListToJsonObject(List<RoutingRule> rules) + { + if (rules.Count == 0) + { + throw new ArgumentException("Rule list is empty."); + } + + var matchKind = rules[0].MatchKind; + var outboundTag = rules[0].OutboundTag; + + if (rules.Any(r => r.OutboundTag != outboundTag) || rules.Any(r => r.MatchKind != matchKind)) + { + throw new ArgumentException("Rules must have the same matcher kind and outbound tag."); + } + + List<string> toolConfigList = rules.Select(r => r.ToolConfigString).ToList(); + + return new V4ConfigJsonObjects.RoutingRule(OutboundTag: outboundTag, + Ip: (matchKind is HostMatchKind.Ip or HostMatchKind.GeoIp) ? toolConfigList : null, + Domains: (matchKind.IsDomain() || matchKind == HostMatchKind.GeoSite) ? toolConfigList : null + ); + } + + public RoutingRule CloneGeositeWithCnAttribute(string outboundTag) + { + if (MatchKind is not HostMatchKind.GeoSite) + { + throw new ArgumentException("Matcher kind must be GeoSite."); + } + + return new RoutingRule(HostMatchKind.GeoSite, $"{MatchString}@cn", outboundTag); + } + + public RoutingRuleMatcher GetMatcher() => new(MatchKind, MatchString); + + public V4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]); + + object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); +} + +public record Routing(List<RoutingRule> Rules) : IV4ConfigObject, ISingConfigObject +{ + public List<RoutingRule> CreateGeositeCnDirectRules() + { + return Rules.Where(r => r.MatchKind is HostMatchKind.GeoSite) + .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList(); + } + + public SingConfigJsonObjects.Route ToJsonObjectSing() + { + List<SingConfigJsonObjects.RouteRule> ruleJsonObjects = [ new SingConfigJsonObjects.RouteRule(Outbound: "dns-out", Protocol: "dns")]; + ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTag(Rules).Values.Select(RoutingRule.ListToJsonObjectSing)); + return new SingConfigJsonObjects.Route(ruleJsonObjects); + } + + public V4ConfigJsonObjects.Routing ToJsonObjectV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true) + { + List<V4ConfigJsonObjects.RoutingRule> ruleJsonObjects = []; + + if (directGeositeCn) + { + ruleJsonObjects.Add(RoutingRule.ListToJsonObject(CreateGeositeCnDirectRules())); + } + + ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(RoutingRule.ListToJsonObject)); + + return new V4ConfigJsonObjects.Routing(ruleJsonObjects, domainStrategy); + } + + object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); + + object ISingConfigObject.ToJsonObjectSing() => ToJsonObjectSing(); + + public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag) + { + return new Routing( + proxyFile.RoutingRuleMatchers.Select(m => m.ToRoutingRule(outboundTag)).ToList()); + } + + public static Routing FromProxyFileForSing(ProxyFile proxyFile, GeoSiteData geoSiteData, string outboundTag, string? directCnOutboundTag = null) + { + List<RoutingRule> rules = []; + + if (directCnOutboundTag is not null) + { + rules.AddRange(proxyFile.GetChinaRulesByGeoSite(geoSiteData).Select(m => m.ToRoutingRule(directCnOutboundTag)).ToList()); + } + + rules.AddRange(proxyFile.GetRulesFlattenGeoSite(geoSiteData).Where(m => m.MatchKind.IsSupportedInSingRoute()).Select(m => m.ToRoutingRule(outboundTag)).ToList()); + + return new Routing( + rules + ); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs new file mode 100644 index 0000000..56b5563 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs @@ -0,0 +1,20 @@ +namespace Crupest.SecretTool;
+
+public static class SingConfigJsonObjects
+{
+ public interface IObject;
+
+ public record OutboundTls(bool Enabled);
+ public record V2rayTransportBase(string Type);
+ public record V2rayWebsocketTransport(string Path, Dictionary<string, string>? Headers = null) : V2rayTransportBase("ws");
+ public record OutboundBase(string Tag, string Type) : IObject;
+ public record VmessOutbound(string Tag, string Server, int ServerPort, string Uuid, string Security = "auto",
+ V2rayTransportBase? Transport = null, OutboundTls? Tls = null): OutboundBase(Tag, "vmess");
+
+ public record RouteRule(List<string>? Domain = null, List<string>? DomainSuffix = null, List<string>? DomainKeyword = null,
+ List<string>? DomainRegex = null, List<string>? IpCidr = null, List<string>? SourceIpCidr = null, string? Protocol = null,
+ List<int>? Port = null, List<int>? SourcePort = null, List<string>? PortRange = null, List<string>? SourcePortRange = null,
+ string? Network = null, List<string>? Inbound = null, string? Outbound = null) : IObject;
+
+ public record Route(List<RouteRule> Rules) : IObject;
+}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs new file mode 100644 index 0000000..b112e1c --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs @@ -0,0 +1,40 @@ +namespace Crupest.SecretTool; + +public record StaticHostRule(HostMatchKind MatchKind, string MatchString, List<string> ResolveResult) +{ + public string AddressString() + { + return MatchKind switch + { + HostMatchKind.DomainFull => MatchString, + HostMatchKind.DomainSuffix => $"domain:{MatchString}", + HostMatchKind.DomainKeyword => $"keyword:{MatchString}", + HostMatchKind.DomainRegex => $"regexp:{MatchString}", + _ => throw new ArgumentOutOfRangeException($"Match kind {MatchKind} is not allowed in static host rule."), + }; + } + + public object ResolveResultToJsonObject() + { + return ResolveResult.Count == 1 ? ResolveResult[0] : ResolveResult; + } +} + +public class StaticHosts(List<StaticHostRule> rules) : IV4ConfigObject +{ + public List<StaticHostRule> Rules { get; } = rules; + + public Dictionary<string, object> ToJsonObjectV4() => + Rules.ToDictionary(rule => rule.AddressString(), rule => rule.ResolveResultToJsonObject()); + + object IV4ConfigObject.ToJsonObjectV4() + { + return ToJsonObjectV4(); + } + + public static StaticHosts CreateFromHostMatchConfigString(string configString) + { + var config = new HostMatchConfig(configString, HostMatchKindExtensions.DomainMatchKinds, minComponentCount: 1); + return new StaticHosts(config.Items.Select(i => new StaticHostRule(i.Kind, i.MatchString, [.. i.Values])).ToList()); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs new file mode 100644 index 0000000..8a57c9f --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs @@ -0,0 +1,56 @@ +namespace Crupest.SecretTool; + +public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoData) +{ + public ProxyFile ProxyFile => proxyFile; + public GeoSiteData GeoData => geoData; + + private static string ToSurgeRuleString(HostMatchKind kind, string value) + { + var ruleType = kind switch + { + HostMatchKind.DomainFull => "DOMAIN", + HostMatchKind.DomainSuffix => "DOMAIN-SUFFIX", + HostMatchKind.DomainKeyword => "DOMAIN-KEYWORD", + HostMatchKind.DomainRegex => "URL-REGEX", + _ => throw new Exception("Unacceptable matcher kind for Surge rule.") + }; + + return $"{ruleType},{value}"; + } + + public static string GenerateSurgeRuleSetString(List<RoutingRuleMatcher> rules) + { + return string.Join('\n', rules.Select(r => ToSurgeRuleString(r.MatchKind, r.MatchString))); + } + + public string GenerateChinaRuleSet() + { + return GenerateSurgeRuleSetString(proxyFile.GetChinaRulesByGeoSite(GeoData)); + } + + public string GenerateGlobalRuleSet() + { + return GenerateSurgeRuleSetString(proxyFile.GetRulesFlattenGeoSite(geoData, true)); + } + + public static void GenerateTo(ProxyFile proxyFile, GeoSiteData geoSiteData, string cnPath, string globalPath, bool silent) + { + var generator = new SurgeConfigGenerator(proxyFile, geoSiteData); + File.WriteAllText(cnPath, generator.GenerateChinaRuleSet()); + if (!silent) Console.WriteLine($"China rule set written to {cnPath}."); + File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet()); + if (!silent) Console.WriteLine($"Global rule set written to {globalPath}."); + } + + public static void GenerateTo(string directory, string cnPath, string globalPath, bool clean, bool silent) + { + var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); + var proxyFile = new ProxyFile(Path.Combine(directory, ToolConfig.ProxyConfigFileName)); + var generator = new SurgeConfigGenerator(proxyFile, geoSiteData); + File.WriteAllText(cnPath, generator.GenerateChinaRuleSet()); + if (!silent) Console.WriteLine($"China rule set written to {cnPath}."); + File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet()); + if (!silent) Console.WriteLine($"Global rule set written to {globalPath}."); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs new file mode 100644 index 0000000..1fe91b1 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs @@ -0,0 +1,231 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Crupest.SecretTool; + +public class Template +{ + private enum ParseState + { + Text, + Dollar, + LeftBracket, + VariableName, + VariableNameFinish, + } + + private interface ITemplateNode + { + string Render(Dictionary<string, string> values); + } + + private class TextNode(string text) : ITemplateNode + { + + public string Text { get; } = text; + + public string Render(Dictionary<string, string> values) + { + return Text; + } + } + + private class VariableNode(string variableName) : ITemplateNode + { + public string VariableName { get; } = variableName; + + public string Render(Dictionary<string, string> values) + { + return values.GetValueOrDefault(VariableName) ?? ""; + } + } + + public Template(string templateString) + { + TemplateString = templateString; + Nodes = Parse(templateString); + VariableNames = Nodes.OfType<VariableNode>().Select(node => node.VariableName).ToList(); + } + + private static List<ITemplateNode> Parse(string templateString) + { + int lineNumber = 1; + int columnNumber = 0; + List<ITemplateNode> nodes = []; + ParseState state = ParseState.Text; + StringBuilder stringBuilder = new(); + + string GetPosition() => $"line {lineNumber} column{columnNumber}"; + + [DoesNotReturn] + void ReportInvalidState(string message) + { + throw new Exception($"Invalid state at {GetPosition()}: {message}"); + } + + [DoesNotReturn] + void ReportInvalidCharacter(char c) + { + throw new FormatException($"Unexpected '{c}' at {GetPosition()}."); + } + + void FinishText() + { + if (state != ParseState.Text) + { + ReportInvalidState($"Can't call FinishText here."); + } + + if (stringBuilder.Length > 0) + { + nodes.Add(new TextNode(stringBuilder.ToString())); + stringBuilder.Clear(); + } + } + + foreach (var c in templateString) + { + if (c == '\n') + { + lineNumber++; + columnNumber = 0; + } + + columnNumber++; + + switch (c) + { + case '$': + if (state == ParseState.Text) + { + FinishText(); + state = ParseState.Dollar; + } + else if (state == ParseState.Dollar) + { + if (stringBuilder.Length > 0) + { + throw new Exception($"Invalid state at {GetPosition()}: when we meet the second '$', text builder should be empty."); + } + stringBuilder.Append(c); + state = ParseState.Text; + } + else + { + throw new FormatException($"Unexpected '$' at {GetPosition()}."); + } + break; + case '{': + if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.Dollar) + { + state = ParseState.LeftBracket; + } + else + { + throw new Exception($"Unexpected '{{' at {GetPosition()}."); + } + break; + case '}': + if (state == ParseState.Text) + { + stringBuilder.Append(c); + state = ParseState.Text; + } + else if (state == ParseState.VariableName || state == ParseState.VariableNameFinish) + { + nodes.Add(new VariableNode(stringBuilder.ToString())); + stringBuilder.Clear(); + state = ParseState.Text; + } + else + { + ReportInvalidCharacter(c); + } + break; + default: + if (state == ParseState.Dollar) + { + ReportInvalidCharacter(c); + } + + if (char.IsWhiteSpace(c)) + { + if (state == ParseState.LeftBracket || state == ParseState.VariableNameFinish) + { + continue; + } + else if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.VariableName) + { + state = ParseState.VariableNameFinish; + } + else + { + ReportInvalidCharacter(c); + } + } + else + { + if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.LeftBracket || state == ParseState.VariableName) + { + stringBuilder.Append(c); + state = ParseState.VariableName; + } + else + { + ReportInvalidCharacter(c); + } + } + break; + } + } + + if (state == ParseState.Text) + { + FinishText(); + } + else + { + throw new FormatException("Unexpected end of template string."); + } + + return nodes; + } + + public string TemplateString { get; } + private List<ITemplateNode> Nodes { get; set; } + public List<string> VariableNames { get; } + + public string Generate(Dictionary<string, string> values, bool allowMissingVariable = false) + { + StringBuilder stringBuilder = new(); + foreach (var node in Nodes) + { + if (node is TextNode textNode) + { + stringBuilder.Append(textNode.Text); + } + else if (node is VariableNode variableNode) + { + var hasValue = values.TryGetValue(variableNode.VariableName, out var value); + if (!hasValue && !allowMissingVariable) + { + throw new Exception($"Variable '{variableNode.VariableName}' is not set."); + } + stringBuilder.Append(hasValue ? value : string.Empty); + } + } + return stringBuilder.ToString(); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs new file mode 100644 index 0000000..809fba1 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs @@ -0,0 +1,271 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Crupest.SecretTool; + +public interface IV4ConfigObject +{ + object ToJsonObjectV4(); +} + +public interface ISingConfigObject +{ + object ToJsonObjectSing(); +} + +public class ToolConfigBase(Template template, List<Proxy> proxies, Routing router) +{ + protected class JsonInterfaceConverter<Interface> : JsonConverter<Interface> + { + public override Interface Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write( + Utf8JsonWriter writer, + Interface value, + JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, typeof(object), options); + } + } + + public const string VmessConfigFileName = "vmess.txt"; + public const string ProxyConfigFileName = "proxy.txt"; + + public Template Template { get; set; } = template; + public List<Proxy> Proxies { get; set; } = proxies; + public Routing Routing { get; set; } = router; +} + +public class ToolConfig(Template template, List<Proxy> proxies, Routing router, StaticHosts? hosts) : ToolConfigBase(template, proxies, router) +{ + public const string ConfigTemplateFileName = "config.json.template"; + public const string HostsConfigFileName = "hosts.txt"; + + public static List<string> RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName]; + public static List<string> ConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, HostsConfigFileName]; + + private const string ProxyAnchor = "PROXY_ANCHOR"; + private const string RoutingAnchor = "ROUTING_ANCHOR"; + private const string HostsAnchor = "HOSTS_ANCHOR"; + + public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V2RAY_GEOSITE_USE_CN"; + + private static bool UseCnGeoSite => Environment.GetEnvironmentVariable(AddCnAttributeToGeositeEnvironmentVariable) switch + { + "0" or "false" or "off" or "disable" => false, + _ => true + }; + + public StaticHosts Hosts { get; set; } = hosts is null ? new StaticHosts([]) : hosts; + + public string ToJsonStringV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true, bool pretty = true) + { + var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }); + // TODO: Make interface converter generic. + jsonOptions.Converters.Add(new JsonInterfaceConverter<V4ConfigJsonObjects.IOutboundSettings>()); + jsonOptions.Converters.Add(new JsonInterfaceConverter<V4ConfigJsonObjects.IOutboundStreamSettings>()); + + var templateValues = new Dictionary<string, string> + { + [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))), + [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(domainStrategy, directGeositeCn), jsonOptions), + [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions), + }; + + var configString = Template.Generate(templateValues); + + if (pretty) + { + var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions) + { + WriteIndented = true, + }; + return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(configString, jsonOptionsPretty), jsonOptionsPretty); + } + else + { + return configString; + } + } + + public static ToolConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string? hostsPath) + { + foreach (var path in new List<string>([templatePath, vmessPath, proxyPath])) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Required config file not found: {path}."); + } + } + + ProxyFile proxyFile = new(proxyPath); + string templateString, vmessString; + string? hostsString; + + string file = ""; + try + { + file = templatePath; + templateString = File.ReadAllText(templatePath); + file = vmessPath; + vmessString = File.ReadAllText(vmessPath); + hostsString = hostsPath is not null ? File.ReadAllText(hostsPath) : null; + } + catch (Exception e) + { + throw new Exception($"Error reading config file {file}.", e); + } + + try + { + file = templatePath; + var template = new Template(templateString); + file = vmessPath; + var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy"); + file = proxyPath; + var routing = Routing.FromProxyFile(proxyFile, "proxy"); + file = hostsPath ?? ""; + var hosts = hostsString is not null ? StaticHosts.CreateFromHostMatchConfigString(hostsString) : null; + return new ToolConfig(template, [vmess], routing, hosts); + } + catch (Exception e) + { + throw new Exception($"Error parsing config file {file}.", e); + } + } + + public static ToolConfig FromDirectory(string directory) + { + return FromFiles( + Path.Join(directory, ConfigTemplateFileName), + Path.Join(directory, VmessConfigFileName), + Path.Join(directory, ProxyConfigFileName), + Path.Join(directory, HostsConfigFileName) + ); + } + + public static void FromDirectoryAndWriteToFile(string directory, string outputPath) + { + var config = FromDirectory(directory); + File.WriteAllText(outputPath, config.ToJsonStringV4()); + } +} + +public class SingToolConfig(Template template, List<Proxy> proxies, Routing router, string inboundsString) : ToolConfigBase(template, proxies, router) +{ + + public const string ConfigTemplateFileName = "sing-config.json.template"; + public const string ConfigInboundsPcFileName = "sing-inbounds-pc.json"; + public const string ConfigInboundsMobileFileName = "sing-inbounds-mobile.json"; + + public static List<string> RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, ConfigInboundsMobileFileName, ConfigInboundsPcFileName]; + + private const string ProxyAnchor = "PROXY_ANCHOR"; + private const string RouteAnchor = "ROUTE_ANCHOR"; + private const string InboundsAnchor = "INBOUNDS_ANCHOR"; + + public string InboundsString { get; } = inboundsString; + + public string ToSingConfigString(bool pretty = true) + { + var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }); + // TODO: Make interface converter generic. + jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.OutboundBase>()); + jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.V2rayTransportBase>()); + + var templateValues = new Dictionary<string, string> + { + [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectSing(), jsonOptions))), + [RouteAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectSing(), jsonOptions), + [InboundsAnchor] = InboundsString + }; + + var configString = Template.Generate(templateValues); + + if (pretty) + { + var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions) + { + WriteIndented = true, + }; + return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(configString, jsonOptionsPretty), jsonOptionsPretty); + } + else + { + return configString; + } + } + + public static SingToolConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string inboundsPath, bool clean, bool silent) + { + foreach (var path in new List<string>([templatePath, vmessPath, proxyPath, inboundsPath])) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Required config file not found: {path}."); + } + } + + var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); + + ProxyFile proxyFile = new(proxyPath); + string templateString, vmessString, inboundsString; + + string file = ""; + try + { + file = templatePath; + templateString = File.ReadAllText(templatePath); + file = vmessPath; + vmessString = File.ReadAllText(vmessPath); + file = inboundsPath; + inboundsString = File.ReadAllText(inboundsPath); + } + catch (Exception e) + { + throw new Exception($"Error reading config file {file}.", e); + } + + try + { + file = templatePath; + var template = new Template(templateString); + file = vmessPath; + var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy-out"); + file = proxyPath; + var routing = Routing.FromProxyFileForSing(proxyFile, geoSiteData, "proxy-out", "direct-out"); + return new SingToolConfig(template, [vmess], routing, inboundsString); + } + catch (Exception e) + { + throw new Exception($"Error parsing config file {file}.", e); + } + } + + public static SingToolConfig FromDirectory(string directory, bool isMobile, bool clean, bool silent) + { + return FromFiles( + Path.Join(directory, ConfigTemplateFileName), + Path.Join(directory, VmessConfigFileName), + Path.Join(directory, ProxyConfigFileName), + isMobile ? Path.Join(directory, ConfigInboundsMobileFileName) : Path.Join(directory, ConfigInboundsPcFileName), + clean, silent + ); + } +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs new file mode 100644 index 0000000..3e81dbb --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs @@ -0,0 +1,25 @@ +namespace Crupest.SecretTool; + +public static class V4ConfigJsonObjects +{ + public interface IObject; + public interface IOutboundSettings : IObject; + public interface IOutboundStreamSettings : IObject; + + public record WsSettings(string Path, Dictionary<string, string> Headers) : IObject; + public record WsStreamSettings(string Network, string Security, WsSettings WsSettings) : IOutboundStreamSettings; + public record VnextServerUser(string Id, int AlterId, string Security, int Level) : IObject; + public record VnextServer(string Address, int Port, List<VnextServerUser> Users) : IObject; + public record VmessOutboundSettings(List<VnextServer> Vnext) : IOutboundSettings; + public record HttpOutboundUser(string User, string Pass) : IObject; + public record HttpOutboundServer(string Address, int Port, List<HttpOutboundUser> Users) : IObject; + public record HttpOutboundSettings(List<HttpOutboundServer> Servers) : IOutboundSettings; + public record Outbound(string Tag, string Protocol, IOutboundSettings Settings, + IOutboundStreamSettings? StreamSettings) : IObject; + + public record RoutingRule(string DomainMatcher = "mph", string Type = "field", List<string>? Domains = null, List<string>? Ip = null, + string? Port = null, string? SourcePort = null, string? Network = null, List<string>? Source = null, + List<string>? User = null, List<string>? InboundTag = null, List<string>? Protocol = null, string? Attrs = null, + string? OutboundTag = null, string? BalancerTag = null) : IObject; + public record Routing(List<RoutingRule> Rules, string DomainStrategy = "IpOnDemand", string DomainMatcher = "mph") : IObject; +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs new file mode 100644 index 0000000..a50e9be --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs @@ -0,0 +1,31 @@ +namespace Crupest.SecretTool; + +public static class V5ConfigJsonObjects +{ + public record OutboundObject(string Protocol, object Settings, string Tag, object? StreamSettings) + { + public static OutboundObject VmessViaWs(string tag, string address, int port, string uuid, string path) + { + return new OutboundObject("vmess", new VmessSettings(address, port, uuid), tag, StreamSettingsObject.Ws(path)); + } + + public static OutboundObject Http(string tag, string address, int port) + { + return new OutboundObject("http", new HttpSettingsObject(address, port), tag, null); + } + } + + public record WsSettingsObject(string Path, Dictionary<string, string> Headers); + + public record StreamSettingsObject(string Transport, object TransportSettings, string Security, object SecuritySettings) + { + public static StreamSettingsObject Ws(string path) + { + return new StreamSettingsObject("ws", new WsSettingsObject(path, new()), "tls", new()); + } + } + + public record VmessSettings(string Address, int Port, string Uuid); + + public record HttpSettingsObject(string Address, int Port); +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template b/store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template new file mode 100644 index 0000000..424e996 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/config.json.template @@ -0,0 +1,63 @@ +{ + "log": { + "loglevel": "warning" + }, + "inbounds": [ + { + "port": 2081, + "listen": "127.0.0.1", + "tag": "socks-inbound", + "protocol": "socks", + "settings": { + "auth": "noauth" + } + }, + { + "port": 2080, + "listen": "127.0.0.1", + "tag": "http-inbound", + "protocol": "http", + "settings": { + "auth": "noauth" + } + } + ], + "outbounds": [ + { + "protocol": "freedom", + "settings": {}, + "tag": "direct" + }, + { + "protocol": "blackhole", + "settings": {}, + "tag": "blocked" + }, + ${PROXY_ANCHOR} + ], + "routing": ${ROUTING_ANCHOR}, + "dns": { + "hosts": ${HOSTS_ANCHOR}, + "servers": [ + "https://doh.pub/dns-query", + "1.1.1.1", + "8.8.8.8", + "localhost" + ] + }, + "policy": { + "levels": { + "0": { + "uplinkOnly": 0, + "downlinkOnly": 0 + } + }, + "system": { + "statsInboundUplink": false, + "statsInboundDownlink": false, + "statsOutboundUplink": false, + "statsOutboundDownlink": false + } + }, + "other": {} +} diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template b/store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template new file mode 100644 index 0000000..01ccf7a --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template @@ -0,0 +1,55 @@ +{ + "log": { + "access": { + "type": "Console", + "level": "Info" + } + }, + "dns": { + "nameServer": [{ + "address": "https://doh.pub/dns-query" + }, { + "address": "1.1.1.1" + }, { + "address": "8.8.8.8" + }, { + "address": "localhost" + }], + "staticHosts": ${HOSTS_ANCHOR} + }, + "inbounds": [{ + { + "protocol": "socks", + "port": 2081, + "listen": "127.0.0.1", + "tag": "socks-inbound", + "settings": { + "auth": "noauth" + } + }, + { + "protocol": "http", + "port": 2080, + "listen": "127.0.0.1", + "tag": "http-inbound", + "settings": { + "auth": "noauth" + } + } + }], + "outbounds": [ + { + "protocol": "freedom", + "settings": {}, + "tag": "direct" + }, + { + "protocol": "blackhole", + "settings": {}, + "tag": "blocked" + }, + ${PROXY_ANCHOR} + ], + "router": ${ROUTER_ANCHOR} +} + diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt b/store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt new file mode 100644 index 0000000..88d5015 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/hosts.txt @@ -0,0 +1,2 @@ +cdn.jsdelivr.net cdn.jsdelivr.net.cdn.cloudflare.net + diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt b/store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt new file mode 100644 index 0000000..39800f9 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/proxy.txt @@ -0,0 +1,50 @@ +GeoSite microsoft +GeoSite google +GeoSite youtube +GeoSite x +GeoSite facebook +GeoSite discord +GeoSite reddit +GeoSite twitch +GeoSite quora +GeoSite telegram +GeoSite imgur +GeoSite stackexchange +GeoSite medium + +GeoSite duckduckgo +GeoSite wikimedia +GeoSite gitbook +GeoSite github +GeoSite gitlab +GeoSite sourceforge +GeoSite creativecommons +GeoSite archive +GeoSite matrix +GeoSite tor + +GeoSite python +GeoSite ruby +GeoSite rust +GeoSite nodejs +GeoSite npmjs +GeoSite qt +GeoSite docker +GeoSite v2ray +GeoSite homebrew + +GeoSite azure +GeoSite akamai +GeoSite aws +GeoSite jsdelivr +GeoSite fastly +GeoSite heroku +GeoSite bootstrap +GeoSite vercel + +GeoSite ieee +GeoSite sci-hub +GeoSite libgen +GeoSite z-library + +sagernet.org diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template new file mode 100644 index 0000000..d7e55a0 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template @@ -0,0 +1,45 @@ +{
+ "log": {
+ "disabled": false,
+ "level": "info",
+ "timestamp": true
+ },
+ "dns": {
+ "servers": [
+ {
+ "tag": "ali-doh",
+ "address": "https://dns.alidns.com/dns-query",
+ "address_resolver": "ali"
+ },
+ {
+ "tag": "ali",
+ "address": "223.5.5.5"
+ },
+ {
+ "tag": "cloudflare",
+ "address": "1.1.1.1"
+ },
+ {
+ "tag": "google",
+ "address": "8.8.8.8"
+ }
+ ]
+ },
+ "inbounds": ${INBOUNDS_ANCHOR},
+ "outbounds": [
+ {
+ "type": "direct",
+ "tag": "direct-out"
+ },
+ {
+ "type": "block",
+ "tag": "block-out"
+ },
+ {
+ "tag": "dns-out",
+ "type": "dns"
+ },
+ ${PROXY_ANCHOR}
+ ],
+ "route": ${ROUTE_ANCHOR}
+}
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json new file mode 100644 index 0000000..5038c40 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-mobile.json @@ -0,0 +1,11 @@ +[
+ {
+ "tag": "tun-in",
+ "type": "tun",
+ "auto_route": true,
+ "strict_route": true,
+ "address": [ "172.23.0.1/30", "fdfe:acbd:9876::1/126"],
+ "sniff": true,
+ "sniff_override_destination": true
+ }
+]
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json new file mode 100644 index 0000000..956d751 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/sing-inbounds-pc.json @@ -0,0 +1,14 @@ +[
+ {
+ "tag": "http-in",
+ "type": "http",
+ "listen": "127.0.0.1",
+ "listen_port": 3080
+ },
+ {
+ "tag": "socks-in",
+ "type": "socks",
+ "listen": "127.0.0.1",
+ "listen_port": 3081
+ }
+]
\ No newline at end of file diff --git a/store/works/Crupest.SecretTool/build-secret.bash b/store/works/Crupest.SecretTool/build-secret.bash new file mode 100755 index 0000000..8878049 --- /dev/null +++ b/store/works/Crupest.SecretTool/build-secret.bash @@ -0,0 +1,41 @@ +#! /usr/bin/env bash + +set -e + +function print_argument_error_message_and_exit() { + argument_error_message="You must specify exactly one argument, the build target (win-x64 | linux-x64 | osx-x64)." + echo "$argument_error_message" + exit 1 +} + + + +if [[ $# != 1 ]]; then + print_argument_error_message_and_exit +fi + +case "$1" in + win-x64 | linux-x64 | osx-x64) + echo "Build target: $1" + ;; + *) + print_argument_error_message_and_exit + ;; +esac + +secret_dir=$(realpath "$(dirname "$0")") + +echo "Secret dir: ${secret_dir}" + +echo "Check dotnet..." +dotnet --version + +echo "Enter \"secret\" dir..." +pushd "$secret_dir" + +echo "Begin to build..." +dotnet publish Crupest.SecretTool -c Release -o "$secret_dir/publish" --sc -r "$1" + +popd + +echo "Finish!" diff --git a/store/works/Crupest.SecretTool/build-secret.ps1 b/store/works/Crupest.SecretTool/build-secret.ps1 new file mode 100644 index 0000000..8aa7987 --- /dev/null +++ b/store/works/Crupest.SecretTool/build-secret.ps1 @@ -0,0 +1,25 @@ +if ($args.Count -ne 1 || $args[0] -notmatch "^win-x64|linux-x64|osx-x64$") +{ + Write-Error "You must specify exactly one argument, the build target (win-x64 | linux-x64 | osx-x64)." + exit 1 +} + +Write-Output "Secret dir: $PSScriptRoot" + +Write-Output "Check dotnet..." +dotnet --version +if ($LASTEXITCODE -ne 0) +{ + Write-Error "dotnet not found." + exit 2 +} + +Write-Output "Enter `"secret`" dir..." +Push-Location $PSScriptRoot + +Write-Output "Begin to build..." +dotnet publish Crupest.SecretTool -c Release -o "$secret_dir/publish" --sc -r $args[0] + +Pop-Location + +Write-Host "Finish!" -ForegroundColor Green diff --git a/store/works/Crupest.SecretTool/tools/cru-proxy-edit b/store/works/Crupest.SecretTool/tools/cru-proxy-edit new file mode 100755 index 0000000..51a33e1 --- /dev/null +++ b/store/works/Crupest.SecretTool/tools/cru-proxy-edit @@ -0,0 +1,12 @@ +#! /usr/bin/env bash + +set -e + +p="$HOME/codes/crupest/tools/Crupest.SecretTool/publish/proxy.txt" + +if [[ ! -f "$p" ]]; then + echo "File $p does not exist!" >&2 + exit 1 +fi + +exec vim "$p" diff --git a/store/works/Crupest.SecretTool/tools/cru-proxy-log b/store/works/Crupest.SecretTool/tools/cru-proxy-log new file mode 100755 index 0000000..6ec6ee1 --- /dev/null +++ b/store/works/Crupest.SecretTool/tools/cru-proxy-log @@ -0,0 +1,13 @@ +#! /usr/bin/env bash + +set -e + +if [[ -e /proc ]]; then + # I don't believe your system is Linux but there is no /proc. + exec journalctl --user -u crupest-secret-tool "$@" +elif [[ "$(uname)" == "Darwin" ]]; then + exec less "$HOME/.local/state/Crupest.SecretTool/log" +else + echo "Not supported on systems other than macOS and Linux now." >&2 + exit 1 +fi diff --git a/store/works/Crupest.SecretTool/tools/crupest-secret-tool.service b/store/works/Crupest.SecretTool/tools/crupest-secret-tool.service new file mode 100644 index 0000000..df6d172 --- /dev/null +++ b/store/works/Crupest.SecretTool/tools/crupest-secret-tool.service @@ -0,0 +1,8 @@ +[Unit] +Description=crupest v2ray service + +[Service] +ExecStart=%h/.local/bin/Crupest.SecretTool + +[Install] +WantedBy=default.target diff --git a/store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml b/store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml new file mode 100644 index 0000000..9b85f13 --- /dev/null +++ b/store/works/Crupest.SecretTool/tools/crupest-secret-tool.xml @@ -0,0 +1,49 @@ +<!-- + MIT License + + Copyright (c) 2008-2020 Kohsuke Kawaguchi, Sun Microsystems, Inc., CloudBees, + Inc., Oleg Nenashev and other contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +--> + +<!-- + This is a sample configuration of the Windows Service Wrapper. + This configuration file should be placed near the WinSW executable, the name should be the same. + E.g. for myapp.exe the configuration file name should be myapp.xml + + You can find more information about configuration options here: +https://github.com/kohsuke/winsw/blob/master/doc/xmlConfigFile.md +--> +<service> + <id>crupest-secret-tool</id> + <name>Crupest Secret Tool</name> + <description>Crupest Secret Tool (powered by WinSW)</description> + + <!-- Path to the executable, which should be started --> + <executable>%BASE%\Crupest.SecretTool.exe</executable> + + <onfailure action="restart" delay="10 sec" /> + <onfailure action="restart" delay="30 sec" /> + <onfailure action="restart" delay="50 sec" /> + + <workingdirectory>%BASE%</workingdirectory> + + <startmode>Automatic</startmode> +</service>
\ No newline at end of file diff --git a/store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist b/store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist new file mode 100644 index 0000000..bdfe490 --- /dev/null +++ b/store/works/Crupest.SecretTool/tools/life.crupest.secret-tool.plist @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>life.crupest.secret-tool</string> + <key>ProgramArguments</key> + <array> + <string>/Users/crupest/.local/bin/Crupest.SecretTool</string> + </array> + <key>KeepAlive</key> + <true/> + <key>StandardOutPath</key> + <string>/Users/crupest/.local/state/Crupest.SecretTool/log</string> + <key>StandardErrorPath</key> + <string>/Users/crupest/.local/state/Crupest.SecretTool/error</string> +</dict> +</plist> diff --git a/store/works/README.md b/store/works/README.md new file mode 100644 index 0000000..b280870 --- /dev/null +++ b/store/works/README.md @@ -0,0 +1,9 @@ +This directory contains some of my works that I have created. I moved them from individual repositories to here for easier management. Some works are stopped for its temporary purpose. Here are some of the descriptions of them. + +- `life`: Something I created in university time. Some for lecture tasks, some are small toys to solve some interesting problems. Maybe more will be added in future. + +- `solutions`: Answers to leetcode and acwing algorithms questions. Stopped now for a well-known reason. Maybe more will be added in future, too. + +- `teapot`: Answer to the pre-interview test for my first job. Dropped now definitely. + +- `ProxyChanger`: Written a long time ago to solve the proxy switching problem in Firefox for a also well-known problem. |