diff options
Diffstat (limited to 'store')
74 files changed, 3429 insertions, 0 deletions
| diff --git a/store/assets/crupest-transparent.png b/store/assets/crupest-transparent.pngBinary files differ new 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. | 
