diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..f511e7afaf --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,48 @@ +# Copilot instructions for dev-sidecar + +## Commands + +- Dependency management: + - Install dependencies for the entire workspace from the repository root using `pnpm install`. Avoid using `npm install` for dependency installation in this project. + - If `pnpm install` reports dependency conflicts or version mismatches, update the relevant `package.json` entries to compatible versions (or adjust `peerDependencies`), then re-run `pnpm install`. + +- Linting: + - Lint the repository from the root with `pnpm lint`. + - Auto-fix lint issues from the root with `pnpm lint:fix`. + - If `pnpm lint` or `pnpm lint:fix` fails, review the reported errors and manually fix the files indicated by the linter, then re-run the command. + +- Testing: + - Run package tests where they live: + - `pnpm --filter @docmirror/dev-sidecar test` + - `pnpm --filter @docmirror/mitmproxy test` + - Run a single test file by passing it after `--`, for example: + - `pnpm --filter @docmirror/dev-sidecar test -- test/regex.test.js` + - `pnpm --filter @docmirror/mitmproxy test -- test/proxyTest.js` + +- GUI development and packaging (from `packages/gui`): + - `npm run electron` + - `npm run electron:build` + - `npm run serve` + - `npm run lint` + - For GUI debugging: run `npm run electron` and open the Electron developer tools (application menu View → Toggle Developer Tools or the platform shortcut) to inspect renderer pages, console logs, and IPC traffic. + +## High-level architecture + +- This is a pnpm workspace monorepo with four packages: + - `packages/core`: shared app logic, config, shell helpers, system proxy handling, and plugin/module code. + - `packages/mitmproxy`: the HTTP(S) proxy, DNS, interception, PAC, and response/request rewrite layer. + - `packages/gui`: the Electron + Vue 2 desktop app. + - `packages/cli`: a small CLI entrypoint that loads user config and starts the proxy service. +- `packages/core/src/index.js` exposes the main API and owns process-level error handling plus config/state wiring. +- `packages/mitmproxy/src/index.js` creates the proxy server(s), applies proxy options, and reports status/errors back to the host process. +- `packages/gui/src/background.js` is the Electron main process: it loads config, creates the main window, tray, IPC bridges, and Windows-specific power-monitor behavior. +- Renderer code lives under `packages/gui/src/view/`; IPC/bridge code lives under `packages/gui/src/bridge/`. +- The CLI reads `packages/cli/src/user_config.json5`, prints a banner, and starts the core API with the mitmproxy service path. + +## Repo-specific conventions + +- Most runtime code in `core`, `mitmproxy`, and `cli` uses CommonJS; GUI code is the Electron/Vue app and is organized around the Electron main process plus renderer/bridge split. +- Keep GUI source imports aligned with the existing file layout and `.js` module naming used in the Electron entrypoints. +- Preserve the startup/shutdown flow: ensure the sequence remains where `core` initializes and manages the proxy lifecycle, `mitmproxy` performs network interception, and the GUI communicates exclusively through IPC bridges and the `core` API; avoid direct cross-component calls that bypass these boundaries. +- Native/module setup matters here: the repo uses a root `.npmrc` with a PhantomJS mirror and C++17 build flags for native modules. +- The project documents Node 22.x, Python 3.11/setuptools, and VS 2022 C++ tooling as the expected local environment for Windows builds. diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index a874eef652..5127ea19ef 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -1,31 +1,48 @@ -name: Build And Release +name: "Build And Release" on: push: branches: - release* + tags: + - 'v*' + paths-ignore: + - "_script/**" + - "doc/**" + - "**/*.md" + - "**/.gitignore" + - "**/LICENSE" + workflow_dispatch: + +permissions: + contents: write jobs: # job 1 build-and-upload: - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ contains(matrix.os, '-arm') && matrix.os || format('{0}-latest', matrix.os) }} env: ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder + VUE_APP_PUBLISH_PROVIDER: generic strategy: fail-fast: false matrix: os: - windows - ubuntu + # - ubuntu-24.04-arm - macos node: - 22 steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v4.1.7 + with: + submodules: true + fetch-depth: 0 - - name: Setup pnpm + - name: "Setup pnpm" uses: pnpm/action-setup@v4 - name: 'Setup Node.js "${{ matrix.node }}.x" environment' @@ -35,26 +52,25 @@ jobs: registry-url: https://npm.pkg.github.com/ cache: pnpm - - name: Setup Python environment (Mac) Because of electron-builder install-app-deps requires Python setup tools - if: matrix.os == 'macos' + - name: "Setup Python environment, because of electron-builder install-app-deps requires Python setup tools" uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: "3.10" - - name: Get package info + - name: "Get package info" id: package-info uses: luizfelipelaviola/get-package-info@v1 with: - path: ./packages/mitmproxy + path: ./packages/gui - - name: Print + - name: "Print" run: | echo "version = ${{ steps.package-info.outputs.version }}"; echo "github.ref_type = ${{ github.ref_type }}"; echo "github.ref = ${{ github.ref }}"; echo "github.ref_name = ${{ github.ref_name }}"; - - name: 'npm -v | pnpm -v | python --version' + - name: "npm -v | pnpm -v | python --version" run: | echo "======================================================================"; echo "npm -v"; @@ -71,7 +87,7 @@ jobs: echo "--------------------"; python --version; - - name: Setup electron cahce + - name: "Setup electron cache" uses: actions/cache@v4 with: path: ${{ github.workspace }}/.cache/electron @@ -79,7 +95,7 @@ jobs: restore-keys: | ${{ runner.os }}-electron-cache- - - name: Setup electron-builder cahce + - name: "Setup electron-builder cache" uses: actions/cache@v4 with: path: ${{ github.workspace }}/.cache/electron-builder @@ -87,6 +103,17 @@ jobs: restore-keys: | ${{ runner.os }}-electron-builder-cache- + - name: "Set C++17 compiler flags (non-Windows)" + if: ${{ matrix.os != 'windows' }} + run: | + echo "CXXFLAGS=-std=c++17" >> $GITHUB_ENV + echo "CFLAGS=-std=c11" >> $GITHUB_ENV + + - name: "Set C++17 compiler flags (Windows)" + if: ${{ matrix.os == 'windows' }} + run: | + echo "CL=/std:c++17" >> $env:GITHUB_ENV + - name: "'pnpm install' Because we need to install optional dependencies" run: | echo "======================================================================"; @@ -97,17 +124,76 @@ jobs: echo "--------------------"; pnpm install; - - name: 'test packages/core' + - name: "test packages/core" run: | cd packages/core; pnpm run test; - - name: 'test packages/mitmproxy' + - name: "test packages/mitmproxy" run: | cd packages/mitmproxy; pnpm run test; - - name: 'npm run electron:build' + - name: "Cache Flatpak runtime (Linux)" + if: ${{ matrix.os == 'ubuntu' }} + id: cache-flatpak-runtime + uses: actions/cache@v4 + with: + path: ~/.local/share/flatpak + key: flatpak-runtime-freedesktop-21.08 + + - name: "Install Flatpak tooling (Linux)" + if: ${{ matrix.os == 'ubuntu' }} + run: | + sudo apt-get update -y + sudo apt-get install -y flatpak flatpak-builder xdg-desktop-portal appstream fuse3 + # Allow flatpak-builder's bwrap sandbox to create user namespaces on Ubuntu 24.04 + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + # Remove bwrap AppArmor profile if present; it can block sandbox operations beyond userns + sudo apparmor_parser -R /etc/apparmor.d/bwrap 2>/dev/null || true + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + if [ "${{ steps.cache-flatpak-runtime.outputs.cache-hit }}" != "true" ]; then + flatpak install --user -y flathub org.freedesktop.Platform//21.08 org.freedesktop.Sdk//21.08 + fi + flatpak --version + flatpak-builder --version + + - name: "Cache Flatpak runtime (Linux ARM64)" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + id: cache-flatpak-runtime-linux-arm64 + uses: actions/cache@v4 + with: + path: ~/.local/share/flatpak + key: flatpak-runtime-freedesktop-21.08 + + - name: "Install Flatpak tooling (Linux ARM64)" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + run: | + sudo apt-get update -y + sudo apt-get install -y flatpak flatpak-builder xdg-desktop-portal appstream fuse3 + # Allow flatpak-builder's bwrap sandbox to create user namespaces on Ubuntu 24.04 + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + # Remove bwrap AppArmor profile if present; it can block sandbox operations beyond userns + sudo apparmor_parser -R /etc/apparmor.d/bwrap 2>/dev/null || true + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + if [ "${{ steps.cache-flatpak-runtime-linux-arm64.outputs.cache-hit }}" != "true" ]; then + flatpak install --user -y flathub org.freedesktop.Platform//21.08 org.freedesktop.Sdk//21.08 + fi + flatpak --version + flatpak-builder --version + + - name: "Special preparation for Linux ARM64 build" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + run: | + echo "======================================================================"; + echo "Run _script/linux-arm64-prepare.sh"; + echo "--------------------"; + bash _script/linux-arm64-prepare.sh; + + - name: "npm run electron:build (Windows/macOS)" + if: ${{ matrix.os != 'ubuntu' }} + env: + VUE_APP_PUBLISH_URL: https://github.com/docmirror/dev-sidecar/releases/download/v${{ steps.package-info.outputs.version }} run: | echo "======================================================================"; echo "cd packages/gui"; @@ -120,6 +206,23 @@ jobs: echo "--------------------"; npm run electron:build; + - name: "npm run electron:build (Linux & Linux ARM64)" + if: ${{ matrix.os == 'ubuntu' || matrix.os == 'ubuntu-24.04-arm' }} + env: + DEBUG: "@malept/flatpak-bundler" + VUE_APP_PUBLISH_URL: https://github.com/docmirror/dev-sidecar/releases/download/v${{ steps.package-info.outputs.version }} + run: | + echo "======================================================================"; + echo "cd packages/gui"; + echo "--------------------"; + cd packages/gui; + ls -lah; + + echo "======================================================================"; + echo "npm run electron:build"; + echo "--------------------"; + dbus-run-session -- npm run electron:build; + - name: 'Print dir "packages/gui/dist_electron/"' run: | echo "======================================================================"; @@ -129,162 +232,340 @@ jobs: dir || ls -lah; # Rename artifacts - - name: 'Rename artifacts - Windows' + - name: "Rename artifacts - Windows" if: ${{ matrix.os == 'windows' }} run: | cd packages/gui/dist_electron; - ren DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe; + ren DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe; ren DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe; ren DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe; - ren DevSidecar-${{ steps.package-info.outputs.version }}.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe; dir; - - name: 'Rename artifacts - Linux' + - name: "Rename artifacts - Linux" if: ${{ matrix.os == 'ubuntu' }} run: | cd packages/gui/dist_electron; - mv DevSidecar-${{ steps.package-info.outputs.version }}-amd64.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb; + mv DevSidecar-${{ steps.package-info.outputs.version }}-amd64.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb; mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage; - mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz; + mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz; + mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.rpm DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm; + mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.pkg.tar.xz DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz; + # mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.flatpak DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak; #------------------------------------------------------------------------------------------------------------------------- mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb; mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage; mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz; + mv DevSidecar-${{ steps.package-info.outputs.version }}-aarch64.rpm DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm; + mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.pkg.tar.xz DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz; + # arm64 flatpak is on Linux ARM64 runner #------------------------------------------------------------------------------------------------------------------------- mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb; mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage; mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz; + mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.rpm DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm; + # armv7l flatpak is on Linux ARM64 runner ls -lah; - - name: 'Rename artifacts - macOS' + - name: "Rename artifacts - Linux ARM64" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + run: | + mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.flatpak DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.flatpak; + #------------------------------------------------------------------------------------------------------------------------- + mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.flatpak DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.flatpak; + ls -lah; + - name: "Rename artifacts - macOS" if: ${{ matrix.os == 'macos' }} run: | cd packages/gui/dist_electron; - mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg; + mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg; mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg; - mv DevSidecar-${{ steps.package-info.outputs.version }}-universal.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg; ls -lah; - #region Upload artifacts - Windows - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe' + # region Upload artifacts - Windows + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'windows' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe' + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe + name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'windows' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe' + # endregion Upload artifacts - Windows + + # region Upload artifacts - Linux + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" uses: actions/upload-artifact@v4.4.0 - if: ${{ matrix.os == 'windows' }} + if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe' + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" if-no-files-found: error - #endregion Upload artifacts - Windows - - #region Upload artifacts - Linux - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb' + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage' + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" + if-no-files-found: error + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz' + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" if-no-files-found: error - #------------------------------------------------------------------------------------------------------------------------- - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb' + # - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" + # uses: actions/upload-artifact@v4.4.0 + # if: ${{ matrix.os == 'ubuntu' }} + # with: + # path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak + # name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" + # if-no-files-found: error + + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" if-no-files-found: error - #------------------------------------------------------------------------------------------------------------------------- - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" + if-no-files-found: error + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" + if-no-files-found: error + + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" if-no-files-found: error - #endregion Upload artifacts - Linux + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" + if-no-files-found: error + # endregion Upload artifacts - Linux + + # region Upload artifacts - Linux ARM64 + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.flatpak" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.flatpak + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.flatpak" + if-no-files-found: error + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.flatpak" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.flatpak + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.flatpak" + if-no-files-found: error + # endregion Upload artifacts - Linux ARM64 # Upload artifacts - macOS - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'macos' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg' + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg + name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg' + - name: "Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'macos' }} with: path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" + if-no-files-found: error + + # region Upload update ZIPs (for auto-update, per-arch since native modules are arch-specific) + - name: "Upload update ZIP (Win x64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/update-win-x64-${{ steps.package-info.outputs.version }}.zip + name: "update-win-x64-${{ steps.package-info.outputs.version }}.zip" + if-no-files-found: error + - name: "Upload update ZIP (Win ia32)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/update-win-ia32-${{ steps.package-info.outputs.version }}.zip + name: "update-win-ia32-${{ steps.package-info.outputs.version }}.zip" + if-no-files-found: warn + - name: "Upload update ZIP (Win arm64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/update-win-arm64-${{ steps.package-info.outputs.version }}.zip + name: "update-win-arm64-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg' + - name: "Upload update ZIP (macOS x64)" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'macos' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg' + path: packages/gui/dist_electron/update-mac-x64-${{ steps.package-info.outputs.version }}.zip + name: "update-mac-x64-${{ steps.package-info.outputs.version }}.zip" if-no-files-found: error + - name: "Upload update ZIP (macOS arm64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'macos' }} + with: + path: packages/gui/dist_electron/update-mac-arm64-${{ steps.package-info.outputs.version }}.zip + name: "update-mac-arm64-${{ steps.package-info.outputs.version }}.zip" + if-no-files-found: error + - name: "Upload update ZIP (Linux x64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/update-linux-x64-${{ steps.package-info.outputs.version }}.zip + name: "update-linux-x64-${{ steps.package-info.outputs.version }}.zip" + if-no-files-found: error + # endregion Upload update ZIPs + # region Upload auto-update metadata files (latest*.yml + blockmaps + macOS zip target) + - name: "Upload latest.yml (Windows)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/latest.yml + name: "latest.yml" + if-no-files-found: error + - name: "Upload blockmap (Win x64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe.blockmap + name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe.blockmap" + if-no-files-found: error + - name: "Upload blockmap (Win ia32)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe.blockmap + name: "DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe.blockmap" + if-no-files-found: warn + - name: "Upload blockmap (Win arm64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'windows' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe.blockmap + name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe.blockmap" + if-no-files-found: warn + - name: "Upload latest-mac.yml" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'macos' }} + with: + path: packages/gui/dist_electron/latest-mac.yml + name: "latest-mac.yml" + if-no-files-found: error + - name: "Upload blockmap (macOS x64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'macos' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg.blockmap + name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg.blockmap" + if-no-files-found: error + - name: "Upload blockmap (macOS arm64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'macos' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg.blockmap + name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg.blockmap" + if-no-files-found: error + - name: "Upload macOS zip target (x64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'macos' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-x64.zip + name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.zip" + if-no-files-found: error + - name: "Upload macOS zip target (arm64)" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'macos' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-arm64.zip + name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.zip" + if-no-files-found: error + - name: "Upload latest-linux.yml" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/latest-linux.yml + name: "latest-linux.yml" + if-no-files-found: error + # endregion Upload auto-update metadata files # job 2 download-and-release: @@ -292,113 +573,221 @@ jobs: needs: - build-and-upload steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v4.1.7 - - name: Get package info + - name: "Get package info" id: package-info uses: luizfelipelaviola/get-package-info@v1 with: - path: ./packages/mitmproxy + path: ./packages/gui - name: 'Make "release" dir' run: mkdir release # Download artifacts - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-x86_64.exe" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe' + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.deb" path: release - - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.tar.gz" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.rpm" path: release - #------------------------------------------------------------------------------------------------------------------------- - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.pkg.tar.xz" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage' + #- name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" + # uses: actions/download-artifact@v4.1.8 + # with: + # name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.flatpak" + # path: release + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage" path: release - #------------------------------------------------------------------------------------------------------------------------- - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.rpm" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz' + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.pkg.tar.xz" + path: release + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb" + path: release + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage" + path: release + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz" + path: release + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.rpm" + path: release + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-x86_64.dmg" + path: release + - name: "Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg' + # region Download update ZIPs (for auto-update, per-arch) + - name: "Download update ZIP (Win x64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "update-win-x64-${{ steps.package-info.outputs.version }}.zip" + path: release + - name: "Download update ZIP (Win ia32)" + uses: actions/download-artifact@v4.1.8 + with: + name: "update-win-ia32-${{ steps.package-info.outputs.version }}.zip" + path: release + - name: "Download update ZIP (Win arm64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "update-win-arm64-${{ steps.package-info.outputs.version }}.zip" + path: release + - name: "Download update ZIP (macOS x64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "update-mac-x64-${{ steps.package-info.outputs.version }}.zip" + path: release + - name: "Download update ZIP (macOS arm64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "update-mac-arm64-${{ steps.package-info.outputs.version }}.zip" + path: release + - name: "Download update ZIP (Linux x64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "update-linux-x64-${{ steps.package-info.outputs.version }}.zip" + path: release + # endregion Download update ZIPs + + # region Download auto-update metadata files (latest*.yml + blockmaps + macOS zip target) + - name: "Download latest.yml" + uses: actions/download-artifact@v4.1.8 + with: + name: "latest.yml" + path: release + - name: "Download blockmap (Win x64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe.blockmap" + path: release + - name: "Download blockmap (Win ia32)" + uses: actions/download-artifact@v4.1.8 + continue-on-error: true + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe.blockmap" + path: release + - name: "Download blockmap (Win arm64)" + uses: actions/download-artifact@v4.1.8 + continue-on-error: true + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe.blockmap" + path: release + - name: "Download latest-mac.yml" + uses: actions/download-artifact@v4.1.8 + with: + name: "latest-mac.yml" + path: release + - name: "Download blockmap (macOS x64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg.blockmap" + path: release + - name: "Download blockmap (macOS arm64)" + uses: actions/download-artifact@v4.1.8 + with: + name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg.blockmap" + path: release + - name: "Download macOS zip target (x64)" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-x64.zip" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg' + - name: "Download macOS zip target (arm64)" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg' + name: "DevSidecar-${{ steps.package-info.outputs.version }}-arm64.zip" path: release - - name: 'Download DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg' + - name: "Download latest-linux.yml" uses: actions/download-artifact@v4.1.8 with: - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg' + name: "latest-linux.yml" path: release + # endregion Download auto-update metadata files - name: 'Print files from "release" dir' run: | ls -lah release; - - name: Create a draft release + - name: "Create a draft release" uses: wangliang181230/github-action-ghr@master env: GITHUB_TOKEN: ${{ github.token }} GHR_PATH: release/ GHR_TITLE: ${{ github.ref_name }} GHR_REPLACE: true - GHR_DRAFT: true + GHR_DRAFT: true \ No newline at end of file diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml new file mode 100644 index 0000000000..9c3ebf8dac --- /dev/null +++ b/.github/workflows/build-cli.yml @@ -0,0 +1,101 @@ +name: "Build CLI (SEA)" + +on: + push: + branches: + - release* + tags: + - 'v*' + paths: + - "packages/cli/**" + - "packages/core/**" + - "packages/mitmproxy/**" + - "pnpm-workspace.yaml" + - "pnpm-lock.yaml" + - "package.json" + - ".github/workflows/build-cli.yml" + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + env: + ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron + ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder + steps: + - name: "Checkout" + uses: actions/checkout@v4.1.7 + + - name: "Setup pnpm" + uses: pnpm/action-setup@v4 + + - name: "Setup Node.js 22.x" + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://npm.pkg.github.com/ + cache: pnpm + + - name: "Install dependencies" + run: pnpm install + + - name: "Test CLI" + run: pnpm --filter @docmirror/dev-sidecar-cli test + + - name: "Cache Node.js binaries (node-bin)" + uses: actions/cache@v4 + with: + path: packages/cli/dist/node-bin + key: cli-node-bin-${{ runner.os }}-${{ hashFiles('packages/cli/scripts/build.js') }} + + - name: "Build all platforms (SEA)" + run: node packages/cli/scripts/build.js --all + + - name: "Generate SHA256SUMS" + run: | + cd packages/cli/dist + sha256sum ds-cli-* > SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: "Print dist" + run: | + ls -lah packages/cli/dist/ds-cli-* + file packages/cli/dist/ds-cli-* 2>/dev/null || true + + - name: "Upload artifacts" + uses: actions/upload-artifact@v4.4.0 + with: + name: ds-cli-${{ github.sha }} + path: | + packages/cli/dist/ds-cli-* + packages/cli/dist/SHA256SUMS.txt + if-no-files-found: error + + release: + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: "Download artifacts" + uses: actions/download-artifact@v4.1.8 + with: + pattern: ds-cli-* + merge-multiple: true + path: release + + - name: "Print release dir" + run: | + chmod +x release/ds-cli-* + ls -lah release/ + + - name: "Create release" + uses: wangliang181230/github-action-ghr@master + env: + GITHUB_TOKEN: ${{ github.token }} + GHR_PATH: release/ + GHR_TITLE: ${{ github.ref_name }} + GHR_REPLACE: true + GHR_DRAFT: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..3316db6c27 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '37 23 * * *' + workflow_dispatch: + + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/defender-for-devops.yml b/.github/workflows/defender-for-devops.yml new file mode 100644 index 0000000000..12f76f47b2 --- /dev/null +++ b/.github/workflows/defender-for-devops.yml @@ -0,0 +1,51 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# +# Microsoft Security DevOps (MSDO) is a command line application which integrates static analysis tools into the development cycle. +# MSDO installs, configures and runs the latest versions of static analysis tools +# (including, but not limited to, SDL/security and compliance tools). +# +# The Microsoft Security DevOps action is currently in beta and runs on the windows-latest queue, +# as well as Windows self hosted agents. ubuntu-latest support coming soon. +# +# For more information about the action , check out https://github.com/microsoft/security-devops-action +# +# Please note this workflow do not integrate your GitHub Org with Microsoft Defender For DevOps. You have to create an integration +# and provide permission before this can report data back to azure. +# Read the official documentation here : https://learn.microsoft.com/en-us/azure/defender-for-cloud/quickstart-onboard-github + +name: "Microsoft Defender For Devops" + +permissions: + contents: read + security-events: write + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '36 8 * * 1' + +jobs: + MSDO: + # currently only windows latest is supported + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 5.0.x + 6.0.x + - name: Run Microsoft Security DevOps + uses: microsoft/security-devops-action@v1.6.0 + id: msdo + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.msdo.outputs.sarifFile }} diff --git a/.github/workflows/npm-run-electron.yml b/.github/workflows/npm-run-electron.yml index 7684d10983..5517d1c8c4 100644 --- a/.github/workflows/npm-run-electron.yml +++ b/.github/workflows/npm-run-electron.yml @@ -1,4 +1,4 @@ -name: npm run electron +name: "npm run electron" on: push: @@ -7,15 +7,19 @@ on: - test* - release* paths-ignore: - - '_script/**' - - 'doc/**' - - '**/*.md' - - '**/.gitignore' - - '**/LICENSE' + - "_script/**" + - "doc/**" + - "**/*.md" + - "**/.gitignore" + - "**/LICENSE" + workflow_dispatch: + +permissions: + contents: read jobs: npm-run-electron: - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ contains(matrix.os, '-arm') && matrix.os || format('{0}-latest', matrix.os) }} env: ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder @@ -25,14 +29,18 @@ jobs: os: - windows - ubuntu + # - ubuntu-24.04-arm # 因难以申请到 ARM64 runner而放弃;注意该系统名称不应使用latest后缀 - macos node: - 22 steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v4.1.7 + with: + submodules: true + fetch-depth: 0 - - name: Setup pnpm + - name: "Setup pnpm" uses: pnpm/action-setup@v4 - name: 'Setup Node.js "${{ matrix.node }}.x" environment' @@ -42,19 +50,18 @@ jobs: registry-url: https://npm.pkg.github.com/ cache: pnpm - - name: Setup Python environment (Mac) Because of electron-builder install-app-deps requires Python setup tools - if: matrix.os == 'macos' + - name: "Setup Python environment, because of electron-builder install-app-deps requires Python setup tools" uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: "3.10" - - name: Print + - name: "Print" run: | echo "github.ref_type = ${{ github.ref_type }}"; echo "github.ref = ${{ github.ref }}"; echo "github.ref_name = ${{ github.ref_name }}"; - - name: 'npm -v | pnpm -v | python --version' + - name: "npm -v | pnpm -v | python --version" run: | echo "======================================================================"; echo "npm -v"; @@ -71,7 +78,7 @@ jobs: echo "--------------------"; python --version; - - name: Setup electron cahce + - name: "Setup electron cache" uses: actions/cache@v4 with: path: ${{ github.workspace }}/.cache/electron @@ -79,7 +86,7 @@ jobs: restore-keys: | ${{ runner.os }}-electron-cache- - - name: Setup electron-builder cahce + - name: "Setup electron-builder cache" uses: actions/cache@v4 with: path: ${{ github.workspace }}/.cache/electron-builder @@ -87,7 +94,18 @@ jobs: restore-keys: | ${{ runner.os }}-electron-builder-cache- - - name: pnpm install + - name: "Set C++17 compiler flags (Linux / macOS)" + if: ${{ matrix.os != 'windows' }} + run: | + echo "CXXFLAGS=-std=c++17" >> $GITHUB_ENV + echo "CFLAGS=-std=c11" >> $GITHUB_ENV + + - name: "Set C++17 compiler flags (Windows)" + if: ${{ matrix.os == 'windows' }} + run: | + echo "CL=/std:c++17" >> $env:GITHUB_ENV + + - name: "pnpm install" run: | echo "======================================================================"; dir || ls -lah; @@ -97,12 +115,10 @@ jobs: echo "--------------------"; pnpm install; - - name: npm run electron + - name: "npm run electron" + working-directory: packages/gui run: | echo "======================================================================"; - echo "cd packages/gui"; - echo "--------------------"; - cd packages/gui; dir || ls -lah; echo "======================================================================"; diff --git a/.github/workflows/publish-to-aur.yml b/.github/workflows/publish-to-aur.yml new file mode 100644 index 0000000000..f1d846f75b --- /dev/null +++ b/.github/workflows/publish-to-aur.yml @@ -0,0 +1,127 @@ +name: "Publish To AUR" + +on: + release: + types: + - published + workflow_dispatch: + inputs: + version: + description: "Package version to publish (e.g. 2.0.2). Leave empty to use the latest release tag." + required: false + type: string + +jobs: + publish-to-aur: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: "Checkout" + uses: actions/checkout@v4.1.7 + + # ── Resolve version ────────────────────────────────────────────────────── + - name: "Resolve version" + id: version + run: | + if [[ -n "${{ inputs.version }}" ]]; then + VERSION="${{ inputs.version }}" + else + # Strip leading 'v' from the release tag (e.g. v2.0.2 → 2.0.2) + VERSION="${GITHUB_REF_NAME#v}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Resolved version: ${VERSION}" + + # ── Download release assets and compute SHA256 checksums ───────────────── + - name: "Download Linux x86_64 tar.gz and compute SHA256" + id: sha256_x86_64 + run: | + VERSION="${{ steps.version.outputs.version }}" + URL="https://github.com/docmirror/dev-sidecar/releases/download/v${VERSION}/DevSidecar-${VERSION}-linux-x86_64.tar.gz" + echo "Downloading: ${URL}" + curl -fsSL -o /tmp/dev-sidecar-x86_64.tar.gz "${URL}" + SHA256=$(sha256sum /tmp/dev-sidecar-x86_64.tar.gz | awk '{print $1}') + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "x86_64 SHA256: ${SHA256}" + + - name: "Download Linux aarch64 tar.gz and compute SHA256" + id: sha256_aarch64 + run: | + VERSION="${{ steps.version.outputs.version }}" + URL="https://github.com/docmirror/dev-sidecar/releases/download/v${VERSION}/DevSidecar-${VERSION}-linux-arm64.tar.gz" + echo "Downloading: ${URL}" + curl -fsSL -o /tmp/dev-sidecar-aarch64.tar.gz "${URL}" + SHA256=$(sha256sum /tmp/dev-sidecar-aarch64.tar.gz | awk '{print $1}') + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "aarch64 SHA256: ${SHA256}" + + # ── Update PKGBUILD ─────────────────────────────────────────────────────── + - name: "Update PKGBUILD with new version and checksums" + run: | + VERSION="${{ steps.version.outputs.version }}" + SHA256_X86_64="${{ steps.sha256_x86_64.outputs.sha256 }}" + SHA256_AARCH64="${{ steps.sha256_aarch64.outputs.sha256 }}" + + PKGBUILD="packages/aur/PKGBUILD" + + # Update pkgver + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "${PKGBUILD}" + + # Reset pkgrel to 1 on every new upstream version + sed -i "s/^pkgrel=.*/pkgrel=1/" "${PKGBUILD}" + + # Update checksums + sed -i "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('${SHA256_X86_64}')/" "${PKGBUILD}" + sed -i "s/^sha256sums_aarch64=.*/sha256sums_aarch64=('${SHA256_AARCH64}')/" "${PKGBUILD}" + + echo "======== Updated PKGBUILD ========" + cat "${PKGBUILD}" + + # ── Push to AUR via git + SSH ───────────────────────────────────────────── + - name: "Set up SSH key for AUR" + run: | + install -dm700 ~/.ssh + + # Write the private key + echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur_id + chmod 600 ~/.ssh/aur_id + + # Trust AUR's host key + ssh-keyscan -t rsa,ecdsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null + chmod 644 ~/.ssh/known_hosts + + # SSH config: use this key for AUR (printf avoids heredoc indentation issues) + printf 'Host aur.archlinux.org\n IdentityFile ~/.ssh/aur_id\n User aur\n' >> ~/.ssh/config + chmod 600 ~/.ssh/config + + - name: "Clone AUR repository" + run: | + git clone ssh://aur@aur.archlinux.org/dev-sidecar-bin.git /tmp/aur-dev-sidecar-bin + + - name: "Copy PKGBUILD and generate .SRCINFO" + run: | + cp packages/aur/PKGBUILD /tmp/aur-dev-sidecar-bin/PKGBUILD + + # Generate .SRCINFO via helper script (makepkg is not available on the ubuntu + # runner since pacman is not installed) + cd /tmp/aur-dev-sidecar-bin + python3 "$GITHUB_WORKSPACE/packages/aur/gen_srcinfo.py" + + - name: "Commit and push to AUR" + run: | + cd /tmp/aur-dev-sidecar-bin + + git config user.name "${{ vars.AUR_USERNAME }}" + git config user.email "${{ vars.AUR_EMAIL }}" + + git add PKGBUILD .SRCINFO + + # Only commit if there are actual changes + if git diff --cached --quiet; then + echo "No changes to commit – AUR is already up to date." + else + git commit -m "Update to v${{ steps.version.outputs.version }}" + git push + echo "Successfully pushed to AUR." + fi diff --git a/.github/workflows/test-and-upload.yml b/.github/workflows/test-and-upload.yml index a270ef159b..e952d8ebc0 100644 --- a/.github/workflows/test-and-upload.yml +++ b/.github/workflows/test-and-upload.yml @@ -1,4 +1,7 @@ -name: Test And Upload +name: "Test And Upload" + +permissions: + contents: read on: push: @@ -7,44 +10,51 @@ on: - 1.x - develop - test* + - fix* paths-ignore: - - '_script/**' - - 'doc/**' - - '**/*.md' - - '**/.gitignore' - - '**/LICENSE' + - "_script/**" + - "doc/**" + - "**/*.md" + - "**/.gitignore" + - "**/LICENSE" pull_request: branches: - master - develop - 1.x paths-ignore: - - '_script/**' - - 'doc/**' - - '**/*.md' - - '**/.gitignore' - - '**/LICENSE' + - "_script/**" + - "doc/**" + - "**/*.md" + - "**/.gitignore" + - "**/LICENSE" + workflow_dispatch: jobs: test-and-upload: - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ contains(matrix.os, '-arm') && matrix.os || format('{0}-latest', matrix.os) }} env: ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder + VUE_APP_PUBLISH_CHANNEL: beta strategy: fail-fast: false matrix: os: - windows - ubuntu + # - ubuntu-24.04-arm - macos node: - 22 steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v4.1.7 + with: + submodules: true + fetch-depth: 0 - - name: Setup pnpm + - name: "Setup pnpm" uses: pnpm/action-setup@v4 - name: 'Setup Node.js "${{ matrix.node }}.x" environment' @@ -54,26 +64,38 @@ jobs: registry-url: https://npm.pkg.github.com/ cache: pnpm - - name: Setup Python environment (Mac) Because of electron-builder install-app-deps requires Python setup tools - if: matrix.os == 'macos' + - name: "Setup Python environment, because of electron-builder install-app-deps requires Python setup tools" uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: "3.10" - - name: Get package info + - name: "Get package info" id: package-info uses: luizfelipelaviola/get-package-info@v1 with: - path: ./packages/mitmproxy + path: ./packages/gui + + - name: "Compute beta build version" + shell: bash + run: | + BASE_VERSION="${{ steps.package-info.outputs.version }}" + BUILD_VERSION="${BASE_VERSION}-beta.${{ github.run_number }}" + echo "BUILD_VERSION=${BUILD_VERSION}" >> "$GITHUB_ENV" - - name: Print + - name: "Apply beta version to packages/gui/package.json" + shell: bash + run: | + node -e "const fs=require('fs');const p='packages/gui/package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.env.BUILD_VERSION;fs.writeFileSync(p,JSON.stringify(j,null,2)+'\\n');console.log('set gui version to',j.version);" + + - name: "Print" run: | echo "version = ${{ steps.package-info.outputs.version }}"; + echo "build version = ${{ env.BUILD_VERSION }}"; echo "github.ref_type = ${{ github.ref_type }}"; echo "github.ref = ${{ github.ref }}"; echo "github.ref_name = ${{ github.ref_name }}"; - - name: 'npm -v | pnpm -v | python --version' + - name: "npm -v | pnpm -v | python --version" run: | echo "======================================================================"; echo "npm -v"; @@ -90,7 +112,7 @@ jobs: echo "--------------------"; python --version; - - name: Setup electron cahce + - name: "Setup electron cache" uses: actions/cache@v4 with: path: ${{ github.workspace }}/.cache/electron @@ -98,7 +120,7 @@ jobs: restore-keys: | ${{ runner.os }}-electron-cache- - - name: Setup electron-builder cahce + - name: "Setup electron-builder cache" uses: actions/cache@v4 with: path: ${{ github.workspace }}/.cache/electron-builder @@ -106,6 +128,17 @@ jobs: restore-keys: | ${{ runner.os }}-electron-builder-cache- + - name: "Set C++17 compiler flags (non-Windows)" + if: ${{ matrix.os != 'windows' }} + run: | + echo "CXXFLAGS=-std=c++17" >> $GITHUB_ENV + echo "CFLAGS=-std=c11" >> $GITHUB_ENV + + - name: "Set C++17 compiler flags (Windows)" + if: ${{ matrix.os == 'windows' }} + run: | + echo "CL=/std:c++17" >> $env:GITHUB_ENV + - name: "'pnpm install' Because we need to install optional dependencies" run: | echo "======================================================================"; @@ -116,17 +149,74 @@ jobs: echo "--------------------"; pnpm install; - - name: 'test packages/core' + - name: "test packages/core" run: | cd packages/core; pnpm run test; - - name: 'test packages/mitmproxy' + - name: "test packages/mitmproxy" run: | cd packages/mitmproxy; pnpm run test; - - name: 'npm run electron:build' + - name: "Cache Flatpak runtime (Linux)" + if: ${{ matrix.os == 'ubuntu' }} + id: cache-flatpak-runtime + uses: actions/cache@v4 + with: + path: ~/.local/share/flatpak + key: flatpak-runtime-freedesktop-21.08 + + - name: "Install Flatpak tooling (Linux)" + if: ${{ matrix.os == 'ubuntu' }} + run: | + sudo apt-get update -y + sudo apt-get install -y flatpak flatpak-builder xdg-desktop-portal appstream fuse3 + # Allow flatpak-builder's bwrap sandbox to create user namespaces on Ubuntu 24.04 + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + # Remove bwrap AppArmor profile if present; it can block sandbox operations beyond userns + sudo apparmor_parser -R /etc/apparmor.d/bwrap 2>/dev/null || true + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + if [ "${{ steps.cache-flatpak-runtime.outputs.cache-hit }}" != "true" ]; then + flatpak install --user -y flathub org.freedesktop.Platform//21.08 org.freedesktop.Sdk//21.08 + fi + flatpak --version + flatpak-builder --version + + - name: "Cache Flatpak runtime (Linux ARM64)" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + id: cache-flatpak-runtime-linux-arm64 + uses: actions/cache@v4 + with: + path: ~/.local/share/flatpak + key: flatpak-runtime-freedesktop-21.08 + + - name: "Install Flatpak tooling (Linux ARM64)" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + run: | + sudo apt-get update -y + sudo apt-get install -y flatpak flatpak-builder xdg-desktop-portal appstream fuse3 + # Allow flatpak-builder's bwrap sandbox to create user namespaces on Ubuntu 24.04 + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + # Remove bwrap AppArmor profile if present; it can block sandbox operations beyond userns + sudo apparmor_parser -R /etc/apparmor.d/bwrap 2>/dev/null || true + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + if [ "${{ steps.cache-flatpak-runtime-linux-arm64.outputs.cache-hit }}" != "true" ]; then + flatpak install --user -y flathub org.freedesktop.Platform//21.08 org.freedesktop.Sdk//21.08 + fi + flatpak --version + flatpak-builder --version + + - name: "Special preparation for Linux ARM64 build" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + run: | + echo "======================================================================"; + echo "Run _script/linux-arm64-prepare.sh"; + echo "--------------------"; + bash _script/linux-arm64-prepare.sh; + + - name: "npm run electron:build (Windows/macOS)" + if: ${{ matrix.os != 'ubuntu' }} run: | echo "======================================================================"; echo "cd packages/gui"; @@ -139,6 +229,22 @@ jobs: echo "--------------------"; npm run electron:build; + - name: "npm run electron:build (Linux & Linux ARM64)" + if: ${{ matrix.os == 'ubuntu' || matrix.os == 'ubuntu-24.04-arm' }} + env: + DEBUG: "@malept/flatpak-bundler" + run: | + echo "======================================================================"; + echo "cd packages/gui"; + echo "--------------------"; + cd packages/gui; + ls -lah; + + echo "======================================================================"; + echo "npm run electron:build"; + echo "--------------------"; + dbus-run-session -- npm run electron:build; + - name: 'Print dir "packages/gui/dist_electron/"' run: | echo "======================================================================"; @@ -148,158 +254,202 @@ jobs: dir || ls -lah; # Rename artifacts - - name: 'Rename artifacts - Windows' + - name: "Rename artifacts - Windows" if: ${{ matrix.os == 'windows' }} run: | cd packages/gui/dist_electron; - ren DevSidecar-${{ steps.package-info.outputs.version }}-x64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe; - ren DevSidecar-${{ steps.package-info.outputs.version }}-ia32.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe; - ren DevSidecar-${{ steps.package-info.outputs.version }}-arm64.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe; - ren DevSidecar-${{ steps.package-info.outputs.version }}.exe DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe; + ren DevSidecar-${{ env.BUILD_VERSION }}-x64.exe DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe; + ren DevSidecar-${{ env.BUILD_VERSION }}-ia32.exe DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe; + ren DevSidecar-${{ env.BUILD_VERSION }}-arm64.exe DevSidecar-${{ env.BUILD_VERSION }}-windows-arm64.exe; dir; - - name: 'Rename artifacts - Linux' + - name: "Rename artifacts - Linux" if: ${{ matrix.os == 'ubuntu' }} run: | cd packages/gui/dist_electron; - mv DevSidecar-${{ steps.package-info.outputs.version }}-amd64.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb; - mv DevSidecar-${{ steps.package-info.outputs.version }}-x86_64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage; - mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz; + mv DevSidecar-${{ env.BUILD_VERSION }}-amd64.deb DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb; + mv DevSidecar-${{ env.BUILD_VERSION }}-x86_64.AppImage DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage; + mv DevSidecar-${{ env.BUILD_VERSION }}-x64.tar.gz DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz; + mv DevSidecar-${{ env.BUILD_VERSION }}-x86_64.rpm DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm; + # mv DevSidecar-${{ env.BUILD_VERSION }}-x86_64.flatpak DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.flatpak; #------------------------------------------------------------------------------------------------------------------------- - mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb; - mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage; - mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz; + mv DevSidecar-${{ env.BUILD_VERSION }}-arm64.deb DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb; + mv DevSidecar-${{ env.BUILD_VERSION }}-arm64.AppImage DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage; + mv DevSidecar-${{ env.BUILD_VERSION }}-arm64.tar.gz DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz; + mv DevSidecar-${{ env.BUILD_VERSION }}-aarch64.rpm DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.rpm; + # arm64 flatpak is on Linux ARM64 runner #------------------------------------------------------------------------------------------------------------------------- - mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.deb DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb; - mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.AppImage DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage; - mv DevSidecar-${{ steps.package-info.outputs.version }}-armv7l.tar.gz DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz; + mv DevSidecar-${{ env.BUILD_VERSION }}-armv7l.deb DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb; + mv DevSidecar-${{ env.BUILD_VERSION }}-armv7l.AppImage DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage; + mv DevSidecar-${{ env.BUILD_VERSION }}-armv7l.tar.gz DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz; + mv DevSidecar-${{ env.BUILD_VERSION }}-armv7l.rpm DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.rpm; + # armv7l flatpak is on Linux ARM64 runner ls -lah; - - name: 'Rename artifacts - macOS' + - name: "Rename artifacts - Linux ARM64" + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + run: | + mv DevSidecar-${{ env.BUILD_VERSION }}-arm64.flatpak DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.flatpak; + #------------------------------------------------------------------------------------------------------------------------- + mv DevSidecar-${{ env.BUILD_VERSION }}-armv7l.flatpak DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.flatpak; + ls -lah; + - name: "Rename artifacts - macOS" if: ${{ matrix.os == 'macos' }} run: | cd packages/gui/dist_electron; - mv DevSidecar-${{ steps.package-info.outputs.version }}-x64.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg; - mv DevSidecar-${{ steps.package-info.outputs.version }}-arm64.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg; - mv DevSidecar-${{ steps.package-info.outputs.version }}-universal.dmg DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg; + mv DevSidecar-${{ env.BUILD_VERSION }}-x64.dmg DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg; + mv DevSidecar-${{ env.BUILD_VERSION }}-arm64.dmg DevSidecar-${{ env.BUILD_VERSION }}-macos-arm64.dmg; ls -lah; - #region Upload artifacts - Windows - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe' + # region Upload artifacts - Windows + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'windows' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-x64.exe' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe + name: "DevSidecar-${{ env.BUILD_VERSION }}-windows-x86_64.exe" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'windows' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-ia32.exe' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe + name: "DevSidecar-${{ env.BUILD_VERSION }}-windows-ia32.exe" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-windows-arm64.exe" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'windows' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-arm64.exe' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-windows-arm64.exe + name: "DevSidecar-${{ env.BUILD_VERSION }}-windows-arm64.exe" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe' + # endregion Upload artifacts - Windows + + # region Upload artifacts - Linux + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb" uses: actions/upload-artifact@v4.4.0 - if: ${{ matrix.os == 'windows' }} + if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-windows-universal.exe' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.deb" if-no-files-found: error - #endregion Upload artifacts - Windows - - #region Upload artifacts - Linux - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-amd64.deb' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.AppImage" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x86_64.AppImage' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.tar.gz" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-x64.tar.gz' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.rpm" if-no-files-found: error - #------------------------------------------------------------------------------------------------------------------------- - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb' + # - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.flatpak" + # uses: actions/upload-artifact@v4.4.0 + # if: ${{ matrix.os == 'ubuntu' }} + # with: + # path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.flatpak + # name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-x86_64.flatpak" + # if-no-files-found: error + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.deb' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.deb" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.AppImage' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.AppImage" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-arm64.tar.gz' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.tar.gz" if-no-files-found: error - #------------------------------------------------------------------------------------------------------------------------- - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.rpm" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.deb' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.rpm + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.rpm" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage' + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.AppImage' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.deb" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'ubuntu' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-linux-armv7l.tar.gz' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.AppImage" if-no-files-found: error - #endregion Upload artifacts - Linux + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.tar.gz" + if-no-files-found: error + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.rpm" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.rpm + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.rpm" + if-no-files-found: error + # endregion Upload artifacts - Linux - # Upload artifacts - macOS - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg' + # region Upload artifacts - Linux ARM64 + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.flatpak" uses: actions/upload-artifact@v4.4.0 - if: ${{ matrix.os == 'macos' }} + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-x64.dmg' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.flatpak + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-arm64.flatpak" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg' + # ------------------------------------------------------------------------------------------------------------------------- + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.flatpak" + uses: actions/upload-artifact@v4.4.0 + if: ${{ matrix.os == 'ubuntu-24.04-arm' }} + with: + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.flatpak + name: "DevSidecar-${{ env.BUILD_VERSION }}-linux-armv7l.flatpak" + if-no-files-found: error + # endregion Upload artifacts - Linux ARM64 + + # Upload artifacts - macOS + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'macos' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-arm64.dmg' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg + name: "DevSidecar-${{ env.BUILD_VERSION }}-macos-x86_64.dmg" if-no-files-found: error - - name: 'Upload DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg' + - name: "Upload DevSidecar-${{ env.BUILD_VERSION }}-macos-arm64.dmg" uses: actions/upload-artifact@v4.4.0 if: ${{ matrix.os == 'macos' }} with: - path: packages/gui/dist_electron/DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg - name: 'DevSidecar-${{ steps.package-info.outputs.version }}-macos-universal.dmg' + path: packages/gui/dist_electron/DevSidecar-${{ env.BUILD_VERSION }}-macos-arm64.dmg + name: "DevSidecar-${{ env.BUILD_VERSION }}-macos-arm64.dmg" if-no-files-found: error + # endregion Upload artifacts - macOS \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1152e9c7ba..e613c2a615 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,9 @@ out gen *.log *.lnk +.venv +packages/cli/dist/* +!packages/cli/dist/sea-config.json +packages/cli/.nyc_output +.mimocode +.claude diff --git a/.npmrc b/.npmrc index bf2e7648b0..5d88899063 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,9 @@ -shamefully-hoist=true +registry=https://registry.npmmirror.com +ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron +# Native modules (e.g. @parcel/watcher) require C++17. +# In CI, CXXFLAGS=-std=c++17 (Linux/macOS) and CL=/std:c++17 (Windows) are +# set before `pnpm install` so node-gyp compiles with the correct standard. +# For local builds on Linux/macOS set it in your shell: +# export CXXFLAGS="-std=c++17" +# For local builds on Windows set in your shell: +# set CL=/std:c++17 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..902b2c90c8 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..e04b7a23e0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Dependency Management +- Always use `pnpm install` (not `npm install`) from the repository root. The project uses pnpm workspaces with `shamefully-hoist=true`. +- If `pnpm install` reports conflicts, update the relevant `package.json` entries to compatible versions and re-run. + +### Linting +- `pnpm lint` — lint the entire repo (ESLint flat config via `@antfu/eslint-config`) +- `pnpm lint:fix` — auto-fix lint issues + +### Testing +- `pnpm --filter @docmirror/dev-sidecar test` — run core package tests (Mocha + Chai) +- `pnpm --filter @docmirror/mitmproxy test` — run proxy package tests +- Run a single test file: + - `pnpm --filter @docmirror/dev-sidecar test -- test/regex.test.js` + - `pnpm --filter @docmirror/mitmproxy test -- test/proxyTest.js` + +### GUI Development (from `packages/gui/`) +- `npm run electron` — launch the Electron app in dev mode (starts Vue dev server + Electron) +- `npm run serve` — run only the Vue dev server (port 8080) +- `npm run electron:build` — production build (Vue build + electron-builder) +- `npm run lint` — lint GUI code only +- For debugging: run `npm run electron` and open DevTools (`F12` or View → Toggle Developer Tools) + +### Python Environment (needed for native module builds) +```shell +uv init . +uv sync +.venv/Scripts/activate # Windows; use `source .venv/bin/activate` on Linux/macOS +``` + +## Committing Changes + +When work is finished and ready to commit, the AI assistant stages files, but the **human runs the commit command manually** — every contributor signs their own commits with their personal GPG/SSH key, which the assistant cannot access. + +1. **Review first**: run `git status`, `git diff`, and `git log --oneline -10` (to match the repo's message style). Stage only intended files; never stage secrets, build artifacts, or unrelated changes. +2. **Stage properly**: use `git add ` for new/modified files and `git rm ` for deletions (or `git add -u` to record filesystem deletions). Verify the staged set with `git status` before proceeding. +3. **Do NOT run `git commit` yourself.** Instead, print the **full `git commit` command** and let the user execute it manually, e.g.: + ``` + git commit -m "fix(cli): 修复 xxx" + ``` +4. **Commit message style**: follow the repo's convention — a `type(scope): subject` prefix (`fix(scope):`, `feat(scope):`, `chore:`, ...) with a Chinese summary; include a body of bullet points for non-trivial changes. Do not add signing flags (`--no-gpg-sign`, `--no-verify`) or commit/push on the user's behalf unless explicitly requested. +5. **Push only when explicitly asked**, and only after the user has committed. + +## Architecture + +This is a **pnpm workspace monorepo** (`pnpm@9.13.2`) for a developer-sidecar proxy tool that accelerates access to GitHub, npm, Docker Hub, and other foreign sites for Chinese developers. It works by running a local MITM HTTPS proxy, injecting a root CA certificate, and applying DNS optimization, SNI rewriting, and request interception/redirection rules. + +### Package dependency graph +``` +gui ──depends-on──> core ──forks-as-child-process──> mitmproxy + ^ ^ +cli ──────────────────┴────────────────────────────────┘ +``` + +### Packages + +**`packages/core`** (`@docmirror/dev-sidecar`) — The orchestrator. +- Entry: `src/index.js` → `src/expose.js`. Exports `startup()`, `shutdown()`, plus `config`, `event`, `shell`, `server`, `proxy`, `plugin`, `status`. +- Startup sequence: merge config → fork mitmproxy child process → set OS-level system proxy → start plugins (git, node, pip, overwall). +- Config merges 4 layers: defaults (`src/config/index.js`, ~470 lines) → remote shared → remote personal → user overrides (`~/.dev-sidecar/config.json`). +- Shell helpers (`src/shell/`) abstract OS commands: setting system proxy, installing CA certs, enabling loopback, killing processes by port. +- Plugins (`src/modules/plugin/`) follow a uniform `{ key, config, status, plugin: Factory(context) }` pattern. + +**`packages/mitmproxy`** (`@docmirror/mitmproxy`) — The proxy engine (runs as a child process). +- Entry: `src/index.js`. Creates HTTP and HTTPS proxy servers on consecutive ports (default: 31180 HTTP, 31181 HTTPS). +- Interceptor pipeline (`src/lib/interceptor/`): priority-ordered interceptors match domains+paths and apply actions (redirect, proxy, abort, cache, SNI rewrite, OPTIONS preflight, response replace, script injection). +- TLS/cert handling (`src/lib/proxy/tls/`): generates a local CA root cert (`~/.dev-sidecar/dev-sidecar.ca.crt`), then creates per-domain fake certs signed by it using `node-forge`. Fake servers are LRU-cached. +- DNS system (`src/lib/dns/`): multi-provider DNS resolution (UDP, TCP, DoH, DoT, preset IPs). Supports SNI-specific DNS lookup. +- Speed test (`src/lib/speed/`): measures latency/availability to domains, used for IP selection. +- `RequestCounter` (`src/lib/choice/`): dynamic backup failover — tracks success/failure per backend, switches after 3 consecutive errors or <40% success rate. + +**`packages/gui`** (`@docmirror/dev-sidecar-gui`) — Electron + Vue 3 desktop app. +- Main process: `src/background.js` — creates BrowserWindow, system tray, IPC bridges, single-instance lock, Windows shutdown hook. +- Renderer: Vue 3 with Vue Router (hash mode), Ant Design Vue 4, dark theme support. +- IPC bridge (`src/bridge/`): dynamic RPC — main process exposes a flat API list, renderer calls methods via `ipcRenderer.invoke('apiInvoke', [path, args])`. Core events (status, error, speed) flow main→renderer via `webContents.send`. +- Pages: dashboard (index), accelerator server, system proxy, settings, help, plus per-plugin pages (free-eye, git, node, overwall, pip). + +**`packages/cli`** (`@docmirror/dev-sidecar-cli`) — Headless CLI launcher. Reads user config, calls `DevSidecar.api.startup()`. + +**`packages/aur/`** — Arch Linux PKGBUILD (not a JS package). **`packages/cli2/`** — abandoned placeholder, ignore it. + +### Key conventions +- **Module systems**: `core`, `mitmproxy`, and `cli` use implicit CommonJS (`.js` files, no `"type": "module"`). `gui` uses ESM (`"type": "module"`). The root `package.json` declares `"type": "module"` but this only affects root-level scripts. +- **Shared JSON5 parser**: `@docmirror/mitmproxy/src/json` is used across all packages for JSON5 config parsing. +- **Logging**: log4js-based; log files at `~/.dev-sidecar/logs/core.log`, `gui.log`, `server.log`. Logger factory at `packages/core/src/utils/util.logger.js`. Every category writes to file and, by default, also to stdout (`std` appender); set `DEV_SIDECAR_LOG_TO_CONSOLE=false` to keep logs file-only (CLI daemon sets this automatically). +- **Status/event bus**: `core/src/event.js` (EventEmitter) and `core/src/status.js` (central status tree updated via events). +- **CA certificate**: stored at `~/.dev-sidecar/dev-sidecar.ca.crt` and `~/.dev-sidecar/dev-sidecar.ca.key.pem`. Generated locally on first run. +- **Config on disk**: user overrides saved as diffs in `~/.dev-sidecar/config.json`. Merged runtime config written as `running.json` for the child process. + +### Build environment requirements +- Node.js 22.x +- Python 3.11 with setuptools (or use `uv` with the project's `.python-version` and `pyproject.toml`) +- VS 2022 with C++ desktop development workload (Windows) +- Native modules need C++17: the `.npmrc` sets `CXXFLAGS="-std=c++17"` + +### Vue config gotcha +`packages/gui/vue.config.cjs` sets `concatenateModules: false` in webpack production builds. This is **intentional** — module concatenation breaks ant-design-vue's Symbol-based `provide/inject`, causing menu crashes, dark mode failures, and Select/Dropdown malfunctions. diff --git a/README.md b/README.md index a0a3f0687f..7d7ef8135a 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,20 @@ 开发者边车,命名取自service-mesh的service-sidecar,意为为开发者打辅助的边车工具(以下简称ds) 通过本地代理的方式将https请求代理到一些国内的加速通道上 -GitHub stars +GitHub stars -> Gitee上的同步项目已被封禁,此项目将不再更新与维护 【狗头保命】 +[![Star History Chart](https://star-history.dera.page/svg?repos=docmirror/dev-sidecar&type=date&legend=top-left)](https://star-history.dera.page/#docmirror/dev-sidecar&type=date&legend=top-left) + +> Gitee上的同步项目已被封禁,请认准本项目唯一官方仓库地址[https://github.com/docmirror/dev-sidecar](https://github.com/docmirror/dev-sidecar) 【狗头保命】 > > 我将继续奋战在开源一线,为社区贡献更多更好的开源项目。 +> > 感兴趣的可以关注我的主页 [【github】](https://github.com/greper) [【gitee】](https://gitee.com/greper) ## 打个广告 > [https://github.com/certd/certd](https://github.com/certd/certd) +> > 我的开源证书管理工具项目,全自动申请和部署证书,有需求的可以去试试,帮忙点个star ## 重要提醒 @@ -27,7 +31,12 @@ > ------------------------------重要提醒2--------------------------------- > -> 注意:本应用启动会自动修改系统代理,所以会与其他代理软件有冲突,请务必不要一起使用。 +> 注意:本应用启动会自动修改系统代理,所以会与其他代理软件有冲突,一起使用时请谨慎使用。 +> +> 与 `Watt Toolkit(原Steam++)` 共用时,请以hosts模式启动Watt Toolkit +> +> 与 `TUN网卡模式运行的游戏加速器` 可以共用 +> > 本应用主要目的在于直连访问github,如果你已经有飞机了,那建议还是不要用这个自行车(ds)了 ## 一、 特性 @@ -75,8 +84,8 @@ **_安全警告_**: -- 请勿使用来源不明的服务地址,有隐私和账号泄露风险 -- 本应用及服务端承诺不收集任何信息。介意者请使用安全模式。 +- 请勿使用来源不明的服务/远程配置地址,有隐私和账号泄露风险 +- 本应用及服务/默认远程配置端承诺不收集任何信息。介意者请使用安全模式。 ## 二、快速开始 @@ -89,10 +98,13 @@ - release下载 [Github Release](https://github.com/docmirror/dev-sidecar/releases) -> Windows: 请选择DevSidecar-x.x.x.exe -> Mac: 请选择DevSidecar-x.x.x.dmg -> Ubuntu: 请选择DevSidecar-x.x.x.deb -> 其他linux: 请选择DevSidecar-x.x.x.AppImage (未做测试,不保证能用) +> Windows: 请选择DevSidecar-x.x.x-windows-universal.exe +> +> Mac: 请选择DevSidecar-x.x.x-macos-universal.dmg +> +> Debian系及其他支持deb安装包的Linux: 请选择DevSidecar-x.x.x-linux-[架构].deb +> +> 其他Linux: 请选择DevSidecar-x.x.x-linux-[架构].AppImage (未做测试,不保证能用) > linux安装说明请参考 [linux安装文档](./doc/linux.md) @@ -100,6 +112,8 @@ #### 2)安装后打开 +界面应大致如下图所示: + > 注意:mac版安装需要在“系统偏好设置->安全性与隐私->通用”中解锁并允许应用安装 ![](./doc/index.png) @@ -111,13 +125,14 @@ 更多有关根证书的说明,请参考 [为什么要安装根证书?](./doc/caroot.md) > 根证书是本地随机生成的,所以不用担心根证书的安全问题(本应用不收集任何用户信息) +> > 你也可以在加速服务设置中自定义根证书(PEM格式的证书与私钥) -> 火狐浏览器需要[手动安装证书](#3浏览器打开提示证书不受信任) +> 火狐浏览器需要[手动安装证书](#3火狐浏览器火狐浏览器不走系统的根证书需要在选项中添加根证书) #### 4)开始加速吧 -去试试打开github +去试试打开github、huggingface、docker hub吧 ### 2.2、开启前 vs 开启后 @@ -131,7 +146,7 @@ ### 3.1、安全模式 -- 此模式:关闭拦截、关闭增强、开启dns优选、开启测速 +- 此模式:关闭拦截、关闭增强、不使用远程配置、开启dns优选、开启测速 - 最安全,无需安装证书,可以在浏览器地址栏左侧查看域名证书 - 功能也最弱,只有特性1,相当于查询github的国外ip,手动改hosts一个意思。 - github的可访问性不稳定,取决于IP测速,如果有绿色ip存在,就 `有可能` 可以直连访问。 @@ -139,13 +154,13 @@ ### 3.2、默认模式 -- 此模式:开启拦截、关闭增强、开启dns优选、开启测速 +- 此模式:开启拦截、关闭增强、使用远程配置、开启dns优选、开启测速 - 需要安装证书,通过修改sni直连访问github - 功能上包含特性1/2/3/4。 ## 四、 最佳实践 -- 把dev-sidecar一直开着就行了(注意windows下开着ds重启电脑,会无法上网,重新打开ds即可。) +- 把dev-sidecar一直开着就行了(注意部分版本的windows下开着ds重启电脑,可能会无法上网,重新打开ds即可。) - 建议遇到打开比较慢的国外网站,可以尝试将该域名添加到dns设置中(注意:被\*\*\*封杀的无效) ### 其他加速 @@ -162,50 +177,61 @@ > 2. clone 出来的 remote "origin" 为fastgit的地址,需要手动改回来 > 3. 你也可以直接使用他们的clone加速工具 [fgit-go](https://github.com/FastGitORG/fgit-go) -#### 2)`github.com` 的镜像网站(注意:部分镜像网站不能登录) +#### 2) 镜像网站 +##### 1)`github.com` 的镜像网站(注意:部分镜像网站不能登录) + +> 1. ~~[hub.fastgit.org](https://hub.fastgit.org/) (2024/11/18:这个好像失效了?)~~ +> 2. ~~[github.com.cnpmjs.org](https://github.com.cnpmjs.org/) 这个很容易超限(2024/11/18:这个好像失效了?)~~ +> 3. ~~[bgithub.xyz](https://bgithub.xyz/)(edge浏览器可能报毒)~~ +> 4. [kkgithub.com](https://kkgithub.com/) +> 5. [5github.com](https://5github.com) -> 1. [hub.fastgit.org](https://hub.fastgit.org/) (2024/11/18:这个好像失效了?) -> 2. [github.com.cnpmjs.org](https://github.com.cnpmjs.org/) 这个很容易超限(2024/11/18:这个好像失效了?) -> 3. [dgithub.xyz](https://dgithub.xyz/) +##### 2) `youtube.com`的镜像网站(建议ipv6访问) +> 1. [https://s3.dualstack.us-east-1.amazonaws.com/zhifan/ytb.html](https://s3.dualstack.us-east-1.amazonaws.com/zhifan/ytb.html) +> 2. [https://s3.dualstack.us-east-1.amazonaws.com/zhifan/ytb2.html](https://s3.dualstack.us-east-1.amazonaws.com/zhifan/ytb2.html) ## 五、api ### 5.1、拦截配置 -没有配置域名的不会拦截,其他根据配置进行拦截处理 +没有配置域名的不会拦截,其他根据配置进行拦截处理。 -```js -const intercepts = { +在【加速服务-拦截设置】中配置,格式如下:(更多内容参见[wiki](https://github.com/docmirror/dev-sidecar/wiki/%E5%8A%A0%E9%80%9F%E6%9C%8D%E5%8A%A1%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)) + +```json +{ // 要拦截的域名 - 'github.com': { + "github.com": { // 需要拦截url的正则表达式 - '/.*/.*/releases/download/': { + "/.*/.*/releases/download/": { // 拦截类型 - // redirect: url, // 临时重定向(url会变,一些下载资源可以通过此方式配置) - // proxy: url, // 代理(url不会变,没有跨域问题) - // abort: true, // 取消请求(适用于被***封锁的资源,找不到替代,直接取消请求,快速失败,节省时间) - // success: true, // 直接返回成功请求(某些请求不想发出去,可以伪装成功返回) - // cacheDays: 1, // GET请求的使用缓存,单位:天(常用于一些静态资源) - // options: true, // OPTIONS请求直接返回成功请求(该功能存在一定风险,请谨慎使用) - // optionsMaxAge: 2592000, // OPTIONS请求缓存时间,默认:2592000(一个月) - redirect: 'download.fastgit.org' + // "redirect": "url", // 临时重定向(url会变,一些下载资源可以通过此方式配置) + // "proxy": "url", // 代理(url不会变,没有跨域问题) + // "abort": true, // 取消请求(适用于被***封锁的资源,找不到替代,直接取消请求,快速失败,节省时间) + // "success": true, // 直接返回成功请求(某些请求不想发出去,可以伪装成功返回) + // "cacheDays": 1, // GET请求的使用缓存,单位:天(常用于一些静态资源) + // "options": true, // OPTIONS请求直接返回成功请求(该功能存在一定风险,请谨慎使用) + // "optionsMaxAge": 2592000, // OPTIONS请求缓存时间,默认:2592000(一个月) + + // 拦截配置示例: + "redirect": "download.fastgit.org" }, - '.*': { - proxy: 'github.com', - sni: 'baidu.com' // 修改sni,规避***握手拦截 + ".*": { + "proxy": "github.com", + "sni": "baidu.com" // 修改sni,规避***握手拦截 } }, - 'ajax.googleapis.com': { - '.*': { - proxy: 'ajax.loli.net', // 代理请求,url不会变 - backup: ['ajax.proxy.ustclug.org'], // 备份,当前代理请求失败后,将会切换到备用地址 - test: 'ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js', - replace: '/(.*)/xxx'// 当加速地址的链接和原链接不是完全相同时,可以通过正则表达式replace,此时proxy通过$1$2来重组url, proxy:'ajax.loli.net/xxx/$1' + "ajax.googleapis.com": { + ".*": { + "proxy": "ajax.loli.net", // 代理请求,url不会变 + "backup": ["ajax.proxy.ustclug.org"], // 备份,当前代理请求失败后,将会切换到备用地址 + "test": "ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js", + "replace": "/(.*)/xxx" // 当加速地址的链接和原链接不是完全相同时,可以通过正则表达式replace,此时proxy通过$1$2来重组url, proxy:'ajax.loli.net/xxx/$1' } }, - 'clients*.google.com': { - '.*': { - abort: true // 取消请求,被***封锁的资源,找不到替代,直接取消请求,快速失败,节省时间 + "clients*.google.com": { + ".*": { + "abort": true // 取消请求,被***封锁的资源,找不到替代,直接取消请求,快速失败,节省时间 } } } @@ -214,7 +240,8 @@ const intercepts = { ### 5.2、DNS优选配置 某些域名解析出来的ip会无法访问,(比如api.github.com会被解析到新加坡的ip上,新加坡的服务器在上午挺好,到了晚上就卡死,基本不可用) -通过从dns上获取ip列表,切换不同的ip进行尝试,最终会挑选到一个最快的ip + +通过从dns上获取ip列表,切换不同的ip进行尝试,最终会挑选到一个最快的ip(该功能需要事先配置好所用DNS),更多说明参见[wiki](https://github.com/docmirror/dev-sidecar/wiki/%E5%8A%A0%E9%80%9F%E6%9C%8D%E5%8A%A1%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) ```json { @@ -253,15 +280,13 @@ networksetup -setwebproxy 'WiFi' 127.0.0.1 31181 如果有上面的错误提示,请尝试如下方法: > 取消访问偏好设置需要管理员密码 +> > 系统偏好设置—>安全性与隐私—> 通用—> 高级—> 访问系统范围的偏好设置需要输入管理员密码(取消勾选) ### 6.2、没有加速效果 -> 本应用仅支持https加速,请务必确认你访问的网站地址是https开头的 - -1. 本应用仅支持https加速 - 请务必确认你访问的地址是https开头的 - 比如: [https://github.com/](https://github.com/) +1. 本应用默认仅开启https加速,一般足够覆盖需求。 + 如果你访问的是仅支持http协议的网站,请手动在【系统代理】中打开【代理HTTP请求】 2. 检查浏览器是否装了什么插件,与ds有冲突 3. 检查是否安装了其他代理软件,与ds有冲突 4. 请确认浏览器的代理设置为使用IE代理/或者使用系统代理状态 @@ -272,18 +297,22 @@ networksetup -setwebproxy 'WiFi' 127.0.0.1 31181 ### 6.3、浏览器打开提示证书不受信任 ![](./doc/crt-error.png) -一般是证书安装位置不对,重新安装证书后,重启浏览器 -#### 1)windows: 请确认证书已正确安装在“信任的根证书颁发机构”下 +一般是证书安装位置不对,重新安装根证书后,重启浏览器 + +#### 1)windows: 请确认证书已正确安装在“本地计算机-将所有的证书都放入下列存储:受信任的根证书颁发机构”下 #### 2)mac: 请确认证书已经被安装并已经设置信任 #### 3)火狐浏览器:火狐浏览器不走系统的根证书,需要在选项中添加根证书 1. 火狐浏览器->选项->隐私与安全->证书->查看证书 + ![](./doc/Firefox/1.png) 2. 证书颁发机构->导入 3. 选择证书文件 `C:\Users(用户)\Administrator(你的账号)\.dev-sidecar\dev-sidecar.ca.crt`(Mac或linux为 `~/.dev-sidecar` 目录) + ![](./doc/Firefox/2.png) 4. 勾选信任由此证书颁发机构来标识网站,确定即可 + ![](./doc/Firefox/3.png) ### 6.4、打开github显示连接超时 @@ -297,7 +326,8 @@ DevSidecar Warning: Error: www.github.com:443, 代理请求超时 ### 6.5、查看日志是否有报错 -如果还是不行,请在下方加作者好友,将服务日志发送给作者进行分析 +如果还是不行,请在下方加官方QQ群或提issue,附上服务日志(server.log)以便进行分析 + 日志打开方式:加速服务->右边日志按钮->打开日志文件夹 ![](./doc/log.png) @@ -338,6 +368,12 @@ npm config delete proxy npm config delete https-proxy ``` +### 6.9、其他问题 + +请查阅[wiki](https://github.com/docmirror/dev-sidecar/wiki) + +也可以查阅[有文档tag的issue](https://github.com/docmirror/dev-sidecar/issues?q=is%3Aissue%20label%3ADocumentation),它们被开发者认证为相当于文档级别的参考issue。 + ## 七、在其他程序使用 - [java程序使用](./doc/other.md#Java程序使用) @@ -346,17 +382,30 @@ npm config delete https-proxy ### 8.1、准备环境 -#### 1)安装 `nodejs` +#### 1)安装 `nodejs` 及其他环境 推荐安装 nodejs `22.x.x` 的版本,其他版本未做测试 +Windows上需要msvc,推荐使用VS 2022(node-gyp对VS 2026支持可能存在问题),安装时选择C++桌面开发工作负载即可。 + +另外还需要带distutils的python,推荐安装自带setuptools的python 3.11版本。如果本地有uv,则可以简单的运行以下命令 + +```shell +uv init . +uv sync +.venv/Scripts/activate.ps1 # for windows pwsh +.venv/Scripts/activate.bat # for windows cmd +source .venv/bin/activate # for linux/mac +``` + +这会根据.python-version文件自动安装python 3.11版本。如不想使用python 3.11,也可删除.python-version文件,pyproject.toml已经指定了所需依赖。 + #### 2)安装 `pnpm` -运行如下命令即可安装所需依赖: +运行如下命令即可安装: ```shell npm install -g pnpm --registry=https://registry.npmmirror.com - ``` ### 8.2、开发调试模式启动 @@ -379,6 +428,7 @@ npm run electron ``` > 如果electron依赖包下载不动,可以开启ds的npm加速 +> 如果pnpm install只是单纯卡住,大概是因为你忘记进python环境了 ### 8.3、打包成可执行文件 @@ -395,13 +445,13 @@ npm run electron:build 欢迎bug反馈,需求建议,技术交流等 -1、 加群(请备注dev-sidecar,或简称DS) +加官方QQ群(请备注dev-sidecar,或简称DS) - QQ 1群:390691483,人数:500 / 500(满) -- QQ 2群:[667666069](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=n4nksr4sji93vZtD5e8YEHRT6qbh6VyQ&authKey=XKBZnzmoiJrAFyOT4V%2BCrgX5c13ds59b84g%2FVRhXAIQd%2FlAiilsuwDRGWJct%2B570&noverify=0&group_code=667666069),人数:447 / 500 +- QQ 2群:[667666069](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=n4nksr4sji93vZtD5e8YEHRT6qbh6VyQ&authKey=XKBZnzmoiJrAFyOT4V%2BCrgX5c13ds59b84g%2FVRhXAIQd%2FlAiilsuwDRGWJct%2B570&noverify=0&group_code=667666069),人数:500 / 500(满) - QQ 3群:419807815,人数:500 / 500(满) -- QQ 4群:[438148299](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=i_NCBB5f_Bkm2JsEV1tLs2TkQ79UlCID&authKey=nMsVJbJ6P%2FGNO7Q6vsVUadXRKnULUURwR8zvUZJnP3IgzhHYPhYdcBCHvoOh8vYr&noverify=0&group_code=438148299),人数:203 / 1000 -- QQ 5群:[767622917](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nAWi_Rxj7mM4Unp5LMiatmUWhGimtbcB&authKey=aswmlWGjbt3GIWXtvjB2GJqqAKuv7hWjk6UBs3MTb%2Biyvr%2Fsbb1kA9CjF6sK7Hgg&noverify=0&group_code=767622917),人数:016 / 200(new) +- QQ 4群:[438148299](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=i_NCBB5f_Bkm2JsEV1tLs2TkQ79UlCID&authKey=nMsVJbJ6P%2FGNO7Q6vsVUadXRKnULUURwR8zvUZJnP3IgzhHYPhYdcBCHvoOh8vYr&noverify=0&group_code=438148299),人数:1004 / 2000(推荐) +- QQ 5群:[767622917](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nAWi_Rxj7mM4Unp5LMiatmUWhGimtbcB&authKey=aswmlWGjbt3GIWXtvjB2GJqqAKuv7hWjk6UBs3MTb%2Biyvr%2Fsbb1kA9CjF6sK7Hgg&noverify=0&group_code=767622917),人数:200 / 500 ## 十、求star @@ -413,7 +463,7 @@ npm run electron:build ## 十一、感谢 -本项目使用lerna包管理工具 +本项目曾使用lerna包管理工具 [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) diff --git "a/_script/0\343\200\201updateDependencies.bat" "b/_script/0\343\200\201updateDependencies.bat" index 2a22d7262c..84a238f0df 100644 --- "a/_script/0\343\200\201updateDependencies.bat" +++ "b/_script/0\343\200\201updateDependencies.bat" @@ -11,3 +11,5 @@ ncu -u # cd ../packages/mitmproxy # ncu -u + +cmd diff --git "a/_script/1\343\200\201setupEnv.bat" "b/_script/1\343\200\201setupEnv.bat" index 207335d6e0..7fb29b1291 100644 --- "a/_script/1\343\200\201setupEnv.bat" +++ "b/_script/1\343\200\201setupEnv.bat" @@ -2,3 +2,5 @@ node -v cd ../ npm install -g pnpm --registry=https://registry.npmmirror.com + +cmd diff --git "a/_script/2\343\200\201installProject.bat" "b/_script/2\343\200\201installProject.bat" index 0808882984..51d12d3e82 100644 --- "a/_script/2\343\200\201installProject.bat" +++ "b/_script/2\343\200\201installProject.bat" @@ -2,4 +2,6 @@ node -v cd ../ chcp 65001 -pnpm install +pnpm install --registry=https://registry.npmmirror.com + +cmd diff --git "a/_script/3\343\200\201buildAndRun.bat" "b/_script/3\343\200\201buildAndRun.bat" index d4350b8065..86fa14a1bc 100644 --- "a/_script/3\343\200\201buildAndRun.bat" +++ "b/_script/3\343\200\201buildAndRun.bat" @@ -3,3 +3,5 @@ node -v cd ../packages/gui chcp 65001 npm run electron + +cmd diff --git "a/_script/4.1\343\200\201runTestCore.bat" "b/_script/4.1\343\200\201runTestCore.bat" index 2974c5e48a..08374b3e86 100644 --- "a/_script/4.1\343\200\201runTestCore.bat" +++ "b/_script/4.1\343\200\201runTestCore.bat" @@ -2,3 +2,5 @@ node -v cd ../packages/core pnpm run test + +cmd diff --git "a/_script/4.2\343\200\201runTestMitmproxy.bat" "b/_script/4.2\343\200\201runTestMitmproxy.bat" index aea5b944c0..24a0ba45c8 100644 --- "a/_script/4.2\343\200\201runTestMitmproxy.bat" +++ "b/_script/4.2\343\200\201runTestMitmproxy.bat" @@ -2,3 +2,5 @@ node -v cd ../packages/mitmproxy pnpm run test + +cmd diff --git "a/_script/5\343\200\201generateSetupFile.bat" "b/_script/5\343\200\201generateSetupFile.bat" index ab3bc4763d..3d8e253493 100644 --- "a/_script/5\343\200\201generateSetupFile.bat" +++ "b/_script/5\343\200\201generateSetupFile.bat" @@ -6,3 +6,5 @@ if not exist "dist_electron" mkdir "dist_electron" start dist_electron npm run electron:build + +cmd diff --git a/_script/dev.ps1 b/_script/dev.ps1 new file mode 100644 index 0000000000..d5419d68e0 --- /dev/null +++ b/_script/dev.ps1 @@ -0,0 +1,144 @@ +#Requires -Version 5.1 +<# + dev-sidecar 开发环境统一启动/检查脚本 + + 用法(在仓库根目录或任意目录执行): + pwsh -File _script\dev.ps1 -Action start # 启动开发环境并等待端口就绪 + pwsh -File _script\dev.ps1 -Action check # 检查端口监听状态 + pwsh -File _script\dev.ps1 -Action stop # 停止开发环境(按端口结束进程) + pwsh -File _script\dev.ps1 -Action restart # 停止后重新启动 + + 可选参数: + -Port 8081 # 前端 dev server 端口,默认 8081 + -Foreground # start 时在前台运行,日志直接输出到当前终端(Ctrl+C 停止) + -SkipKill # start/restart 时不先结束已占用端口的进程 +#> +param( + [ValidateSet('start', 'check', 'stop', 'restart')] + [string]$Action = 'start', + + [int]$Port = 8081, + + [switch]$Foreground, + + [switch]$SkipKill +) + +$ErrorActionPreference = 'SilentlyContinue' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$guiDir = Join-Path $repoRoot 'packages\gui' +$electronDev = Join-Path $PSScriptRoot 'electron-dev.mjs' +$proxyHttpPort = 31180 +$proxyHttpsPort = 31181 +$ports = @($Port, $proxyHttpPort, $proxyHttpsPort) + +function Get-Listeners([int[]]$TargetPorts) { + Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $TargetPorts -contains $_.LocalPort } +} + +function Show-Status([string]$Title) { + Write-Host "== $Title ==" + $listeners = @(Get-Listeners $ports) + if ($listeners.Count -gt 0) { + $listeners | Sort-Object LocalPort | ForEach-Object { + $procName = (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName + Write-Host (" {0}:{1,-6} PID {2,-7} {3}" -f $_.LocalAddress, $_.LocalPort, $_.OwningProcess, $procName) + } + } else { + Write-Host ' no listeners' + } + Write-Host '' +} + +function Stop-DevSidecar([int[]]$TargetPorts) { + Write-Host '== stopping dev-sidecar ==' + $listeners = @(Get-Listeners $TargetPorts) + $procIds = @($listeners | Select-Object -ExpandProperty OwningProcess -Unique) + if ($procIds.Count -eq 0) { + Write-Host ' no listeners' + } else { + foreach ($procId in $procIds) { + Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue + Write-Host " killed PID $procId" + } + } + Start-Sleep -Milliseconds 600 + Write-Host '' +} + +function Wait-Ports([int[]]$TargetPorts, [int]$TimeoutSec = 120) { + $deadline = (Get-Date).AddSeconds($TimeoutSec) + $ready = @{} + + while ((Get-Date) -lt $deadline) { + $listeners = @(Get-Listeners $TargetPorts) + foreach ($p in $TargetPorts) { + if ($listeners.LocalPort -contains $p) { + $ready[$p] = $true + } + } + + if ($ready.Count -ge $TargetPorts.Count) { + break + } + + Start-Sleep -Seconds 1 + } + + if ($ready.Count -lt $TargetPorts.Count) { + $missing = @($TargetPorts | Where-Object { -not $ready.ContainsKey($_) }) + Write-Host "等待端口超时,未就绪端口: $($missing -join ', ')" + } else { + Write-Host "端口已全部就绪: $($TargetPorts -join ', ')" + } +} + +function Start-DevSidecar([int]$DevPort) { + $node = (Get-Command node -ErrorAction Stop).Source + + if ($Foreground) { + Write-Host "== starting dev-sidecar in foreground (Ctrl+C to stop) ==" + Push-Location $guiDir + try { + & $node $electronDev '--port' "$DevPort" + } finally { + Pop-Location + } + return + } + + Write-Host "== starting dev-sidecar ==" + $proc = Start-Process -FilePath $node -ArgumentList @($electronDev, '--port', "$DevPort") -WorkingDirectory $guiDir -WindowStyle Hidden -PassThru + Write-Host " launcher PID: $($proc.Id)" + Wait-Ports $ports + Show-Status 'port status after start' +} + +switch ($Action) { + 'check' { + Show-Status "port status (dev: $Port, http proxy: $proxyHttpPort, https proxy: $proxyHttpsPort)" + break + } + + 'stop' { + Stop-DevSidecar $ports + Show-Status 'port status after stop' + break + } + + 'start' { + if (-not $SkipKill) { + Stop-DevSidecar $ports + } + Start-DevSidecar $Port + break + } + + 'restart' { + Stop-DevSidecar $ports + Start-DevSidecar $Port + break + } +} diff --git a/_script/electron-dev.mjs b/_script/electron-dev.mjs new file mode 100644 index 0000000000..149891f7a8 --- /dev/null +++ b/_script/electron-dev.mjs @@ -0,0 +1,120 @@ +import { spawn } from 'node:child_process' +import { createRequire } from 'node:module' +import process from 'node:process' +import { setTimeout as delay } from 'node:timers/promises' + +const guiDir = process.cwd() +const require = createRequire(import.meta.url) + +function resolveDevServer () { + const argv = process.argv.slice(2) + const portIndex = argv.indexOf('--port') + const port = portIndex >= 0 ? Number.parseInt(argv[portIndex + 1], 10) : 8080 + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + throw new Error(`无效的端口号: ${argv[portIndex + 1]}`) + } + return { port, url: `http://localhost:${port}` } +} + +const { port: devServerPort, url: devServerUrl } = resolveDevServer() +const state = { + closing: false, + devServer: null, + electron: null, +} + +function spawnCommand (entry, args = [], extraEnv = {}) { + return spawn(entry, args, { + cwd: guiDir, + env: { ...process.env, ...extraEnv }, + shell: false, + stdio: 'inherit', + windowsHide: false, + }) +} + +function resolveVueCliServiceBin () { + return require.resolve('@vue/cli-service/bin/vue-cli-service.js', { + paths: [guiDir], + }) +} + +function resolveElectronBin () { + return require('electron') +} + +async function waitForServer (url, child) { + const timeoutAt = Date.now() + 120000 + + while (Date.now() < timeoutAt) { + if (child.exitCode != null || child.signalCode != null) { + throw new Error('Dev server exited before it became ready') + } + + try { + const response = await fetch(url, { method: 'GET' }) + if (response.ok || response.status >= 200) { + return + } + } catch { + // Keep polling until the dev server is reachable. + } + + await delay(500) + } + + throw new Error(`Timed out waiting for ${url}`) +} + +function stopChild (child) { + if (!child || child.exitCode != null || child.signalCode != null) { + return + } + + child.kill('SIGTERM') +} + +async function shutdown (code = 0) { + if (state.closing) { + return + } + + state.closing = true + stopChild(state.electron) + stopChild(state.devServer) + process.exitCode = code +} + +process.on('SIGINT', () => { + void shutdown(0) +}) +process.on('SIGTERM', () => { + void shutdown(0) +}) + +async function main () { + const vueCliServiceBin = resolveVueCliServiceBin() + const electronBin = resolveElectronBin() + + state.devServer = spawnCommand(process.execPath, [vueCliServiceBin, 'serve', '--port', String(devServerPort)]) + state.devServer.on('exit', (code, signal) => { + if (!state.closing) { + void shutdown(code ?? (signal ? 1 : 0)) + } + }) + + try { + await waitForServer(devServerUrl, state.devServer) + state.electron = spawnCommand(electronBin, ['.'], { + WEBPACK_DEV_SERVER_URL: devServerUrl, + }) + state.electron.on('exit', (code, signal) => { + void shutdown(code ?? (signal ? 1 : 0)) + }) + } catch (error) { + console.error(error) + await shutdown(1) + } +} + +void main() diff --git a/_script/linux-arm64-prepare.sh b/_script/linux-arm64-prepare.sh new file mode 100644 index 0000000000..3dc9a2ba7d --- /dev/null +++ b/_script/linux-arm64-prepare.sh @@ -0,0 +1,4 @@ +cd $GITHUB_WORKSPACE/packages/gui +rm vue.config.js +cp linux-arm64.vue.config.js vue.config.js +cd $GITHUB_WORKSPACE \ No newline at end of file diff --git a/package.json b/package.json index 18291e8f27..7501388f81 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dev-sidecar-parent", "type": "module", "private": false, - "packageManager": "pnpm@9.13.2", + "packageManager": "pnpm@11.21.0", "author": "Greper", "license": "MPL-2.0", "scripts": { @@ -10,14 +10,8 @@ "lint:fix": "eslint . --fix" }, "devDependencies": { - "@antfu/eslint-config": "^3.9.1", - "eslint": "^9.15.0", - "eslint-plugin-format": "^0.1.2" - }, - "pnpm": { - "supportedArchitectures": { - "os": ["current"], - "cpu": ["x64", "arm64", "ia32"] - } + "@antfu/eslint-config": "^3.16.0", + "eslint": "^9.39.5", + "eslint-plugin-format": "^0.1.3" } } diff --git a/packages/aur/PKGBUILD b/packages/aur/PKGBUILD new file mode 100644 index 0000000000..fb401197f6 --- /dev/null +++ b/packages/aur/PKGBUILD @@ -0,0 +1,54 @@ +# Maintainer: Greper +pkgname=dev-sidecar-bin +pkgver=2.0.2 +pkgrel=1 +pkgdesc="给开发者的边车辅助工具,通过代理的方式来改善国内访问github等境外网站的情况" +arch=('x86_64' 'aarch64') +url="https://github.com/docmirror/dev-sidecar" +license=('MPL-2.0') +depends=('libnotify' 'libappindicator-gtk3' 'libxtst' 'nss' 'libxss' 'gtk3') +provides=('dev-sidecar') +conflicts=('dev-sidecar') +options=('!strip') + +source_x86_64=("https://github.com/docmirror/dev-sidecar/releases/download/v${pkgver}/DevSidecar-${pkgver}-linux-x86_64.tar.gz") +source_aarch64=("https://github.com/docmirror/dev-sidecar/releases/download/v${pkgver}/DevSidecar-${pkgver}-linux-arm64.tar.gz") + +sha256sums_x86_64=('SKIP') +sha256sums_aarch64=('SKIP') + +package() { + local _installdir="${pkgdir}/opt/${pkgname%-bin}" + + install -dm755 "${_installdir}" + cp -r "${srcdir}/"* "${_installdir}/" + + # chrome-sandbox must be setuid root + if [[ -f "${_installdir}/chrome-sandbox" ]]; then + chmod 4755 "${_installdir}/chrome-sandbox" + fi + + # symlink the main executable into PATH + install -dm755 "${pkgdir}/usr/bin" + ln -sf "/opt/${pkgname%-bin}/dev-sidecar" "${pkgdir}/usr/bin/dev-sidecar" + + # desktop entry + install -dm755 "${pkgdir}/usr/share/applications" + cat > "${pkgdir}/usr/share/applications/dev-sidecar.desktop" << EOF +[Desktop Entry] +Name=DevSidecar +Comment=给开发者的边车辅助工具 +Exec=/opt/dev-sidecar/dev-sidecar %U +Icon=dev-sidecar +Terminal=false +Type=Application +Categories=Utility;System; +StartupNotify=true +EOF + + # application icon (if provided by the package) + if [[ -f "${_installdir}/resources/app/public/logo/linux.png" ]]; then + install -Dm644 "${_installdir}/resources/app/public/logo/linux.png" \ + "${pkgdir}/usr/share/pixmaps/dev-sidecar.png" + fi +} diff --git a/packages/aur/gen_srcinfo.py b/packages/aur/gen_srcinfo.py new file mode 100644 index 0000000000..e8abd8ab0b --- /dev/null +++ b/packages/aur/gen_srcinfo.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Generate .SRCINFO from PKGBUILD for AUR publishing. + +This script is used by the publish-to-aur GitHub Actions workflow because +makepkg/pacman are not available on Ubuntu runners. + +Run it from the directory that contains PKGBUILD: + python3 gen_srcinfo.py +""" + +import pathlib +import re + +pkgbuild = pathlib.Path("PKGBUILD").read_text() + + +def extract(var): + m = re.search(rf"^{var}=(.+)", pkgbuild, re.MULTILINE) + return m.group(1).strip().strip("'\"") if m else "" + + +pkgname = extract("pkgname") +pkgver = extract("pkgver") +pkgrel = extract("pkgrel") +pkgdesc = extract("pkgdesc").strip("'\"") +arch_line = re.search(r"^arch=\((.+)\)", pkgbuild, re.MULTILINE).group(1) +archs = re.findall(r"'([^']+)'", arch_line) +url = extract("url") +license_ = re.search(r"^license=\('([^']+)'\)", pkgbuild, re.MULTILINE).group(1) +depends_m = re.search(r"^depends=\((.+?)\)", pkgbuild, re.MULTILINE | re.DOTALL) +depends = re.findall(r"'([^']+)'", depends_m.group(1)) if depends_m else [] +provides_m = re.search(r"^provides=\('([^']+)'\)", pkgbuild, re.MULTILINE) +provides = [provides_m.group(1)] if provides_m else [] +conflicts_m = re.search(r"^conflicts=\('([^']+)'\)", pkgbuild, re.MULTILINE) +conflicts = [conflicts_m.group(1)] if conflicts_m else [] + +src_x86 = re.search(r'^source_x86_64=\("([^"]+)"\)', pkgbuild, re.MULTILINE) +src_aarch = re.search(r'^source_aarch64=\("([^"]+)"\)', pkgbuild, re.MULTILINE) +sha_x86 = re.search(r"^sha256sums_x86_64=\('([^']+)'\)", pkgbuild, re.MULTILINE) +sha_aarch = re.search(r"^sha256sums_aarch64=\('([^']+)'\)", pkgbuild, re.MULTILINE) + +lines = [] +lines.append("# Generated by gen_srcinfo.py") +lines.append(f"pkgbase = {pkgname}") +lines.append(f"\tpkgdesc = {pkgdesc}") +lines.append(f"\tpkgver = {pkgver}") +lines.append(f"\tpkgrel = {pkgrel}") +lines.append(f"\turl = {url}") +for a in archs: + lines.append(f"\tarch = {a}") +lines.append(f"\tlicense = {license_}") +for d in depends: + lines.append(f"\tdepends = {d}") +for p in provides: + lines.append(f"\tprovides = {p}") +for c in conflicts: + lines.append(f"\tconflicts = {c}") +if src_x86: + lines.append(f"\tsource_x86_64 = {src_x86.group(1)}") +if sha_x86: + lines.append(f"\tsha256sums_x86_64 = {sha_x86.group(1)}") +if src_aarch: + lines.append(f"\tsource_aarch64 = {src_aarch.group(1)}") +if sha_aarch: + lines.append(f"\tsha256sums_aarch64 = {sha_aarch.group(1)}") +lines.append("") +lines.append(f"pkgname = {pkgname}") + +pathlib.Path(".SRCINFO").write_text("\n".join(lines) + "\n") +print(".SRCINFO generated:") +print("\n".join(lines)) diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 0000000000..f33d7f3943 --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,6 @@ +# SEA 打包产物 +dist/ + +# 覆盖率报告 +.nyc_output/ +coverage/ diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000000..974e556af0 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,298 @@ +# @docmirror/dev-sidecar-cli + +开发者边车(Dev Sidecar)命令行版本,为 GitHub、npm、Docker Hub 等境外站点提供加速代理。 + +## 安装 + +```bash +npm install -g @docmirror/dev-sidecar-cli +``` + +## 开发 + +### 环境要求 + +- Node.js >= 18 +- pnpm >= 9 + +### 依赖安装 + +在仓库根目录执行,安装 CLI 及其依赖(core、mitmproxy),不包含 GUI: + +```bash +pnpm install --filter @docmirror/dev-sidecar-cli... +``` + +### 运行 + +```bash +node packages/cli/cli.js +``` + +### 测试 + +```bash +# 运行 CLI 测试 +pnpm --filter @docmirror/dev-sidecar-cli test + +# 运行测试并查看覆盖率 +npx nyc --reporter=text pnpm --filter @docmirror/dev-sidecar-cli test + +# 运行全部包的测试 +pnpm --filter @docmirror/dev-sidecar test +pnpm --filter @docmirror/mitmproxy test +``` + +### 项目结构 + +``` +packages/cli/ +├── cli.js # bin 入口,路由到 src/index.js +├── sea-config.json # SEA 打包配置 +├── scripts/ +│ └── build.js # SEA 打包脚本(支持 --all 交叉编译) +├── src/ +│ ├── index.js # 主入口,命令路由 + 守护进程模式 +│ ├── sea-entry.js # SEA 入口,同进程启动代理 +│ ├── banner.txt # ASCII art banner +│ ├── mitmproxy.js # fork 模式的代理入口 +│ ├── plugin-worker.js # 插件操作临时子进程 +│ ├── free-eye-worker.js # free_eye 测试临时子进程 +│ └── commands/ +│ ├── start.js # start 逻辑 + PID/端口/GUI 检测 +│ ├── stop.js # stop 逻辑 +│ ├── restart.js # restart 逻辑 +│ ├── status.js # status 逻辑 +│ ├── plugin.js # plugin 命令路由 + overwall 解锁检测 +│ ├── service.js # 开机自启动 (systemd/launchd/注册表) +│ └── gui.js # GUI 启停 + 端口检测 +└── test/ + ├── start.test.js # 端口检测、配置读取、PID 逻辑测试 + ├── plugin.test.js # 插件列表、overwall 解锁测试 + ├── status.test.js # 状态格式化、文件逻辑测试 + ├── gui.test.js # 端口检测、GUI 检测测试 + ├── service.test.js # 开机自启动测试 + └── index.test.js # 命令路由、help、version 测试 +``` + +### 添加新命令 + +1. 在 `src/commands/` 下创建 `.js` +2. 在 `src/index.js` 的 `routeCommand()` switch 中添加 case +3. 在 `test/` 下创建对应测试文件 + +### 添加新插件 + +插件列表从 core 的 `src/modules/plugin/index.js` 动态读取,CLI 无需修改。如果插件有特殊行为(如 `free_eye` 的一次性测试),在 `src/commands/plugin.js` 中添加分支处理。 + +## 命令 + +```bash +ds-cli # 启动 CLI 守护进程(默认) +ds-cli start # 启动 CLI 守护进程 +ds-cli stop # 停止 CLI 守护进程 +ds-cli restart # 重启 CLI 守护进程 +ds-cli status # 显示 CLI 运行状态 +ds-cli version # 显示版本号 +ds-cli plugin start # 启动单个插件 +ds-cli plugin stop # 停止单个插件 +ds-cli service install # 注册开机自启动 +ds-cli service uninstall # 移除开机自启动 +``` + +### GUI 操作参数 + +通过 `--gui` 或 `--all` 参数控制操作对象: + +```bash +ds-cli start --gui # 仅启动 GUI +ds-cli start --all # 同时启动 CLI 和 GUI +ds-cli stop --gui # 仅停止 GUI +ds-cli stop --all # 同时停止 CLI 和 GUI +ds-cli restart --gui # 仅重启 GUI +ds-cli restart --all # 同时重启 CLI 和 GUI +ds-cli status # 显示状态(自动包含 GUI 状态) +``` + +### 启动 + +```bash +ds-cli +``` + +启动后进程在后台运行,终端立即返回: + +``` +dev-sidecar 已在后台启动,PID: 12345 +``` + +### 停止 + +```bash +ds-cli stop +``` + +### 查看状态 + +```bash +ds-cli status +``` + +输出示例: + +``` +dev-sidecar 运行状态: + 代理服务: 运行中 + 系统代理: 已开启 + 开机启动: 已注册 + GUI: 未运行 + 插件: + git 已启用 + node 已启用 + pip 已启用 + free_eye 未启用 +``` + +### 插件管理 + +```bash +ds-cli plugin start git # 启动 git 加速 +ds-cli plugin stop git # 停止 git 加速 +``` + +支持的插件及行为: + +| 插件 | start | stop | 说明 | +|------|-------|------|------| +| `git` | 设置 git 全局代理 | 清除 git 全局代理 | 持久开关,重启终端后仍生效 | +| `node` | 设置 npm 代理和 registry | 清除 npm 代理 | 持久开关 | +| `pip` | 设置 pip 代理 | 清除 pip 代理 | 持久开关 | +| `free_eye` | 运行一次性测试并输出结果 | 不适用 | 无持久开关状态,`stop` 命令会提示不适用 | + +`free_eye` 启动时输出示例: + +``` +正在运行 free_eye 测试... + +=== free_eye 测试结果 === +完成时间: 2025-07-25T14:00:00.000Z +总测试数: 10 +已完成: 10 + +摘要: + PASS example.com - 响应正常 + PASS github.com - 连接成功 +``` + +### 开机自启动 + +```bash +ds-cli service install # 注册开机自启动 +ds-cli service uninstall # 移除 +``` + +各平台实现: + +| 平台 | 机制 | 说明 | +|------|------|------| +| Linux | systemd user service | 自动重启崩溃进程,`systemctl --user` 管理 | +| macOS | launchd | `~/Library/LaunchAgents/com.dev-sidecar.cli.plist` | +| Windows | 注册表 Run 键 | `HKCU\...\Run`,不依赖 Task Scheduler | + +## 启动流程 + +执行 `ds-cli`(或 `ds-cli start`)后: + +1. 检查实例锁(`~/.dev-sidecar/dev-sidecar.lock`)判断 CLI/GUI 是否已运行,若已运行则提示并退出 +2. 若无冲突,fork 一个 detached 子进程作为守护进程,父进程写入 PID 文件后立即退出,终端恢复控制权 +3. 守护进程启动: + - 获取实例锁,防止 CLI/GUI 重复运行 + - 从 `~/.dev-sidecar/config.json` 加载用户配置(与 GUI 版共享) + - 启动 mitmproxy 代理服务器(默认端口 31180 HTTP / 31181 HTTPS) + - 设置系统代理 + - 启动已启用的插件(git、node、pip 等) + - 通过代理下载远程加速规则配置 + - 运行状态(代理服务、系统代理、插件开关)事件驱动写入 `~/.dev-sidecar/running.json`(app.status 字段) +4. 守护进程监听 SIGINT / SIGTERM / exit 信号,收到后恢复系统代理并清理文件 + +## 其他命令 + +```bash +ds-cli stop # 读取 PID 文件,发送 SIGINT,等待进程退出后清理 +ds-cli restart # 停止当前守护进程后重新启动 +ds-cli status # 通过实例锁判断运行状态,读取 running.json 显示代理、系统代理、插件状态 +ds-cli plugin start # fork 临时子进程启动指定插件后退出 +ds-cli plugin stop # fork 临时子进程停止指定插件后退出 +``` + +## 配置文件 + +- `~/.dev-sidecar/config.json` — 用户配置(与 GUI 版共享) +- `~/.dev-sidecar/remote_config.json5` — 远程共享规则(自动下载) +- `~/.dev-sidecar/remote_config_personal.json5` — 远程个人规则(自动下载) +- `~/.dev-sidecar/setting.json` — 软件设置(含 overwall 解锁标记) +- `~/.dev-sidecar/ds-cli.pid` — 守护进程 PID 文件 +- `~/.dev-sidecar/dev-sidecar.lock` — 实例互斥锁(proper-lockfile,异常退出由 stale 机制自动接管) +- `~/.dev-sidecar/running.json` — mitmproxy 子进程启动配置 + 实例信息(app.instance)+ 运行时状态(app.status) + +## 日志 + +日志写入 `~/.dev-sidecar/logs/core.log`,按日期轮转,支持压缩。 + +**stdout 与文件分离**: +- 命令执行结果(`status`/`proxy on/off`/`plugin`/`service` 等)输出到 stdout +- 守护进程运行日志只写文件,不输出到 stdout(通过环境变量 `DEV_SIDECAR_LOG_TO_CONSOLE=false` 控制,`--daemon` 启动时自动设置) +- 调试守护进程时可临时开启 stdout 日志:`DEV_SIDECAR_LOG_TO_CONSOLE=true ds-cli --daemon` + +## 构建打包 + +CLI 使用 Node.js SEA(Single Executable Applications)打包为单个可执行文件。 + +### 一键打包 + +```bash +# 打包本机平台(自动识别) +node packages/cli/scripts/build.js + +# 打包所有平台(交叉编译) +node packages/cli/scripts/build.js --all +``` + +脚本自动完成:esbuild 打包 → 下载 Node.js 二进制 → SEA blob 生成(使用下载的 node,保证与运行时版本一致)→ 注入 blob → 验证。 + +### 输出产物 + +打包完成后在 `packages/cli/dist/` 下生成: + +| 命令 | 产物 | +|------|------| +| `node scripts/build.js` | `ds-cli--<本机平台>` | +| `node scripts/build.js --all` | 5 个平台的二进制 | + +支持的平台:`linux-x64`、`linux-arm64`、`macos-x64`、`macos-arm64`、`windows-x64`。 + +### 自动构建(CI) + +推送到 `release*` 分支或 `v*` 标签时,GitHub Actions 自动构建所有平台。打 `v*` 标签时自动创建 GitHub Release(draft 模式)。 + +## 测试 + +```bash +pnpm --filter @docmirror/dev-sidecar-cli test +``` + +测试覆盖率(nyc): + +| 文件 | 语句覆盖 | 分支覆盖 | 函数覆盖 | 行覆盖 | +|------|---------|---------|---------|--------| +| `start.js` | 40.57% | 26.08% | 66.66% | 38.09% | +| `gui.js` | 30.26% | 16.12% | 46.15% | 27.14% | +| **总计** | **35.17%** | **20.37%** | **56.00%** | **32.33%** | + +已覆盖:端口检测、配置读取、PID 文件逻辑、插件列表、overwall 解锁检测、状态格式化、命令路由解析。 + +未覆盖(需集成测试环境):`startDaemon`、`stopDaemon`、`restartDaemon`、`startGui`、`stopGui`、`restartGui` 等涉及 fork 子进程和外部命令的函数。 + +## 许可证 + +MPL-2.0 diff --git a/packages/cli/package.json b/packages/cli/package.json index 29535a6745..47df52bcce 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@docmirror/dev-sidecar-cli", - "version": "2.0.0", + "version": "2.2.1", "private": false, "description": "给开发者的加速代理工具", "author": "docmirror.cn", @@ -12,12 +12,21 @@ "代理" ], "main": "src/index.js", - "bin": "./cli.js", + "bin": { + "ds-cli": "./cli.js" + }, "scripts": { - "start": "node ./src" + "start": "node ./src", + "test": "mocha" }, "dependencies": { "@docmirror/dev-sidecar": "workspace:*", "@docmirror/mitmproxy": "workspace:*" + }, + "devDependencies": { + "chai": "^4.5.0", + "esbuild": "^0.28.2", + "mocha": "^11.8.0", + "postject": "^1.0.0-alpha.6" } } diff --git a/packages/cli/scripts/build.js b/packages/cli/scripts/build.js new file mode 100644 index 0000000000..e0293aa2b2 --- /dev/null +++ b/packages/cli/scripts/build.js @@ -0,0 +1,398 @@ +#!/usr/bin/env node +// ds-cli SEA 打包脚本 +// 用法: +// node scripts/build.js # 仅打包本机平台 +// node scripts/build.js --all # 打包所有平台(从 Node.js 官方获取可用平台列表) + +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') +const crypto = require('node:crypto') +const { execSync } = require('node:child_process') +const https = require('node:https') +const http = require('node:http') +const tar = require('tar') + +const ROOT = path.resolve(__dirname, '..') +const DIST = path.join(ROOT, 'dist') +const VERSION = require(path.join(ROOT, 'package.json')).version +const NODE_VERSION = 'v24.14.0' +const SENTINEL = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2' + +// ── 平台识别 ────────────────────────────────────────── + +function getCurrentPlatform () { + const p = process.platform + const a = process.arch + if (p === 'linux') return a === 'arm64' ? 'linux-arm64' : 'linux-x64' + if (p === 'darwin') return a === 'arm64' ? 'macos-arm64' : 'macos-x64' + if (p === 'win32') return 'windows-x64' + return 'unknown' +} + +function getNodeDownloadUrl (platform) { + const base = `https://nodejs.org/dist/${NODE_VERSION}` + const map = { + 'linux-x64': `${base}/node-${NODE_VERSION}-linux-x64`, + 'linux-x64-armv7l': `${base}/node-${NODE_VERSION}-linux-armv7l.tar.gz`, + 'linux-arm64': `${base}/node-${NODE_VERSION}-linux-arm64.tar.gz`, + 'macos-x64': `${base}/node-${NODE_VERSION}-darwin-x64.tar.gz`, + 'macos-arm64': `${base}/node-${NODE_VERSION}-darwin-arm64.tar.gz`, + 'windows-x64': `${base}/win-x64/node.exe`, + 'windows-arm64': `${base}/win-arm64/node.exe`, + } + return map[platform] +} + +function needsExtraction (platform) { + return platform !== 'windows-x64' && platform !== 'linux-x64' +} + +function getOutputName (platform) { + return platform === 'windows-x64' || platform === 'windows-arm64' + ? `ds-cli-${VERSION}-${platform}.exe` + : `ds-cli-${VERSION}-${platform}` +} + +// ── 下载与校验 ──────────────────────────────────────── + +function download (url, dest) { + return new Promise((resolve, reject) => { + const mod = url.startsWith('https') ? https : http + const file = fs.createWriteStream(dest) + mod.get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + file.close() + fs.unlinkSync(dest) + return download(res.headers.location, dest).then(resolve, reject) + } + if (res.statusCode !== 200) { + file.close() + fs.unlinkSync(dest) + return reject(new Error(`HTTP ${res.statusCode}: ${url}`)) + } + res.pipe(file) + file.on('finish', () => { file.close(); resolve() }) + }).on('error', (err) => { file.close(); try { fs.unlinkSync(dest) } catch {} ; reject(err) }) + }) +} + +function sha256 (filePath) { + const data = fs.readFileSync(filePath) + return crypto.createHash('sha256').update(data).digest('hex') +} + +async function extractTarGz (tarPath, destDir) { + await tar.extract({ file: tarPath, cwd: destDir }) +} + +// ── 动态获取可用平台 + 校验和 ────────────────────────── + +async function fetchChecksums () { + const url = `https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt` + const tmpFile = path.join(DIST, 'shasums.txt') + fs.mkdirSync(DIST, { recursive: true }) + await download(url, tmpFile) + const content = fs.readFileSync(tmpFile, 'utf-8') + fs.unlinkSync(tmpFile) + + const checksums = {} // filename -> sha256 + const platforms = new Set() + + for (const line of content.split('\n')) { + const parts = line.trim().split(/\s+/) + if (parts.length < 2) continue + const [hash, filename] = parts + if (!/^[a-f0-9]{64}$/.test(hash)) continue + + checksums[filename] = hash + + // 从文件名提取平台 + const tarMatch = filename.match(/node-v[^ ]+?-(linux|darwin|aix|sunos)-(x64|arm64|armv7l|ppc64|s390x)\.tar\.gz$/) + if (tarMatch) { + const mapped = mapNodePlatform(`${tarMatch[1]}-${tarMatch[2]}`) + if (mapped) platforms.add(mapped) + } + const binMatch = filename.match(/(?:node-v[^ ]+?-)?(linux-x64|win-x64|win-arm64)(?:\/node\.exe)?$/) + if (binMatch) { + const mapped = mapNodePlatform(binMatch[1]) + if (mapped) platforms.add(mapped) + } + } + + return { checksums, platforms: [...platforms].sort() } +} + +function mapNodePlatform (nodePlatform) { + const map = { + 'linux-x64': 'linux-x64', + 'linux-arm64': 'linux-arm64', + 'linux-armv7l': 'linux-x64-armv7l', + 'darwin-x64': 'macos-x64', + 'darwin-arm64': 'macos-arm64', + 'win-x64': 'windows-x64', + 'win-arm64': 'windows-arm64', + } + return map[nodePlatform] +} + +// ── 增量构建 ────────────────────────────────────────── + +function hashDir (hash, dir) { + if (!fs.existsSync(dir)) return + for (const f of fs.readdirSync(dir, { recursive: true })) { + if (f.endsWith('.js')) { + hash.update(fs.readFileSync(path.join(dir, f))) + } + } +} + +function computeSourceHash () { + const hash = crypto.createHash('sha256') + // 入口文件 + hash.update(fs.readFileSync(path.join(ROOT, 'src/sea-entry.js'))) + // src/ 下所有 js 文件 + hashDir(hash, path.join(ROOT, 'src')) + // 打包进 bundle 的依赖源码(core / mitmproxy) + hashDir(hash, path.join(ROOT, '../core/src')) + hashDir(hash, path.join(ROOT, '../mitmproxy/src')) + // package.json(版本号变化也应触发重建) + hash.update(fs.readFileSync(path.join(ROOT, 'package.json'))) + return hash.digest('hex') +} + +function getCachedBuildHash () { + const hashFile = path.join(DIST, 'build-hash.txt') + if (!fs.existsSync(hashFile)) return null + return fs.readFileSync(hashFile, 'utf-8').trim() +} + +function saveBuildHash (hash) { + fs.writeFileSync(path.join(DIST, 'build-hash.txt'), hash) +} + +// ── 主流程 ──────────────────────────────────────────── + +async function main () { + const buildAll = process.argv.includes('--all') + const currentPlatform = getCurrentPlatform() + + console.log(`版本: v${VERSION}`) + console.log(`本机系统: ${os.type()} ${os.release()} (${os.arch()})`) + console.log(`本机平台: ${currentPlatform}`) + console.log(`Node.js: ${NODE_VERSION}`) + console.log() + + fs.mkdirSync(DIST, { recursive: true }) + fs.mkdirSync(path.join(DIST, 'node-bin'), { recursive: true }) + + // 增量构建检查 + const currentHash = computeSourceHash() + const cachedHash = getCachedBuildHash() + const bundle = path.join(DIST, 'ds-cli-bundle.js') + const blob = path.join(DIST, 'ds-cli-prep.blob') + const skipBuild = cachedHash === currentHash && fs.existsSync(bundle) && fs.existsSync(blob) + + if (skipBuild) { + console.log('==> 源码未变化,跳过 esbuild 和 blob 生成(使用缓存)') + } else { + // 清理旧构建产物(保留 node-bin 缓存) + console.log('==> 清理旧构建产物...') + for (const f of fs.readdirSync(DIST)) { + if (f.startsWith('ds-cli-') || f === 'sea-config.json' || f === 'ds-cli-bundle.js' || f === 'ds-cli-prep.blob') { + fs.rmSync(path.join(DIST, f), { force: true }) + } + } + console.log() + + // Step 1: esbuild + console.log('==> Step 1: esbuild 打包...') + const esbuild = require('esbuild') + await esbuild.build({ + entryPoints: [path.join(ROOT, 'src/sea-entry.js')], + bundle: true, + platform: 'node', + target: 'node18', + format: 'cjs', + outfile: bundle, + external: [ + 'node:*', + // 原生 .node 模块无法打进 SEA bundle,运行时 require 失败会被调用方 try/catch 兜底 + '@starknt/sysproxy', + // free-eye 为 ESM 模块且依赖源码目录数据,独立可执行文件中不可用; + // core 以相对路径 require 它,必须用通配符匹配,包名前缀匹配不到 + '*free-eye', + ], + }) + const bundleSize = (fs.statSync(bundle).size / 1024 / 1024).toFixed(1) + console.log(` 完成: ${bundle} (${bundleSize}MB)\n`) + } + + // Step 2: 获取校验和 + 确定目标平台 + console.log('==> Step 2: 获取平台信息和校验和...') + const { checksums, platforms: availablePlatforms } = await fetchChecksums() + const targets = buildAll ? availablePlatforms : [currentPlatform] + console.log(` 目标平台: ${targets.join(', ')}`) + console.log() + + // Step 3: 并行下载 Node.js 二进制 + console.log('==> Step 3: 下载 Node.js 二进制(并行)...') + const downloadTasks = targets.map(platform => downloadNodeBinary(platform, checksums)) + const results = await Promise.allSettled(downloadTasks) + + let downloadFailed = false + for (let i = 0; i < results.length; i++) { + const result = results[i] + const platform = targets[i] + if (result.status === 'fulfilled') { + console.log(` ${platform} 下载完成`) + } else { + console.error(` ${platform} 下载失败: ${result.reason.message}`) + downloadFailed = true + } + } + if (downloadFailed) process.exit(1) + console.log() + + // Step 4: 生成 SEA blob + // 使用已下载的当前平台 node 二进制生成 blob,保证 blob 与目标运行时(NODE_VERSION)完全一致, + // 避免 host node 版本与运行时版本不兼容导致的 "v8::ToLocalChecked Empty MaybeLocal" 崩溃 + if (!skipBuild) { + console.log('==> Step 4: 生成 SEA blob...') + const seaConfig = path.join(DIST, 'sea-config.json') + fs.writeFileSync(seaConfig, JSON.stringify({ + main: bundle, + output: blob, + disableExperimentalSEAWarning: true, + })) + const blobNode = path.join(DIST, 'node-bin', `node-${currentPlatform}`) + const seaNode = fs.existsSync(blobNode) ? blobNode : process.execPath + execSync(`"${seaNode}" --experimental-sea-config "${seaConfig}"`, { stdio: 'inherit' }) + saveBuildHash(currentHash) + console.log() + } + + // Step 5: 注入 blob + console.log('==> Step 5: 注入 SEA blob...') + for (const platform of targets) { + const nodeBin = path.join(DIST, 'node-bin', `node-${platform}`) + if (!fs.existsSync(nodeBin)) { + console.log(` ${platform} 跳过(二进制不存在)`) + continue + } + + const output = path.join(DIST, getOutputName(platform)) + fs.copyFileSync(nodeBin, output) + execSync(`npx postject "${output}" NODE_SEA_BLOB "${blob}" --sentinel-fuse ${SENTINEL}`, { + stdio: 'pipe', + }) + if (process.platform !== 'win32') { + fs.chmodSync(output, 0o755) + } + const size = (fs.statSync(output).size / 1024 / 1024).toFixed(1) + console.log(` ${platform} 完成: ${size}MB`) + } + console.log() + + // Step 6: 验证 + console.log('==> Step 6: 验证...') + const verifyBin = path.join(DIST, getOutputName(currentPlatform)) + if (fs.existsSync(verifyBin)) { + try { + const result = execSync(`"${verifyBin}" version`, { encoding: 'utf-8' }).trim() + if (result === VERSION) { + console.log(` 验证通过: v${result}`) + } else { + console.error(` 验证失败: 期望 v${VERSION}, 实际 ${result}`) + process.exit(1) + } + // 冒烟测试:加载 core(校验 bundle 完整性,如 free-eye 等外部模块是否正确排除) + execSync(`"${verifyBin}" status`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }) + console.log(' 冒烟测试通过: status') + } catch (e) { + console.error(` 验证失败: ${e.message}`) + process.exit(1) + } + } + console.log() + + // 输出结果 + console.log('==> 打包完成!') + const files = fs.readdirSync(DIST).filter(f => f.startsWith('ds-cli-') && !f.endsWith('.js') && !f.endsWith('.blob') && !f.endsWith('.json')) + for (const f of files) { + const size = (fs.statSync(path.join(DIST, f)).size / 1024 / 1024).toFixed(1) + console.log(` ${f} (${size}MB)`) + } +} + +// ── 下载单个平台的 Node.js 二进制(含校验) ─────────── + +async function downloadNodeBinary (platform, checksums) { + const nodeBin = path.join(DIST, 'node-bin', `node-${platform}`) + + // 如果已缓存且校验通过,跳过 + if (fs.existsSync(nodeBin)) { + const expectedHash = checksums[getNodeFilename(platform)] + if (expectedHash) { + const actualHash = sha256(nodeBin) + if (actualHash === expectedHash) { + return // 缓存有效,跳过 + } + // 校验失败,重新下载 + fs.rmSync(nodeBin, { force: true }) + } + } + + const url = getNodeDownloadUrl(platform) + if (!url) throw new Error(`${platform} 不支持`) + + const tmpFile = path.join(DIST, 'node-bin', `tmp-${platform}`) + await download(url, tmpFile) + + // SHA256 校验 + const expectedHash = checksums[getNodeFilename(platform)] + if (expectedHash) { + const actualHash = sha256(tmpFile) + if (actualHash !== expectedHash) { + fs.unlinkSync(tmpFile) + throw new Error(`SHA256 校验失败: 期望 ${expectedHash}, 实际 ${actualHash}`) + } + } + + if (needsExtraction(platform)) { + const extractDir = path.join(DIST, 'node-bin', `extract-${platform}`) + fs.mkdirSync(extractDir, { recursive: true }) + await extractTarGz(tmpFile, extractDir) + const entries = fs.readdirSync(extractDir, { recursive: true }) + const nodeEntry = entries.find(e => path.basename(e) === 'node' && path.dirname(e).endsWith('bin')) + if (nodeEntry) { + fs.copyFileSync(path.join(extractDir, nodeEntry), nodeBin) + } + fs.rmSync(extractDir, { recursive: true, force: true }) + fs.unlinkSync(tmpFile) + } else { + fs.renameSync(tmpFile, nodeBin) + } + + if (process.platform !== 'win32') { + fs.chmodSync(nodeBin, 0o755) + } +} + +// 获取 SHASUMS256.txt 中对应的文件名 +function getNodeFilename (platform) { + const map = { + 'linux-x64': `node-${NODE_VERSION}-linux-x64`, + 'linux-arm64': `node-${NODE_VERSION}-linux-arm64.tar.gz`, + 'macos-x64': `node-${NODE_VERSION}-darwin-x64.tar.gz`, + 'macos-arm64': `node-${NODE_VERSION}-darwin-arm64.tar.gz`, + 'windows-x64': `win-x64/node.exe`, + 'windows-arm64': `win-arm64/node.exe`, + } + return map[platform] +} + +main().catch((e) => { + console.error('打包失败:', e.message) + process.exit(1) +}) diff --git a/packages/cli/src/commands/gui.js b/packages/cli/src/commands/gui.js new file mode 100644 index 0000000000..1a06a905ab --- /dev/null +++ b/packages/cli/src/commands/gui.js @@ -0,0 +1,172 @@ +const fs = require('node:fs') +const path = require('node:path') +const { execSync, spawn } = require('node:child_process') +const net = require('node:net') +const jsonApi = require('@docmirror/mitmproxy/src/json') + +const DEFAULT_PORT = 31181 + +function getUserBase () { + return path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') +} + +function getProxyPort () { + const configPath = path.join(getUserBase(), 'config.json') + if (!fs.existsSync(configPath)) return DEFAULT_PORT + try { + const config = jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + return config?.server?.port || DEFAULT_PORT + } catch { + return DEFAULT_PORT + } +} + +function isPortInUse (port) { + return new Promise((resolve) => { + const server = net.createServer() + server.once('error', () => resolve(true)) + server.once('listening', () => { server.close(); resolve(false) }) + server.listen(port, '127.0.0.1') + }) +} + +function isGuiRunningSync () { + try { + if (process.platform === 'win32') { + const out = execSync('tasklist /fi "imagename eq dev-sidecar.exe" /fo csv /nh', { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + return out.includes('dev-sidecar.exe') + } + // Linux/macOS: 检查 dev-sidecar (Electron GUI) 进程 + // GUI 进程特征:包含 electron 或 .app(macOS 应用包) + const out = execSync('pgrep -x dev-sidecar', { encoding: 'utf-8' }).trim() + if (!out) return false + const pids = out.split('\n').filter(Boolean) + for (const pid of pids) { + try { + const args = execSync(`ps -p ${pid} -o args=`, { encoding: 'utf-8' }).trim() + if (args.includes('electron') || args.includes('.app')) { + return true + } + } catch {} + } + return false + } catch { + return false + } +} + +function getGuiPidByPort () { + try { + if (process.platform === 'win32') { + const out = execSync('tasklist /fi "imagename eq dev-sidecar.exe" /fo csv /nh', { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + const lines = out.split(/\r?\n/).filter(l => l.includes('dev-sidecar.exe')) + if (lines.length > 0) { + const match = lines[0].match(/"(\d+)"/) + return match ? parseInt(match[1], 10) : null + } + return null + } + // Linux/macOS: 查找 GUI 进程 + const out = execSync('pgrep -x dev-sidecar', { encoding: 'utf-8' }).trim() + if (!out) return null + const pids = out.split('\n').filter(Boolean) + for (const pid of pids) { + try { + const args = execSync(`ps -p ${pid} -o args=`, { encoding: 'utf-8' }).trim() + if (args.includes('electron') || args.includes('.app') || (!args.includes('--daemon') && !args.includes('ds-cli'))) { + return parseInt(pid, 10) + } + } catch {} + } + return null + } catch { + return null + } +} + +function isGuiRunning () { + return isGuiRunningSync() +} + +function startGui () { + if (isGuiRunningSync()) { + console.log('dev-sidecar GUI 已在运行') + return + } + + if (process.platform === 'darwin') { + spawn('open', ['-a', 'dev-sidecar'], { detached: true, stdio: 'ignore' }).unref() + } else if (process.platform === 'win32') { + execSync('start "" "dev-sidecar"', { stdio: 'ignore' }) + } else { + const paths = [ + '/opt/dev-sidecar/dev-sidecar', + '/usr/bin/dev-sidecar', + '/usr/local/bin/dev-sidecar', + 'dev-sidecar', + ] + for (const p of paths) { + try { + spawn(p, [], { detached: true, stdio: 'ignore', env: process.env }).unref() + break + } catch {} + } + } + + console.log('dev-sidecar GUI 已启动') +} + +function stopGui () { + const pid = getGuiPidByPort() + if (!pid) { + console.log('dev-sidecar GUI 未在运行') + return + } + + try { + process.kill(pid, 'SIGTERM') + console.log(`已发送停止信号到 GUI 进程 (PID: ${pid})`) + } catch (e) { + console.error(`停止 GUI 失败: ${e.message}`) + } +} + +function restartGui () { + stopGui() + let waited = 0 + const interval = setInterval(() => { + if (!isGuiRunningSync() || waited >= 5000) { + clearInterval(interval) + startGui() + } + waited += 200 + }, 200) +} + +function readConfig () { + const configPath = path.join(getUserBase(), 'config.json') + if (!fs.existsSync(configPath)) return {} + try { + return jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + } catch { + return {} + } +} + +function writeConfig (config) { + const configPath = path.join(getUserBase(), 'config.json') + fs.mkdirSync(path.dirname(configPath), { recursive: true }) + fs.writeFileSync(configPath, jsonApi.stringify(config)) +} + +module.exports = { + isGuiRunning, isPortInUse, getProxyPort, + startGui, stopGui, restartGui, + readConfig, writeConfig, +} diff --git a/packages/cli/src/commands/plugin.js b/packages/cli/src/commands/plugin.js new file mode 100644 index 0000000000..002a97a08a --- /dev/null +++ b/packages/cli/src/commands/plugin.js @@ -0,0 +1,102 @@ +const { fork } = require('node:child_process') +const fs = require('node:fs') +const path = require('node:path') +const jsonApi = require('@docmirror/mitmproxy/src/json') + +function getUserBase () { + return path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') +} + +function readConfig () { + const configPath = path.join(getUserBase(), 'config.json') + if (!fs.existsSync(configPath)) return {} + try { + return jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + } catch { + return {} + } +} + +function writeConfig (config) { + const configPath = path.join(getUserBase(), 'config.json') + fs.mkdirSync(path.dirname(configPath), { recursive: true }) + fs.writeFileSync(configPath, jsonApi.stringify(config)) +} + +function getValidPlugins () { + return Object.keys(require('@docmirror/dev-sidecar/src/modules/plugin')) +} + +function getSettingsPath () { + const userBase = process.env.USERPROFILE || process.env.HOME || '/' + const dir = path.join(userBase, '.dev-sidecar') + const newPath = path.join(dir, 'setting.json') + const oldPath = path.join(dir, 'setting.json5') + if (!fs.existsSync(newPath) && fs.existsSync(oldPath)) return oldPath + return newPath +} + +function isOverwallUnlocked () { + const settingPath = getSettingsPath() + if (!fs.existsSync(settingPath)) return false + try { + const setting = jsonApi.parse(fs.readFileSync(settingPath, 'utf-8')) + return setting?.overwall === true + } catch { + return false + } +} + +function handlePlugin (action, name) { + let validPlugins = getValidPlugins() + + if (!isOverwallUnlocked()) { + validPlugins = validPlugins.filter(p => p !== 'overwall') + } + + if (!action || !name) { + console.error('用法: ds-cli plugin ') + console.error(`可用插件: ${validPlugins.join(', ')}`) + process.exit(1) + } + + if (!['start', 'stop'].includes(action)) { + console.error(`无效操作: ${action},可用: start, stop`) + process.exit(1) + } + + if (!validPlugins.includes(name)) { + console.error(`无效插件: ${name},可用: ${validPlugins.join(', ')}`) + process.exit(1) + } + + // free_eye 是一次性测试功能,直接 fork 执行 + if (name === 'free_eye') { + if (action === 'stop') { + console.log('free_eye 是一次性测试功能,stop 命令不适用') + return + } + const workerPath = path.join(__dirname, '../free-eye-worker.js') + const child = fork(workerPath) + child.on('exit', (code) => { + process.exit(code || 0) + }) + return + } + + // git/node/pip 不依赖代理服务,fork worker 立即生效 + const workerPath = path.join(__dirname, '../plugin-worker.js') + const child = fork(workerPath, [action, name]) + child.on('exit', (code) => { + // 同时持久化到 config.json(重启后生效) + const config = readConfig() + config.plugin = config.plugin || {} + config.plugin[name] = config.plugin[name] || {} + config.plugin[name].enabled = action === 'start' + writeConfig(config) + console.log(`已${action === 'start' ? '启用' : '禁用'}插件 ${name}`) + process.exit(code || 0) + }) +} + +module.exports = { handlePlugin, isOverwallUnlocked, getSettingsPath, getValidPlugins } diff --git a/packages/cli/src/commands/restart.js b/packages/cli/src/commands/restart.js new file mode 100644 index 0000000000..3595eb1278 --- /dev/null +++ b/packages/cli/src/commands/restart.js @@ -0,0 +1,42 @@ +const fs = require('node:fs') +const { PID_FILE } = require('./start') + +function isAlive (pid) { + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function restartDaemon () { + const { startDaemon } = require('./start') + + // 先停止 + if (fs.existsSync(PID_FILE)) { + const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10) + if (isAlive(pid)) { + console.log(`正在停止 dev-sidecar (PID: ${pid})...`) + process.kill(pid, 'SIGINT') + + // 等待进程退出(最多 5 秒) + for (let i = 0; i < 50; i++) { + if (!isAlive(pid)) break + await sleep(100) + } + + // 清理残留的 PID 文件 + if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE) + } + } + + // 再启动 + await startDaemon() +} + +module.exports = { restartDaemon } diff --git a/packages/cli/src/commands/service.js b/packages/cli/src/commands/service.js new file mode 100644 index 0000000000..00213cf54e --- /dev/null +++ b/packages/cli/src/commands/service.js @@ -0,0 +1,207 @@ +const fs = require('node:fs') +const path = require('node:path') +const { execSync } = require('node:child_process') + +function getExePath () { + // SEA 模式: process.argv[0] 就是 ds-cli 二进制 + // 开发模式: process.argv[0] 是 node, process.argv[1] 是 cli.js + if (process.argv[0] && !process.argv[0].includes('node')) { + return process.argv[0] + } + return `${process.argv[0]} ${process.argv[1]}` +} + +function tryExec (cmd) { + try { + return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() + } catch { + return null + } +} + +// ── Linux (systemd user service) ───────────────────── + +const LINUX_SERVICE_DIR = path.join( + process.env.HOME || '/', '.config/systemd/user', +) +const LINUX_SERVICE_PATH = path.join(LINUX_SERVICE_DIR, 'ds-cli.service') + +function installLinux () { + const exePath = getExePath() + const service = `[Unit] +Description=DevSidecar CLI Proxy +After=network.target + +[Service] +Type=simple +ExecStart=${exePath} start --daemon +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target +` + try { + fs.mkdirSync(LINUX_SERVICE_DIR, { recursive: true }) + fs.writeFileSync(LINUX_SERVICE_PATH, service) + tryExec('systemctl --user daemon-reload') + const result = tryExec('systemctl --user enable ds-cli') + if (result === null) { + console.log('已生成 systemd service 文件,但 enable 失败') + console.log(` 文件位置: ${LINUX_SERVICE_PATH}`) + console.log(' 请手动执行: systemctl --user enable ds-cli') + } else { + console.log('已注册开机自启动 (systemd user service)') + } + } catch (e) { + console.error(`注册开机自启动失败: ${e.message}`) + console.log(` 请手动将以下内容保存到 ${LINUX_SERVICE_PATH}:`) + console.log(service) + } +} + +function uninstallLinux () { + if (fs.existsSync(LINUX_SERVICE_PATH)) { + tryExec('systemctl --user disable ds-cli') + tryExec('systemctl --user stop ds-cli') + fs.unlinkSync(LINUX_SERVICE_PATH) + tryExec('systemctl --user daemon-reload') + console.log('已移除开机自启动') + } else { + console.log('开机自启动未注册') + } +} + +function isInstalledLinux () { + return fs.existsSync(LINUX_SERVICE_PATH) +} + +// ── macOS (launchd) ────────────────────────────────── + +const MAC_PLIST_NAME = 'com.dev-sidecar.cli.plist' +const MAC_PLIST_PATH = path.join( + process.env.HOME || '/', 'Library/LaunchAgents', MAC_PLIST_NAME, +) + +function installMac () { + const exePath = getExePath() + const logPath = path.join(process.env.HOME || '/', '.dev-sidecar/logs/cli.log') + const plist = ` + + + + Label + com.dev-sidecar.cli + ProgramArguments + + ${exePath} + start + + RunAtLoad + + KeepAlive + + StandardOutPath + ${logPath} + StandardErrorPath + ${logPath} + + +` + try { + fs.mkdirSync(path.dirname(MAC_PLIST_PATH), { recursive: true }) + fs.writeFileSync(MAC_PLIST_PATH, plist) + tryExec(`launchctl load ${MAC_PLIST_PATH}`) + console.log('已注册开机自启动 (launchd)') + } catch (e) { + console.error(`注册开机自启动失败: ${e.message}`) + } +} + +function uninstallMac () { + if (fs.existsSync(MAC_PLIST_PATH)) { + tryExec(`launchctl unload ${MAC_PLIST_PATH}`) + fs.unlinkSync(MAC_PLIST_PATH) + console.log('已移除开机自启动') + } else { + console.log('开机自启动未注册') + } +} + +function isInstalledMac () { + return fs.existsSync(MAC_PLIST_PATH) +} + +// ── Windows (Registry Run key) ─────────────────────── + +const WIN_REG_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' +const WIN_REG_VALUE = 'ds-cli' + +function installWindows () { + const exePath = getExePath() + try { + execSync( + `reg add "${WIN_REG_KEY}" /v "${WIN_REG_VALUE}" /t REG_SZ /d "\\"${exePath}\\" start" /f`, + { stdio: 'ignore' }, + ) + console.log('已注册开机自启动 (注册表)') + } catch (e) { + console.error(`注册开机自启动失败: ${e.message}`) + } +} + +function uninstallWindows () { + try { + execSync(`reg delete "${WIN_REG_KEY}" /v "${WIN_REG_VALUE}" /f`, { stdio: 'ignore' }) + console.log('已移除开机自启动') + } catch { + console.log('开机自启动未注册') + } +} + +function isInstalledWindows () { + try { + const out = execSync(`reg query "${WIN_REG_KEY}" /v "${WIN_REG_VALUE}"`, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + return out.includes(WIN_REG_VALUE) + } catch { + return false + } +} + +// ── 统一接口 ───────────────────────────────────────── + +function install () { + switch (process.platform) { + case 'linux': return installLinux() + case 'darwin': return installMac() + case 'win32': return installWindows() + default: + console.error(`不支持的平台: ${process.platform}`) + process.exit(1) + } +} + +function uninstall () { + switch (process.platform) { + case 'linux': return uninstallLinux() + case 'darwin': return uninstallMac() + case 'win32': return uninstallWindows() + default: + console.error(`不支持的平台: ${process.platform}`) + process.exit(1) + } +} + +function isInstalled () { + switch (process.platform) { + case 'linux': return isInstalledLinux() + case 'darwin': return isInstalledMac() + case 'win32': return isInstalledWindows() + default: return false + } +} + +module.exports = { install, uninstall, isInstalled } diff --git a/packages/cli/src/commands/start.js b/packages/cli/src/commands/start.js new file mode 100644 index 0000000000..0d6f59758c --- /dev/null +++ b/packages/cli/src/commands/start.js @@ -0,0 +1,133 @@ +const fs = require('node:fs') +const path = require('node:path') +const net = require('node:net') +const { fork, execSync } = require('node:child_process') +const jsonApi = require('@docmirror/mitmproxy/src/json') + +const DEFAULT_PORT = 31181 + +function getUserBase () { + return path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') +} + +function getPidFile () { + return path.join(getUserBase(), 'ds-cli.pid') +} + +function getProxyPort () { + const configPath = path.join(getUserBase(), 'config.json') + if (!fs.existsSync(configPath)) return DEFAULT_PORT + try { + const config = jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + return config?.server?.port || DEFAULT_PORT + } catch { + return DEFAULT_PORT + } +} + +function isPortInUse (port) { + return new Promise((resolve) => { + const server = net.createServer() + server.once('error', () => resolve(true)) + server.once('listening', () => { server.close(); resolve(false) }) + server.listen(port, '127.0.0.1') + }) +} + +function isAlive (pid) { + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +function isDsCliProcess (pid) { + try { + if (process.platform === 'win32') { + const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh`, { encoding: 'utf-8' }) + return out.toLowerCase().includes('node') + } + const out = execSync(`ps -p ${pid} -o args=`, { encoding: 'utf-8' }) + return out.includes('--daemon') + } catch { + return false + } +} + +function isRunning () { + const pidFile = getPidFile() + if (!fs.existsSync(pidFile)) return false + const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10) + if (!isAlive(pid)) return false + if (!isDsCliProcess(pid)) { + fs.unlinkSync(pidFile) + return false + } + return true +} + +function isGuiRunning () { + try { + if (process.platform === 'win32') { + const out = execSync('tasklist /fi "imagename eq dev-sidecar.exe" /fo csv /nh', { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + return out.includes('dev-sidecar.exe') + } + // Linux/macOS: 查找 dev-sidecar 进程 + // GUI 进程特征:包含 electron 或 .app(macOS 应用包) + const out = execSync('pgrep -x dev-sidecar', { encoding: 'utf-8' }).trim() + if (!out) return false + const pids = out.split('\n').filter(Boolean) + for (const pid of pids) { + try { + const args = execSync(`ps -p ${pid} -o args=`, { encoding: 'utf-8' }).trim() + if (args.includes('electron') || args.includes('.app')) { + return true + } + } catch {} + } + return false + } catch { + return false + } +} + +async function startDaemon () { + // 锁检查:锁被持有说明 CLI 或 GUI 已在运行 + const DevSidecar = require('@docmirror/dev-sidecar') + if (await DevSidecar.api.instance.isLocked()) { + const instance = await DevSidecar.api.instance.readInstance() + const typeLabel = instance?.type === 'gui' ? 'GUI' : 'CLI' + console.log(`dev-sidecar ${typeLabel} 已在运行中${instance?.pid ? `(PID: ${instance.pid})` : ''},请先关闭后再启动 CLI`) + return + } + + // 端口占用兜底检测 + const port = getProxyPort() + if (await isPortInUse(port)) { + console.log(`代理端口 ${port} 已被占用,dev-sidecar 可能已在运行`) + return + } + + const childPath = path.join(__dirname, '../index.js') + const child = fork(childPath, ['--daemon'], { + detached: true, + stdio: 'ignore', + env: { + ...process.env, + DEV_SIDECAR_LOG_TO_CONSOLE: 'false', // 后台守护进程日志只写文件 + }, + }) + child.unref() + + fs.mkdirSync(path.dirname(getPidFile()), { recursive: true }) + fs.writeFileSync(getPidFile(), String(child.pid)) + + console.log(`dev-sidecar 已在后台启动,PID: ${child.pid}`) +} + +module.exports = { startDaemon, isRunning, PID_FILE: getPidFile(), getProxyPort, isPortInUse, isAlive, isDsCliProcess, isGuiRunning } diff --git a/packages/cli/src/commands/status.js b/packages/cli/src/commands/status.js new file mode 100644 index 0000000000..6d1b77ab1b --- /dev/null +++ b/packages/cli/src/commands/status.js @@ -0,0 +1,97 @@ +const fs = require('node:fs') +const path = require('node:path') + +function getUserBase () { + return path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') +} + +function getRunningJsonPath () { + return path.join(getUserBase(), 'running.json') +} + +function isAlive (pid) { + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +// 插件列表:free_eye 是一次性插件,无持久化状态,不显示; +// overwall 仅解锁后(setting.json 中 overwall === true)才显示 +function getPluginNames () { + const names = ['git', 'node', 'pip'] + try { + const { isOverwallUnlocked } = require('./plugin') + if (isOverwallUnlocked()) { + names.push('overwall') + } + } catch {} + return names +} + +function printStatus (status) { + const serverRunning = status.server?.enabled || false + const proxyEnabled = status.proxy?.enabled || false + + let autoStart = '未知' + try { + const { isInstalled } = require('./service') + autoStart = isInstalled() ? '已注册' : '未注册' + } catch {} + + // 读取 running.json 中的实例信息 + let instanceInfo = '' + try { + const DevSidecar = require('@docmirror/dev-sidecar') + const instance = DevSidecar.api.instance.readInstance() + if (instance) { + instanceInfo = ` 运行实例: ${instance.type === 'gui' ? 'GUI' : 'CLI'}${instance.pid ? ` (PID: ${instance.pid})` : ''}${instance.startTime ? `,启动于 ${instance.startTime}` : ''}` + } + } catch {} + + console.log('dev-sidecar 运行状态:') + console.log(` 代理服务: ${serverRunning ? '运行中' : '未运行'}`) + console.log(` 系统代理: ${proxyEnabled ? '已开启' : '未开启'}`) + console.log(` 开机启动: ${autoStart}`) + if (instanceInfo) { + console.log(instanceInfo) + } + console.log(' 插件:') + + for (const name of getPluginNames()) { + const enabled = status.plugin?.[name]?.enabled || false + const label = name.padEnd(8) + console.log(` ${label} ${enabled ? '已启用' : '未启用'}`) + } +} + +async function showStatus () { + // 锁新鲜 = 有实例在运行(GUI 或 CLI),替代 status.json/PID 文件判断 + const DevSidecar = require('@docmirror/dev-sidecar') + const running = await DevSidecar.api.instance.isLocked() + + let autoStart = '未知' + try { + const { isInstalled } = require('./service') + autoStart = isInstalled() ? '已注册' : '未注册' + } catch {} + + if (!running) { + console.log('dev-sidecar 未在运行') + console.log(` 开机启动: ${autoStart}`) + return + } + + // 读取 running.json 中的运行时状态(由状态事件驱动写入) + let status = {} + try { + const data = JSON.parse(fs.readFileSync(getRunningJsonPath(), 'utf-8')) + status = data?.app?.status || {} + } catch {} + + printStatus(status) +} + +module.exports = { showStatus, getPluginNames } diff --git a/packages/cli/src/commands/stop.js b/packages/cli/src/commands/stop.js new file mode 100644 index 0000000000..8e95264d94 --- /dev/null +++ b/packages/cli/src/commands/stop.js @@ -0,0 +1,41 @@ +const fs = require('node:fs') +const { PID_FILE } = require('./start') + +function isAlive (pid) { + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +function stopDaemon () { + if (!fs.existsSync(PID_FILE)) { + console.log('dev-sidecar 未在运行') + return + } + + const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10) + if (!isAlive(pid)) { + console.log('dev-sidecar 进程已不存在,清理 PID 文件') + fs.unlinkSync(PID_FILE) + return + } + + process.kill(pid, 'SIGINT') + console.log(`已发送停止信号到 PID: ${pid}`) + + let waited = 0 + const interval = setInterval(() => { + if (!isAlive(pid) || waited >= 5000) { + clearInterval(interval) + if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE) + console.log('dev-sidecar 已停止') + return + } + waited += 200 + }, 200) +} + +module.exports = { stopDaemon } diff --git a/packages/cli/src/free-eye-worker.js b/packages/cli/src/free-eye-worker.js new file mode 100644 index 0000000000..bd5d234f76 --- /dev/null +++ b/packages/cli/src/free-eye-worker.js @@ -0,0 +1,30 @@ +const DevSidecar = require('@docmirror/dev-sidecar') + +DevSidecar.api.config.reload() + +async function run () { + console.log('正在运行 free_eye 测试...\n') + const result = await DevSidecar.api.plugin.free_eye.start() + + console.log('=== free_eye 测试结果 ===') + console.log(`完成时间: ${result.finishedAt}`) + console.log(`总测试数: ${result.totalTests}`) + console.log(`已完成: ${result.completedTests}`) + + if (result.summaries && result.summaries.length > 0) { + console.log('\n摘要:') + for (const s of result.summaries) { + console.log(` ${s}`) + } + } + + if (result.error) { + console.error(`\n错误: ${result.error}`) + process.exit(1) + } +} + +run().catch((e) => { + console.error('测试执行失败:', e.message) + process.exit(1) +}) diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index b45cab80bf..015de37524 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -1,37 +1,204 @@ const fs = require('node:fs') -const DevSidecar = require('@docmirror/dev-sidecar') -const jsonApi = require('@docmirror/mitmproxy/src/json') - -// 启动服务 -const mitmproxyPath = './mitmproxy' -async function startup () { - const banner = fs.readFileSync('./banner.txt') - console.log(banner.toString()) - - const configPath = './user_config.json5' - if (fs.existsSync(configPath)) { - const file = fs.readFileSync(configPath) - let userConfig +const path = require('node:path') + +// CLI 命令输出与日志分离:默认日志只写文件,stdout 只输出命令结果; +// 调试时可显式设置 DEV_SIDECAR_LOG_TO_CONSOLE=true +process.env.DEV_SIDECAR_LOG_TO_CONSOLE ??= 'false' + +const args = process.argv.slice(2) +const isDaemon = args.includes('--daemon') + +if (isDaemon) { + runDaemon() +} else { + routeCommand(args) +} + +// ── 守护进程模式 ────────────────────────────────────────── + +function runDaemon () { + const DevSidecar = require('@docmirror/dev-sidecar') + const log = require('@docmirror/dev-sidecar/src/utils/util.log-or-console') + + const mitmproxyPath = path.join(__dirname, 'mitmproxy.js') + + const userBasePath = path.join( + process.env.USERPROFILE || process.env.HOME || '/', + '.dev-sidecar', + ) + const PID_FILE = path.join(userBasePath, 'ds-cli.pid') + + async function startup () { + // 获取实例锁,防止 CLI/GUI 重复运行 try { - userConfig = jsonApi.parse(file.toString()) - console.info(`读取和解析 user_config.json5 成功:${configPath}`) + await DevSidecar.api.instance.acquireLock({ log }) } catch (e) { - console.error(`读取或解析 user_config.json5 失败: ${configPath}, error:`, e) - userConfig = {} + log.error('另一个 dev-sidecar 实例正在运行,CLI 启动失败:', e.message) + process.exit(1) } - DevSidecar.api.config.set(userConfig) + try { + await DevSidecar.api.instance.writeInstance({ + type: 'cli', + pid: process.pid, + command: process.argv.join(' '), + startTime: new Date().toISOString(), + }) + } catch (e) { + log.error('写入 running.json 实例信息失败:', e.message) + } + + const banner = fs.readFileSync(path.join(__dirname, 'banner.txt')) + log.info(banner.toString()) + + DevSidecar.api.config.reload() + await DevSidecar.api.startup({ mitmproxyPath }) + await DevSidecar.api.config.startAutoDownloadRemoteConfig() + log.info('dev-sidecar 已启动') } - await DevSidecar.api.startup({ mitmproxyPath }) - console.log('dev-sidecar 已启动') + async function onClose () { + log.info('on sigint') + await DevSidecar.api.shutdown() + log.info('on closed') + cleanupFiles() + process.exit(0) + } + + function cleanupFiles () { + try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE) } catch {} + } + + process.on('SIGINT', onClose) + process.on('SIGTERM', onClose) + process.on('exit', cleanupFiles) + + startup() } -async function onClose () { - console.log('on sigint ') - await DevSidecar.api.shutdown() - console.log('on closed ') - process.exit(0) +// ── 帮助信息 ────────────────────────────────────────────── + +function printHelp () { + console.log(`用法: ds-cli <命令> [选项] + +命令: + start 启动守护进程 + stop 停止守护进程 + restart 重启守护进程 + status 显示运行状态 + version 显示版本号 + proxy on 开启系统代理 + proxy off 关闭系统代理 + plugin start 启用插件 (git/node/pip/overwall/free_eye) + plugin stop 禁用插件 + service install 注册开机自启动 + service uninstall 移除开机自启动 + help 显示此帮助信息 + +选项: + --gui 仅操作 GUI + --all 同时操作 CLI 和 GUI`) } -process.on('SIGINT', onClose) -startup() +// ── 命令路由 ────────────────────────────────────────────── + +function routeCommand (args) { + const flags = args.filter(a => a.startsWith('--')) + const positional = args.filter(a => !a.startsWith('--')) + const command = positional[0] || 'start' + const value = positional[1] + + const guiMode = flags.includes('--gui') + const allMode = flags.includes('--all') + + const runCli = !guiMode || allMode + const runGui = guiMode || allMode + + switch (command) { + case 'start': { + const { startDaemon } = require('./commands/start') + const { startGui } = require('./commands/gui') + const tasks = [] + if (runCli) tasks.push(startDaemon()) + if (runGui) tasks.push(Promise.resolve(startGui())) + Promise.all(tasks).then(() => process.exit(0)) + break + } + case 'stop': { + const { stopDaemon } = require('./commands/stop') + const { stopGui } = require('./commands/gui') + if (runCli) stopDaemon() + if (runGui) stopGui() + break + } + case 'restart': { + const { restartDaemon } = require('./commands/restart') + const { restartGui } = require('./commands/gui') + const tasks = [] + if (runCli) tasks.push(restartDaemon()) + if (runGui) tasks.push(Promise.resolve(restartGui())) + Promise.all(tasks).then(() => process.exit(0)) + break + } + case 'status': { + const { showStatus } = require('./commands/status') + showStatus().then(() => process.exit(0)) + break + } + case 'version': { + const pkgPath = path.join(__dirname, '../package.json') + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) + console.log(pkg.version) + break + } + case 'plugin': { + const { handlePlugin } = require('./commands/plugin') + handlePlugin(value, positional[2]) + break + } + case 'proxy': { + const { readConfig, writeConfig } = require('./commands/gui') + if (value === 'on' || value === 'off') { + // 持久化到 config.json + const config = readConfig() + config.proxy = config.proxy || {} + config.proxy.enabled = value === 'on' + writeConfig(config) + + // fork worker 立即设置/取消系统代理 + const { fork } = require('node:child_process') + const workerPath = path.join(__dirname, 'proxy-worker.js') + const child = fork(workerPath, [value]) + child.on('exit', (code) => { + process.exit(code || 0) + }) + } else { + console.error('用法: ds-cli proxy ') + process.exit(1) + } + break + } + case 'service': { + const { install, uninstall } = require('./commands/service') + if (value === 'install') install() + else if (value === 'uninstall') uninstall() + else { + console.error('用法: ds-cli service ') + process.exit(1) + } + break + } + case 'help': { + printHelp() + break + } + default: + if (flags.includes('--help') || flags.includes('-h')) { + printHelp() + } else { + console.error(`未知命令: ${command}`) + printHelp() + process.exit(1) + } + process.exit(1) + } +} diff --git a/packages/cli/src/mitmproxy.js b/packages/cli/src/mitmproxy.js index c3b0720bd3..da527295fe 100644 --- a/packages/cli/src/mitmproxy.js +++ b/packages/cli/src/mitmproxy.js @@ -7,7 +7,7 @@ const log = require('@docmirror/mitmproxy/src/utils/util.log.server') // 当前 const home = process.env.USER_HOME || process.env.HOME || 'C:/Users/Administrator/' let configPath -if (process.argv && process.argv.length > 3) { +if (process.argv && process.argv.length >= 3) { configPath = process.argv[2] } else { configPath = path.join(home, '.dev-sidecar/running.json') diff --git a/packages/cli/src/plugin-worker.js b/packages/cli/src/plugin-worker.js new file mode 100644 index 0000000000..f4a99997f4 --- /dev/null +++ b/packages/cli/src/plugin-worker.js @@ -0,0 +1,21 @@ +const DevSidecar = require('@docmirror/dev-sidecar') + +const action = process.argv[2] +const name = process.argv[3] + +DevSidecar.api.config.reload() + +async function run () { + if (action === 'start') { + await DevSidecar.api.plugin[name].start() + console.log(`插件 ${name} 已启动`) + } else if (action === 'stop') { + await DevSidecar.api.plugin[name].close() + console.log(`插件 ${name} 已停止`) + } +} + +run().catch((e) => { + console.error(`操作失败:`, e.message) + process.exit(1) +}) diff --git a/packages/cli/src/proxy-worker.js b/packages/cli/src/proxy-worker.js new file mode 100644 index 0000000000..fae4deea7d --- /dev/null +++ b/packages/cli/src/proxy-worker.js @@ -0,0 +1,22 @@ +const DevSidecar = require('@docmirror/dev-sidecar') + +DevSidecar.api.config.reload() + +const action = process.argv[2] + +async function run () { + if (action === 'on') { + await DevSidecar.api.proxy.start() + DevSidecar.api.instance.updateStatus('proxy.enabled', true) + console.log('系统代理已开启') + } else if (action === 'off') { + await DevSidecar.api.proxy.close() + DevSidecar.api.instance.updateStatus('proxy.enabled', false) + console.log('系统代理已关闭') + } +} + +run().catch((e) => { + console.error(`操作失败:`, e.message) + process.exit(1) +}) diff --git a/packages/cli/src/sea-entry.js b/packages/cli/src/sea-entry.js new file mode 100644 index 0000000000..4862e00daf --- /dev/null +++ b/packages/cli/src/sea-entry.js @@ -0,0 +1,302 @@ +#!/usr/bin/env node +// SEA (Single Executable Application) 入口 +// 同进程启动代理,不使用 fork() + +const fs = require('node:fs') +const path = require('node:path') +const lodash = require('lodash') + +// CLI 命令输出与日志分离:默认日志只写文件,stdout 只输出命令结果 +process.env.DEV_SIDECAR_LOG_TO_CONSOLE ??= 'false' + +const userBase = path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') +const PID_FILE = path.join(userBase, 'ds-cli.pid') + +// ── 加载配置 ────────────────────────────────────────── + +function loadConfig () { + const defConfig = require('@docmirror/dev-sidecar/src/config/index.js') + const configLoader = require('@docmirror/dev-sidecar/src/config/local-config-loader') + const mergeApi = require('@docmirror/dev-sidecar/src/merge') + const jsonApi = require('@docmirror/mitmproxy/src/json') + + // 读取用户配置 + const userConfigPath = configLoader.getUserConfigPath() + let userConfig = {} + if (fs.existsSync(userConfigPath)) { + try { + userConfig = jsonApi.parse(fs.readFileSync(userConfigPath, 'utf-8')) + } catch {} + } + + // 读取远程配置 + const remoteConfig = configLoader.getRemoteConfig() + const personalRemoteConfig = configLoader.getRemoteConfig('_personal') + + // 合并(与 core 相同的合并顺序) + const merged = lodash.cloneDeep(userConfig) + mergeApi.doMerge(merged, personalRemoteConfig) + mergeApi.doMerge(merged, remoteConfig) + mergeApi.doMerge(merged, defConfig) + mergeApi.doMerge(merged, remoteConfig) + mergeApi.doMerge(merged, personalRemoteConfig) + if (userConfig != null) { + mergeApi.doMerge(merged, userConfig) + } + mergeApi.deleteNullItems(merged) + + return merged +} + +// ── 准备服务配置 ────────────────────────────────────── + +function prepareServerConfig (allConfig) { + const serverConfig = lodash.cloneDeep(allConfig.server) + const intercepts = serverConfig.intercepts + const dnsMapping = serverConfig.dns.mapping + + if (allConfig.plugin) { + lodash.each(allConfig.plugin, (value) => { + const plugin = value + if (!plugin.enabled) return + if (plugin.intercepts) lodash.merge(intercepts, plugin.intercepts) + if (plugin.dns) lodash.merge(dnsMapping, plugin.dns) + }) + } + + if (allConfig.app) serverConfig.app = allConfig.app + if (serverConfig.intercept.enabled === false) serverConfig.intercepts = {} + serverConfig.plugin = allConfig.plugin + if (allConfig.proxy && allConfig.proxy.enabled) serverConfig.proxy = allConfig.proxy + + return serverConfig +} + +// ── 启动代理 ────────────────────────────────────────── + +async function startProxy (serverConfig) { + const mitmproxy = require('@docmirror/mitmproxy') + + // 设置 CA 证书路径 + if (serverConfig.setting && serverConfig.setting.userBasePath) { + mitmproxy.config.setDefaultCABasePath(serverConfig.setting.userBasePath) + } + + // 设置根目录(GUI 脚本路径) + serverConfig.setting.rootDir = path.join(userBase, '../dev-sidecar-gui/') + + await mitmproxy.start(serverConfig) + return mitmproxy +} + +// ── 主流程 ────────────────────────────────────────── + +const args = process.argv.slice(2) +const isDaemon = args.includes('--daemon') + +if (isDaemon) { + runDaemon() +} else { + routeCommand(args) +} + +async function runDaemon () { + const log = require('@docmirror/dev-sidecar/src/utils/util.log-or-console') + + async function startup () { + const log = require('@docmirror/dev-sidecar/src/utils/util.log-or-console') + + // 获取实例锁,防止 CLI/GUI 重复运行 + const DevSidecar = require('@docmirror/dev-sidecar') + try { + await DevSidecar.api.instance.acquireLock({ log }) + } catch (e) { + log.error('另一个 dev-sidecar 实例正在运行,CLI 启动失败:', e.message) + process.exit(1) + } + try { + await DevSidecar.api.instance.writeInstance({ + type: 'cli', + pid: process.pid, + command: process.argv.join(' '), + startTime: new Date().toISOString(), + }) + } catch (e) { + log.error('写入 running.json 实例信息失败:', e.message) + } + + const BANNER = ` ____ _____ _ __ + / __ \\___ _ __ / ___/(_)___/ /__ _________ ______ + / / / / _ \\ | / /_____\\__ \\/ / __ / _ \\/ ___/ __ \`/ ___/ + / /_/ / __/ |/ /_____/__/ / / /_/ / __/ /__/ /_/ / / +/_____/\\___/|___/ /____/_/\\__,_/\\___/\\___/\\__,_/_/ + + +==================== 开发者边车 ====================` + log.info(BANNER) + + const allConfig = loadConfig() + const serverConfig = prepareServerConfig(allConfig) + + // 写入 running.json(供调试),保留现有 instance 信息 + const runningConfigPath = path.join(userBase, 'running.json') + try { + const jsonApi = require('@docmirror/mitmproxy/src/json') + let existingInstance + if (fs.existsSync(runningConfigPath)) { + try { + const existing = JSON.parse(fs.readFileSync(runningConfigPath, 'utf-8')) + existingInstance = existing?.app?.instance + } catch {} + } + if (existingInstance) { + if (!serverConfig.app) { + serverConfig.app = {} + } + serverConfig.app.instance = existingInstance + } + fs.writeFileSync(runningConfigPath, jsonApi.stringify(serverConfig)) + } catch {} + + const mitmproxy = await startProxy(serverConfig) + log.info('dev-sidecar 已启动(同进程模式)') + + // 主动同步状态到 running.json(SEA 模式不走 core 的 server/proxy 模块,状态事件不会自动触发) + DevSidecar.api.instance.updateStatus('server.enabled', true) + DevSidecar.api.instance.updateStatus('proxy.enabled', !!(allConfig.proxy && allConfig.proxy.enabled)) + } + + async function onClose () { + const log = require('@docmirror/dev-sidecar/src/utils/util.log-or-console') + log.info('on sigint') + try { + const mitmproxy = require('@docmirror/mitmproxy') + await mitmproxy.close() + } catch {} + log.info('on closed') + cleanupFiles() + process.exit(0) + } + + function cleanupFiles () { + try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE) } catch {} + } + + process.on('SIGINT', onClose) + process.on('SIGTERM', onClose) + process.on('exit', cleanupFiles) + + await startup() +} + +async function routeCommand (args) { + const flags = args.filter(a => a.startsWith('--')) + const positional = args.filter(a => !a.startsWith('--')) + const command = positional[0] || 'start' + + switch (command) { + case 'start': { + // 锁检查:锁被持有说明 CLI 或 GUI 已在运行 + const DevSidecar = require('@docmirror/dev-sidecar') + if (await DevSidecar.api.instance.isLocked()) { + const instance = await DevSidecar.api.instance.readInstance() + const typeLabel = instance?.type === 'gui' ? 'GUI' : 'CLI' + console.log(`dev-sidecar ${typeLabel} 已在运行中${instance?.pid ? `(PID: ${instance.pid})` : ''},请先关闭后再启动 CLI`) + process.exit(0) + break + } + const { fork } = require('node:child_process') + const child = fork(__filename, ['--daemon'], { detached: true, stdio: 'ignore' }) + child.unref() + fs.mkdirSync(path.dirname(PID_FILE), { recursive: true }) + fs.writeFileSync(PID_FILE, String(child.pid)) + console.log(`dev-sidecar 已在后台启动,PID: ${child.pid}`) + process.exit(0) + break + } + case 'stop': { + const { stopDaemon } = require('./commands/stop') + stopDaemon() + break + } + case 'restart': { + const { restartDaemon } = require('./commands/restart') + restartDaemon().then(() => process.exit(0)) + break + } + case 'status': { + const { showStatus } = require('./commands/status') + showStatus().then(() => process.exit(0)) + break + } + case 'version': { + console.log('2.2.1') + break + } + case 'plugin': { + const { handlePlugin } = require('./commands/plugin') + handlePlugin(positional[1], positional[2]) + break + } + case 'proxy': { + const { readConfig, writeConfig } = require('./commands/gui') + if (positional[1] === 'on' || positional[1] === 'off') { + const config = readConfig() + config.proxy = config.proxy || {} + config.proxy.enabled = positional[1] === 'on' + writeConfig(config) + + const { fork } = require('node:child_process') + const workerPath = path.join(__dirname, 'proxy-worker.js') + const child = fork(workerPath, [positional[1]]) + child.on('exit', (code) => { + process.exit(code || 0) + }) + } else { + console.error('用法: ds-cli proxy ') + process.exit(1) + } + break + } + case 'service': { + const { install, uninstall } = require('./commands/service') + if (positional[1] === 'install') install() + else if (positional[1] === 'uninstall') uninstall() + else { + console.error('用法: ds-cli service ') + process.exit(1) + } + break + } + case 'help': { + printHelp() + break + } + default: + console.error(`未知命令: ${command}`) + printHelp() + process.exit(1) + } +} + +function printHelp () { + console.log(`用法: ds-cli <命令> [选项] + +命令: + start 启动守护进程 + stop 停止守护进程 + restart 重启守护进程 + status 显示运行状态 + version 显示版本号 + proxy on 开启系统代理 + proxy off 关闭系统代理 + plugin start 启用插件 (git/node/pip/overwall/free_eye) + plugin stop 禁用插件 + service install 注册开机自启动 + service uninstall 移除开机自启动 + help 显示此帮助信息 + +选项: + --gui 仅操作 GUI + --all 同时操作 CLI 和 GUI`) +} diff --git a/packages/cli/src/user_config.json5 b/packages/cli/src/user_config.json5 deleted file mode 100644 index 56afb1fdd0..0000000000 --- a/packages/cli/src/user_config.json5 +++ /dev/null @@ -1,42 +0,0 @@ -{ - "app": { - "autoStart": { - "enabled": true - }, - "mode": "default" - }, - "plugin": { - "node": { - "setting": { - "yarnRegistry": "null" - } - }, - "git": { - "enabled": true - }, - "overwall": { - "enabled": false, - "targets": { - "*gagedigital.com": true, - "*yonsz.net": true, - "*bootstrapcdn.com": true, - "*cloudflare.com": true, - "help.yonsz.net": true - } - } - }, - "server": { - "intercepts": { - "dev-sidecar.docmirror.cn": { - ".*": { - "proxy": "dev-sidecar-preview.docmirror.cn" - } - }, - "test1111.gagedigital.com": { - ".*": { - "proxy": "test1.gagedigital.com" - } - } - } - } -} diff --git a/packages/cli/test/gui.test.js b/packages/cli/test/gui.test.js new file mode 100644 index 0000000000..53889bb1ef --- /dev/null +++ b/packages/cli/test/gui.test.js @@ -0,0 +1,76 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +describe('gui', function () { + function withTempHome (fn) { + const originalHome = process.env.HOME + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + const userBase = path.join(tmpDir, '.dev-sidecar') + fs.mkdirSync(userBase, { recursive: true }) + process.env.HOME = tmpDir + try { + fn(userBase, tmpDir) + } finally { + process.env.HOME = originalHome + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + } + + describe('isPortInUse', function () { + const { isPortInUse } = require('../src/commands/gui') + + it('should return false for an available port', async function () { + const port = 49152 + Math.floor(Math.random() * 1000) + assert.isFalse(await isPortInUse(port)) + }) + + it('should return true for a port in use', async function () { + const net = require('node:net') + const server = net.createServer() + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const port = server.address().port + assert.isTrue(await isPortInUse(port)) + server.close() + }) + }) + + describe('getProxyPort', function () { + const { getProxyPort } = require('../src/commands/gui') + + it('should return default port when no config exists', function () { + withTempHome(() => { + assert.strictEqual(getProxyPort(), 31181) + }) + }) + + it('should read port from config', function () { + withTempHome((userBase) => { + fs.writeFileSync(path.join(userBase, 'config.json'), JSON.stringify({ server: { port: 8080 } })) + assert.strictEqual(getProxyPort(), 8080) + }) + }) + + it('should return default for malformed config', function () { + withTempHome((userBase) => { + fs.writeFileSync(path.join(userBase, 'config.json'), 'not json') + assert.strictEqual(getProxyPort(), 31181) + }) + }) + }) + + describe('GUI process detection', function () { + const { execSync } = require('node:child_process') + + it('should not find a non-existent GUI process', function () { + let found = true + try { + execSync('pgrep -x dev-sidecar_nonexistent_name', { stdio: 'ignore' }) + } catch { + found = false + } + assert.isFalse(found) + }) + }) +}) diff --git a/packages/cli/test/index.test.js b/packages/cli/test/index.test.js new file mode 100644 index 0000000000..bf7e82e194 --- /dev/null +++ b/packages/cli/test/index.test.js @@ -0,0 +1,134 @@ +const { assert } = require('chai') +const { execSync } = require('node:child_process') +const path = require('node:path') + +describe('index', function () { + const cliPath = path.join(__dirname, '../cli.js') + + describe('command routing', function () { + it('should parse --gui flag', function () { + const args = ['start', '--gui'] + const flags = args.filter(a => a.startsWith('--')) + const positional = args.filter(a => !a.startsWith('--')) + assert.deepEqual(flags, ['--gui']) + assert.deepEqual(positional, ['start']) + }) + + it('should parse --all flag', function () { + const args = ['stop', '--all'] + const flags = args.filter(a => a.startsWith('--')) + const positional = args.filter(a => !a.startsWith('--')) + assert.deepEqual(flags, ['--all']) + assert.deepEqual(positional, ['stop']) + }) + + it('should parse multiple flags', function () { + const args = ['restart', '--gui', '--all'] + const flags = args.filter(a => a.startsWith('--')) + const positional = args.filter(a => !a.startsWith('--')) + assert.include(flags, '--gui') + assert.include(flags, '--all') + assert.deepEqual(positional, ['restart']) + }) + + it('should default to start command', function () { + const args = [] + const command = args[0] || 'start' + assert.strictEqual(command, 'start') + }) + + it('should extract plugin subcommand', function () { + const args = ['plugin', 'start', 'git'] + const positional = args.filter(a => !a.startsWith('--')) + const command = positional[0] + const action = positional[1] + const name = positional[2] + assert.strictEqual(command, 'plugin') + assert.strictEqual(action, 'start') + assert.strictEqual(name, 'git') + }) + + it('should extract service subcommand', function () { + const args = ['service', 'install'] + const positional = args.filter(a => !a.startsWith('--')) + assert.strictEqual(positional[0], 'service') + assert.strictEqual(positional[1], 'install') + }) + + it('should determine run targets from flags', function () { + // --gui: only GUI + let flags = ['--gui'] + let runCli = !flags.includes('--gui') || flags.includes('--all') + let runGui = flags.includes('--gui') || flags.includes('--all') + assert.isFalse(runCli) + assert.isTrue(runGui) + + // --all: both + flags = ['--all'] + runCli = !flags.includes('--gui') || flags.includes('--all') + runGui = flags.includes('--gui') || flags.includes('--all') + assert.isTrue(runCli) + assert.isTrue(runGui) + + // no flags: only CLI + flags = [] + runCli = !flags.includes('--gui') || flags.includes('--all') + runGui = flags.includes('--gui') || flags.includes('--all') + assert.isTrue(runCli) + assert.isFalse(runGui) + }) + }) + + describe('help command', function () { + it('should display help with ds-cli help', function () { + const out = execSync(`node ${cliPath} help`, { encoding: 'utf-8' }) + assert.include(out, '用法: ds-cli <命令>') + assert.include(out, 'start') + assert.include(out, 'stop') + assert.include(out, 'restart') + assert.include(out, 'status') + assert.include(out, 'version') + assert.include(out, 'proxy') + assert.include(out, 'plugin') + assert.include(out, 'service') + assert.include(out, 'help') + }) + + it('should show all commands in help', function () { + const out = execSync(`node ${cliPath} help`, { encoding: 'utf-8' }) + assert.include(out, '启动守护进程') + assert.include(out, '停止守护进程') + assert.include(out, '重启守护进程') + assert.include(out, '显示运行状态') + assert.include(out, '显示版本号') + assert.include(out, '注册开机自启动') + assert.include(out, '移除开机自启动') + }) + + it('should show options in help', function () { + const out = execSync(`node ${cliPath} help`, { encoding: 'utf-8' }) + assert.include(out, '--gui') + assert.include(out, '--all') + }) + }) + + describe('version command', function () { + it('should display version number', function () { + const out = execSync(`node ${cliPath} version`, { encoding: 'utf-8' }).trim() + assert.match(out, /^\d+\.\d+\.\d+$/) + }) + }) + + describe('unknown command', function () { + it('should show error and help for unknown command', function () { + try { + execSync(`node ${cliPath} foobar`, { encoding: 'utf-8' }) + assert.fail('should have thrown') + } catch (e) { + assert.include(e.stderr, '未知命令: foobar') + // help 输出到 stdout + assert.include(e.stdout, '用法: ds-cli') + } + }) + }) +}) diff --git a/packages/cli/test/plugin.test.js b/packages/cli/test/plugin.test.js new file mode 100644 index 0000000000..7f77a81eb0 --- /dev/null +++ b/packages/cli/test/plugin.test.js @@ -0,0 +1,115 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +describe('plugin', function () { + describe('getValidPlugins (via core module)', function () { + it('should return an array of plugin names', function () { + const plugins = Object.keys(require('@docmirror/dev-sidecar/src/modules/plugin')) + assert.isArray(plugins) + assert.isAbove(plugins.length, 0) + }) + + it('should include known plugins', function () { + const plugins = Object.keys(require('@docmirror/dev-sidecar/src/modules/plugin')) + assert.include(plugins, 'git') + assert.include(plugins, 'node') + assert.include(plugins, 'pip') + assert.include(plugins, 'overwall') + assert.include(plugins, 'free_eye') + }) + }) + + describe('isOverwallUnlocked logic', function () { + const jsonApi = require('@docmirror/mitmproxy/src/json') + + it('should return false when setting.json does not exist', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const settingPath = path.join(tmpDir, 'setting.json') + assert.isFalse(fs.existsSync(settingPath)) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should return false when overwall is false', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const settingPath = path.join(tmpDir, 'setting.json') + fs.writeFileSync(settingPath, JSON.stringify({ overwall: false })) + const setting = jsonApi.parse(fs.readFileSync(settingPath, 'utf-8')) + assert.isFalse(setting?.overwall === true) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should return true when overwall is true', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const settingPath = path.join(tmpDir, 'setting.json') + fs.writeFileSync(settingPath, JSON.stringify({ overwall: true })) + const setting = jsonApi.parse(fs.readFileSync(settingPath, 'utf-8')) + assert.isTrue(setting?.overwall === true) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should return false when setting.json is invalid', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const settingPath = path.join(tmpDir, 'setting.json') + fs.writeFileSync(settingPath, '{ invalid }') + let result = false + try { + const setting = jsonApi.parse(fs.readFileSync(settingPath, 'utf-8')) + result = setting?.overwall === true + } catch { + result = false + } + assert.isFalse(result) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should prefer setting.json over setting.json5', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const newPath = path.join(tmpDir, 'setting.json') + const oldPath = path.join(tmpDir, 'setting.json5') + fs.writeFileSync(newPath, JSON.stringify({ overwall: true })) + fs.writeFileSync(oldPath, JSON.stringify({ overwall: false })) + + // 模拟 getSettingsPath 逻辑 + let settingPath = newPath + if (!fs.existsSync(newPath) && fs.existsSync(oldPath)) { + settingPath = oldPath + } + assert.strictEqual(settingPath, newPath) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should fallback to setting.json5 when setting.json does not exist', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const oldPath = path.join(tmpDir, 'setting.json5') + fs.writeFileSync(oldPath, JSON.stringify({ overwall: true })) + + const newPath = path.join(tmpDir, 'setting.json') + let settingPath = newPath + if (!fs.existsSync(newPath) && fs.existsSync(oldPath)) { + settingPath = oldPath + } + assert.strictEqual(settingPath, oldPath) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + }) +}) diff --git a/packages/cli/test/proxy.test.js b/packages/cli/test/proxy.test.js new file mode 100644 index 0000000000..2eab8b4af5 --- /dev/null +++ b/packages/cli/test/proxy.test.js @@ -0,0 +1,136 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +describe('proxy', function () { + const jsonApi = require('@docmirror/mitmproxy/src/json') + + function getUserBase () { + return path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') + } + + describe('readConfig/writeConfig', function () { + it('should read existing config', function () { + const configPath = path.join(getUserBase(), 'config.json') + if (fs.existsSync(configPath)) { + const config = jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + assert.isObject(config) + } + }) + + it('should return empty object for missing config', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const configPath = path.join(tmpDir, 'nonexistent.json') + assert.isFalse(fs.existsSync(configPath)) + let config = {} + if (fs.existsSync(configPath)) { + config = jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + } + assert.deepEqual(config, {}) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should persist proxy enabled state', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const configPath = path.join(tmpDir, 'config.json') + const config = { proxy: { enabled: true } } + fs.writeFileSync(configPath, jsonApi.stringify(config)) + const read = jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + assert.isTrue(read.proxy.enabled) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should persist plugin enabled state', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const configPath = path.join(tmpDir, 'config.json') + const config = { plugin: { git: { enabled: true }, node: { enabled: false } } } + fs.writeFileSync(configPath, jsonApi.stringify(config)) + const read = jsonApi.parse(fs.readFileSync(configPath, 'utf-8')) + assert.isTrue(read.plugin.git.enabled) + assert.isFalse(read.plugin.node.enabled) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + }) + + describe('proxy.env file', function () { + it('should create proxy.env with correct env vars', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const envFile = path.join(tmpDir, 'proxy.env') + const lines = [ + 'export HTTPS_PROXY="http://127.0.0.1:31181"', + 'export https_proxy="http://127.0.0.1:31181"', + ] + fs.writeFileSync(envFile, lines.join('\n') + '\n') + const content = fs.readFileSync(envFile, 'utf-8') + assert.include(content, 'HTTPS_PROXY') + assert.include(content, '127.0.0.1:31181') + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should include HTTP_PROXY when proxyHttp is true', function () { + const lines = [ + 'export HTTPS_PROXY="http://127.0.0.1:31181"', + 'export https_proxy="http://127.0.0.1:31181"', + 'export HTTP_PROXY="http://127.0.0.1:31180"', + 'export http_proxy="http://127.0.0.1:31180"', + ] + assert.include(lines.join('\n'), 'HTTP_PROXY') + assert.include(lines.join('\n'), '31180') + }) + }) + + describe('shell detection logic', function () { + it('should detect zsh from SHELL env', function () { + const originalShell = process.env.SHELL + process.env.SHELL = '/bin/zsh' + // 模拟 detectShell 逻辑 + const shell = process.env.SHELL || '' + const detected = shell.includes('zsh') ? 'zsh' : shell.includes('bash') ? 'bash' : 'bash' + assert.strictEqual(detected, 'zsh') + process.env.SHELL = originalShell + }) + + it('should detect bash from SHELL env', function () { + const originalShell = process.env.SHELL + process.env.SHELL = '/bin/bash' + const shell = process.env.SHELL || '' + const detected = shell.includes('zsh') ? 'zsh' : shell.includes('bash') ? 'bash' : 'bash' + assert.strictEqual(detected, 'bash') + process.env.SHELL = originalShell + }) + + it('should fallback to bash when SHELL is unknown', function () { + const originalShell = process.env.SHELL + process.env.SHELL = '/bin/fish' + const shell = process.env.SHELL || '' + const detected = shell.includes('zsh') ? 'zsh' : shell.includes('bash') ? 'bash' : 'bash' + assert.strictEqual(detected, 'bash') + process.env.SHELL = originalShell + }) + + it('should map shell to correct profile path', function () { + const home = process.env.HOME || '/' + const profiles = { + zsh: path.join(home, '.zshrc'), + bash: path.join(home, '.bashrc'), + fish: path.join(home, '.config/fish/config.fish'), + } + assert.include(profiles.zsh, '.zshrc') + assert.include(profiles.bash, '.bashrc') + assert.include(profiles.fish, 'config.fish') + }) + }) +}) diff --git a/packages/cli/test/service.test.js b/packages/cli/test/service.test.js new file mode 100644 index 0000000000..4d8c0edb27 --- /dev/null +++ b/packages/cli/test/service.test.js @@ -0,0 +1,90 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +describe('service', function () { + const { isInstalled } = require('../src/commands/service') + + describe('isInstalled', function () { + it('should return a boolean', function () { + const result = isInstalled() + assert.isBoolean(result) + }) + + it('should return false on fresh system (no service file)', function () { + // 在测试环境中,service 文件通常不存在 + // 除非之前测试安装过 + const home = process.env.HOME || '/' + const servicePath = path.join(home, '.config/systemd/user/ds-cli.service') + if (!fs.existsSync(servicePath)) { + assert.isFalse(isInstalled()) + } + }) + }) + + describe('Linux service file', function () { + it('should contain correct ExecStart with --daemon', function () { + const home = process.env.HOME || '/' + const servicePath = path.join(home, '.config/systemd/user/ds-cli.service') + if (fs.existsSync(servicePath)) { + const content = fs.readFileSync(servicePath, 'utf-8') + assert.include(content, 'ExecStart=') + assert.include(content, 'start --daemon') + assert.include(content, 'Restart=on-failure') + assert.include(content, 'After=network.target') + assert.include(content, 'WantedBy=default.target') + } + }) + + it('should have correct systemd unit structure', function () { + const home = process.env.HOME || '/' + const servicePath = path.join(home, '.config/systemd/user/ds-cli.service') + if (fs.existsSync(servicePath)) { + const content = fs.readFileSync(servicePath, 'utf-8') + assert.include(content, '[Unit]') + assert.include(content, '[Service]') + assert.include(content, '[Install]') + assert.include(content, 'Type=simple') + } + }) + }) + + describe('service file path', function () { + it('should be in correct systemd user directory', function () { + const home = process.env.HOME || '/' + const expectedDir = path.join(home, '.config/systemd/user') + // 如果目录存在,service 文件应该在其中 + if (fs.existsSync(expectedDir)) { + const servicePath = path.join(expectedDir, 'ds-cli.service') + // 文件可能存在也可能不存在,取决于是否已安装 + if (fs.existsSync(servicePath)) { + assert.isTrue(fs.existsSync(servicePath)) + } + } + }) + }) + + describe('install/uninstall lifecycle', function () { + it('should be idempotent - install twice does not error', function () { + // 如果已安装,再次安装不应报错 + const home = process.env.HOME || '/' + const servicePath = path.join(home, '.config/systemd/user/ds-cli.service') + if (fs.existsSync(servicePath)) { + // 已安装状态,再次检查 isInstalled 应返回 true + assert.isTrue(isInstalled()) + } + }) + + it('should report not installed when service file absent', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + // 在临时目录中,service 文件不存在 + const fakePath = path.join(tmpDir, 'ds-cli.service') + assert.isFalse(fs.existsSync(fakePath)) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + }) +}) diff --git a/packages/cli/test/start.test.js b/packages/cli/test/start.test.js new file mode 100644 index 0000000000..149402a5af --- /dev/null +++ b/packages/cli/test/start.test.js @@ -0,0 +1,77 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +describe('start', function () { + const { isPortInUse, getProxyPort, isAlive } = require('../src/commands/start') + + function withTempHome (fn) { + const originalHome = process.env.HOME + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + const userBase = path.join(tmpDir, '.dev-sidecar') + fs.mkdirSync(userBase, { recursive: true }) + process.env.HOME = tmpDir + try { + fn(userBase, tmpDir) + } finally { + process.env.HOME = originalHome + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + } + + describe('isPortInUse', function () { + it('should return false for an available port', async function () { + const port = 49152 + Math.floor(Math.random() * 1000) + assert.isFalse(await isPortInUse(port)) + }) + + it('should return true for a port in use', async function () { + const net = require('node:net') + const server = net.createServer() + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const port = server.address().port + assert.isTrue(await isPortInUse(port)) + server.close() + }) + }) + + describe('getProxyPort', function () { + it('should return default port when config does not exist', function () { + withTempHome((userBase) => { + assert.strictEqual(getProxyPort(), 31181) + }) + }) + + it('should read port from config.json', function () { + withTempHome((userBase) => { + fs.writeFileSync(path.join(userBase, 'config.json'), JSON.stringify({ server: { port: 12345 } })) + assert.strictEqual(getProxyPort(), 12345) + }) + }) + + it('should return default port when config has no server.port', function () { + withTempHome((userBase) => { + fs.writeFileSync(path.join(userBase, 'config.json'), JSON.stringify({ server: {} })) + assert.strictEqual(getProxyPort(), 31181) + }) + }) + + it('should return default port when config is invalid JSON', function () { + withTempHome((userBase) => { + fs.writeFileSync(path.join(userBase, 'config.json'), '{ invalid json }') + assert.strictEqual(getProxyPort(), 31181) + }) + }) + }) + + describe('isAlive', function () { + it('should return true for current process', function () { + assert.isTrue(isAlive(process.pid)) + }) + + it('should return false for non-existent PID', function () { + assert.isFalse(isAlive(999999999)) + }) + }) +}) diff --git a/packages/cli/test/status.test.js b/packages/cli/test/status.test.js new file mode 100644 index 0000000000..ba68666fc3 --- /dev/null +++ b/packages/cli/test/status.test.js @@ -0,0 +1,169 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +describe('status', function () { + describe('printStatus logic', function () { + it('should format status with all plugins enabled', function () { + const status = { + server: { enabled: true }, + proxy: { enabled: true }, + plugin: { + git: { enabled: true }, + node: { enabled: true }, + pip: { enabled: true }, + overwall: { enabled: false }, + }, + } + assert.isTrue(status.server.enabled) + assert.isTrue(status.proxy.enabled) + assert.isTrue(status.plugin.git.enabled) + assert.isFalse(status.plugin.overwall.enabled) + }) + + it('should handle missing plugin fields gracefully', function () { + const status = { server: { enabled: false }, proxy: { enabled: false }, plugin: {} } + const gitEnabled = status.plugin?.git?.enabled || false + assert.isFalse(gitEnabled) + }) + + it('should handle empty status object', function () { + const status = {} + assert.isFalse(status.server?.enabled || false) + assert.isFalse(status.proxy?.enabled || false) + }) + }) + + describe('plugin list', function () { + it('should not include free_eye (one-shot plugin without persistent status)', function () { + const { getPluginNames } = require('../src/commands/status') + const names = getPluginNames() + assert.notInclude(names, 'free_eye') + }) + + it('should include overwall only when unlocked', function () { + const { getPluginNames } = require('../src/commands/status') + // 测试环境未解锁 setting.json 时不应包含 overwall + const names = getPluginNames() + assert.include(names, 'git') + assert.include(names, 'node') + assert.include(names, 'pip') + }) + }) + + describe('isOverwallUnlocked', function () { + it('should return false when setting.json does not exist', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const settingPath = path.join(tmpDir, 'setting.json') + const exists = fs.existsSync(settingPath) + assert.isFalse(exists) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should return true when setting.json has overwall: true', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const settingPath = path.join(tmpDir, 'setting.json') + fs.writeFileSync(settingPath, JSON.stringify({ overwall: true })) + const setting = JSON.parse(fs.readFileSync(settingPath, 'utf-8')) + assert.isTrue(setting?.overwall === true) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + }) + + describe('running.json file logic', function () { + it('should read status from running.json app.status', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const runningPath = path.join(tmpDir, 'running.json') + const data = { + app: { + instance: { type: 'cli', pid: 12345, startTime: '2026-01-01T00:00:00.000Z' }, + status: { + server: { enabled: true }, + proxy: { enabled: true }, + plugin: { git: { enabled: true }, node: { enabled: true } }, + }, + }, + } + fs.writeFileSync(runningPath, JSON.stringify(data)) + const parsed = JSON.parse(fs.readFileSync(runningPath, 'utf-8')) + assert.isTrue(parsed.app.status.server.enabled) + assert.isTrue(parsed.app.status.proxy.enabled) + assert.isTrue(parsed.app.status.plugin.git.enabled) + assert.strictEqual(parsed.app.instance.type, 'cli') + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should handle missing running.json', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const runningPath = path.join(tmpDir, 'running.json') + assert.isFalse(fs.existsSync(runningPath)) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should handle corrupted running.json', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const runningPath = path.join(tmpDir, 'running.json') + fs.writeFileSync(runningPath, '{ invalid json }') + let error = null + try { JSON.parse(fs.readFileSync(runningPath, 'utf-8')) } catch (e) { error = e } + assert.isNotNull(error) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('should handle stale PID file', function () { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-cli-test-')) + try { + const pidFile = path.join(tmpDir, 'ds-cli.pid') + fs.writeFileSync(pidFile, '999999999') + const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10) + let alive = true + try { process.kill(pid, 0) } catch { alive = false } + assert.isFalse(alive) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + }) + + describe('auto-start status', function () { + it('should detect auto-start registration on Linux', function () { + if (process.platform === 'linux') { + const { isInstalled } = require('../src/commands/service') + const result = isInstalled() + assert.isBoolean(result) + const home = process.env.HOME || '/' + const servicePath = path.join(home, '.config/systemd/user/ds-cli.service') + const expected = fs.existsSync(servicePath) + assert.strictEqual(result, expected) + } + }) + + it('should format status with auto-start info', function () { + const status = { + server: { enabled: true }, + proxy: { enabled: false }, + plugin: { git: { enabled: true } }, + } + const serverRunning = status.server?.enabled || false + const proxyEnabled = status.proxy?.enabled || false + assert.isTrue(serverRunning) + assert.isFalse(proxyEnabled) + }) + }) +}) diff --git a/packages/core/package.json b/packages/core/package.json index cfe10da45b..0bd962b222 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@docmirror/dev-sidecar", - "version": "2.0.0", + "version": "2.2.1", "private": false, "description": "给开发者的加速代理工具", "author": "docmirror.cn", @@ -17,17 +17,19 @@ }, "dependencies": { "@starknt/sysproxy": "^0.0.3", - "@vscode/sudo-prompt": "^9.3.1", + "@vscode/sudo-prompt": "^9.3.2", "fix-path": "^3.0.0", "iconv-lite": "^0.6.3", - "lodash": "^4.17.21", + "lodash": "^4.18.1", "log4js": "^6.9.1", "node-powershell": "^4.0.0", + "proper-lockfile": "^4.1.2", + "request": "^2.88.2", "spawn-sync": "^2.0.0", "winreg": "^1.2.5" }, "devDependencies": { - "chai": "^4.3.4", - "mocha": "^8.2.1" + "chai": "^4.5.0", + "mocha": "^11.8.0" } } diff --git a/packages/core/src/config-api.js b/packages/core/src/config-api.js index 0293aa5545..07610048b4 100644 --- a/packages/core/src/config-api.js +++ b/packages/core/src/config-api.js @@ -60,7 +60,11 @@ const configApi = { if (remoteConfigUrl.startsWith('https://raw.githubusercontent.com/')) { headers['Server-Name'] = 'baidu.com' } - request(remoteConfigUrl, { headers }, (error, response, body) => { + // 禁用环境变量代理(HTTPS_PROXY/HTTP_PROXY): + // 当用户开启 proxy.setEnv 后,环境变量会指向 dev-sidecar 自己的代理端口, + // 启动时本地代理尚未监听,走代理会导致下载失败(ECONNREFUSED 127.0.0.1:31181), + // 新装用户会因此无法下载远程配置,只能使用内置规则。 + request(remoteConfigUrl, { headers, proxy: null }, (error, response, body) => { if (error) { log.error(`下载远程配置失败: ${remoteConfigUrl}, error:`, error, ', response:', response, ', body:', body) reject(error) @@ -104,7 +108,7 @@ const configApi = { let message if (response) { - message = `下载远程配置失败: ${remoteConfigUrl}, message: ${response.message}, code: ${response.statusCode}` + message = `下载远程配置失败: ${remoteConfigUrl}, message: ${response.statusMessage}, code: ${response.statusCode}` } else { message = `下载远程配置失败: response: ${response}` } @@ -280,14 +284,14 @@ const configApi = { const noSetList = list.filter((item) => { return !item.exists }) - if (list.length > 0) { + if (noSetList.length > 0) { const context = { root_ca_cert_path: configApi.get().server.setting.rootCaFile.certPath, } for (const item of noSetList) { if (item.value.includes('${')) { for (const key in context) { - item.value = item.value.replcace(new RegExp(`\${${key}}`, 'g'), context[key]) + item.value = item.value.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), context[key]) } } } diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index c9e8024125..fa330b5477 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -11,6 +11,11 @@ function getRootCaKeyPath () { const defaultConfig = { app: { + metaInfo: { + updateLog: 'GUI v2.0.2自带配置', + version: 202604122348, + id: 'internal', + }, mode: 'default', autoStart: { enabled: false, @@ -18,7 +23,7 @@ const defaultConfig = { remoteConfig: { enabled: true, // 共享远程配置地址 - url: 'https://gitee.com/wangliang181230/dev-sidecar/raw/docmirror2.x/packages/core/src/config/remote_config.json', + url: 'https://raw.giteeusercontent.com/wangliang181230/dev-sidecar-config/raw/main/remote_config.json', // 个人远程配置地址 personalUrl: '', }, @@ -36,6 +41,7 @@ const defaultConfig = { showShutdownTip: true, // 日志相关配置 + logDisabled: false, // 完全禁用日志:控制台不输出,日志文件也不写入 logFileSavePath: path.join(configLoader.getUserBasePath(), '/logs'), // 日志文件保存路径 keepLogFileCount: 15, // 保留日志文件数 maxLogFileSize: 1, // 最大日志文件大小 @@ -45,9 +51,11 @@ const defaultConfig = { enabled: true, host: '127.0.0.1', port: 31181, + fakeServerMaxLength: 100, // fakeServer的最大缓存数量 setting: { NODE_TLS_REJECT_UNAUTHORIZED: true, verifySsl: true, + allowTls12: false, script: { enabled: true, defaultDir: './extra/scripts/', @@ -92,6 +100,11 @@ const defaultConfig = { // } }, }, + // Cloudflare 路由重定向:命中 Cloudflare IP 段时改写为优选地址 + cloudflareRoute: { + enabled: false, + preferredEndpoint: '', // 优选地址,可填写 IP 或 CNAME 域名 + }, intercept: { enabled: true, }, @@ -102,9 +115,9 @@ const defaultConfig = { }, '^(/[\\w-.]+){2,}/?(\\?.*)?$': { // 篡改猴插件地址,以下是高速镜像地址 - tampermonkeyScript: 'https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/tampermonkey.js', + tampermonkeyScript: 'https://raw.giteeusercontent.com/wangliang181230/dev-sidecar-config/raw/main/tampermonkey.js', // Github油猴脚本地址,以下是高速镜像地址 - script: 'https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/GithubEnhanced-High-Speed-Download.user.js', + script: 'https://raw.giteeusercontent.com/wangliang181230/dev-sidecar-config/raw/main/GithubEnhanced-High-Speed-Download.user.js', remark: '注:上面所使用的脚本地址,为高速镜像地址。', desc: '油猴脚本:高速下载 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、项目列表单文件快捷下载、添加 git clone 命令', }, @@ -215,14 +228,22 @@ const defaultConfig = { }, 'ajax.googleapis.com': { '.*': { - proxy: 'ajax.lug.ustc.edu.cn', + proxy: 'ajax.proxy.ustclug.org', backup: ['gapis.geekzu.org'], test: 'ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js', }, }, 'fonts.googleapis.com': { '.*': { - proxy: 'fonts.loli.net', + proxy: 'fonts.googleapis.cn', + backup: ['fonts.loli.net'], + test: 'https://fonts.googleapis.com/css?family=Oswald', + }, + }, + 'fonts.gstatic.com': { + '.*': { + proxy: 'fonts-gstatic.proxy.ustclug.org', + backup: ['gstatic.loli.net'], test: 'https://fonts.googleapis.com/css?family=Oswald', }, }, @@ -235,19 +256,13 @@ const defaultConfig = { 'themes.googleusercontent.com': { '.*': { proxy: 'google-themes.proxy.ustclug.org' }, }, - // 'fonts.gstatic.com': { - // '.*': { - // proxy: 'gstatic.loli.net', - // backup: ['fonts-gstatic.proxy.ustclug.org'] - // } - // }, 'clients*.google.com': { '.*': { abort: false, desc: '设置abort:true可以快速失败,节省时间' } }, 'www.googleapis.com': { '.*': { abort: false, desc: '设置abort:true可以快速失败,节省时间' } }, 'lh*.googleusercontent.com': { '.*': { abort: false, desc: '设置abort:true可以快速失败,节省时间' } }, // mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.0/napi-v3-win32-x64.tar.gz '*.s3.1amazonaws1.com': { '/sqlite3/.*': { - redirect: 'npm.taobao.org/mirrors', + redirect: 'npmmirror.com/mirrors', }, }, // 'packages.elastic.co': { '.*': { proxy: 'elastic.proxy.ustclug.org' } }, @@ -374,30 +389,21 @@ const defaultConfig = { }, dns: { providers: { + safe360: { + server: 'tls://dot.360.cn', + forSNI: true, + }, aliyun: { - type: 'https', - server: 'https://dns.alidns.com/dns-query', - cacheSize: 1000, + server: 'tls://dns.alidns.com', }, cloudflare: { - type: 'https', server: 'https://1.1.1.1/dns-query', - cacheSize: 1000, }, quad9: { - type: 'https', server: 'https://9.9.9.9/dns-query', - cacheSize: 1000, - }, - safe360: { - type: 'https', - server: 'https://doh.360.cn/dns-query', - cacheSize: 1000, }, rubyfish: { - type: 'https', server: 'https://rubyfish.cn/dns-query', - cacheSize: 1000, }, }, mapping: { @@ -419,6 +425,31 @@ const defaultConfig = { '*.jetbrains.com': 'quad9', '*.azureedge.net': 'quad9', }, + /* + * 原本是想将 mapping 中的数据结构由 string 改为 object,但是这样会导致新的配置无法向下兼容,所以将family配置,放到下面的 familyMapping 中 + * + * @param family 可选值:4(只查询IPv4地址,默认值)、6(只查询IPv6地址)......暂不支持同时查IPv4和IPv6地址 + * @since 2.0.2 + */ + familyMapping: { + '*.github.com': '4', + '*github*.com': '4', + '*.github.io': '4', + '*.docker.com': '4', + '*.stackoverflow.com': '4', + '*.electronjs.org': '4', + '*.amazonaws.com': '4', + '*.yarnpkg.com': '4', + '*.cloudfront.net': '4', + '*.cloudflare.com': '4', + 'img.shields.io': '4', + '*.vuepress.vuejs.org': '4', + '*.gh.docmirror.top': '4', + '*.v2ex.com': '4', + '*.pypi.org': '4', + '*.jetbrains.com': '4', + '*.azureedge.net': '4', + }, speedTest: { enabled: true, interval: 300000, diff --git a/packages/core/src/config/remote_config.json5 b/packages/core/src/config/remote_config.json5 index b487923f99..380e048ec6 100644 --- a/packages/core/src/config/remote_config.json5 +++ b/packages/core/src/config/remote_config.json5 @@ -15,8 +15,8 @@ "intercepts": { "github.com": { "^(/[\\w-.]+){2,}/?(\\?.*)?$": { - "tampermonkeyScript": "https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/tampermonkey.js", - "script": "https://gitee.com/wangliang181230/dev-sidecar/raw/scripts/GithubEnhanced-High-Speed-Download.user.js" + "tampermonkeyScript": "https://raw.giteeusercontent.com/wangliang181230/dev-sidecar-config/raw/main/tampermonkey.js", + "script": "https://raw.giteeusercontent.com/wangliang181230/dev-sidecar-config/raw/main/GithubEnhanced-High-Speed-Download.user.js" }, "^(/[^/]+){2}/releases/download/.*$": { "redirect": "ghp.ci/https://github.com", diff --git a/packages/core/src/event.js b/packages/core/src/event.js index ea136cfadd..3f5c47832b 100644 --- a/packages/core/src/event.js +++ b/packages/core/src/event.js @@ -27,7 +27,7 @@ function unregister (id) { for (let i = 0; i < handlers.length; i++) { const handle = handlers[i] if (handle.id === id) { - handlers.splice(i) + handlers.splice(i, 1) return } } diff --git a/packages/core/src/expose.js b/packages/core/src/expose.js index 20dfb3f89b..d64eb11395 100644 --- a/packages/core/src/expose.js +++ b/packages/core/src/expose.js @@ -4,6 +4,7 @@ const event = require('./event') const modules = require('./modules') const shell = require('./shell') const status = require('./status') +const instance = require('./modules/instance') const log = require('./utils/util.log.core') const context = { @@ -30,6 +31,23 @@ const proxy = setupPlugin('proxy', modules.proxy, context, config) const plugin = {} for (const key in modules.plugin) { const target = modules.plugin[key] + if (target == null) { + // 插件不可用(如 SEA 独立可执行文件中无法携带 free-eye),注册为禁用状态 + log.warn(`插件【${key}】不可用,已注册为禁用状态`) + const stub = { + config: { key, enabled: false }, + status: { enabled: false }, + plugin: () => ({ + start: async () => log.warn(`插件【${key}】不可用,无法启动`), + stop: async () => {}, + close: async () => {}, + run: async () => { throw new Error(`插件【${key}】不可用`) }, + }), + } + const stubApi = setupPlugin(`plugin.${key}`, stub, context, config) + plugin[key] = stubApi + continue + } const api = setupPlugin(`plugin.${key}`, target, context, config) plugin[key] = api } @@ -37,80 +55,96 @@ config.resetDefault() const server = modules.server const serverStart = server.start -function newServerStart ({ mitmproxyPath }) { - return serverStart({ mitmproxyPath, plugins: plugin }) +function newServerStart ({ mitmproxyPath, setting }) { + return serverStart({ mitmproxyPath, plugins: plugin, setting }) } server.start = newServerStart -async function startup ({ mitmproxyPath }) { +async function startup ({ mitmproxyPath, setting }) { const conf = config.get() - if (conf.server.enabled) { - try { - await server.start({ mitmproxyPath }) - } catch (err) { - log.error('代理服务启动失败:', err) - } + const tasks = [] + + if (conf.server.enabled && !status.server.enabled) { + tasks.push((async () => { + try { + await server.start({ mitmproxyPath }) + } catch (err) { + log.error('代理服务启动失败:', err) + } + })()) } - if (conf.proxy.enabled) { - try { - await proxy.start() - } catch (err) { - log.error('开启系统代理失败:', err) - } + if (conf.proxy.enabled && !status.proxy.enabled) { + tasks.push((async () => { + try { + await proxy.start() + } catch (err) { + log.error('开启系统代理失败:', err) + } + })()) } + try { - const plugins = [] for (const key in plugin) { - if (conf.plugin[key].enabled) { - const start = async () => { + if (conf.plugin[key].enabled && !status.plugin[key]?.enabled) { + if (key === 'overwall' && setting && setting.overwall !== true) { + log.info(`插件【${key}】未启动:setting.json 未开启 overwall`) + continue + } + tasks.push((async () => { try { await plugin[key].start() log.info(`插件【${key}】已启动`) } catch (err) { log.error(`插件【${key}】启动失败:`, err) } - } - plugins.push(start()) + })()) } } - if (plugins && plugins.length > 0) { - await Promise.all(plugins) - } } catch (err) { log.error('开启插件失败:', err) } + + if (tasks.length > 0) { + // server、系统代理、各插件之间没有相互依赖,并行启动以缩短整体等待时间 + await Promise.all(tasks) + } } async function shutdown () { + const tasks = [] + try { - const plugins = [] for (const key in plugin) { if (status.plugin[key] && status.plugin[key].enabled && plugin[key].close) { - const close = async () => { + tasks.push((async () => { try { await plugin[key].close() log.info(`插件【${key}】已关闭`) } catch (err) { log.error(`插件【${key}】关闭失败:`, err) } - } - plugins.push(close()) + })()) } } - if (plugins.length > 0) { - await Promise.all(plugins) - } } catch (error) { log.error('插件关闭失败:', error) } if (status.proxy.enabled) { - try { - await proxy.close() - log.info('系统代理已关闭') - } catch (err) { - log.error('系统代理关闭失败:', err) - } + tasks.push((async () => { + try { + await proxy.close() + log.info('系统代理已关闭') + } catch (err) { + log.error('系统代理关闭失败:', err) + } + })()) } + + if (tasks.length > 0) { + // 插件关闭(清理 git/npm 配置)与关闭系统代理互不依赖,并行执行 + await Promise.all(tasks) + } + if (status.server.enabled) { try { await server.close() @@ -135,6 +169,7 @@ const api = { server, proxy, plugin, + instance, log, } module.exports = { diff --git a/packages/core/src/modules/instance/index.js b/packages/core/src/modules/instance/index.js new file mode 100644 index 0000000000..a1e5922f8e --- /dev/null +++ b/packages/core/src/modules/instance/index.js @@ -0,0 +1,140 @@ +const fs = require('node:fs') +const path = require('node:path') +const lodash = require('lodash') +const lockfile = require('proper-lockfile') +const event = require('../../event') + +const LOCK_FILE = 'dev-sidecar.lock' +const RUNNING_JSON = 'running.json' + +function getBasePath () { + return path.join(process.env.USERPROFILE || process.env.HOME || '/', '.dev-sidecar') +} + +function getLockPath (userBasePath = getBasePath()) { + return path.join(userBasePath, LOCK_FILE) +} + +function getRunningJsonPath (userBasePath = getBasePath()) { + return path.join(userBasePath, RUNNING_JSON) +} + +function getDefaultLockOptions (log) { + return { + lockfilePath: getLockPath(), + realpath: false, + stale: 10000, + retries: 0, + onCompromised: (err) => { + try { + fs.rmdirSync(getLockPath()) + } catch {} + if (log) { + log.error('锁被篡改,进程退出:', err) + } + process.exit(1) + }, + } +} + +// 获取长锁,失败时抛错(不会无限阻塞) +async function acquireLock ({ log } = {}) { + const release = await lockfile.lock(getLockPath(), getDefaultLockOptions(log)) + watchStatusEvents({ log }) + return release +} + +// 检查锁是否被新鲜持有(非阻塞,用于启动前的友好提示) +async function isLocked () { + try { + return await lockfile.check(getLockPath(), { lockfilePath: getLockPath(), realpath: false, stale: 10000 }) + } catch { + return false + } +} + +function readInstance () { + const filePath = getRunningJsonPath() + if (!fs.existsSync(filePath)) { + return null + } + try { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + return data?.app?.instance || null + } catch { + return null + } +} + +function writeInstance (instance) { + const filePath = getRunningJsonPath() + let data = {} + if (fs.existsSync(filePath)) { + try { + data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + } catch {} + } + if (!data.app) { + data.app = {} + } + data.app.instance = instance + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)) +} + +let statusWriteTimer = null +let statusWriteQueue = {} + +// 将状态写入 running.json 的 app.status(事件驱动,300ms 防抖合并多次更新为一次写入) +function updateStatus (key, value) { + if (typeof key !== 'string' || key.length === 0) { + return + } + statusWriteQueue[key] = value + if (statusWriteTimer) { + return + } + statusWriteTimer = setTimeout(() => { + statusWriteTimer = null + const queue = statusWriteQueue + statusWriteQueue = {} + try { + const filePath = getRunningJsonPath() + let data = {} + if (fs.existsSync(filePath)) { + try { + data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + } catch {} + } + if (!data.app) { + data.app = {} + } + if (!data.app.status) { + data.app.status = {} + } + for (const key in queue) { + lodash.set(data.app.status, key, queue[key]) + } + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)) + } catch {} + }, 300) +} + +// 订阅 core 状态总线,仅同步 *.enabled 开关状态(过滤 free_eye.result 等大 payload) +function watchStatusEvents ({ log } = {}) { + event.register('status', (e) => { + if (!e || typeof e.key !== 'string' || !e.key.endsWith('.enabled')) { + return + } + updateStatus(e.key, e.value) + }) +} + +module.exports = { + acquireLock, + isLocked, + readInstance, + writeInstance, + updateStatus, + getLockPath, + getRunningJsonPath, +} diff --git a/packages/core/src/modules/plugin/free-eye/README.md b/packages/core/src/modules/plugin/free-eye/README.md new file mode 100644 index 0000000000..d416297d8a --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/README.md @@ -0,0 +1,74 @@ +# 网络审查检测器 + +FreeEye 是一个用 JavaScript 编写的网络审查检测器,自动化检测网络环境并推荐可能的规避方法。 + +为中国大陆用户设计,但也可用于其他地区。 + +希望使得用户能够使用本工具回答以下问题: + +1. 我的网络是被审查了,还是只是出现了异常故障? +2. 使用了哪些审查手段? +3. 有哪些规避方法可以绕过这些审查? + +## 使用方法 + +前提条件是你需要在设备上安装 `node.js`。 + +启动向导的方法: + +```bash +git clone https://github.com/cute-omega/free-eye.git +cd free-eye +npm install +npm start +``` + +(如果你不准备进行开发,可以跳过 `npm install`并直接运行 `npm start`;中国大陆用户可能需要设置npm镜像) + +不同测试的代码位于 `checkpoints/` 目录中。每个测试都有唯一的 `tag` 标识。单个测试的参数在 `config.json` 文件中设置,使用测试的 tag 作为键: + +```json +{ + "tag": { + // 测试特定的参数 + } + // ... +} +``` + +## 测试 + +### Route(路由) + +通过尝试创建一个套接字并连接到一个非本地地址,检测设备是否具有互联网连通性。 + +### DNS + +使用系统的 DNS 解析器尝试解析允许和被封锁的主机名。测试被封锁主机名是否存在 DNS 缓存投毒。 + +### TCP + +尝试与已知允许和已知被封锁的 IP 地址建立 TCP 连接。 + +### TLS + +尝试与已知允许但可能遭受审查的 IP 地址(例如对中国用户来说的“干净”的外国 IP)完成 TLS 握手。测试内容包括: + +- 不带任何 SNI 的握手 +- 带已知允许的 SNI 的握手 +- 带已知被封锁的 SNI 的握手 + +还测试将 TLS 记录分片作为一种规避方法,通过尝试对被封锁的 SNI 进行握手但分片 ClientHello 来实现。 + +## 编写你自己的测试 + +如果你想编写自定义测试,只需实现 `template.js` 中描述的接口,将测试模块保存到 `checkpoints/` 目录,并在 `config.json` 中添加该测试的参数。 + +## 相关项目 + +本项目受到来自 [wallpunch/wizard](https://github.com/wallpunch/wizard) 的启发; +用作 [docmirror/dev-sidecar](https://github.com/docmirror/dev-sidecar) 中的网络检测插件。 + +--- + +不论在哪里,人们的目光都应该是自由的。 diff --git a/packages/core/src/modules/plugin/free-eye/checkpoints/dns.js b/packages/core/src/modules/plugin/free-eye/checkpoints/dns.js new file mode 100644 index 0000000000..25dddc3757 --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/checkpoints/dns.js @@ -0,0 +1,152 @@ +import { randomBytes } from 'node:crypto' +import { promises as dns } from 'node:dns' +import { TestGroup } from '../template.js' +import { FAMILY_VALUES, getCensorsString, getResultIcon } from '../utils.js' + +class DnsTester extends TestGroup { + /** + * A test group to assess the system's DNS resolver + */ + constructor (globalConfig, globalResults) { + super(globalConfig, globalResults, 'DNS') + } + + static getTestTag () { + return 'DNS' + } + + static getPrereqs () { + return ['Route'] + } + + getDefaultResults () { + return { + IPv4: false, + IPv6: false, + } + } + + checkIfShouldSkip (globalResults) { + /** + * Skip this test if all routing tests failed + */ + let skip = true + for (const family in FAMILY_VALUES) { + if (Object.values(globalResults.Route[family]).includes(true)) { + this.results[family] = {} + skip = false + } + } + if (skip) { + return 'no routable networks' + } + return null + } + + async startTest () { + this.testPrefix = `${randomBytes(30).toString('hex')}.` + console.log(`Using POISON test prefix: ${this.testPrefix}`) + + for (const family in FAMILY_VALUES) { + if (this.results[family] === false) { + continue // not routable + } + for (const host of this.config.allow) { + this.startResolveTest(host, family, false) + } + for (const host of this.config.block) { + this.startResolveTest(host, family, true) + } + } + } + + async startResolveTest (host, family, testPoison) { + const testPrefs = [''] + if (testPoison) { + testPrefs.push(this.testPrefix) + } + for (const prefix of testPrefs) { + this.startTestThread( + DnsTester.resolveThread, + [family, prefix + host], + `${family}, ${host}${prefix ? ', POISON' : ''}`, + this.config.timeout, + ) + } + } + + logResults () { + let resStr = '' + for (const [family, results] of Object.entries(this.results)) { + if (results === false) { + continue + } + this.results[family] = true + const allowList = this.config.allow + const allowOkCnt = allowList.reduce((sum, host) => sum + (results[host] || 0), 0) + const allowTotal = allowList.length + let resIcon + if (allowOkCnt === allowTotal) { // DNS can resolve + resIcon = getResultIcon(true) + } else if (allowOkCnt === 0) { // DNS can't resolve + resIcon = getResultIcon(false) + this.results[family] = false + } else { // test inconclusive + resIcon = getResultIcon(null, `resolved ${allowOkCnt}/${allowTotal}`) + } + resStr += `${family}: DNS ${resIcon}\n` + + const censors = [] + const blockList = this.config.block + const blockOkCnt = blockList.reduce((sum, host) => sum + (results[host] || 0), 0) + const blockTotal = blockList.length + if (blockOkCnt < blockTotal) { + censors.push(`DNS blocking: ${blockTotal - blockOkCnt}/${blockTotal} blocked`) + } + + const blockPoisonCnt = blockList.reduce((sum, host) => sum + (results[this.testPrefix + host] || 0), 0) + if (blockPoisonCnt > 0) { + censors.push(`DNS poisoning: ${blockPoisonCnt}/${blockTotal} poisoned`) + } + resStr += getCensorsString(censors) + } + return resStr + } + + static async resolveThread (timeout, logger, results, family, host) { + if (results[family] === false) { + return // Not routable + } + results[family][host] = 0 // default to failed + try { + let records + if (FAMILY_VALUES[family] === 4) { + records = await dns.resolve4(host) + } else { + records = await dns.resolve6(host) + } + if (!timeout.isSet) { + logger(`Got ${records.length} records`) + results[family][host] = 1 + } else { + logger(`Timeout occurred for ${host}`) + } + } catch (error) { + if (!timeout.isSet) { + logger(`Failed with error: ${error.message}`) + results[family][host] = 0 // explicitly set to failed + } else { + logger(`Timeout occurred for ${host}`) + } + } + } +} + +function getClientTests () { + return [DnsTester] +} + +export default { + DnsTester, + getClientTests, +} diff --git a/packages/core/src/modules/plugin/free-eye/checkpoints/route.js b/packages/core/src/modules/plugin/free-eye/checkpoints/route.js new file mode 100644 index 0000000000..4eb1100561 --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/checkpoints/route.js @@ -0,0 +1,142 @@ +import { createSocket } from 'node:dgram' +import { createConnection } from 'node:net' +import { TestGroup } from '../template.js' +import { FAMILY_VALUES, getResultIcon, PROTOCOL_VALUES } from '../utils.js' + +const ROUTE_TEST_DGRAM = Buffer.from('122401000000000000000006676f6f676c6503636f6d0000010001', 'hex') + +class RouteTester extends TestGroup { + /** + * A test group to assess the system's routing capability + */ + constructor (globalConfig, globalResults) { + super(globalConfig, globalResults, 'Route') + } + + static getTestTag () { + return 'Route' + } + + getDefaultResults () { + return { + IPv4: { + TCP: false, + UDP: false, + }, + IPv6: { + TCP: false, + UDP: false, + }, + } + } + + async startTest () { + for (const family in FAMILY_VALUES) { + for (const protocol in PROTOCOL_VALUES) { + const dst = [this.config.addrs[family], this.config.port] + this.startTestThread( + RouteTester.routeThread, + [family, protocol, dst], + `${family}, ${protocol}`, + this.config.timeout, + ) + } + } + } + + logResults () { + let resStr = '' + for (const family in FAMILY_VALUES) { + resStr += `${family}: ` + for (const protocol in PROTOCOL_VALUES) { + const resIcon = this.results[family][protocol] ? getResultIcon(true) : getResultIcon(false) + resStr += `${protocol} ${resIcon} ` + } + resStr += '\n' + } + return resStr + } + + static async routeThread (timeout, logger, results, family, protocol, dst) { + let sock + try { + logger('Creating socket...') + if (protocol === 'TCP') { + sock = createConnection({ + host: dst[0], + port: dst[1], + family: FAMILY_VALUES[family] === 4 ? 4 : 6, + }) + } else { // UDP + sock = createSocket({ + type: FAMILY_VALUES[family] === 4 ? 'udp4' : 'udp6', + }) + } + + if (timeout.isSet) { + if (sock) { + if (protocol === 'UDP') { + sock.close() + } else { + sock.destroy() + } + } + return + } + + if (protocol === 'TCP') { + logger(`Connecting socket to ${dst[0]}:${dst[1]}`) + await new Promise((resolve, reject) => { + sock.connect(dst[1], dst[0], resolve) + sock.on('error', reject) + sock.on('timeout', () => reject(new Error('timeout'))) + }) + } else { // UDP + logger(`Sending datagram to ${dst[0]}:${dst[1]}`) + sock.send(ROUTE_TEST_DGRAM, 0, ROUTE_TEST_DGRAM.length, dst[1], dst[0]) + } + if (protocol === 'UDP') { + sock.close() + } else { + sock.destroy() + } + } catch (error) { + if (!timeout.isSet) { + logger(`Failed with exception: ${error.message}`) + // For routing test, connection errors often mean the network is routable + // but the service is not available (e.g., TCP to DNS port) + if (error.code === 'ECONNREFUSED' || error.code === 'EINVAL' || error.code === 'ENETUNREACH' || error.message.includes('timeout')) { + logger('Routing successful!') + results[family][protocol] = true + if (sock) { + if (protocol === 'UDP') { + sock.close() + } else { + sock.destroy() + } + } + return + } + } + if (sock) { + if (protocol === 'UDP') { + sock.close() + } else { + sock.destroy() + } + } + return + } + logger('Routing successful!') + results[family][protocol] = true + } +} + +function getClientTests () { + return [RouteTester] +} + +export default { + RouteTester, + getClientTests, +} diff --git a/packages/core/src/modules/plugin/free-eye/checkpoints/tcp.js b/packages/core/src/modules/plugin/free-eye/checkpoints/tcp.js new file mode 100644 index 0000000000..32c353f69b --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/checkpoints/tcp.js @@ -0,0 +1,164 @@ +import { createConnection } from 'node:net' +import { TestGroup } from '../template.js' +import { FAMILY_VALUES, getCensorsString, getResultIcon } from '../utils.js' + +class TcpTester extends TestGroup { + /** + * A test group to assess the system's ability to establish + * TCP connections + */ + constructor (globalConfig, globalResults) { + super(globalConfig, globalResults, 'TCP') + } + + static getTestTag () { + return 'TCP' + } + + static getPrereqs () { + return ['Route'] + } + + getDefaultResults () { + return { + IPv4: false, + IPv6: false, + } + } + + checkIfShouldSkip (globalResults) { + /** + * Skip if TCP routing tests all failed + */ + let skip = true + for (const family in FAMILY_VALUES) { + if (globalResults.Route[family].TCP) { + this.results[family] = {} + skip = false + } + } + if (skip) { + return 'no routable TCP networks' + } + return null + } + + async startTest () { + for (const family in FAMILY_VALUES) { + if (this.results[family] === false) { + continue // not routable + } + for (const port of this.config.ports) { + this.results[family][port] = {} + const addrs = this.config.addrs[family] + for (const key of ['allow', 'block']) { + for (const addr of addrs[key]) { + this.startTestThread( + TcpTester.tcpThread, + [family, port, addr], + `${key}, ${addr}:${port}`, + this.config.timeout, + ) + } + } + } + } + } + + logResults () { + let resStr = '' + for (const [family, portRes] of Object.entries(this.results)) { + if (portRes === false) { + continue + } + resStr += `${family}: ` + const censors = [] + const addrs = this.config.addrs[family] + for (const [port, results] of Object.entries(portRes)) { + const dstTag = `TCP:${port}` + const allowList = addrs.allow + const allowOkCnt = allowList.reduce((sum, addr) => sum + (results[addr] === null ? 1 : 0), 0) + const allowTotal = allowList.length + let resIcon + if (allowOkCnt === allowTotal) { // can connect + resIcon = getResultIcon(true) + } else if (allowOkCnt === 0) { // can't connect + resIcon = getResultIcon(false) + } else { // test inconclusive + resIcon = getResultIcon(null, `connected ${allowOkCnt}/${allowTotal}`) + } + resStr += `${dstTag} ${resIcon} ` + + const blockList = addrs.block + const blocksTotal = blockList.length + + const timeoutCnt = blockList.reduce((sum, addr) => sum + (results[addr] === 'timeout' ? 1 : 0), 0) + if (timeoutCnt > 0) { + censors.push(`Blocked ${dstTag} handshake timeouts: ${timeoutCnt}/${blocksTotal} timeouts`) + } + + const errorCnt = blockList.reduce((sum, addr) => sum + (results[addr] === 'error' ? 1 : 0), 0) + if (errorCnt > 0) { + censors.push(`Blocked ${dstTag} handshake errors: ${errorCnt}/${blocksTotal} errors`) + } + } + resStr += `\n${getCensorsString(censors)}` + } + return resStr + } + + static async tcpThread (timeout, logger, results, family, port, addr) { + results[family][port][addr] = 'timeout' + + const sock = createConnection({ + host: addr, + port, + family: FAMILY_VALUES[family] === 4 ? 4 : 6, + }) + + // Set socket timeout + sock.setTimeout(1000) // 1 second timeout + + if (timeout.isSet) { + sock.destroy() + return + } + + try { + const dst = `${addr}:${port}` + logger(`Connecting socket to ${dst}`) + await new Promise((resolve, reject) => { + sock.on('connect', resolve) + sock.on('error', reject) + sock.on('timeout', () => reject(new Error('timeout'))) + }) + if (timeout.isSet) { + sock.destroy() + return + } + } catch (error) { + if (!timeout.isSet) { + logger(`Failed with exception: ${error.message}`) + if (error.message === 'timeout') { + results[family][port][addr] = 'timeout' + } else { + results[family][port][addr] = 'error' + } + } + sock.destroy() + return + } + logger('Connected!') + results[family][port][addr] = null + sock.destroy() + } +} + +function getClientTests () { + return [TcpTester] +} + +export default { + TcpTester, + getClientTests, +} diff --git a/packages/core/src/modules/plugin/free-eye/checkpoints/tls.js b/packages/core/src/modules/plugin/free-eye/checkpoints/tls.js new file mode 100644 index 0000000000..dd09610db7 --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/checkpoints/tls.js @@ -0,0 +1,190 @@ +import net from 'node:net' +import tls from 'node:tls' +import { TestGroup } from '../template.js' +import { FAMILY_VALUES, getCensorsString, getResultIcon, LogColors } from '../utils.js' + +const SNI_TEST_STRATEGIES = ['none', 'allow', 'block', 'frag'] + +class TlsTester extends TestGroup { + /** + * A test group to assess the system's ability to + * establish TLS connections + */ + constructor (globalConfig, globalResults) { + super(globalConfig, globalResults, 'TLS') + } + + static getTestTag () { + return 'TLS' + } + + static getPrereqs () { + return ['Route', 'TCP'] + } + + getDefaultResults () { + return { + IPv4: false, + IPv6: false, + } + } + + checkIfShouldSkip (globalResults) { + /** + * Skip if TCP test failed + */ + let skip = true + for (const family in FAMILY_VALUES) { + const tcpRes = globalResults.TCP && globalResults.TCP[family] + if (tcpRes && tcpRes[443] && Object.values(tcpRes[443]).includes(null)) { + this.results[family] = {} + skip = false + } + } + if (skip) { + return 'cannot make TCP connections' + } + return null + } + + async startTest () { + for (const family in FAMILY_VALUES) { + if (this.results[family] === false) { + continue + } + const addr = this.config.addrs[family] + if (!addr) { + this.results[family] = false + continue + } + this.results[family] = this.results[family] || {} + for (const strategy of SNI_TEST_STRATEGIES) { + let sni = null + if (strategy === 'allow') { + sni = this.config.snis.allow + } else if (strategy === 'block' || strategy === 'frag') { + sni = this.config.snis.block + } + this.startTestThread( + TlsTester.tlsThread, + [family, addr, sni, strategy], + `${family}, ${strategy}`, + this.config.timeout, + ) + } + } + } + + logResults () { + let resStr = '' + for (const [family, results] of Object.entries(this.results)) { + if (results === false) { + continue + } + resStr += `${family}: ` + const noneIcon = getResultIcon(results.none === null) + resStr += `IP-only ${noneIcon} ` + const allowIcon = getResultIcon(results.allow === null) + resStr += `SNI ${allowIcon}\n` + + const censors = [] + const blockRes = results.block + if (blockRes !== null) { + censors.push(`Blocked SNI handshake ${blockRes}`) + } + resStr += getCensorsString(censors) + + const fragRes = results.frag + if (fragRes === null) { + resStr += ` Circumvention found: ${LogColors.GREEN}TLS record fragmentation${LogColors.RESET}\n` + } else if (fragRes) { + resStr += ` ${LogColors.RED}TLS record fragmentation ${fragRes}${LogColors.RESET}\n` + } else { + resStr += ' TLS record fragmentation test inconclusive\n' + } + } + return resStr + } + + static async tlsThread (timeout, logger, results, family, addr, sni, strategy) { + results[family][strategy] = 'timeout' + + const sock = net.createConnection({ + host: addr, + port: 443, + family: FAMILY_VALUES[family] === 4 ? 4 : 6, + }) + sock.setTimeout(1000) + + if (timeout.isSet) { + sock.destroy() + return + } + + try { + const dst = `${addr}:443` + logger(`Connecting socket to ${dst}`) + await new Promise((resolve, reject) => { + sock.on('connect', resolve) + sock.on('error', reject) + sock.on('timeout', () => reject(new Error('timeout'))) + }) + } catch (error) { + if (!timeout.isSet) { + logger(`Connect failed with exception: ${error.message}`) + results[family][strategy] = error.message === 'timeout' ? 'timeout' : 'error' + } + sock.destroy() + return + } + + let errMsg = null + try { + logger('Attempting TLS handshake') + await new Promise((resolve, reject) => { + const tlsSock = tls.connect({ + socket: sock, + servername: sni || undefined, + rejectUnauthorized: false, + }, resolve) + tlsSock.on('error', (error) => { + logger(`TLS error: ${error.code} - ${error.message}`) + if (strategy === 'block' && error.code === 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE') { + resolve() + } else if ((strategy === 'allow' || strategy === 'none') && error.code === 'ECONNRESET') { + resolve() + } else { + reject(error) + } + }) + }) + } catch (error) { + if (error.code !== 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE') { + errMsg = error.message + } + } + + if (timeout.isSet) { + sock.destroy() + return + } + + if (errMsg === null) { + logger('TLS handshake complete!') + results[family][strategy] = null + } else { + logger(`TLS handshake failed with error: ${errMsg}`) + results[family][strategy] = 'error' + } + sock.destroy() + } +} + +function getClientTests () { + return [TlsTester] +} + +export default { + TlsTester, + getClientTests, +} diff --git a/packages/core/src/modules/plugin/free-eye/client.js b/packages/core/src/modules/plugin/free-eye/client.js new file mode 100644 index 0000000000..4c6c97d191 --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/client.js @@ -0,0 +1,152 @@ +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import utils from './utils.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const printHeader = (utils && utils.printHeader) || (utils && utils.default && utils.default.printHeader) + +const TEST_PACKAGE_DIR = 'checkpoints' +const PLUGIN_RELATIVE_PATH = path.join('packages', 'core', 'src', 'modules', 'plugin', 'free-eye') + +function locatePluginRoot () { + const localConfig = path.join(__dirname, 'config.json') + if (fs.existsSync(localConfig)) { + return __dirname + } + + let current = __dirname + for (let i = 0; i < 8; i += 1) { + const candidate = path.join(current, PLUGIN_RELATIVE_PATH) + if (fs.existsSync(path.join(candidate, 'config.json'))) { + return candidate + } + const parent = path.dirname(current) + if (parent === current) { + break + } + current = parent + } + return __dirname +} + +const PLUGIN_ROOT = locatePluginRoot() +const pluginRequire = createRequire(path.join(PLUGIN_ROOT, 'index.js')) + +function resolveTestsDir (customDir) { + const fallbackDir = path.join(PLUGIN_ROOT, TEST_PACKAGE_DIR) + if (!customDir) { + return fallbackDir + } + if (path.isAbsolute(customDir)) { + return fs.existsSync(customDir) ? customDir : fallbackDir + } + const candidate = path.join(PLUGIN_ROOT, customDir) + return fs.existsSync(candidate) ? candidate : fallbackDir +} + +async function loadAllTests (testsDir, globalConfig) { + const tests = [] + const resolvedDir = resolveTestsDir(testsDir) + if (!fs.existsSync(resolvedDir)) { + throw new Error(`Tests directory not found: ${resolvedDir}`) + } + const files = fs.readdirSync(resolvedDir).filter(file => file.endsWith('.js') && file !== '__init__.js') + + for (const file of files) { + const modulePath = path.join(resolvedDir, file) + + const module = pluginRequire(modulePath) + const getClientTests = module.getClientTests || (module.default && module.default.getClientTests) + if (typeof getClientTests === 'function') { + for (const testCls of getClientTests()) { + if (testCls.getTestTag() in globalConfig) { + tests.push(testCls) + } + } + } + } + return tests +} + +function getNextTest (todoTests, doneTests) { + for (const testCls of todoTests) { + let allPrereqsDone = true + for (const testTag of testCls.getPrereqs()) { + if (!doneTests.includes(testTag)) { + allPrereqsDone = false + break + } + } + if (allPrereqsDone) { + return testCls + } + } + return null +} + +async function runTests (options = {}) { + const { testsDir, config } = options + + const globalConfig = (config && typeof config === 'object') ? config : null + if (!globalConfig) { + throw new Error('FreeEye runtime config is required.') + } + + const globalResults = {} + const summaries = [] + const todoTests = await loadAllTests(testsDir, globalConfig) + console.log( + `Loaded ${todoTests.length} tests: ${ + todoTests.map(t => t.getTestTag()).join(' ')}`, + ) + + const doneTests = [] + while (todoTests.length > 0) { + const TestCls = getNextTest(todoTests, doneTests) + if (!TestCls) { + break + } + + const testGroup = new TestCls(globalConfig, globalResults) + const testTag = testGroup.testTag + const summary = { tag: testTag, skipped: false } + printHeader(`${testTag} Test`, false) + if (testGroup.skipReason === null) { + const [testTime, testResults] = await testGroup.runTest() + summary.duration = testTime + summary.output = testResults + summary.resultSnapshot = testGroup.results + printHeader(`${testTag} Results: (done in ${testTime.toFixed(3)}s)`, true) + console.log(testResults) + } else { + summary.skipped = true + summary.skipReason = testGroup.skipReason + summary.output = `Test skipped because ${testGroup.skipReason}` + console.log(summary.output) + } + summaries.push(summary) + todoTests.splice(todoTests.indexOf(TestCls), 1) + doneTests.push(testTag) + } + console.log('All tests complete!') + return { + results: globalResults, + summaries, + totalTests: summaries.length, + completedTests: summaries.filter(item => !item.skipped).length, + } +} + +// 独立运行时入口(CLI 模式),在 dev-sidecar 中不走此路径 +if (process.argv[1] != null && process.argv[1].endsWith('client.js')) { + runTests().catch((error) => { + console.error(error) + process.exitCode = 1 + }) +} + +export default { runTests } diff --git a/packages/core/src/modules/plugin/free-eye/config.js b/packages/core/src/modules/plugin/free-eye/config.js new file mode 100644 index 0000000000..c0180918fb --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/config.js @@ -0,0 +1,75 @@ +/** + * FreeEye 网络检测插件默认配置。 + * + * 测试用例 (Route/DNS/TCP/TLS) 的具体参数定义在 `setting.config` 中, + * 用户可在 `~/.dev-sidecar/config.json` 的 `plugin.free_eye.setting.config` + * 路径下按需覆盖。 + */ +const defaultTimeout = 3 + +const defaultTestConfig = { + // ---- Route 测试:探测 IPv4/IPv6 路由可达性 ---- + Route: { + timeout: defaultTimeout, + addrs: { + IPv4: '8.8.8.8', + IPv6: '2001:4860:4860::8888', + }, + port: 53, + }, + + // ---- DNS 测试:检测 DNS 劫持 / 污染 ---- + DNS: { + timeout: defaultTimeout, + allow: [ + 'baidu.com', + 'google.com', + ], + block: [ + 'google.com', + 'twitter.com', + 'facebook.com', + 'youtube.com', + ], + }, + + // ---- TCP 测试:检测 TCP 阻断 ---- + TCP: { + timeout: defaultTimeout, + ports: [80, 443], + addrs: { + IPv4: { + allow: ['8.8.8.8'], + block: ['142.250.80.4'], + }, + IPv6: { + allow: ['2001:4860:4860::8888'], + block: ['2607:f8b0:4005:0809:0000:0000:0000:200e'], + }, + }, + }, + + // ---- TLS 测试:检测 TLS SNI 阻断 / 分片绕过 ---- + TLS: { + timeout: defaultTimeout, + addrs: { + IPv4: '8.8.8.8', + IPv6: '2001:4860:4860::8888', + }, + snis: { + allow: 'google.com', + block: 'twitter.com', + }, + }, +} + +export default { + name: '网络检测', + statusOff: true, + enabled: false, + tip: '运行网络检测来评估当前网络环境', + setting: { + testsDir: 'checkpoints', + config: defaultTestConfig, + }, +} diff --git a/packages/core/src/modules/plugin/free-eye/index.js b/packages/core/src/modules/plugin/free-eye/index.js new file mode 100644 index 0000000000..c8991a685f --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/index.js @@ -0,0 +1,161 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import clientModule from './client.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const runTests = clientModule.runTests +import freeEyeConfig from './config.js' + +const PLUGIN_STATUS_KEY = 'plugin.free_eye' + +const FreeEyePlugin = function (context) { + const { config, event, log } = context + let lastResult = null + + const resolvePath = (targetPath, defaultRelative) => { + const fallback = path.join(__dirname, defaultRelative) + if (!targetPath) { + return fallback + } + if (path.isAbsolute(targetPath)) { + return targetPath + } + const candidates = [ + path.join(__dirname, targetPath), + path.join(process.cwd(), targetPath), + ] + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate + } + } + return fallback + } + + const emitStatus = (key, value) => { + event.fire('status', { key, value }) + } + + const captureLogs = async (executor) => { + const logs = [] + const originalLog = console.log + const originalError = console.error + const push = (level, args) => { + const message = args.map((item) => { + if (item instanceof Error) { + return item.stack || item.message + } + if (typeof item === 'object') { + try { + return JSON.stringify(item) + } catch (err) { + return String(item) + } + } + return String(item) + }).join(' ') + logs.push({ level, message, timestamp: Date.now() }) + // Also write to system log so it follows configured logging format + if (level === 'error') { + log.error(message) + } else { + log.info(message) + } + } + console.log = (...args) => { + push('info', args) + originalLog(...args) + } + console.error = (...args) => { + push('error', args) + originalError(...args) + } + try { + const result = await executor() + return { result, logs } + } finally { + console.log = originalLog + console.error = originalError + } + } + + const storeResult = (payload) => { + lastResult = payload + emitStatus(`${PLUGIN_STATUS_KEY}.result`, lastResult) + } + + const executeTests = async () => { + const currentConfig = config.get() + const setting = currentConfig.plugin.free_eye.setting || {} + try { + const { result, logs } = await captureLogs(() => runTests(setting)) + const payload = { + finishedAt: new Date().toISOString(), + totalTests: result.totalTests, + completedTests: result.completedTests, + summaries: result.summaries, + results: result.results, + logs, + } + storeResult(payload) + return payload + } catch (err) { + const payload = { + finishedAt: new Date().toISOString(), + error: err.message, + } + storeResult(payload) + throw err + } + } + + const api = { + async start () { + emitStatus(`${PLUGIN_STATUS_KEY}.enabled`, true) + log.info('启动【FreeEye】插件') + try { + return await executeTests() + } catch (err) { + log.error('FreeEye runTests failed:', err) + throw err + } + }, + + async close () { + emitStatus(`${PLUGIN_STATUS_KEY}.enabled`, false) + log.info('关闭【FreeEye】插件') + }, + + async restart () { + await api.close() + return api.start() + }, + + isEnabled () { + const pluginConfig = config.get().plugin.free_eye + return pluginConfig && pluginConfig.enabled + }, + + async run () { + return executeTests() + }, + + async getLastResult () { + return lastResult + }, + } + return api +} + +export default { + key: 'free_eye', + config: freeEyeConfig, + status: { + enabled: false, + result: null, + }, + plugin: FreeEyePlugin, +} diff --git a/packages/core/src/modules/plugin/free-eye/package.json b/packages/core/src/modules/plugin/free-eye/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/core/src/modules/plugin/free-eye/template.js b/packages/core/src/modules/plugin/free-eye/template.js new file mode 100644 index 0000000000..babe60499a --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/template.js @@ -0,0 +1,143 @@ +import { performance } from 'node:perf_hooks' + +export class TestThread { + /** + * A single test that should be run in its own thread and + * preempted when its timeout is reached + */ + constructor (func, args, logHdr, timeout, results) { + this.logHdr = logHdr + this.log('Starting test...') + + this.timeoutEvent = { isSet: false } + this.results = results + this.startTime = performance.now() + this.timeout = timeout * 1000 // convert to ms + + // Start the async function and store the promise + this.runPromise = this.run(func, args) + } + + log (s) { + console.log(this.logHdr + s) + } + + async run (func, args) { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + this.timeoutEvent.isSet = true + reject(new Error('Test timed out!')) + }, this.timeout) + }) + + try { + await Promise.race([ + func(this.timeoutEvent, this.log.bind(this), this.results, ...args), + timeoutPromise, + ]) + } catch (error) { + if (error.message === 'Test timed out!') { + this.log('Test timed out!') + } else { + this.log(`Test failed: ${error.message}`) + } + } + } +} + +export class TestGroup { + /** + * A group of related tests that can be run in parallel. + */ + constructor (globalConfig, globalResults, testTag) { + this.testTag = testTag + this.startTime = performance.now() + this.config = globalConfig[testTag] + this.threads = [] + + this.results = this.getDefaultResults() + globalResults[testTag] = this.results + this.skipReason = this.checkIfShouldSkip(globalResults) + } + + /** + * Return a string identifying this test group + */ + static getTestTag () { + return '' + } + + /** + * Return the tags of other tests this test relies on + */ + static getPrereqs () { + return [] + } + + /** + * Return this test's default (i.e. all failed) results + */ + getDefaultResults () { + return {} + } + + checkIfShouldSkip (globalResults) { + /** + * If earlier results indicate this test shouldn't be run + * return a string indicating why (otherwise return null) + */ + return null + } + + async runTest () { + /** + * Wait for all running test threads to complete or + * timeout, then log the results and return a summary. + */ + await this.startTest() + // Wait for all threads to complete + await Promise.all(this.threads.map(thread => thread.runPromise)) + const testResults = this.logResults() + const testTime = (performance.now() - this.startTime) / 1000 // convert to seconds + return [testTime, testResults] + } + + async startTest () { + /** + * Implemented by subclasses to create the test threads + */ + // To be implemented by subclasses + } + + startTestThread (func, args, logTag, timeout) { + /** + * Create a new test thread in this test group + */ + const threadIdx = this.threads.length + const logHdr = `${this.testTag} #${threadIdx} (${logTag}): ` + const thread = new TestThread(func, args, logHdr, timeout, this.results) + this.threads.push(thread) + } + + logResults () { + /** + * Log the results of the completed test threads and + * return a string summarizing the results. + */ + return '' + } +} + +function getClientTests () { + /** + * Return a list of TestGroup classes defined in this + * module that clients should run + */ + return [] +} + +export default { + TestThread, + TestGroup, + getClientTests, +} diff --git a/packages/core/src/modules/plugin/free-eye/utils.js b/packages/core/src/modules/plugin/free-eye/utils.js new file mode 100644 index 0000000000..65b30b413d --- /dev/null +++ b/packages/core/src/modules/plugin/free-eye/utils.js @@ -0,0 +1,79 @@ +/** + * Various utility functions shared by tests + */ + +// Linking socket constants to human-readable strings +export const FAMILY_VALUES = { + IPv4: 4, + IPv6: 6, +} +export const PROTOCOL_VALUES = { + TCP: 'tcp', + UDP: 'udp', +} + +export class LogColors { + /** + * ANSI color codes for pretty terminal output + * Update: Dev-sidecar doesn't support ANSI color codes in its console output. + */ + static RESET = '' + static RED = '' + static GREEN = '' + static YELLOW = '' + static BLUE = '' + static MAGENTA = '' + static CYAN = '' + static WHITE = '' +} + +export const DISPLAY_WIDTH = 50 + +export function printHeader (title, isRes) { // test start, test res + const sep = `\n${'='.repeat(DISPLAY_WIDTH)}\n` + console.log( + (isRes ? LogColors.MAGENTA : LogColors.CYAN) + + sep + title.padStart(Math.floor((DISPLAY_WIDTH + title.length) / 2)).padEnd(DISPLAY_WIDTH) + sep + + LogColors.RESET, + ) +} + +export function getResultIcon (success, infoStr = null) { + let resColor, resIcon + if (success === true) { + resColor = LogColors.GREEN + resIcon = '✔' + } else if (success === false) { + resColor = LogColors.RED + resIcon = '✖' + } else { // test inconclusive + resColor = LogColors.YELLOW + resIcon = '?' + } + if (infoStr !== null) { + resIcon += ` ${infoStr}` + } + return `(${resColor}${resIcon}${LogColors.RESET})` +} + +export function getCensorsString (censors) { + let resStr = '' + if (censors && censors.length > 0) { + for (const c of censors) { + resStr += ` Censorship detected: ${LogColors.RED}${c}${LogColors.RESET}\n` + } + } else { + resStr += ' No censorship detected\n' + } + return resStr +} + +export default { + FAMILY_VALUES, + PROTOCOL_VALUES, + LogColors, + DISPLAY_WIDTH, + printHeader, + getResultIcon, + getCensorsString, +} diff --git a/packages/core/src/modules/plugin/git/index.js b/packages/core/src/modules/plugin/git/index.js index 59fb27dc5f..402341bfe1 100644 --- a/packages/core/src/modules/plugin/git/index.js +++ b/packages/core/src/modules/plugin/git/index.js @@ -24,7 +24,6 @@ const Plugin = function (context) { }, async save (newConfig) { - }, async setProxy (ip, port) { diff --git a/packages/core/src/modules/plugin/index.js b/packages/core/src/modules/plugin/index.js index c5d50f79a1..1e3686bef5 100644 --- a/packages/core/src/modules/plugin/index.js +++ b/packages/core/src/modules/plugin/index.js @@ -3,4 +3,15 @@ module.exports = { git: require('./git'), pip: require('./pip'), overwall: require('./overwall'), + // free-eye 为 ESM 模块,CJS require() 得到 { default: ... },需解包 + // 独立可执行文件(SEA)中无法打包/携带 free-eye,加载失败时降级为不可用,而不是崩溃 + get free_eye () { + try { + return require('./free-eye').default + } catch (e) { + const log = require('@docmirror/dev-sidecar/src/utils/util.log-or-console') + log.warn('加载 free-eye 插件失败,该插件不可用:', e.message) + return null + } + }, } diff --git a/packages/core/src/modules/plugin/node/config.js b/packages/core/src/modules/plugin/node/config.js index 1463f5094a..ab6544a68d 100644 --- a/packages/core/src/modules/plugin/node/config.js +++ b/packages/core/src/modules/plugin/node/config.js @@ -11,18 +11,35 @@ module.exports = { 'cafile': false, 'NODE_EXTRA_CA_CERTS': false, 'NODE_TLS_REJECT_UNAUTHORIZED': false, - 'yarnRegistry': 'default', 'registry': 'https://registry.npmjs.org', // 可以选择切换官方或者淘宝镜像 + 'registryList': { + taobao: { + name: 'taobao镜像', + value: 'https://registry.npmmirror.com', + }, + ustclug: { + name: '中国科学技术大学镜像', + value: 'https://npmreg.proxy.ustclug.org', + }, + }, + 'yarnRegistry': 'default', + 'yarnRegistryList': { + taobao: { + name: 'taobao镜像', + value: 'https://registry.npmmirror.com', + }, + }, }, - variables: { - phantomjs_cdnurl: 'https://npmmirror.com/mirrors/phantomjs', - chromedriver_cdnurl: 'https://npmmirror.com/mirrors/chromedriver', - sass_binary_site: 'https://npmmirror.com/mirrors/node-sass', + // npm 11 开始不再接受未知的 npm config,以下镜像变量全部改为直接设置系统环境变量 + variables: {}, + envVariables: { ELECTRON_MIRROR: 'https://npmmirror.com/mirrors/electron/', - NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node', + ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/', + PHANTOMJS_CDNURL: 'https://npmmirror.com/mirrors/phantomjs', CHROMEDRIVER_CDNURL: 'https://npmmirror.com/mirrors/chromedriver', + SASS_BINARY_SITE: 'https://npmmirror.com/mirrors/node-sass', + NVM_NODEJS_ORG_MIRROR: 'https://npmmirror.com/mirrors/node', OPERADRIVER: 'https://npmmirror.com/mirrors/operadriver', - ELECTRON_BUILDER_BINARIES_MIRROR: 'https://npmmirror.com/mirrors/electron-builder-binaries/', PYTHON_MIRROR: 'https://npmmirror.com/mirrors/python', }, } diff --git a/packages/core/src/modules/plugin/node/index.js b/packages/core/src/modules/plugin/node/index.js index e646877e02..1c4cc4f274 100644 --- a/packages/core/src/modules/plugin/node/index.js +++ b/packages/core/src/modules/plugin/node/index.js @@ -1,6 +1,31 @@ +const fs = require('node:fs') +const path = require('node:path') const jsonApi = require('@docmirror/mitmproxy/src/json') const nodeConfig = require('./config') +function getUserNpmrcPath () { + return path.join(process.env.USERPROFILE || process.env.HOME || '', '.npmrc') +} + +function isKeyInUserNpmrc (key) { + try { + if (!fs.existsSync(getUserNpmrcPath())) { + return false + } + const content = fs.readFileSync(getUserNpmrcPath(), 'utf8') + const target = key.toLowerCase() + return content.split(/\r?\n/).some((line) => { + const index = line.indexOf('=') + if (index <= 0) { + return false + } + return line.slice(0, index).trim().toLowerCase() === target + }) + } catch { + return false + } +} + const NodePlugin = function (context) { const { config, shell, event, log } = context const nodeApi = { @@ -27,7 +52,7 @@ const NodePlugin = function (context) { }, async save (newConfig) { - nodeApi.setVariables() + await nodeApi.setVariables() }, async getNpmEnv () { const command = config.get().plugin.node.setting.command || 'npm' @@ -43,25 +68,35 @@ const NodePlugin = function (context) { async setNpmEnv (list) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [] + // npm config set 支持一次设置多个 key=value,合并成一条命令,避免启动多个 npm 进程拖慢速度 + const setArgs = [] + const deleteKeys = [] for (const item of list) { if (item.value != null && item.value.length > 0 && item.value !== 'default' && item.value !== 'null') { - cmds.push(`${command} config set ${item.key} ${item.value}`) + setArgs.push(`${item.key}=${item.value}`) } else { - cmds.push(`${command} config delete ${item.key}`) + deleteKeys.push(item.key) } } + + const cmds = [] + if (setArgs.length > 0) { + cmds.push(`${command} config set ${setArgs.join(' ')}`) + } + if (deleteKeys.length > 0) { + cmds.push(`${command} config delete ${deleteKeys.join(' ')}`) + } return await shell.exec(cmds, { type: 'cmd' }) }, async unsetNpmEnv (list) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [] - for (const item of list) { - cmds.push(`${command} config delete ${item} `) - } - return await shell.exec(cmds, { type: 'cmd' }) + // npm config delete 支持一次删除多个 key,合并成一条命令 + const keys = list.map((item) => { + return typeof item === 'string' ? item : item.key + }).filter(Boolean) + return await shell.exec([`${command} config delete ${keys.join(' ')}`], { type: 'cmd' }) }, async setYarnEnv (list) { @@ -99,16 +134,57 @@ const NodePlugin = function (context) { hadSet: currentMap[key] === map[key], }) } + + // 环境变量形式的镜像配置,直接检查 process.env + const envMap = config.get().plugin.node.envVariables || {} + const defaultEnvMap = nodeConfig.envVariables || {} + for (const key in envMap) { + const oldValue = process.env[key] + list.push({ + key, + value: envMap[key], + oldValue, + exists: oldValue != null, + hadSet: oldValue === envMap[key], + defaultValue: defaultEnvMap[key], + }) + } return list }, async setVariables () { - const list = await nodeApi.getVariables() - const noSetList = list.filter((item) => { - return !item.exists + const nodeConfig = config.get().plugin.node + const envMap = nodeConfig.envVariables || {} + const variables = nodeConfig.variables || {} + + // 清理旧版通过 npm config 写入的镜像变量,避免 npm 未知配置告警。 + // 只有用户级 .npmrc 中确实存在这些 key 时才启动 npm 执行删除,避免每次启动/应用都白跑一遍 npm。 + const staleKeys = Object.keys(envMap).filter((key) => isKeyInUserNpmrc(key)) + if (staleKeys.length > 0) { + try { + await nodeApi.unsetNpmEnv(staleKeys) + } catch (e) { + log.warn('删除旧的 npm config 镜像变量失败:', e) + } + } + + // 每次启动/应用时都强制重写所有镜像环境变量,覆盖手动修改的值 + const envList = Object.keys(envMap).map((key) => { + return { key, value: envMap[key] } }) - if (noSetList.length > 0) { - return nodeApi.setNpmEnv(noSetList) + if (envList.length > 0) { + await shell.setSystemEnv({ list: envList }) + } + + // 其余非环境变量形式的 npm config 变量(当前默认为空),仅在确实配置了 variables 时才查询 npm + if (Object.keys(variables).length > 0) { + const list = await nodeApi.getVariables() + const noSetList = list.filter((item) => { + return !item.hadSet && envMap[item.key] == null + }) + if (noSetList.length > 0) { + await nodeApi.setNpmEnv(noSetList) + } } }, @@ -124,39 +200,40 @@ const NodePlugin = function (context) { async setProxy (ip, port) { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [ - `${command} config set proxy=http://${ip}:${port - 1}`, - `${command} config set https-proxy=http://${ip}:${port}`, + // npm config set 支持一次设置多个 key=value,合并成一条命令 + const setArgs = [ + `proxy=http://${ip}:${port - 1}`, + `https-proxy=http://${ip}:${port}`, ] const env = [] /** - * 'strict-ssl': false, - cafile: true, - NODE_EXTRA_CA_CERTS: true, - NODE_TLS_REJECT_UNAUTHORIZED: false + * 'strict-ssl': false, + * 'cafile': true, + * 'NODE_EXTRA_CA_CERTS': true, + * 'NODE_TLS_REJECT_UNAUTHORIZED': false */ const nodeConfig = config.get().plugin.node const rootCaCertFile = config.get().server.setting.rootCaFile.certPath if (nodeConfig.setting['strict-ssl']) { - cmds.push(`${command} config set strict-ssl false`) + setArgs.push('strict-ssl=false') } if (nodeConfig.setting.cafile) { - cmds.push(`${command} config set cafile "${rootCaCertFile}"`) + setArgs.push(`cafile=${rootCaCertFile}`) } if (nodeConfig.setting.NODE_EXTRA_CA_CERTS) { - cmds.push(`${command} config set NODE_EXTRA_CA_CERTS "${rootCaCertFile}"`) + setArgs.push(`NODE_EXTRA_CA_CERTS=${rootCaCertFile}`) env.push({ key: 'NODE_EXTRA_CA_CERTS', value: rootCaCertFile }) } if (nodeConfig.setting.NODE_TLS_REJECT_UNAUTHORIZED) { - cmds.push(`${command} config set NODE_TLS_REJECT_UNAUTHORIZED 0`) + setArgs.push('NODE_TLS_REJECT_UNAUTHORIZED=0') env.push({ key: 'NODE_TLS_REJECT_UNAUTHORIZED', value: '0' }) } - const ret = await shell.exec(cmds, { type: 'cmd' }) + const ret = await shell.exec([`${command} config set ${setArgs.join(' ')}`], { type: 'cmd' }) if (env.length > 0) { await shell.setSystemEnv({ list: env }) } @@ -169,13 +246,8 @@ const NodePlugin = function (context) { async unsetProxy () { const command = config.get().plugin.node.setting.command || 'npm' - const cmds = [ - `${command} config delete proxy`, - `${command} config delete https-proxy`, - `${command} config delete NODE_EXTRA_CA_CERTS`, - `${command} config delete strict-ssl`, - ] - const ret = await shell.exec(cmds, { type: 'cmd' }) + // npm config delete 支持一次删除多个 key,合并成一条命令 + const ret = await shell.exec([`${command} config delete proxy https-proxy strict-ssl cafile NODE_EXTRA_CA_CERTS NODE_TLS_REJECT_UNAUTHORIZED`], { type: 'cmd' }) event.fire('status', { key: 'plugin.node.enabled', value: false }) log.info('关闭【NPM】代理成功') return ret diff --git a/packages/core/src/modules/plugin/overwall/config.js b/packages/core/src/modules/plugin/overwall/config.js index 870cae95d5..762c2b947a 100644 --- a/packages/core/src/modules/plugin/overwall/config.js +++ b/packages/core/src/modules/plugin/overwall/config.js @@ -1,9 +1,11 @@ module.exports = { name: '梯子', enabled: false, // 默认关闭梯子 + restartServer: true, // 首页开关切换后需要重启代理服务才能生效 server: {}, serverDefault: { 'ow-prod.docmirror.top': { + id: 0, // 自带默认服务器 ID 固定为 0,用户新增服务器依次为 1、2、3... port: 443, path: 'X2dvX292ZXJfd2FsbF8', password: 'dev_sidecar_is_666', diff --git a/packages/core/src/modules/plugin/overwall/index.js b/packages/core/src/modules/plugin/overwall/index.js index 7842449815..c98889bf34 100644 --- a/packages/core/src/modules/plugin/overwall/index.js +++ b/packages/core/src/modules/plugin/overwall/index.js @@ -4,11 +4,13 @@ const Plugin = function (context) { const { config, shell, event, log } = context const api = { async start () { - // event.fire('status', { key: 'plugin.overwall.enabled', value: true }) + event.fire('status', { key: 'plugin.overwall.enabled', value: true }) + log.info('开启【Overwall】代理成功') }, async close () { - // event.fire('status', { key: 'plugin.overwall.enabled', value: false }) + event.fire('status', { key: 'plugin.overwall.enabled', value: false }) + log.info('关闭【Overwall】代理成功') }, async restart () { @@ -49,5 +51,8 @@ const Plugin = function (context) { module.exports = { key: 'overwall', config: pluginConfig, + status: { + enabled: false, + }, plugin: Plugin, } diff --git a/packages/core/src/modules/plugin/pip/config.js b/packages/core/src/modules/plugin/pip/config.js index c55a6bf2df..49e799406a 100644 --- a/packages/core/src/modules/plugin/pip/config.js +++ b/packages/core/src/modules/plugin/pip/config.js @@ -9,5 +9,47 @@ module.exports = { command: 'pip', trustedHost: 'pypi.org', registry: 'https://pypi.org/simple/', // 可以选择切换官方或者淘宝镜像 + registryList: { + aliyun: { + name: '阿里镜像', + value: 'https://mirrors.aliyun.com/pypi/simple/', + }, + baidu: { + name: '百度镜像', + value: 'https://mirror.baidu.com/pypi/simple/', + }, + douban: { + name: '豆瓣镜像', + value: 'http://pypi.douban.com/simple/', + }, + sohu: { + name: '搜狐镜像', + value: 'http://mirrors.sohu.com/Python/', + }, + ustclug: { + name: '中科大镜像', + value: 'https://pypi.mirrors.ustc.edu.cn/simple/', + }, + bfsu: { + name: '北京外国语大学镜像', + value: 'https://mirrors.bfsu.edu.cn/pypi/web/simple/', + }, + nju: { + name: '南京大学镜像', + value: 'https://mirror.nju.edu.cn/pypi/web/simple/', + }, + tsinghua: { + name: '清华大学镜像', + value: 'https://pypi.tuna.tsinghua.edu.cn/simple/', + }, + hust: { + name: '华中科大镜像', + value: 'https://mirrors.hust.edu.cn/pypi/web/simple/', + }, + sdut: { + name: '山东理工大学镜像', + value: 'http://pypi.sdutlinux.org/', + }, + }, }, } diff --git a/packages/core/src/modules/plugin/pip/index.js b/packages/core/src/modules/plugin/pip/index.js index 0ac81aa37a..1d1c4fc3c0 100644 --- a/packages/core/src/modules/plugin/pip/index.js +++ b/packages/core/src/modules/plugin/pip/index.js @@ -6,9 +6,13 @@ const PipPlugin = function (context) { async start () { await api.setRegistry({ registry: config.get().plugin.pip.setting.registry }) await api.setTrustedHost(config.get().plugin.pip.setting.trustedHost) + event.fire('status', { key: 'plugin.pip.enabled', value: true }) + log.info('开启【Pip】代理成功') }, async close () { + event.fire('status', { key: 'plugin.pip.enabled', value: false }) + log.info('关闭【Pip】代理成功') }, async restart () { @@ -17,8 +21,8 @@ const PipPlugin = function (context) { }, async save (newConfig) { - await api.setVariables() }, + async getPipEnv () { const command = config.get().plugin.pip.setting.command let ret = await shell.exec([`${command} config list`], { type: 'cmd' }) @@ -30,10 +34,10 @@ const PipPlugin = function (context) { if (!line.startsWith('global')) { continue } - const key = line.substring(0, line.indexOf('=')) - let value = line.substring(line.indexOf('=') + 1) + const key = line.substring(0, line.indexOf('=')).trim() + let value = line.substring(line.indexOf('=') + 1).trim() if (value.startsWith('\'')) { - value = value.startsWith(1, value.length - 1) + value = value.slice(1, -1) } vars[key] = value } @@ -75,11 +79,9 @@ const PipPlugin = function (context) { }, async setProxy (ip, port) { - }, async unsetProxy () { - }, } return api diff --git a/packages/core/src/modules/proxy/index.js b/packages/core/src/modules/proxy/index.js index 7a01cef6bc..d45a0f9eae 100644 --- a/packages/core/src/modules/proxy/index.js +++ b/packages/core/src/modules/proxy/index.js @@ -17,16 +17,19 @@ const ProxyPlugin = function (context) { async setProxy () { const ip = '127.0.0.1' const port = config.get().server.port - const setEnv = config.get().proxy.setEnv - await shell.setSystemProxy({ ip, port, setEnv }) + const proxyConfig = config.get().proxy || {} + const setEnv = proxyConfig.setEnv ?? false + const setCaBundle = proxyConfig.setCaBundle ?? false + await shell.setSystemProxy({ ip, port, setEnv, setCaBundle }) log.info(`开启系统代理成功:${ip}:${port}`) event.fire('status', { key: 'proxy.enabled', value: true }) return { ip, port } }, async unsetProxy (setEnv) { - if (setEnv) { - setEnv = config.get().proxy.setEnv + if (setEnv == null) { + const proxyConfig = config.get().proxy || {} + setEnv = proxyConfig.setEnv ?? false } try { await shell.setSystemProxy({ setEnv }) @@ -56,6 +59,7 @@ module.exports = { other: [], proxyHttp: false, // false=只代理HTTPS请求 true=同时代理HTTP和HTTPS请求 setEnv: false, + setCaBundle: false, // 排除国内域名 所需配置 excludeDomesticDomainAllowList: true, // 是否排除国内域名,默认:需要排除 diff --git a/packages/core/src/modules/server/index.js b/packages/core/src/modules/server/index.js index 58fe413ed7..5894d28d5b 100644 --- a/packages/core/src/modules/server/index.js +++ b/packages/core/src/modules/server/index.js @@ -19,6 +19,16 @@ function sleep (time) { }, time) }) } + +function onceExit (child) { + return new Promise((resolve) => { + if (child.exitCode != null || child.signalCode != null) { + resolve(true) + return + } + child.once('exit', () => resolve(true)) + }) +} const serverApi = { async startup () { if (config.get().server.startup) { @@ -30,7 +40,13 @@ const serverApi = { return this.close() } }, - async start ({ mitmproxyPath, plugins }) { + async start ({ mitmproxyPath, plugins, setting }) { + // 防止重复启动:如果已有子进程存活,直接返回 + if (server && server.process && !server.process.killed && server.process.exitCode == null) { + log.warn('server is already running, skip start (pid:', server.id, ')') + return { port: server.port } + } + const allConfig = config.get() const serverConfig = lodash.cloneDeep(allConfig.server) @@ -67,7 +83,11 @@ const serverApi = { plugin.overrideRunningConfig(serverConfig) } } - serverConfig.plugin = allConfig.plugin + serverConfig.plugin = lodash.cloneDeep(allConfig.plugin || {}) + if (setting && setting.overwall !== true && serverConfig.plugin.overwall) { + // setting.json 未开启 overwall 时,梯子插件不生效 + serverConfig.plugin.overwall.enabled = false + } if (allConfig.proxy && allConfig.proxy.enabled) { serverConfig.proxy = allConfig.proxy @@ -77,6 +97,20 @@ const serverApi = { const basePath = serverConfig.setting.userBasePath const runningConfigPath = path.join(basePath, '/running.json') try { + // 保留现有的 instance 信息(启动类型、pid 等),避免被配置覆盖 + let existingInstance + if (fs.existsSync(runningConfigPath)) { + try { + const existing = JSON.parse(fs.readFileSync(runningConfigPath, 'utf-8')) + existingInstance = existing?.app?.instance + } catch {} + } + if (existingInstance) { + if (!serverConfig.app) { + serverConfig.app = {} + } + serverConfig.app.instance = existingInstance + } fs.writeFileSync(runningConfigPath, jsonApi.stringify(serverConfig)) log.info('保存 running.json 运行时配置文件成功:', runningConfigPath) } catch (e) { @@ -87,6 +121,7 @@ const serverApi = { server = { id: serverProcess.pid, process: serverProcess, + port: serverConfig.port, close () { serverProcess.send({ type: 'action', event: { key: 'close' } }) }, @@ -116,48 +151,34 @@ const serverApi = { event.fire('error', { key: 'server', value: code, error: msg.event, message: msg.message }) } else if (msg.type === 'speed') { event.fire('speed', msg.event) + } else if (msg.type === 'traffic') { + event.fire('traffic', msg.event) } }) return { port: serverConfig.port } }, async kill () { if (server) { - server.process.kill('SIGINT') - await sleep(1000) + const child = server.process + if (child.exitCode == null && child.signalCode == null) { + const exited = onceExit(child) + child.kill('SIGINT') + const exitedBySigint = await Promise.race([exited, sleep(1000).then(() => false)]) + if (!exitedBySigint) { + log.warn('server process 未在 1 秒内响应 SIGINT,尝试强制结束') + child.kill('SIGKILL') + await Promise.race([exited, sleep(1000).then(() => false)]) + } + } } fireStatus(false) }, async close () { return await serverApi.kill() }, - async close1 () { - return new Promise((resolve, reject) => { - if (server) { - // fireStatus('ing')// 关闭中 - server.close((err) => { - if (err) { - log.warn('close error:', err) - if (err.code === 'ERR_SERVER_NOT_RUNNING') { - log.info('代理服务关闭成功') - resolve() - return - } - log.warn('代理服务关闭失败:', err) - reject(err) - } else { - log.info('代理服务关闭成功') - resolve() - } - }) - } else { - log.info('server is null') - resolve() - } - }) - }, - async restart ({ mitmproxyPath }) { + async restart ({ mitmproxyPath, setting }) { await serverApi.kill() - await serverApi.start({ mitmproxyPath }) + await serverApi.start({ mitmproxyPath, setting }) }, getServer () { return server diff --git a/packages/core/src/shell/scripts/kill-by-port.js b/packages/core/src/shell/scripts/kill-by-port.js index 248eb4a49d..5d570fc06b 100644 --- a/packages/core/src/shell/scripts/kill-by-port.js +++ b/packages/core/src/shell/scripts/kill-by-port.js @@ -2,19 +2,85 @@ const Shell = require('../shell') const execute = Shell.execute +/** + * 终止占用指定端口的进程 + * + * 各平台均采用 主方案 + 备选方案 的策略: + * - windows: pwsh (Get-NetTCPConnection) → cmd (netstat + taskkill) + * - linux: lsof → fuser + * - mac: lsof → fuser + */ const executor = { async windows (exec, { port }) { - const cmds = [`for /f "tokens=5" %a in ('netstat -aon ^| find ":${port}" ^| find "LISTENING"') do (taskkill /f /pid %a & exit /B) `] - await exec(cmds, { type: 'cmd' }) - return true + // 主方案:PowerShell(更可靠,跨平台一致,Win7+ 默认可用) + try { + const cmds = [ + // 查找处于 Listen 状态的 TCP 连接并终止对应进程 + `$conn = Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Listen' } | Select-Object -First 1; if ($conn) { Stop-Process -Id $conn.OwningProcess -Force }`, + ] + await exec(cmds, { type: 'ps' }) + return true + } catch (psError) { + // 备选方案:CMD netstat + taskkill(Win7 无 pwsh 或 pwsh 执行失败时回退) + // 分两步执行,避免 for /f 在 cmd /s /c 下的引号解析问题 + try { + const output = await exec([`netstat -aon | find ":${port}"`], { type: 'cmd', printErrorLog: false }) + if (!output) { + throw new Error('没有找到占用该端口的进程') + } + + // 解析 netstat 输出,提取处于 LISTENING 状态的 PID + const lines = output.split(/\r?\n/) + let killed = false + for (const line of lines) { + if (!line.includes('LISTENING')) { + continue + } + const parts = line.trim().split(/\s+/) + const pid = parts[parts.length - 1] + if (pid && /^\d+$/.test(pid)) { + await exec([`taskkill /f /pid ${pid} /t`], { type: 'cmd', printErrorLog: false }) + killed = true + } + } + if (!killed) { + throw new Error('未找到处于 LISTENING 状态的进程') + } + return true + } catch (cmdError) { + // 两种方案都失败,抛出包含原始错误信息的异常 + throw new Error( + `终止占用端口 ${port} 的进程失败。\n` + + `PowerShell 方案: ${psError.message}\n` + + `CMD 方案: ${cmdError.message}`, + ) + } + } }, + async linux (exec, { port }) { - await exec(`kill \`lsof -i:${port} |grep 'dev-sidecar\\|electron\\|@docmirro' |awk '{print $2}'\``) - return true + // 主方案:lsof + try { + await exec(`kill $(lsof -i:${port} -t 2>/dev/null) 2>/dev/null || true`) + return true + } catch (_lsofError) { + // 备选方案:fuser + try { + await exec(`fuser -k ${port}/tcp 2>/dev/null || true`) + return true + } catch (fuserError) { + throw new Error( + `终止占用端口 ${port} 的进程失败。\n` + + `lsof 方案失败\n` + + `fuser 方案: ${fuserError.message}`, + ) + } + } }, + async mac (exec, { port }) { - await exec(`kill \`lsof -i:${port} |grep 'dev-side\\|Elect' |awk '{print $2}'\``) - return true + // macOS 与 Linux 采用相同策略 + return executor.linux(exec, { port }) }, } diff --git a/packages/core/src/shell/scripts/set-system-env.js b/packages/core/src/shell/scripts/set-system-env.js index 828e5cdb81..c2e648ad34 100644 --- a/packages/core/src/shell/scripts/set-system-env.js +++ b/packages/core/src/shell/scripts/set-system-env.js @@ -1,26 +1,61 @@ /** * 设置环境变量 */ +const Registry = require('winreg') const Shell = require('../shell') const execute = Shell.execute const executor = { async windows (exec, { list }) { - const cmds = [] - for (const item of list) { - // [Environment]::SetEnvironmentVariable('FOO', 'bar', 'Machine') - cmds.push(`[Environment]::SetEnvironmentVariable('${item.key}', '${item.value}', 'Machine')`) + const regKey = new Registry({ + hive: Registry.HKCU, + key: '\\Environment', + }) + + const setItem = (item) => { + const value = item.value == null ? '' : String(item.value) + return new Promise((resolve, reject) => { + regKey.set(item.key, Registry.REG_SZ, value, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) } - const ret = await exec(cmds, { type: 'ps' }) - const cmds2 = [] - for (const item of list) { - // [Environment]::SetEnvironmentVariable('FOO', 'bar', 'Machine') - cmds2.push(`set ${item.key}=""`) + try { + for (const item of list) { + await setItem(item) + } + + // 广播环境变量变更(setx 一个临时值触发 WM_SETTINGCHANGE) + try { + await exec('setx DS_REFRESH "1"', { type: 'cmd' }) + } catch { + // 广播失败不影响主流程 + } + + // inject into current process so subsequent exec/child processes can inherit immediately + let envUpdateError = null + try { + for (const item of list) { + if (item.value == null) { + delete process.env[item.key] + } else { + process.env[item.key] = String(item.value) + } + } + } catch (e) { + envUpdateError = e.message || String(e) + } + + return { success: true, scope: 'User:winreg', envUpdateError } + } catch (e) { + return { success: false, error: 'Failed to set environment variables', details: e.message || String(e) } } - await exec(cmds2, { type: 'cmd' }) - return ret }, async linux (exec, { port }) { throw new Error('暂未实现此功能') diff --git a/packages/core/src/shell/scripts/set-system-proxy/index.js b/packages/core/src/shell/scripts/set-system-proxy/index.js index 1cd9b11605..dadda3b35c 100644 --- a/packages/core/src/shell/scripts/set-system-proxy/index.js +++ b/packages/core/src/shell/scripts/set-system-proxy/index.js @@ -5,6 +5,7 @@ const fs = require('node:fs') const path = require('node:path') const request = require('request') const Registry = require('winreg') +const sudoPrompt = require('@vscode/sudo-prompt') const log = require('../../../utils/util.log.core') const Shell = require('../../shell') const extraPath = require('../extra-path') @@ -24,12 +25,81 @@ function getDomesticDomainAllowListTmpFilePath () { return path.join(config.get().server.setting.userBasePath, '/domestic-domain-allowlist.txt') } +// 通过 HKCU\Environment 注册表直接写入/删除环境变量,避免每次 setx 都启动 PowerShell 造成数秒卡顿 +function createEnvRegKey () { + return new Registry({ + hive: Registry.HKCU, + key: '\\Environment', + }) +} + +function setWindowsEnvVariable (regKey, key, value) { + return new Promise((resolve, reject) => { + regKey.set(key, Registry.REG_SZ, value == null ? '' : String(value), (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +function removeWindowsEnvVariable (regKey, key) { + return new Promise((resolve) => { + regKey.remove(key, (removeErr) => { + resolve(!removeErr) + }) + }) +} + +async function broadcastWindowsEnvChange (exec) { + try { + await exec('setx DS_REFRESH "1"', { type: 'cmd' }) + } catch { + // 广播失败不影响主流程 + } +} + +async function setWindowsEnvVariables (exec, envList) { + if (!envList || envList.length === 0) { + return + } + + const regKey = createEnvRegKey() + for (const item of envList) { + await setWindowsEnvVariable(regKey, item.key, item.value) + process.env[item.key] = String(item.value) + } + await broadcastWindowsEnvChange(exec) +} + +async function removeWindowsEnvVariables (exec, keys) { + if (!keys || keys.length === 0) { + return + } + + const regKey = createEnvRegKey() + let removed = false + for (const key of keys) { + const existed = await removeWindowsEnvVariable(regKey, key) + if (existed) { + delete process.env[key] + removed = true + } + } + if (removed) { + await broadcastWindowsEnvChange(exec) + } +} + async function downloadDomesticDomainAllowListAsync () { loadConfig() const remoteFileUrl = config.get().proxy.remoteDomesticDomainAllowListFileUrl log.info('开始下载远程 domestic-domain-allowlist.txt 文件:', remoteFileUrl) - request(remoteFileUrl, (error, response, body) => { + // 禁用环境变量代理:防止走 dev-sidecar 自己的代理(127.0.0.1:31181)导致启动时下载失败 + request(remoteFileUrl, { proxy: null }, (error, response, body) => { if (error) { log.error(`下载远程 domestic-domain-allowlist.txt 文件失败: ${remoteFileUrl}, error:`, error, ', response:', response, ', body:', body) return @@ -183,27 +253,301 @@ function getProxyExcludeIpStr (split) { return excludeIpStr } +function parseMacNetworkServiceByDevice (networkServiceOrder, device) { + if (!networkServiceOrder || !device) { + return null + } + const lines = networkServiceOrder.split(/\r?\n/) + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes(`Device: ${device}`)) { + for (let j = i - 1; j >= 0; j--) { + const serviceLine = lines[j].trim() + const markerIndex = serviceLine.indexOf(') ') + if (serviceLine.startsWith('(') && markerIndex > 0) { + return serviceLine.slice(markerIndex + 2).trim() + } + } + } + } + return null +} + +function parseMacRouteDevice (routeOutput) { + if (!routeOutput) { + return null + } + const routeLines = routeOutput.split(/\r?\n/) + for (const routeLine of routeLines) { + const trimmedLine = routeLine.trim() + if (trimmedLine.startsWith('interface:')) { + return trimmedLine.slice('interface:'.length).trim() || null + } + } + return null +} + +function pickMacNetworkService (listAllNetworkServicesOutput) { + if (!listAllNetworkServicesOutput) { + return null + } + const services = listAllNetworkServicesOutput + .split(/\r?\n/) + .map(item => item.replace(/^\*/, '').trim()) + .filter(item => item && !item.startsWith('An asterisk (*) denotes')) + if (services.length === 0) { + return null + } + const preferredServices = ['Wi-Fi', 'WiFi', 'Ethernet'] + for (const preferredService of preferredServices) { + const matched = services.find(item => item === preferredService) + if (matched) { + return matched + } + } + return services[0] +} + +async function getMacNetworkService (exec) { + try { + const routeOutput = await exec('route -n get 0.0.0.0') + const device = parseMacRouteDevice(routeOutput) + if (device) { + log.info('macOS 代理服务检测:当前网络设备:', device) + try { + const networkServiceOrder = await exec('networksetup -listnetworkserviceorder') + const matchedService = parseMacNetworkServiceByDevice(networkServiceOrder, device) + if (matchedService) { + log.info('macOS 代理服务检测:通过设备名匹配到网络服务:', matchedService) + return matchedService + } + log.warn('macOS 代理服务检测:未通过设备名匹配到网络服务,尝试备用方法') + } catch (e) { + log.warn('macOS 代理服务检测:获取网络服务列表失败:', e.message, ',尝试备用方法') + } + } else { + log.warn('macOS 代理服务检测:未检测到当前网络设备,尝试备用方法') + } + } catch (e) { + log.warn('macOS 代理服务检测:获取路由信息失败:', e.message, ',尝试备用方法') + } + + try { + const allServicesOutput = await exec('networksetup -listallnetworkservices') + const fallbackService = pickMacNetworkService(allServicesOutput) + if (fallbackService) { + log.info('macOS 代理服务检测:通过服务列表备用方法找到网络服务:', fallbackService) + return fallbackService + } + log.warn('macOS 代理服务检测:未通过服务列表找到可用网络服务') + } catch (e) { + log.warn('macOS 代理服务检测:获取所有网络服务列表失败:', e.message) + } + + throw new Error('未找到可用的 macOS 网络服务,无法设置系统代理') +} + +// macOS exit code 14 = "You don't have permission to change the system preferences." +const MACOS_NETWORKSETUP_PERMISSION_ERROR_CODE = 14 + +/** + * POSIX single-quote escaping: wraps `arg` in single quotes, escaping any + * embedded single quotes with the '\''-idiom. This prevents shell + * metacharacter expansion regardless of the character set of the value. + * @param {string|number} arg + * @returns {string} + */ +function shellEscapeArg (arg) { + return "'" + String(arg).replace(/'/g, "'\\''") + "'" +} + +/** + * Strict-validate a proxy host (IPv4 / IPv6 / hostname) and throw if the + * value looks suspicious. This is a defence-in-depth guard for the sudo + * execution path; the primary protection is `shellEscapeArg`. + */ +function validateProxyIp (ip) { + if (typeof ip !== 'string' || !/^[\w.\-:[\]]+$/.test(ip)) { + throw new Error(`无效的代理 IP 地址: ${ip}`) + } +} + +/** + * Strict-validate a TCP port number. + */ +function validateProxyPort (port) { + const n = Number(port) + if (!Number.isInteger(n) || n < 1 || n > 65535) { + throw new Error(`无效的代理端口号: ${port}`) + } +} + +function sudoExecMac (cmd) { + return new Promise((resolve, reject) => { + log.info('以管理员权限执行命令:', cmd) + sudoPrompt.exec(cmd, { name: 'dev-sidecar' }, (error, stdout, stderr) => { + if (stderr) { + log.warn('以管理员权限执行命令,stderr:', stderr) + } + if (error) { + log.error('以管理员权限执行命令失败:', error) + reject(error) + } else { + resolve(stdout) + } + }) + }) +} + +// ── 环境变量代理设置(Linux/macOS) ─────────────────── + +const PROXY_ENV_FILE = path.join( + process.env.USERPROFILE || process.env.HOME || '/', + '.dev-sidecar/proxy.env', +) + +function detectShell () { + // 优先使用 $SHELL 环境变量 + const envShell = process.env.SHELL || '' + if (envShell.includes('zsh')) return 'zsh' + if (envShell.includes('bash')) return 'bash' + if (envShell.includes('fish')) return 'fish' + + // 检查常见 shell 配置文件是否存在 + const home = process.env.HOME || '/' + if (fs.existsSync(path.join(home, '.zshrc'))) return 'zsh' + if (fs.existsSync(path.join(home, '.bashrc'))) return 'bash' + if (fs.existsSync(path.join(home, '.config/fish/config.fish'))) return 'fish' + + return 'bash' // 默认 +} + +function getShellProfilePath (shell) { + const home = process.env.HOME || '/' + switch (shell) { + case 'zsh': return path.join(home, '.zshrc') + case 'fish': return path.join(home, '.config/fish/config.fish') + case 'bash': + default: return path.join(home, '.bashrc') + } +} + +function getSourceCommand (shell, envFile) { + switch (shell) { + case 'fish': return `source "${envFile}"` + case 'zsh': + case 'bash': + default: return `[ -f "${envFile}" ] && source "${envFile}"` + } +} + +function getSourceComment (shell) { + return '# dev-sidecar proxy' +} + +function writeProxyEnvFile (ip, port, proxyHttp) { + const lines = [ + `export HTTPS_PROXY="http://${ip}:${port}"`, + `export https_proxy="http://${ip}:${port}"`, + ] + if (proxyHttp) { + lines.push(`export HTTP_PROXY="http://${ip}:${port - 1}"`) + lines.push(`export http_proxy="http://${ip}:${port - 1}"`) + } + try { + fs.mkdirSync(path.dirname(PROXY_ENV_FILE), { recursive: true }) + fs.writeFileSync(PROXY_ENV_FILE, lines.join('\n') + '\n') + log.info('写入代理环境变量文件:', PROXY_ENV_FILE) + } catch (e) { + log.error('写入代理环境变量文件失败:', e) + } +} + +function addProxyEnvToShellProfile () { + const shell = detectShell() + const profilePath = getShellProfilePath(shell) + const sourceLine = getSourceCommand(shell, PROXY_ENV_FILE) + const comment = getSourceComment(shell) + + try { + let content = '' + if (fs.existsSync(profilePath)) { + content = fs.readFileSync(profilePath, 'utf-8') + } + if (!content.includes(sourceLine)) { + fs.appendFileSync(profilePath, `\n${comment}\n${sourceLine}\n`) + log.info('已添加代理环境变量到:', profilePath) + console.log(`代理环境变量已写入 ${profilePath}`) + console.log(`请执行 source ${profilePath} 使当前终端生效`) + } + } catch (e) { + log.error('添加代理环境变量到 shell profile 失败:', e) + } +} + +function removeProxyEnvFromShellProfile () { + // 删除 proxy.env 文件 + try { + if (fs.existsSync(PROXY_ENV_FILE)) { + fs.unlinkSync(PROXY_ENV_FILE) + log.info('已删除代理环境变量文件:', PROXY_ENV_FILE) + } + } catch (e) { + log.error('删除代理环境变量文件失败:', e) + } + + // 从 shell profile 中移除 source 行 + const shell = detectShell() + const profilePath = getShellProfilePath(shell) + const sourceLine = getSourceCommand(shell, PROXY_ENV_FILE) + const comment = getSourceComment(shell) + + try { + if (fs.existsSync(profilePath)) { + let content = fs.readFileSync(profilePath, 'utf-8') + if (content.includes(sourceLine)) { + const escaped = sourceLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + content = content.replace(new RegExp(`\n${comment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\n${escaped}\n`), '\n') + fs.writeFileSync(profilePath, content) + log.info('已从 shell profile 移除代理环境变量:', profilePath) + console.log(`已从 ${profilePath} 移除代理环境变量`) + console.log(`请执行 source ${profilePath} 使当前终端生效`) + } + } + } catch (e) { + log.error('从 shell profile 移除代理环境变量失败:', e) + } +} + const executor = { async windows (exec, params = {}) { - const { ip, port, setEnv } = params + const { ip, port, setEnv, setCaBundle } = params if (ip != null) { // 设置代理 // 延迟加载config loadConfig() log.info('开始设置windows系统代理:', ip, port, setEnv) - // https - let proxyAddr = `https=http://${ip}:${port}` - // http - if (config.get().proxy.proxyHttp) { - proxyAddr = `http=http://${ip}:${port - 1};${proxyAddr}` + const sysproxy = require('@starknt/sysproxy') + const proxyHttp = config.get().proxy.proxyHttp + + // 同时代理 HTTP+HTTPS 时需要两个端口,使用 WinINET 协议格式(协议=地址:端口); + // 只代理 HTTPS 时,地址和端口分开传参,Windows 会按规范分别填入“地址”和“端口”两个框。 + let proxyAddr + if (proxyHttp) { + proxyAddr = `http=${ip}:${port - 1};https=${ip}:${port}` + } else { + proxyAddr = `${ip}:${port}` } // 读取排除域名 const excludeIpStr = getProxyExcludeIpStr(';') // 设置代理,同时设置排除域名 try { - require('@starknt/sysproxy').triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true) + if (proxyHttp) { + sysproxy.triggerManualProxyByUrl(true, proxyAddr, excludeIpStr, true) + } else { + sysproxy.triggerManualProxy(true, ip, port, excludeIpStr) + } log.info(`设置windows系统代理成功: ${proxyAddr} ......(省略排除IP列表)`) } catch (e1) { log.warn('设置windows系统代理失败:执行 `@starknt/sysproxy` 失败,现尝试通过执行 `sysproxy.exe global ...` 来设置系统代理!\r\n捕获的异常:', e1) @@ -222,19 +566,26 @@ const executor = { if (setEnv) { // 设置全局代理所需的环境变量 try { - await exec(`echo '设置环境变量 HTTPS_PROXY${config.get().proxy.proxyHttp ? '、HTTP_PROXY' : ''}'`) - - log.info(`开启系统代理的同时设置环境变量:HTTPS_PROXY = "http://${ip}:${port}/"`) - await exec(`setx HTTPS_PROXY "http://${ip}:${port}/"`) + const envList = [] + const httpsProxy = `http://${ip}:${port}/` + log.info(`开启系统代理的同时设置环境变量:HTTPS_PROXY = "${httpsProxy}"`) + envList.push({ key: 'HTTPS_PROXY', value: httpsProxy }) if (config.get().proxy.proxyHttp) { - log.info(`开启系统代理的同时设置环境变量:HTTP_PROXY = "http://${ip}:${port - 1}/"`) - await exec(`setx HTTP_PROXY "http://${ip}:${port - 1}/"`) + const httpProxy = `http://${ip}:${port - 1}/` + log.info(`开启系统代理的同时设置环境变量:HTTP_PROXY = "${httpProxy}"`) + envList.push({ key: 'HTTP_PROXY', value: httpProxy }) } - // await addClearScriptIni() + if (setCaBundle) { + const caCertPath = config.get().server.setting.rootCaFile.certPath + log.info(`开启系统代理的同时设置环境变量:REQUEST_CA_BUNDLE = "${caCertPath}"`) + envList.push({ key: 'REQUEST_CA_BUNDLE', value: caCertPath }) + } + + await setWindowsEnvVariables(exec, envList) } catch (e) { - log.error('设置环境变量 HTTPS_PROXY、HTTP_PROXY 失败:', e) + log.error('设置环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE 失败:', e) } } @@ -258,39 +609,26 @@ const executor = { } try { - await exec('echo \'删除环境变量 HTTPS_PROXY、HTTP_PROXY\'') - const regKey = new Registry({ // new operator is optional - hive: Registry.HKCU, // open registry hive HKEY_CURRENT_USER - key: '\\Environment', // key containing autostart programs - }) - regKey.get('HTTPS_PROXY', (err) => { - if (!err) { - regKey.remove('HTTPS_PROXY', async (err) => { - log.warn('删除环境变量 HTTPS_PROXY 失败:', err) - await exec('setx DS_REFRESH "1"') - }) - } - }) - regKey.get('HTTP_PROXY', (err) => { - if (!err) { - regKey.remove('HTTP_PROXY', async (err) => { - log.warn('删除环境变量 HTTP_PROXY 失败:', err) - }) - } - }) + await removeWindowsEnvVariables(exec, ['HTTPS_PROXY', 'HTTP_PROXY', 'REQUEST_CA_BUNDLE']) } catch (e) { - log.error('删除环境变量 HTTPS_PROXY、HTTP_PROXY 失败:', e) + log.error('删除环境变量 HTTPS_PROXY、HTTP_PROXY、REQUEST_CA_BUNDLE 失败:', e) } return true } }, async linux (exec, params = {}) { - const { ip, port } = params + const { ip, port, setEnv } = params if (ip != null) { // 设置代理 // 延迟加载config loadConfig() + // 设置环境变量(独立于 gsettings,即使 gsettings 失败也设置) + if (setEnv) { + writeProxyEnvFile(ip, port, config.get().proxy.proxyHttp) + addProxyEnvToShellProfile() + } + // https const setProxyCmd = [ 'gsettings set org.gnome.system.proxy mode manual', @@ -310,60 +648,85 @@ const executor = { const excludeIpStr = getProxyExcludeIpStr('\', \'') setProxyCmd.push(`gsettings set org.gnome.system.proxy ignore-hosts "['${excludeIpStr}']"`) - await exec(setProxyCmd) + try { + await exec(setProxyCmd) + } catch (e) { + log.warn('gsettings 设置系统代理失败(可能无桌面环境),环境变量已设置') + } } else { // 关闭代理 - const setProxyCmd = [ - 'gsettings set org.gnome.system.proxy mode none', - ] - await exec(setProxyCmd) + if (setEnv) { + removeProxyEnvFromShellProfile() + } + + try { + await exec(['gsettings set org.gnome.system.proxy mode none']) + } catch (e) { + log.warn('gsettings 关闭系统代理失败(可能无桌面环境)') + } } }, async mac (exec, params = {}) { - // exec = _exec - let wifiAdaptor = await exec('sh -c "networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep \'interface\' | cut -d \':\' -f2` -B 1 | head -n 1 "') - wifiAdaptor = wifiAdaptor.trim() - wifiAdaptor = wifiAdaptor.substring(wifiAdaptor.indexOf(' ')).trim() - const { ip, port } = params + const wifiAdaptor = await getMacNetworkService(exec) + const { ip, port, setEnv } = params + + let cmds if (ip != null) { // 设置代理 // 延迟加载config loadConfig() // https - await exec(`networksetup -setsecurewebproxy "${wifiAdaptor}" ${ip} ${port}`) + cmds = [`networksetup -setsecurewebproxy "${wifiAdaptor}" ${ip} ${port}`] // http if (config.get().proxy.proxyHttp) { - await exec(`networksetup -setwebproxy "${wifiAdaptor}" ${ip} ${port - 1}`) + cmds.push(`networksetup -setwebproxy "${wifiAdaptor}" ${ip} ${port - 1}`) } else { - await exec(`networksetup -setwebproxystate "${wifiAdaptor}" off`) + cmds.push(`networksetup -setwebproxystate "${wifiAdaptor}" off`) } // 设置排除域名 const excludeIpStr = getProxyExcludeIpStr('" "') - await exec(`networksetup -setproxybypassdomains "${wifiAdaptor}" "${excludeIpStr}"`) - - // const setEnv = `cat <> ~/.zshrc - // export http_proxy="http://${ip}:${port}" - // export https_proxy="http://${ip}:${port}" - // ENDOF - // source ~/.zshrc - // ` - // await exec(setEnv) + cmds.push(`networksetup -setproxybypassdomains "${wifiAdaptor}" "${excludeIpStr}"`) } else { // 关闭代理 - // https - await exec(`networksetup -setsecurewebproxystate "${wifiAdaptor}" off`) - // http - await exec(`networksetup -setwebproxystate "${wifiAdaptor}" off`) - - // const removeEnv = ` - // sed -ie '/export http_proxy/d' ~/.zshrc - // sed -ie '/export https_proxy/d' ~/.zshrc - // source ~/.zshrc - // ` - // await exec(removeEnv) + // https + http + cmds = [ + `networksetup -setsecurewebproxystate "${wifiAdaptor}" off`, + `networksetup -setwebproxystate "${wifiAdaptor}" off`, + ] + } + + // 先尝试直接执行;若因权限不足(exit code 14)失败,弹出系统授权对话框后重试 + try { + for (const cmd of cmds) { + await exec(cmd) + } + } catch (e) { + if (e.code === MACOS_NETWORKSETUP_PERMISSION_ERROR_CODE) { + log.warn('networksetup 命令需要管理员权限(exit code 14),正在弹出系统授权对话框...') + await sudoExecMac(cmds.join(' && ')) + log.info('以管理员权限执行 networksetup 命令成功') + } else { + throw e + } + } + + // 设置环境变量 + if (setEnv) { + if (ip != null) { + loadConfig() + writeProxyEnvFile(ip, port, config.get().proxy.proxyHttp) + addProxyEnvToShellProfile() + } else { + removeProxyEnvFromShellProfile() + } } }, } -module.exports = async function (args) { +const setSystemProxy = async function (args) { return execute(executor, args) } + +module.exports = setSystemProxy +module.exports.parseMacNetworkServiceByDevice = parseMacNetworkServiceByDevice +module.exports.parseMacRouteDevice = parseMacRouteDevice +module.exports.pickMacNetworkService = pickMacNetworkService diff --git a/packages/core/src/shell/scripts/setup-ca.js b/packages/core/src/shell/scripts/setup-ca.js index fba07fdcb8..eccdcf2e89 100644 --- a/packages/core/src/shell/scripts/setup-ca.js +++ b/packages/core/src/shell/scripts/setup-ca.js @@ -1,19 +1,38 @@ +const fs = require('node:fs') const Shell = require('../shell') const execute = Shell.execute const executor = { async windows (exec, { certPath }) { + if (!certPath) { + throw new Error('证书路径为空,无法安装根证书。请确认证书文件已生成。') + } + if (!fs.existsSync(certPath)) { + throw new Error(`证书文件不存在: ${certPath}`) + } const cmds = [`start "" "${certPath}"`] await exec(cmds, { type: 'cmd' }) return true }, async linux (exec, { certPath }) { + if (!certPath) { + throw new Error('证书路径为空,无法安装根证书。请确认证书文件已生成。') + } + if (!fs.existsSync(certPath)) { + throw new Error(`证书文件不存在: ${certPath}`) + } const cmds = [`sudo cp ${certPath} /usr/local/share/ca-certificates`, 'sudo update-ca-certificates '] await exec(cmds) return true }, async mac (exec, { certPath }) { + if (!certPath) { + throw new Error('证书路径为空,无法安装根证书。请确认证书文件已生成。') + } + if (!fs.existsSync(certPath)) { + throw new Error(`证书文件不存在: ${certPath}`) + } const cmds = [`open "${certPath}"`] await exec(cmds, { type: 'cmd' }) return true diff --git a/packages/core/src/shell/shell.js b/packages/core/src/shell/shell.js index 1dd40bd5eb..c39b78a071 100644 --- a/packages/core/src/shell/shell.js +++ b/packages/core/src/shell/shell.js @@ -59,16 +59,61 @@ class WindowsSystemShell extends SystemShell { ps.dispose() } } else { - let compose = 'chcp 65001' // 'chcp 65001 ' + await childExecCmdWindows('chcp 65001', args) + let ret for (const cmd of cmds) { - compose += ` && ${cmd}` + ret = await childExecCmdWindows(cmd, args) } - // compose += '&& exit' - return await childExec(compose, args) + return ret } } } +function childExecCmdWindows (cmd, options = {}) { + return new Promise((resolve, reject) => { + const execOptions = { ...options, encoding: 'buffer' } + delete execOptions.type + delete execOptions.printErrorLog + + log.info('shell:', cmd) + childProcess.execFile('cmd.exe', ['/d', '/s', '/c', cmd], execOptions, (error, stdout, stderr) => { + // 解码输出:CMD 在 chcp 65001 后通常输出 UTF-8, + // 但内置错误消息可能仍是系统编码(中文 Windows 为 GBK) + const stdoutStr = _decodeBuffer(stdout) + if (error) { + const stderrStr = _decodeBuffer(stderr) + if (options.printErrorLog !== false) { + log.error('cmd 命令执行错误:\n===>\ncommands:', cmd, '\n error:', error, '\n stderr:', stderrStr, '\n<===') + } + reject(new Error(stderrStr || error.message)) + } else { + resolve(stdoutStr.replace('Active code page: 65001\r\n', '')) + } + }) + }) +} + +/** + * 解码 Buffer:先尝试 UTF-8,如果包含乱码则尝试 GBK(中文 Windows 控制台编码) + */ +function _decodeBuffer (buf) { + if (!buf || buf.length === 0) { + return '' + } + const utf8 = buf.toString('utf8') + // 如果 UTF-8 解码结果包含替换字符(U+FFFD),说明原始数据不是 UTF-8 + if (utf8.includes('�')) { + try { + // 尝试 GBK 解码(Windows 中文系统控制台默认编码) + return new TextDecoder('gbk', { fatal: true }).decode(buf) + } catch { + // GBK 解码失败,回退到 latin1 保留原始字节 + return buf.toString('latin1') + } + } + return utf8 +} + function childExec (composeCmds, options = {}) { return new Promise((resolve, reject) => { log.info('shell:', composeCmds) @@ -77,7 +122,9 @@ function childExec (composeCmds, options = {}) { if (options.printErrorLog !== false) { log.error('cmd 命令执行错误:\n===>\ncommands:', composeCmds, '\n error:', error, '\n<===') } - reject(new Error(stderr)) + const err = new Error(`${stderr || error.message} (command: ${composeCmds})`) + err.code = error.code + reject(err) } else { // log.info('cmd 命令完成:', stdout) resolve(stdout.replace('Active code page: 65001\r\n', '')) diff --git a/packages/core/src/utils/util.log-or-console.js b/packages/core/src/utils/util.log-or-console.js index 9195a83443..3fb7afcc51 100644 --- a/packages/core/src/utils/util.log-or-console.js +++ b/packages/core/src/utils/util.log-or-console.js @@ -1,9 +1,18 @@ const dateUtil = require('./util.date') -let log = console +// DEV_SIDECAR_LOG_DISABLED=true 时完全静默:不输出控制台,也不备份历史日志 +const disabled = process.env.DEV_SIDECAR_LOG_DISABLED === 'true' + +// CLI 命令(如 status)会 require core 但不会启动,若默认输出到 console 会污染命令结果。 +// 当 DEV_SIDECAR_LOG_TO_CONSOLE=false 时保持静默,但仍备份,待 setLogger 后回放进日志文件 +const silent = disabled || process.env.DEV_SIDECAR_LOG_TO_CONSOLE === 'false' + +let log = silent + ? { debug () {}, info () {}, warn () {}, error () {} } + : console // 将console中的日志缓存起来,当setLogger时,将控制台的日志写入日志文件 -let backupLogs = [] +let backupLogs = disabled ? null : [] function backup (fun, args) { if (backupLogs === null) { @@ -42,9 +51,12 @@ function printBackups () { } function _doLog (fun, args) { - if (log === console) { - log[fun](...[`[${fun.toUpperCase()}]`, ...args]) - backup(fun, args) // 控制台日志备份起来 + if (log === console || silent) { + // console 模式:带前缀输出并备份;静默模式:只备份不输出,setLogger 后回放 + if (log === console) { + log[fun](...[`[${fun.toUpperCase()}]`, ...args]) + } + backup(fun, args) } else { log[fun](...args) } @@ -52,6 +64,10 @@ function _doLog (fun, args) { module.exports = { setLogger (logger) { + if (disabled) { + return + } + if (logger == null) { log.error('logger 不能为空') return diff --git a/packages/core/src/utils/util.logger.js b/packages/core/src/utils/util.logger.js index a953895d5e..9f33fa3526 100644 --- a/packages/core/src/utils/util.logger.js +++ b/packages/core/src/utils/util.logger.js @@ -7,6 +7,19 @@ const configFromFiles = defaultConfig.configFromFiles // 日志级别 const level = process.env.NODE_ENV === 'development' ? 'debug' : 'info' +// 是否完全禁用日志:不输出 stdout,也不写日志文件 +const logDisabled = process.env.DEV_SIDECAR_LOG_DISABLED === 'true' + +// 是否同时输出到 stdout。默认开启(GUI/开发调试需要),CLI 守护进程通过环境变量关闭 +const logToConsole = !logDisabled && process.env.DEV_SIDECAR_LOG_TO_CONSOLE !== 'false' + +function createNoopLogger () { + const noop = () => {} + return { debug: noop, info: noop, warn: noop, error: noop, level: 'off', category: 'noop' } +} + +const noopLogger = logDisabled ? createNoopLogger() : null + function getDefaultConfigBasePath () { if (configFromFiles.app.logFileSavePath) { let logFileSavePath = configFromFiles.app.logFileSavePath @@ -37,23 +50,26 @@ let log = null // 设置一组日志配置 function log4jsConfigure (categories) { + if (logDisabled) { + return + } + if (log != null) { log.error('当前进程已经设置过日志配置,无法再设置更多日志配置:', categories) return } const config = { - appenders: { - std: { type: 'stdout' }, - }, + appenders: logToConsole ? { std: { type: 'stdout' } } : {}, categories: { - default: { appenders: ['std'], level }, + // default 分类至少需要挂一个 appender(log4js 校验要求),没有 std 时复用第一个文件 appender + default: { appenders: logToConsole ? ['std'] : [categories[0]], level }, }, } for (const category of categories) { config.appenders[category] = { ...appenderConfig, filename: path.join(basePath, `/${category}.log`) } - config.categories[category] = { appenders: [category, 'std'], level } + config.categories[category] = { appenders: logToConsole ? [category, 'std'] : [category], level } } log4js.configure(config) @@ -67,6 +83,10 @@ function log4jsConfigure (categories) { module.exports = { getLogger (category) { + if (logDisabled) { + return noopLogger + } + if (!category) { if (log) { log.error('未指定日志类型,无法配置并获取日志对象!!!') diff --git a/packages/core/src/utils/util.version.js b/packages/core/src/utils/util.version.js index 12a0b8fd12..c084e64b17 100644 --- a/packages/core/src/utils/util.version.js +++ b/packages/core/src/utils/util.version.js @@ -1,9 +1,19 @@ function parseVersion (version) { - const matched = version.match(/^v?(\d{1,2}(?:\.\d{1,2})*)(.*)$/) - return { + const matched = version.match(/^v?(\d{1,2}(?:\.\d{1,2})*)[.-]?(.*)$/) + if (!matched) { + throw new Error(`Invalid version string: ${version}`) + } + const versionInfo = { versions: matched[1].split('.'), // 版本号数组 pre: matched[2], // 预发布版本号 } + + // 将 versions 中的数字字符串转为数字 + for (let i = 0; i < versionInfo.versions.length; i++) { + versionInfo.versions[i] = Number.parseInt(versionInfo.versions[i]) + } + + return versionInfo } /** @@ -14,7 +24,7 @@ function parseVersion (version) { * @param log 日志对象 * @returns {number} 比较线上版本号是否为更新的版本,大于0=是|0=相等|小于0=否|-999=出现异常,比较结果未知 */ -export function isNewVersion (onlineVersion, currentVersion, log = console) { +function isNewVersion (onlineVersion, currentVersion, log = null) { if (onlineVersion === currentVersion) { return 0 } @@ -30,11 +40,11 @@ export function isNewVersion (onlineVersion, currentVersion, log = console) { // 短的数组补0 if (versions1.length < versions2.length) { for (let i = versions1.length; i < versions2.length; i++) { - versions1.push('0') + versions1.push(0) } } else if (versions1.length > versions2.length) { for (let i = versions2.length; i < versions1.length; i++) { - versions2.push('0') + versions2.push(0) } } } @@ -70,3 +80,5 @@ export function isNewVersion (onlineVersion, currentVersion, log = console) { return -999 // 比对异常 } } + +module.exports = { isNewVersion } diff --git a/packages/core/test/instanceTest.js b/packages/core/test/instanceTest.js new file mode 100644 index 0000000000..cfd82d9544 --- /dev/null +++ b/packages/core/test/instanceTest.js @@ -0,0 +1,168 @@ +const { assert } = require('chai') +const fs = require('node:fs') +const path = require('node:path') +const os = require('node:os') + +const instance = require('../src/modules/instance') +const event = require('../src/event') + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + +describe('instance', () => { + let tmpDir + let oldHome + let oldUserProfile + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ds-core-instance-')) + // getUserBasePath 优先读 USERPROFILE(Windows),其次 HOME(Linux/macOS) + oldHome = process.env.HOME + oldUserProfile = process.env.USERPROFILE + process.env.HOME = tmpDir + process.env.USERPROFILE = tmpDir + fs.mkdirSync(path.join(tmpDir, '.dev-sidecar'), { recursive: true }) + }) + + afterEach(() => { + if (oldHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = oldHome + } + if (oldUserProfile === undefined) { + delete process.env.USERPROFILE + } else { + process.env.USERPROFILE = oldUserProfile + } + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + describe('lock', () => { + it('should acquire and release the lock', async () => { + assert.isFalse(await instance.isLocked()) + const release = await instance.acquireLock() + assert.isTrue(await instance.isLocked()) + await release() + assert.isFalse(await instance.isLocked()) + }) + + it('should reject second acquire while lock is held', async () => { + const release = await instance.acquireLock() + try { + await instance.acquireLock() + assert.fail('second acquire should fail') + } catch (e) { + assert.strictEqual(e.code, 'ELOCKED') + } + await release() + }) + + it('should take over a stale lock left by a crashed process', async () => { + const lockPath = instance.getLockPath() + fs.mkdirSync(lockPath, { recursive: true }) + const past = new Date(Date.now() - 30000) + fs.utimesSync(lockPath, past, past) + assert.isFalse(await instance.isLocked()) + const release = await instance.acquireLock() + assert.isTrue(await instance.isLocked()) + await release() + }) + + it('should not take over a fresh lock', async () => { + const lockPath = instance.getLockPath() + fs.mkdirSync(lockPath, { recursive: true }) + assert.isTrue(await instance.isLocked()) + try { + await instance.acquireLock() + assert.fail('should not take over fresh lock') + } catch (e) { + assert.strictEqual(e.code, 'ELOCKED') + } + }) + }) + + describe('instance info', () => { + it('should write and read instance info', () => { + const payload = { type: 'cli', pid: 123, command: 'node --daemon', startTime: '2026-01-01T00:00:00.000Z' } + instance.writeInstance(payload) + assert.deepEqual(instance.readInstance(), payload) + }) + + it('should return null when running.json is missing', () => { + assert.isNull(instance.readInstance()) + }) + + it('should preserve app.instance when updateStatus writes', async () => { + instance.writeInstance({ type: 'cli', pid: 123 }) + instance.updateStatus('server.enabled', true) + await sleep(400) + const data = JSON.parse(fs.readFileSync(instance.getRunningJsonPath(), 'utf-8')) + assert.deepEqual(data.app.instance, { type: 'cli', pid: 123 }) + assert.isTrue(data.app.status.server.enabled) + }) + }) + + describe('updateStatus', () => { + it('should debounce multiple updates into one write', async () => { + instance.updateStatus('server.enabled', true) + instance.updateStatus('proxy.enabled', true) + instance.updateStatus('plugin.git.enabled', true) + const filePath = instance.getRunningJsonPath() + assert.isFalse(fs.existsSync(filePath), 'should not write before debounce flush') + await sleep(400) + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + assert.isTrue(data.app.status.server.enabled) + assert.isTrue(data.app.status.proxy.enabled) + assert.isTrue(data.app.status.plugin.git.enabled) + }) + + it('should set nested keys via dot path', async () => { + instance.updateStatus('plugin.node.enabled', true) + await sleep(400) + const data = JSON.parse(fs.readFileSync(instance.getRunningJsonPath(), 'utf-8')) + assert.isTrue(data.app.status.plugin.node.enabled) + }) + + it('should ignore invalid keys', async () => { + instance.updateStatus('', true) + instance.updateStatus(null, true) + await sleep(400) + const filePath = instance.getRunningJsonPath() + if (fs.existsSync(filePath)) { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + assert.deepEqual(data.app.status, {}) + } + }) + }) + + describe('watchStatusEvents', () => { + it('should sync *.enabled events to running.json', async () => { + const release = await instance.acquireLock() + try { + event.fire('status', { key: 'server.enabled', value: true }) + event.fire('status', { key: 'plugin.git.enabled', value: true }) + await sleep(400) + const data = JSON.parse(fs.readFileSync(instance.getRunningJsonPath(), 'utf-8')) + assert.isTrue(data.app.status.server.enabled) + assert.isTrue(data.app.status.plugin.git.enabled) + } finally { + await release() + } + }) + + it('should filter non-enabled events (e.g. free_eye.result)', async () => { + const release = await instance.acquireLock() + try { + event.fire('status', { key: 'server.enabled', value: true }) + await sleep(400) + event.fire('status', { key: 'plugin.free_eye.result', value: { big: 'x'.repeat(10000) } }) + await sleep(400) + const data = JSON.parse(fs.readFileSync(instance.getRunningJsonPath(), 'utf-8')) + assert.isTrue(data.app.status.server.enabled) + assert.isUndefined(data.app.status.plugin) + } finally { + await release() + } + }) + }) +}) diff --git a/packages/core/test/requestTest.js b/packages/core/test/requestTest.js index 9ae018f51c..07a077ce59 100644 --- a/packages/core/test/requestTest.js +++ b/packages/core/test/requestTest.js @@ -3,7 +3,7 @@ const request = require('request') const options = { url: 'https://raw.githubusercontent.com/docmirror/dev-sidecar/refs/heads/master/packages/core/src/config/remote_config.json5', - // url: 'https://gitee.com/wangliang181230/dev-sidecar/raw/docmirror2.x/packages/core/src/config/remote_config.json', + // url: 'https://raw.giteeusercontent.com/wangliang181230/dev-sidecar-config/raw/main/remote_config.json', servername: 'baidu.com', agent: new HttpsAgent({ keepAlive: true, diff --git a/packages/core/test/setSystemProxyMacTest.js b/packages/core/test/setSystemProxyMacTest.js new file mode 100644 index 0000000000..a7abbe3c54 --- /dev/null +++ b/packages/core/test/setSystemProxyMacTest.js @@ -0,0 +1,79 @@ +const assert = require('node:assert') +const setSystemProxy = require('../src/shell/scripts/set-system-proxy') + +// eslint-disable-next-line no-undef +describe('set-system-proxy mac helpers', () => { + // eslint-disable-next-line no-undef + it('should parse service by device from listnetworkserviceorder output', () => { + const networkServiceOrder = ` +(1) Wi-Fi +(Hardware Port: Wi-Fi, Device: en0) +(2) Thunderbolt Bridge +(Hardware Port: Thunderbolt Bridge, Device: bridge0) +`.trim() + const service = setSystemProxy.parseMacNetworkServiceByDevice(networkServiceOrder, 'en0') + assert.strictEqual(service, 'Wi-Fi') + assert.strictEqual(setSystemProxy.parseMacNetworkServiceByDevice('', 'en0'), null) + assert.strictEqual(setSystemProxy.parseMacNetworkServiceByDevice(networkServiceOrder, ''), null) + }) + + // eslint-disable-next-line no-undef + it('should parse route device from route output', () => { + const routeOutput = ` +route to: default +interface: en0 +flags: +`.trim() + const device = setSystemProxy.parseMacRouteDevice(routeOutput) + assert.strictEqual(device, 'en0') + assert.strictEqual(setSystemProxy.parseMacRouteDevice(''), null) + assert.strictEqual(setSystemProxy.parseMacRouteDevice(null), null) + }) + + // eslint-disable-next-line no-undef + it('should fallback to preferred Wi-Fi service when available', () => { + const listAllNetworkServicesOutput = ` +USB 10/100/1000 LAN +Wi-Fi +Thunderbolt Bridge +`.trim() + const service = setSystemProxy.pickMacNetworkService(listAllNetworkServicesOutput) + assert.strictEqual(service, 'Wi-Fi') + }) + + // eslint-disable-next-line no-undef + it('should fallback to first service when preferred service is unavailable', () => { + const listAllNetworkServicesOutput = ` +USB 10/100/1000 LAN +Thunderbolt Bridge +`.trim() + const service = setSystemProxy.pickMacNetworkService(listAllNetworkServicesOutput) + assert.strictEqual(service, 'USB 10/100/1000 LAN') + }) + + // eslint-disable-next-line no-undef + it('should support disabled service prefix and empty input', () => { + const listAllNetworkServicesOutput = ` +*Wi-Fi +Thunderbolt Bridge +`.trim() + const service = setSystemProxy.pickMacNetworkService(listAllNetworkServicesOutput) + assert.strictEqual(service, 'Wi-Fi') + assert.strictEqual(setSystemProxy.pickMacNetworkService(''), null) + assert.strictEqual(setSystemProxy.pickMacNetworkService(null), null) + }) + + // eslint-disable-next-line no-undef + it('should ignore the "An asterisk" header line produced by networksetup -listallnetworkservices', () => { + const fullOutput = `An asterisk (*) denotes that a network service is disabled. +Ethernet +Wi-Fi +Thunderbolt Bridge` + assert.strictEqual(setSystemProxy.pickMacNetworkService(fullOutput), 'Wi-Fi') + + const fullOutputEthernetOnly = `An asterisk (*) denotes that a network service is disabled. +Ethernet +Thunderbolt Bridge` + assert.strictEqual(setSystemProxy.pickMacNetworkService(fullOutputEthernetOnly), 'Ethernet') + }) +}) diff --git a/packages/core/test/urlTest.js b/packages/core/test/urlTest.js new file mode 100644 index 0000000000..948776d752 --- /dev/null +++ b/packages/core/test/urlTest.js @@ -0,0 +1,12 @@ +// const URL = require('node:url') +// +// const url = 'https://github.com:8080/aaa?x=1#s=2' +// +// const urlObj = new URL.URL(url) +// console.log('new URL.URL(url) -> ', urlObj) +// +// // eslint-disable-next-line node/no-deprecated-api +// const urlObj2 = URL.parse(url) +// console.log('\nURL.parse(url) -> ', urlObj2) +// +// console.log('\n`urlObj.pathname + urlObj.search === urlObj2.path` =', urlObj.pathname + urlObj.search === urlObj2.path) diff --git a/packages/core/test/versionTest.js b/packages/core/test/versionTest.js index 6af2486781..4dbd5494c2 100644 --- a/packages/core/test/versionTest.js +++ b/packages/core/test/versionTest.js @@ -16,9 +16,13 @@ testIsNewVersion('2.1.0', '2.0.0', 2) testIsNewVersion('2.0.0', '2.1.0', -2) testIsNewVersion('2.0.1', '2.0.0', 3) +testIsNewVersion('2.0.10', '2.0.2', 3) +testIsNewVersion('2.0.10.1', '2.0.2.2', 3) +testIsNewVersion('2.0.10-RC1', '2.0.2-RC2', 3) testIsNewVersion('2.0.0', '2.0.1', -3) testIsNewVersion('2.0.0.1', '2.0.0', 4) +testIsNewVersion('2.0.0.1', '2.0.0.X', 4) testIsNewVersion('2.0.0', '2.0.0.1', -4) testIsNewVersion('2.0.0.9.1', '2.0.0.9', 5) @@ -33,3 +37,5 @@ testIsNewVersion('2.0.0-RC1', '2.0.0', -102) testIsNewVersion('2.0.0.0', '2.0.0', 0) testIsNewVersion('x', 'v', -999) + +console.log('版本测试通过') diff --git a/packages/gui/README.md b/packages/gui/README.md index 09c5c06302..6d550671a7 100644 --- a/packages/gui/README.md +++ b/packages/gui/README.md @@ -2,27 +2,7 @@ ## Project setup -``` -yarn install -``` - -### Compiles and hot-reloads for development - -``` -yarn serve -``` - -### Compiles and minifies for production - -``` -yarn build -``` - -### Lints and fixes files - -``` -yarn lint -``` +You should always refer to the [main README](../../README.md) for instructions on how to set up the project. ### Customize configuration diff --git a/packages/gui/babel.config.js b/packages/gui/babel.config.cjs similarity index 100% rename from packages/gui/babel.config.js rename to packages/gui/babel.config.cjs diff --git a/packages/gui/build/mac/1024x1024.png b/packages/gui/build/mac/1024x1024.png deleted file mode 100644 index 7000645eee..0000000000 Binary files a/packages/gui/build/mac/1024x1024.png and /dev/null differ diff --git a/packages/gui/electron-builder.config.cjs b/packages/gui/electron-builder.config.cjs new file mode 100644 index 0000000000..f68200d58d --- /dev/null +++ b/packages/gui/electron-builder.config.cjs @@ -0,0 +1,113 @@ +const publishUrl = process.env.VUE_APP_PUBLISH_URL +const publishProvider = process.env.VUE_APP_PUBLISH_PROVIDER + +// 本地开发自动检测当前平台和架构,CI 构建全部架构 +const isCI = !!process.env.CI +const localArch = process.arch === 'ia32' ? 'ia32' : process.arch === 'arm64' ? 'arm64' : 'x64' + +/** @type {import('electron-builder').Configuration} */ +module.exports = { + appId: 'dev-sidecar', + productName: 'dev-sidecar', + artifactName: 'DevSidecar-${version}-${arch}.${ext}', + copyright: 'Copyright © 2020-' + new Date().getFullYear() + ' Greper, WangLiang, CuteOmega', + directories: { + output: 'dist_electron', + buildResources: 'build', + }, + asar: { + smartUnpack: true, + }, + asarUnpack: [ + 'src/bridge/mitmproxy.js', + 'dist/icon.png', + ], + files: [ + { + from: 'dist', + to: 'dist', + filter: [ + '**/*', + '!win-*/**/*', + '!mac-*/**/*', + '!linux-*/**/*', + '!*.zip', + '!*.dmg', + '!*.blockmap', + '!*.exe', + '!*.AppImage', + '!*.deb', + '!*.rpm', + '!*.tar.gz', + '!*.flatpak', + '!builder-*.yml', + '!builder-*.yaml', + ], + }, + 'src/**/*', + 'package.json', + // extra/ 在 extraResources 中已复制,此处不需要再打包进 asar + ], + extraResources: [ + { + from: 'extra', + to: 'extra', + }, + ], + afterPack: './pkg/after-pack.cjs', + afterAllArtifactBuild: './pkg/after-all-artifact-build.cjs', + nsis: { + oneClick: false, + perMachine: true, + allowElevation: true, + allowToChangeInstallationDirectory: true, + }, + win: { + icon: 'build/icons/icon.ico', + // 必须为 true 才会用 rcedit 写入 exe 图标和版本信息; + // 未配置证书时 electron-builder 会自动跳过签名,不会失败。 + signAndEditExecutable: true, + target: isCI + ? [ + { target: 'nsis', arch: ['x64'] }, + { target: 'nsis', arch: ['ia32'] }, + { target: 'nsis', arch: ['arm64'] }, + ] + : [ + { target: 'nsis', arch: [localArch] }, + ], + }, + linux: { + icon: 'build/mac/', + target: isCI + ? [ + { target: 'deb', arch: ['x64', 'arm64', 'armv7l'] }, + { target: 'AppImage', arch: ['x64', 'arm64', 'armv7l'] }, + { target: 'tar.gz', arch: ['x64', 'arm64', 'armv7l'] }, + { target: 'rpm', arch: ['x64', 'arm64', 'armv7l'] }, + { target: 'flatpak', arch: ['x64'] }, + ] + : [ + { target: 'deb', arch: [localArch] }, + { target: 'AppImage', arch: [localArch] }, + ], + appId: 'cn.docmirror.DevSidecar', + category: 'System', + }, + mac: { + icon: './build/mac/icon.icns', + target: isCI + ? [ + { target: 'dmg', arch: ['x64', 'arm64'] }, + { target: 'zip', arch: ['x64', 'arm64'] }, + ] + : { target: 'dmg', arch: [localArch] }, + category: 'public.app-category.developer-tools', + }, + publish: publishProvider + ? { + provider: publishProvider, + url: publishUrl, + } + : undefined, +} diff --git a/packages/gui/extra/icons/512x512-2.png b/packages/gui/extra/icons/512x512-2.png new file mode 100644 index 0000000000..0702bbd3ed Binary files /dev/null and b/packages/gui/extra/icons/512x512-2.png differ diff --git a/packages/gui/extra/pac/pac.txt b/packages/gui/extra/pac/pac.txt index 6d841cb8ec..f1bbe1fb43 100644 --- a/packages/gui/extra/pac/pac.txt +++ b/packages/gui/extra/pac/pac.txt @@ -1,13 +1,12 @@ [AutoProxy 0.2.9] -! Checksum: BZUefB22itmhAjqqdpvRkA +! Checksum: mXjV7BDZyJY5tDH1GYLiIA ! Expires: 6h ! Title: GFWList4LL ! GFWList with EVERYTHING included -! Last Modified: Sun, 12 Jan 2025 11:56:36 -0500 +! Last Modified: Thu, 09 Apr 2026 11:10:05 +0000 ! ! HomePage: https://github.com/gfwlist/gfwlist ! License: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -! ! GFWList is unlikely to fully comprise the real ! rules being deployed inside GFW system. We try ! our best to keep the list up to date. Please @@ -16,23 +15,34 @@ ! https://github.com/gfwlist/gfwlist/issues/. !---------403/451/503/520 & URL Redirects--------- +||blogjav.net +||zoominfo.com +||ptwxz.com +||miuipolska.pl +||piaotia.com +||wunderground.com +||500px.com +||500px.org !--ehentai |http://85.17.73.31/ -!--||adorama.com ||afreecatv.com ||agnesb.fr +||airitilibrary.com +||abematv.akamaized.net +||linear-abematv.akamaized.net +||vod-abematv.akamaized.net ||akiba-web.com ||altrec.com +||amazonvideo.com ||angela-merkel.de ||angola.org ||anthropic.com ||apartmentratings.com ||apartments.com ||arena.taipei -||asianspiss.com +||assets.bwbx.io ||assimp.org ||athenaeizou.com -||azubu.tv ||bankmobilevibe.com ||banorte.com ||beeg.com @@ -42,16 +52,14 @@ ||bynet.co.il ||byrut.org ||carfax.com -.casinobellini.com ||casinobellini.com ||centauro.com.br ||chobit.cc ||ciciai.com +||cici.com ||claude.ai ||clearsurance.com ||cnbeta.com.tw -||images.comico.tw -||static.comico.tw ||counter.social ||costco.com ||coze.com @@ -62,7 +70,6 @@ ||dawangidc.com ||deezer.com ||desipro.de -||dingchin.com.tw ||discord.com ||discord.gg ||discordapp.com @@ -70,7 +77,6 @@ ||dish.com |http://img.dlsite.jp/ ||dm530.net -share.dmhy.org ||dmhy.org ||dmm.co.jp |http://www.dmm.com/netgame @@ -79,9 +85,7 @@ share.dmhy.org ||dvdpac.com ||eesti.ee ||esurance.com -.expekt.com ||expekt.com -.extmatrix.com ||extmatrix.com ||fakku.net ||fastpic.ru @@ -94,20 +98,22 @@ share.dmhy.org ||funkyimg.com ||fxnetworks.com ||g-area.org -||gettyimages.com +||gettyimages.ca +||gettyimages.us +||gettyimages.hk +||gettyimages.in +||gettyimages.ae +||gettyimages.de +||gettyimages.it ||getuploader.com ||ghidra-sre.org -!--|https://github.com/programthink/zhao -!--|https://raw.githubusercontent.com/programthink/zhao ||glass8.eu ||glype.com ||go141.com -||guo.media ||hautelook.com ||hautelookcdn.com ||wego.here.com -||gamer-cds.cdn.hinet.net -||gamer2-cds.cdn.hinet.net +||grok.com ||hmoegirl.com ||hmvdigital.ca ||hmvdigital.com @@ -122,15 +128,9 @@ share.dmhy.org ||cdn*.i-scmp.com ||ilbe.com ||ilovelongtoes.com -|http://imgmega.com/*.gif.html -|http://imgmega.com/*.jpg.html -|http://imgmega.com/*.jpeg.html -|http://imgmega.com/*.png.html -||imlive.com -||tw.iqiyi.com +||imlive.co ||javhub.net ||javhuge.com -.javlibrary.com ||javlibrary.com ||jcpenney.com ||jims.net @@ -143,8 +143,11 @@ share.dmhy.org ||kkbox.com ||leisurepro.com ||lifemiles.com +||lih.kg ||longtoes.com ||lovetvshow.com +||lpsg.com +||lrfz.com |http://www.m-sport.co.uk ||macgamestore.com ||madonna-av.com @@ -164,14 +167,15 @@ share.dmhy.org ||moodyz.com ||moonbingo.com ||mos.ru +||addons.mozilla.org/*-*/firefox/addon/ublock-origin/* +||addons.mozilla.org/firefox/downloads/file/*/ublock_origin-*.xpi ||msha.gov +||www.msn.com ||muzu.tv ||mvg.jp -.mybet.com ||mybet.com ||mypikpak.com ||nationwide.com -|http://www.nbc.com/live ||neo-miracle.com ||netflix.com ||netflix.net @@ -184,38 +188,35 @@ share.dmhy.org |http://mo.nightlife141.com ||purpose.nike.com ||noxinfluencer.com -@@||cn.noxinfluencer.com ||nordstrom.com ||nordstromimage.com ||nordstromrack.com ||nottinghampost.com ||npsboost.com ||ntdtv.cz -||s1.nudezz.com ||nusatrip.com ||nuuvem.com +||bbs.nyinfor.com ||olehdtv.com ||omni7.jp ||onapp.com -!--We are confused as well ||ontrac.com -@@|http://blog.ontrac.com ||openai.com ||pandora.com -.pandora.tv ||parkansky.com ||phmsociety.org |http://*.pimg.tw/ ||podcast.co +||popai.pro +||primevideo.com +||proyectoclubes.com ||pure18.com -||pytorch.org ||qq.co.za ||r18.com |http://radiko.jp ||ramcity.com.au ||rateyourmusic.com ||rd.com -||rdio.com |https://riseup.net ||sadistic-v.com ||isc.sans.edu @@ -223,23 +224,19 @@ share.dmhy.org ||shiksha.com ||slacker.com ||sm-miracle.com -||softnology.biz ||soylentnews.org ||spotify.com ||spreadshirt.es ||springboardplatform.com -||sprite.org -@@|http://store.sprite.org -||superokayama.com ||superpages.com ||swagbucks.com ||switch1.jp ||tapanwap.com ||gsp.target.com ||login.target.com -!--@@||intl.target.com ||rcam.target.com ||technews.tw +||freeterabox.com ||terabox.com ||thinkgeek.com ||thebodyshop-usa.com @@ -256,9 +253,9 @@ share.dmhy.org |http://viu.tv/ch/ |http://viu.tv/encore/ ||vmpsoft.com -|http://ecsm.vs.com/ ||wanz-factory.com ||ssl.webpack.de +||weebly.com ||wheretowatch.com ||wingamestore.com ||wizcrafts.net @@ -272,31 +269,53 @@ share.dmhy.org ||zattoo.com ||zim.vn ||zozotown.com - !##############General List Start############### -!-------------------Pure IP--------------------- -14.102.250.18 -14.102.250.19 -50.7.31.230:8898 -174.142.105.153 -69.65.19.160 - +!-------------------Coin Pool------------------- +||c3pool.com +||unmineable.com +||666pool.cn +||antpool.com +||crazypool.org +||cruxpool.com +||miningpoolhub.com +||huobipool.com +||poolbinance.com +||hiveon.net +||sparkpool.com +||flypool.org +||nanopool.org +||xnpool.com +||beepool.com +||zhizhu.top +||spiderpool.com +||uupool.cn +||flexpool.io +||beepool.org +||dpool.top +||okpool.me +||binancezh.cc +||btc.com +||r-pool.net +||w-pool.com !----------------------IDN---------------------- +||xn--kcrv3utim32hx9f6qe.com +||xn--1jqvh729avzfcy2d8ummib.com +||xn--9iqy04a7fi01l.com +||xn--u2u927b.com +||xn--11xs86f.icu ||xn--4gq171p.com ||xn--czq75pvv1aj5c.org ||xn--i2ru8q2qg.com +||xn--noss43i.com ||xn--oiq.cc ||xn--p8j9a0d9c9a.xn--q9jyb4c ||xn--9pr62r24a.com - +@@/^https?:\/\/(?=.*?(2x3|ni5|j5o))[a-z0-9.-]+\.xn--ngstr-lra8j\.com$ +||xn--ngstr-lra8j.com !-----------------DNS Poisoning----------------- !---Amazon--- -!-||cdn-images.mailchimp.com +||cdn-images.mailchimp.com ||abebooks.com -|https://*.s3.amazonaws.com -||s3-ap-southeast-2.amazonaws.com - -||43110.cf ||9cache.com ||9gag.com ||agro.hk @@ -310,10 +329,8 @@ share.dmhy.org ||bitterwinter.org ||bnn.co ||businessinsider.com -||boomssr.com ||bwgyhw.com ||castbox.fm -||chinatimes.com ||clyp.it ||cmcn.org ||cmx.im @@ -323,11 +340,9 @@ share.dmhy.org ||disconnect.me ||documentingreality.com ||doubibackup.com -||doubmirror.cf ||encyclopedia.com ||fangeqiang.com ||fanqiangdang.com -||feedly.com ||feedx.net ||flyzy2005.com ||foreignpolicy.com @@ -338,7 +353,6 @@ share.dmhy.org ||globalvoices.org ||glorystar.me ||goregrish.com -||guangnianvpn.com ||hanime.tv ||hbo.com ||spaces.hightail.com @@ -367,17 +381,12 @@ share.dmhy.org ||me.me ||metart.com ||mohu.club -||mohu.ml -||motiyun.com ||msa-it.org ||goo.ne.jp -||go.nesnode.com -||international-news.newsmagazine.asia ||nikkei.com ||nitter.cc ||nitter.net ||niu.moe -||nofile.io ||now.com ||openvpn.org ||onejav.com @@ -386,6 +395,8 @@ share.dmhy.org ||picacomic.com ||pincong.rocks ||pixiv.net +||pixiv.org +||pixivsketch.net ||potato.im ||premproxy.com ||prism-break.org @@ -396,29 +407,25 @@ share.dmhy.org ||quoracdn.net ||qz.com ||cdn.seatguru.com -||secure.raxcdn.com ||redd.it +||redditspace.com ||reddit.com -.redditlist.com +||reddithelp.com |http://redditlist.com ||redditmedia.com ||redditstatic.com -!--defunct ||rixcloud.com ||rixcloud.us ||rsdlmonitor.com ||shadowsocks.be -||shadowsocks9.com ||tn1.shemalez.com ||tn2.shemalez.com ||tn3.shemalez.com ||static.shemalez.com ||six-degrees.io ||softfamous.com -||softsmirror.cf ||sosreader.com ||sspanel.net -||sulian.me ||supchina.com ||teddysun.com ||textnow.me @@ -444,14 +451,12 @@ share.dmhy.org ||wenzhao.ca ||whatsonweibo.com ||wire.com -||blog.workflow.is ||xm.com ||xuehua.us ||yes-news.com ||yigeni.com ||you-get.org ||zzcloud.me - !---Digital Currency Exchange(CRYPTO)--- ||aex.com ||allcoin.com @@ -467,7 +472,6 @@ share.dmhy.org ||bitcoinworld.com ||bitfinex.com ||bithumb.com -||bitinka.com.ar ||bitmex.com ||bnbstatic.com ||btc98.com @@ -477,12 +481,9 @@ share.dmhy.org ||c2cx.com ||chaoex.com ||cobinhood.com -||coin2co.in +||coinbase.com ||coinbene.com -.coinegg.com -||coinegg.com ||coinex.com -!--|https://www.coinexchange.io/ ||coingecko.com ||coingi.com ||coinmarketcap.com @@ -498,7 +499,6 @@ share.dmhy.org ||etherscan.io ||exmo.com ||exrates.me -||exx.com ||f2pool.com ||fatbtc.com ||ftx.com @@ -507,10 +507,11 @@ share.dmhy.org ||hbg.com ||hitbtc.com ||hotcoin.com +||htx.com ||huobi.co ||huobi.com ||huobi.me -!--||huobi.li +||huobi.li ||huobi.pro ||huobi.sc ||huobipro.com @@ -534,305 +535,74 @@ share.dmhy.org ||otcbtc.com ||paxful.com ||poolin.com -||rightbtc.com +||simpleswap.io ||solv.finance ||topbtc.com ||tronscan.org ||xbtce.com ||yobit.net ||zb.com - !----------------Frauds & Scams----------------- !!---Content Farm(fake 500 error)--- ||read01.com ||kknews.cc - -china-mmm.jp.net -.lsxszzg.com -.china-mmm.net ||china-mmm.net -china-mmm.sa.com - !---------------------Groups-------------------- -!!---Afraid FreeDNS--- -.allowed.org -.now.im - +!!---Masterdon--- +||bgme.me +||o3o.ca +||go5.dev +||me.ns.ci +||moresci.sale +||social.edu.ci +||mstdn.social +||douchi.space +||slashine.onl +||social.datalabour.com +||mastodon.online !!---Amazon--- +||payments-jp.amazon.com ||amazon.co.jp -.amazon.com/Dalai-Lama -amazon.com/Prisoner-State-Secret-Journal-Premier -s3-ap-northeast-1.amazonaws.com - +||s3-ap-*.amazonaws.com +||s3.eu-central-1.amazonaws.com +||s3-eu-central-1.amazonaws.com +||s3.us-east-1.amazonaws.com +||s3-ap-northeast-2.amazonaws.com +||s3.ap-northeast-2.amazonaws.com +||s3-ap-northeast-1.amazonaws.com +||s3-ap-southeast-1.amazonaws.com +||s3-ap-southeast-2.amazonaws.com !!---AOL--- -||aolchannels.aol.com -video.aol.ca/video-detail -video.aol.co.uk/video-detail -video.aol.com ||video.aol.com ||search.aol.com www.aolnews.com - !!---AvMoo--- -.avmo.pw -!--|http://avmo.pw -.avmoo.com -|http://avmoo.com -.avmoo.net -|http://avmoo.net +||avmo.pw ||avmoo.pw -.javmoo.xyz -|http://javmoo.xyz -.javtag.com -|http://javtag.com -.javzoo.com -|http://javzoo.com -.tellme.pw - !!---BBC--- -!--.bbc.co.uk/blogs -!--.bbc.co.uk/chinese -!--.bbc.co.uk/news/world-asia-china -!--.bbc.co.uk/tv -!--.bbc.co.uk/zhongwen -!--.bbc.com/ukchina -!--.bbc.com/zhongwen -!--.bbc.com%2Fzhongwen -!--news.bbc.co.uk/onthisday*newsid_2496000/2496277 -!--newsforums.bbc.co.uk -.bbc.com ||bbc.com -.bbc.co.uk ||bbc.co.uk ||bbci.co.uk -.bbcchinese.com ||bbcchinese.com -|http://bbc.in - !!---Bloomberg--- -.bloomberg.cn ||bloomberg.cn -.bloomberg.com ||bloomberg.com -bloomberg.de ||bloomberg.de ||bloombergview.com -.businessweek.com - -!!---ChangeIP--- -.1dumb.com -.25u.com -.2waky.com -.3-a.net -.4dq.com -.4mydomain.com -.4pu.com -.acmetoy.com -.almostmy.com -.americanunfinished.com -.authorizeddns.net -.authorizeddns.org -.authorizeddns.us -.bigmoney.biz -.changeip.name -.changeip.net -.changeip.org -.cleansite.biz -.cleansite.info -.cleansite.us -.compress.to -.ddns.info -.ddns.me.uk -.ddns.mobi -.ddns.ms -.ddns.name -.ddns.us -.dhcp.biz -.dns-dns.com -.dns-stuff.com -.dns04.com -.dns05.com -.dns1.us -.dns2.us -.dnset.com -.dnsrd.com -.dsmtp.com -.dumb1.com -.dynamic-dns.net -.dynamicdns.biz -.dynamicdns.co.uk -.dynamicdns.me.uk -.dynamicdns.org.uk -.dyndns.pro -.dynssl.com -.edns.biz -.epac.to -.esmtp.biz -.ezua.com -.faqserv.com -.fartit.com -.freeddns.com -.freetcp.com -.freewww.biz -.freewww.info -.ftp1.biz -.ftpserver.biz -.gettrials.com -.got-game.org -.gr8domain.biz -.gr8name.biz -.https443.net -.https443.org -.ikwb.com -.instanthq.com -.iownyour.biz -.iownyour.org -.isasecret.com -.itemdb.com -.itsaol.com -.jetos.com -.jkub.com -.jungleheart.com -.justdied.com -.lflink.com -.lflinkup.com -.lflinkup.net -.lflinkup.org -.longmusic.com -.mefound.com -.moneyhome.biz -.mrbasic.com -.mrbonus.com -.mrface.com -.mrslove.com -.my03.com -.mydad.info -.myddns.com -.myftp.info -.myftp.name -.mylftv.com -.mymom.info -.mynetav.net -.mynetav.org -.mynumber.org -.mypicture.info -.mypop3.net -.mypop3.org -.mysecondarydns.com -.mywww.biz -.myz.info -.ninth.biz -.ns01.biz -.ns01.info -.ns01.us -.ns02.biz -.ns02.info -.ns02.us -.ns1.name -.ns2.name -.ns3.name -.ocry.com -.onedumb.com -.onmypc.biz -.onmypc.info -.onmypc.net -.onmypc.org -.onmypc.us -.organiccrap.com -.otzo.com -.ourhobby.com -.pcanywhere.net -.port25.biz -.proxydns.com -.qhigh.com -.qpoe.com -.rebatesrule.net -.sellclassics.com -.sendsmtp.com -.serveuser.com -.serveusers.com -.sexidude.com -.sexxxy.biz -.sixth.biz -.squirly.info -.ssl443.org -.toh.info -.toythieves.com -.trickip.net -.trickip.org -.vizvaz.com -.wha.la -.wikaba.com -.www1.biz -.wwwhost.biz -@@|http://xx.wwwhost.biz -.x24hr.com -.xxuz.com -.xxxy.biz -.xxxy.info -.ygto.com -.youdontcare.com -.yourtrap.com -.zyns.com -.zzux.com - !!--Cloudflare-- -!--||pages.dev - -!!---CloudFront--- -d1b183sg0nvnuh.cloudfront.net -|https://d1b183sg0nvnuh.cloudfront.net -d1c37gjwa26taa.cloudfront.net -|https://d1c37gjwa26taa.cloudfront.net -d3c33hcgiwev3.cloudfront.net -|https://d3c33hcgiwev3.cloudfront.net -||d3rhr7kgmtrq1v.cloudfront.net - -!!---DtDNS--- -!###https://www.dtdns.com/dtsite/faq -.3d-game.com -.4irc.com -.b0ne.com -.chatnook.com -.darktech.org -.deaftone.com -.dtdns.net -.effers.com -.etowns.net -.etowns.org -.flnet.org -.gotgeeks.com -.scieron.com -.slyip.com -.slyip.net -.suroot.com - -!!---DynDNS--- -!###https://help.dyn.com/list-of-dyn-dns-pro-remote-access-domain-names/ -.blogdns.org -.dyndns.org -.dyndns-ip.com -.dyndns-pics.com -.from-sd.com -.from-pr.com -.is-a-hunter.com - +||cloudflarestatus.com +||pages.dev +||workers.dev +||one.one.one.one +||cloudflare-dns.com +||dns.cloudflare.com !!---Dynu--- -.dynu.com ||dynu.com -.dynu.net -.freeddns.org - !!---Facebook--- ||accountkit.com -cdninstagram.com ||cdninstagram.com ||f8.com -||facebook.br -.facebook.com ||facebook.com -!--/^https?:\/\/[^\/]+facebook\.com/ -@@||v6.facebook.com ||facebook.de ||facebook.design ||connect.facebook.net @@ -848,7 +618,6 @@ cdninstagram.com ||fbsbx.com ||fbaddins.com ||fbworkmail.com -.instagram.com ||instagram.com ||m.me ||messenger.com @@ -856,532 +625,74 @@ cdninstagram.com ||oculus.com ||oculuscdn.com ||rocksdb.org -@@||ip6.static.sl-reverse.com ||parse.com ||thefacebook.com ||threads.net ||whatsapp.com ||whatsapp.net - !!---Fandom--- ||auntology.fandom.com ||hongkong.fandom.com - !!---FTChinese--- -.ftchinese.com ||ftchinese.com -!--.ftchinese.com/channel/video -!--.ftchinese.com/premium/001081066 -!--.ftchinese.com/story/00102753 -!--.ftchinese.com/story/001026616 -!--.ftchinese.com/story/001026749 -!--.ftchinese.com/story/001026807 -!--.ftchinese.com/story/001026808 -!--.ftchinese.com/story/001026834 -!--.ftchinese.com/story/001026880 -!--.ftchinese.com/story/001027429 -!--.ftchinese.com/story/001030341 -!--.ftchinese.com/story/001030502 -!--.ftchinese.com/story/001030803 -!--.ftchinese.com/story/001031317 -!--.ftchinese.com/story/001032617 -!--.ftchinese.com/story/001032636 -!--.ftchinese.com/story/001032692 -!--.ftchinese.com/story/001032762 -!--.ftchinese.com/story/001033138 -!--.ftchinese.com/story/001034917 -!--.ftchinese.com/story/001034926 -!--.ftchinese.com/story/001034927 -!--.ftchinese.com/story/001034928 -!--.ftchinese.com/story/001034952 -!--.ftchinese.com/story/001035890 -!--.ftchinese.com/story/001035972 -!--.ftchinese.com/story/001035993 -!--.ftchinese.com/story/001036417 -!--.ftchinese.com/story/001037090 -!--.ftchinese.com/story/001037091 -!--.ftchinese.com/story/001038178 -!--.ftchinese.com/story/001038199 -!--.ftchinese.com/story/001038220 -!--.ftchinese.com/story/001038819 -!--.ftchinese.com/story/001038862 -!--.ftchinese.com/story/001039067 -!--.ftchinese.com/story/001039178 -!--.ftchinese.com/story/001039211 -!--.ftchinese.com/story/001039271 -!--.ftchinese.com/story/001039295 -!--.ftchinese.com/story/001039369 -!--.ftchinese.com/story/001039482 -!--.ftchinese.com/story/001039534 -!--.ftchinese.com/story/001039555 -!--.ftchinese.com/story/001039576 -!--.ftchinese.com/story/001039712 -!--.ftchinese.com/story/001039779 -!--.ftchinese.com/story/001039809 -!--.ftchinese.com/story/001040134 -!--.ftchinese.com/story/001040835 -!--.ftchinese.com/story/001040890 -!--.ftchinese.com/story/001040918 -!--.ftchinese.com/story/001040992 -!--.ftchinese.com/story/001041209 -!--.ftchinese.com/story/001042100 -!--.ftchinese.com/story/001042252 -!--.ftchinese.com/story/001042272 -!--.ftchinese.com/story/001042280 -!--.ftchinese.com/story/001043029 -!--.ftchinese.com/story/001043066 -!--.ftchinese.com/story/001043096 -!--.ftchinese.com/story/001043124 -!--.ftchinese.com/story/001043152 -!--.ftchinese.com/story/001043189 -!--.ftchinese.com/story/001043428 -!--.ftchinese.com/story/001043439 -!--.ftchinese.com/story/001043534 -!--.ftchinese.com/story/001043675 -!--.ftchinese.com/story/001043680 -!--.ftchinese.com/story/001043702 -!--.ftchinese.com/story/001043849 -!--.ftchinese.com/story/001044099 -!--.ftchinese.com/story/001044776 -!--.ftchinese.com/story/001044871 -!--.ftchinese.com/story/001044897 -!--.ftchinese.com/story/001045114 -!--.ftchinese.com/story/001045139 -!--.ftchinese.com/story/001045186 -!--.ftchinese.com/story/001045755 -!--.ftchinese.com/story/001046087 -!--.ftchinese.com/story/001046105 -!--.ftchinese.com/story/001046118 -!--.ftchinese.com/story/001046132 -!--.ftchinese.com/story/001046517 -!--.ftchinese.com/story/001046822 -!--.ftchinese.com/story/001046866 -!--.ftchinese.com/story/001046942 -!--.ftchinese.com/story/001047180 -!--.ftchinese.com/story/001047206 -!--.ftchinese.com/story/001047304 -!--.ftchinese.com/story/001047317 -!--.ftchinese.com/story/001047345 -!--.ftchinese.com/story/001047358 -!--.ftchinese.com/story/001047375 -!--.ftchinese.com/story/001047381 -!--.ftchinese.com/story/001047413 -!--.ftchinese.com/story/001047456 -!--.ftchinese.com/story/001047491 -!--.ftchinese.com/story/001047545 -!--.ftchinese.com/story/001047558 -!--.ftchinese.com/story/001047568 -!--.ftchinese.com/story/001047627 -!--.ftchinese.com/story/001048293 -!--.ftchinese.com/story/001048343 -!--.ftchinese.com/story/001048710 -!--.ftchinese.com/story/001049289 -!--.ftchinese.com/story/001049360 -!--.ftchinese.com/story/001049896 -!--.ftchinese.com/story/001050152 -!--.ftchinese.com/story/001051027 -!--.ftchinese.com/story/001051161 -!--.ftchinese.com/story/001051372 -!--.ftchinese.com/story/001051479 -!--.ftchinese.com/story/001052138 -!--.ftchinese.com/story/001052161 -!--.ftchinese.com/story/001052525 -!--.ftchinese.com/story/001052549 -!--.ftchinese.com/story/001052701 -!--.ftchinese.com/story/001052965 -!--.ftchinese.com/story/001053149 -!--.ftchinese.com/story/001053150 -!--.ftchinese.com/story/001053200 -!--.ftchinese.com/story/001053425 -!--.ftchinese.com/story/001053496 -!--.ftchinese.com/story/001053526 -!--.ftchinese.com/story/001053557 -!--.ftchinese.com/story/001053906 -!--.ftchinese.com/story/001054049 -!--.ftchinese.com/story/001054103 -!--.ftchinese.com/story/001054109 -!--.ftchinese.com/story/001054119 -!--.ftchinese.com/story/001054123 -!--.ftchinese.com/story/001054139 -!--.ftchinese.com/story/001054166 -!--.ftchinese.com/story/001054168 -!--.ftchinese.com/story/001054190 -!--.ftchinese.com/story/001054437 -!--.ftchinese.com/story/001054526 -!--.ftchinese.com/story/001054607 -!--.ftchinese.com/story/001054644 -!--.ftchinese.com/story/001054786 -!--.ftchinese.com/story/001054843 -!--.ftchinese.com/story/001054925 -!--.ftchinese.com/story/001054940 -!--.ftchinese.com/story/001055051 -!--.ftchinese.com/story/001055063 -!--.ftchinese.com/story/001055069 -!--.ftchinese.com/story/001055136 -!--.ftchinese.com/story/001055170 -!--.ftchinese.com/story/001055202 -!--.ftchinese.com/story/001055242 -!--.ftchinese.com/story/001055263 -!--.ftchinese.com/story/001055274 -!--.ftchinese.com/story/001055299 -!--.ftchinese.com/story/001055480 -!--.ftchinese.com/story/001055551 -!--.ftchinese.com/story/001055559 -!--.ftchinese.com/story/001055566 -!--.ftchinese.com/story/001055840 -!--.ftchinese.com/story/001056099 -!--.ftchinese.com/story/001056108 -!--.ftchinese.com/story/001056131 -!--.ftchinese.com/story/001056375 -!--.ftchinese.com/story/001056491 -!--.ftchinese.com/story/001056529 -!--.ftchinese.com/story/001056534 -!--.ftchinese.com/story/001056538 -!--.ftchinese.com/story/001056541 -!--.ftchinese.com/story/001056554 -!--.ftchinese.com/story/001056557 -!--.ftchinese.com/story/001056560 -!--.ftchinese.com/story/001056567 -!--.ftchinese.com/story/001056574 -!--.ftchinese.com/story/001056588 -!--.ftchinese.com/story/001056594 -!--.ftchinese.com/story/001056596 -!--.ftchinese.com/story/001056684 -!--.ftchinese.com/story/001056832 -!--.ftchinese.com/story/001056833 -!--.ftchinese.com/story/001056851 -!--.ftchinese.com/story/001056874 -!--.ftchinese.com/story/001056896 -!--.ftchinese.com/story/001056927 -!--.ftchinese.com/story/001057011 -!--.ftchinese.com/story/001057018 -!--.ftchinese.com/story/001057044 -!--.ftchinese.com/story/001057162 -!--.ftchinese.com/story/001057500 -!--.ftchinese.com/story/001057504 -!--.ftchinese.com/story/001057509 -!--.ftchinese.com/story/001057518 -!--.ftchinese.com/story/001057532 -!--.ftchinese.com/story/001057533 -!--.ftchinese.com/story/001057556 -!--.ftchinese.com/story/001057580 -!--.ftchinese.com/story/001057638 -!--.ftchinese.com/story/001057644 -!--.ftchinese.com/story/001057817 -!--.ftchinese.com/story/001057875 -!--.ftchinese.com/story/001058009 -!--.ftchinese.com/story/001058056 -!--.ftchinese.com/story/001058224 -!--.ftchinese.com/story/001058257 -!--.ftchinese.com/story/001058295 -!--.ftchinese.com/story/001058328 -!--.ftchinese.com/story/001058339 -!--.ftchinese.com/story/001058344 -!--.ftchinese.com/story/001058352 -!--.ftchinese.com/story/001058413 -!--.ftchinese.com/story/001058421 -!--.ftchinese.com/story/001058440 -!--.ftchinese.com/story/001058458 -!--.ftchinese.com/story/001058468 -!--.ftchinese.com/story/001058561 -!--.ftchinese.com/story/001058566 -!--.ftchinese.com/story/001058567 -!--.ftchinese.com/story/001058585 -!--.ftchinese.com/story/001058628 -!--.ftchinese.com/story/001058656 -!--.ftchinese.com/story/001058665 -!--.ftchinese.com/story/001058678 -!--.ftchinese.com/story/001058691 -!--.ftchinese.com/story/001058721 -!--.ftchinese.com/story/001058728 -!--.ftchinese.com/story/001059464 -!--.ftchinese.com/story/001059484 -!--.ftchinese.com/story/001059537 -!--.ftchinese.com/story/001059538 -!--.ftchinese.com/story/001059551 -!--.ftchinese.com/story/001059818 -!--.ftchinese.com/story/001059914 -!--.ftchinese.com/story/001059920 -!--.ftchinese.com/story/001059957 -!--.ftchinese.com/story/001060088 -!--.ftchinese.com/story/001060156 -!--.ftchinese.com/story/001060157 -!--.ftchinese.com/story/001060160 -!--.ftchinese.com/story/001060181 -!--.ftchinese.com/story/001060185 -!--.ftchinese.com/story/001060493 -!--.ftchinese.com/story/001060495 -!--.ftchinese.com/story/001060590 -!--.ftchinese.com/story/001060846 -!--.ftchinese.com/story/001060847 -!--.ftchinese.com/story/001060875 -!--.ftchinese.com/story/001060921 -!--.ftchinese.com/story/001060946 -!--.ftchinese.com/story/001061120 -!--.ftchinese.com/story/001061474 -!--.ftchinese.com/story/001061524 -!--.ftchinese.com/story/001061642 -!--.ftchinese.com/story/001062017 -!--.ftchinese.com/story/001062020 -!--.ftchinese.com/story/001062028 -!--.ftchinese.com/story/001062092 -!--.ftchinese.com/story/001062096 -!--.ftchinese.com/story/001062147 -!--.ftchinese.com/story/001062176 -!--.ftchinese.com/story/001062188 -!--.ftchinese.com/story/001062254 -!--.ftchinese.com/story/001062374 -!--.ftchinese.com/story/001062482 -!--.ftchinese.com/story/001062496 -!--.ftchinese.com/story/001062501 -!--.ftchinese.com/story/001062508 -!--.ftchinese.com/story/001062519 -!--.ftchinese.com/story/001062554 -!--.ftchinese.com/story/001062741 -!--.ftchinese.com/story/001062794 -!--.ftchinese.com/story/001063160 -!--.ftchinese.com/story/001063359 -!--.ftchinese.com/story/001063512 -!--.ftchinese.com/story/001063668 -!--.ftchinese.com/story/001063692 -!--.ftchinese.com/story/001063763 -!--.ftchinese.com/story/001063764 -!--.ftchinese.com/story/001063826 -!--.ftchinese.com/story/001064127 -!--.ftchinese.com/story/001064312 -!--.ftchinese.com/story/001064705 -!--.ftchinese.com/story/001064807 -!--.ftchinese.com/story/001065120 -!--.ftchinese.com/story/001065168 -!--.ftchinese.com/story/001065249 -!--.ftchinese.com/story/001065287 -!--.ftchinese.com/story/001065335 -!--.ftchinese.com/story/001065337 -!--.ftchinese.com/story/001065541 -!--.ftchinese.com/story/001065715 -!--.ftchinese.com/story/001065735 -!--.ftchinese.com/story/001065756 -!--.ftchinese.com/story/001065802 -!--.ftchinese.com/story/001066112 -!--.ftchinese.com/story/001066136 -!--.ftchinese.com/story/001066140 -!--.ftchinese.com/story/001066465 -!--.ftchinese.com/story/001066881 -!--.ftchinese.com/story/001066950 -!--.ftchinese.com/story/001066959 -!--.ftchinese.com/story/001067435 -!--www.ftchinese.com/story/001067479 -!--.ftchinese.com/story/001067528 -!--.ftchinese.com/story/001067545 -!--.ftchinese.com/story/001067572 -!--.ftchinese.com/story/001067648 -!--.ftchinese.com/story/001067650 -!--.ftchinese.com/story/001067680 -!--.ftchinese.com/story/001067692 -!--.ftchinese.com/story/001067871 -!--.ftchinese.com/story/001067923 -!--.ftchinese.com/story/001068062 -!--.ftchinese.com/story/001068248 -!--.ftchinese.com/story/001068278 -!--.ftchinese.com/story/001068379 -!--.ftchinese.com/story/001068483 -!--.ftchinese.com/story/001068506 -!--.ftchinese.com/story/001068547 -!--.ftchinese.com/story/001068616 -!--.ftchinese.com/story/001068622 -!--.ftchinese.com/story/001068707 -!--.ftchinese.com/story/001069146 -!--.ftchinese.com/story/001069373 -!--.ftchinese.com/story/001069516 -!--.ftchinese.com/story/001069517 -!--.ftchinese.com/story/001069687 -!--.ftchinese.com/story/001069741 -!--.ftchinese.com/story/001069861 -!--.ftchinese.com/story/001069952 -!--.ftchinese.com/story/001070053 -!--.ftchinese.com/story/001070177 -!--.ftchinese.com/story/001070307 -!--.ftchinese.com/story/001070809 -!--.ftchinese.com/story/001070990 -!--.ftchinese.com/story/001071042 -!--.ftchinese.com/story/001071044 -!--.ftchinese.com/story/001071106 -!--.ftchinese.com/story/001071166 -!--.ftchinese.com/story/001071181 -!--ftchinese.com/story/001071200 -!--.ftchinese.com/story/001071208 -!--.ftchinese.com/story/001071238 -!--.ftchinese.com/story/001071683 -!--.ftchinese.com/story/001072271 -!--.ftchinese.com/story/001072348 -!--.ftchinese.com/story/001072677 -!--.ftchinese.com/story/001072726 -!--.ftchinese.com/story/001072794 -!--.ftchinese.com/story/001072853 -!--.ftchinese.com/story/001072895 -!--.ftchinese.com/story/001072993 -!--.ftchinese.com/story/001073043 -!--.ftchinese.com/story/001073103 -!--.ftchinese.com/story/001073157 -!--.ftchinese.com/story/001073216 -!--.ftchinese.com/story/001073246 -!--.ftchinese.com/story/001073305 -!--.ftchinese.com/story/001073307 -!--.ftchinese.com/story/001073408 -!--.ftchinese.com/story/001073537 -!--.ftchinese.com/story/001073672 -!--.ftchinese.com/story/001073849 -!--.ftchinese.com/story/001073906 -!--.ftchinese.com/story/001074089 -!--.ftchinese.com/story/001074110 -!--.ftchinese.com/story/001074128 -!--.ftchinese.com/story/001074157 -!--.ftchinese.com/story/001074246 -!--.ftchinese.com/story/001074307 -!--.ftchinese.com/story/001074347 -!--.ftchinese.com/story/001074423 -!--.ftchinese.com/story/001074454 -!--.ftchinese.com/story/001074467 -!--.ftchinese.com/story/001074493 -!--.ftchinese.com/story/001074550 -!--.ftchinese.com/story/001074562 -!--.ftchinese.com/story/001074653 -!--.ftchinese.com/story/001074693 -!--.ftchinese.com/story/001074699 -!--.ftchinese.com/story/001074712 -!--.ftchinese.com/story/001074713 -!--.ftchinese.com/story/001074768 -!--.ftchinese.com/story/001074782 -!--.ftchinese.com/story/001074794 -!--.ftchinese.com/story/001074822 -!--.ftchinese.com/story/001074874 -!--.ftchinese.com/story/001074891 -!--.ftchinese.com/story/001074918 -!--.ftchinese.com/story/001075081 -!--.ftchinese.com/story/001075134 -!--.ftchinese.com/story/001075142 -!--.ftchinese.com/story/001075216 -!--.ftchinese.com/story/001075230 -!--.ftchinese.com/story/001075238 -!--.ftchinese.com/story/001075262 -!--.ftchinese.com/story/001075269 -!--.ftchinese.com/story/001075491 -!--.ftchinese.com/story/001075500 -!--.ftchinese.com/story/001075650 -!--.ftchinese.com/story/001075678 -!--.ftchinese.com/story/001075703 -!--.ftchinese.com/story/001075739 -!--.ftchinese.com/story/001076066 -!--.ftchinese.com/story/001076142 -!--.ftchinese.com/story/001076459 -!--.ftchinese.com/story/001076470 -!--.ftchinese.com/story/001076538 -!--.ftchinese.com/story/001076573 -!--.ftchinese.com/story/001076901 -!--.ftchinese.com/story/001077067 -!--.ftchinese.com/story/001077084 -!--.ftchinese.com/story/001077235 -!--.ftchinese.com/story/001077344 -!--.ftchinese.com/story/001077390 -!--.ftchinese.com/story/001077392 -!--.ftchinese.com/story/001077465 -!--.ftchinese.com/story/001077468 -!--.ftchinese.com/story/001077492 -!--.ftchinese.com/story/001077745 -!--.ftchinese.com/story/001077768 -!--.ftchinese.com/story/001077804 -!--.ftchinese.com/story/001077852 -!--.ftchinese.com/story/001078646 -!--.ftchinese.com/story/001078928 -!--.ftchinese.com/story/001078967 -!--.ftchinese.com/story/001079559 -!--.ftchinese.com/story/001079641 -!--.ftchinese.com/story/001079909 -!--.ftchinese.com/story/001079934 -!--.ftchinese.com/story/001079992 -!--.ftchinese.com/story/001080054 -!--.ftchinese.com/story/001080109 -!--.ftchinese.com/story/001080169 -!--.ftchinese.com/story/001080226 -!--.ftchinese.com/story/001080429 -!--.ftchinese.com/story/001080471 -!--.ftchinese.com/story/001080550 -!--.ftchinese.com/story/001080581 -!--.ftchinese.com/story/001080647 -!--.ftchinese.com/story/001080778 -!--.ftchinese.com/story/001080892 -!--.ftchinese.com/story/001080915 -!--.ftchinese.com/story/001080935 -!--.ftchinese.com/story/001081059 -!--.ftchinese.com/story/001081127 -!--.ftchinese.com/tag/%E5%8D%81%E5%85%AB%E5%B1%8A%E4%B8%89%E4%B8%AD%E5%85%A8%E4%BC%9A -!--.ftchinese.com/tag/%E6%B8%A9%E5%AE%B6%E5%AE%9D -!--.ftchinese.com/tag/%E8%96%84%E7%86%99%E6%9D%A5 -!--.ftchinese.com/video/1437 -!--.ftchinese.com/video/1882 -!--.ftchinese.com/video/2446 -!--.ftchinese.com/video/2601 -!--.ftchinese.com/comments - !!---Google--- -!###https://www.google.com/supported_domains### -!...GFWList doesn't intend to support typosquatting... +||google.dev +||ai.studio +||chromium.org +||goog +||gle +||google +||doc.new +||form.new +||forms.new +||sheet.new +||sheets.new +||spreadsheet.new +||site.new +||sites.new +||website.new +||slides.new +||deck.new +||presentation.new +||googleapis.com ||1e100.net ||466453.com ||abc.xyz -||about.google ||admob.com ||adsense.com ||advertisercommunity.com ||agoogleaday.com -||ai.google ||ampproject.org -@@|https://www.ampproject.org -@@|https://cdn.ampproject.org ||android.com ||androidify.com ||androidtv.com ||api.ai -.appspot.com ||appspot.com ||autodraw.com -||blog.google ||blogblog.com -blogspot.com /^https?:\/\/[^\/]+blogspot\.(.*)/ -.blogspot.hk -.blogspot.jp -.blogspot.tw ||business.page !--||capitalg.com ||certificate-transparency.org ||chrome.com ||chromecast.com -||chromeenterprise.google ||chromeexperiments.com -||chromercise.com ||chromestatus.com -||chromium.org ||cloudfunctions.net -||com.google ||crbug.com ||creativelab5.com -||crisisresponse.google ||crrev.com ||data-vocabulary.org ||debug.com ||deepmind.com ||deja.com -||design.google ||digisfera.com -||dns.google -||hub.docker.com +||docker.com ||docs.new -||domains.google ||duck.com -||environment.google ||feedburner.com ||firebaseio.com +||crashlytics.com ||g.co ||gcr.io ||get.app @@ -1396,70 +707,211 @@ blogspot.com ||godoc.org ||golang.org ||goo.gl -||goo.gle -.google.ae -.google.as -.google.am -.google.at -.google.az -.google.ba -.google.be -.google.bg -.google.ca -.google.cd -.google.ci -.google.co.id -.google.co.jp -.google.co.kr -.google.co.ma -.google.co.uk -.google.com -.google.de -||google.dev -.google.dj -.google.dk -.google.es -.google.fi -.google.fm -.google.fr -.google.gg -.google.gl -.google.gr -.google.ie -.google.is -.google.it -.google.jo -.google.kz -.google.lv -.google.mn -.google.ms -.google.nl -.google.nu -.google.no -.google.ro -.google.ru -.google.rw -.google.sc -.google.sh -.google.sk -.google.sm -.google.sn -.google.tk -.google.tm -.google.to -.google.tt -.google.vu -.google.ws -/^https?:\/\/([^\/]+\.)*google\.(ac|ad|ae|af|ai|al|am|as|at|az|ba|be|bf|bg|bi|bj|bs|bt|by|ca|cat|cd|cf|cg|ch|ci|cl|cm|co.ao|co.bw|co.ck|co.cr|co.id|co.il|co.in|co.jp|co.ke|co.kr|co.ls|co.ma|com|com.af|com.ag|com.ai|com.ar|com.au|com.bd|com.bh|com.bn|com.bo|com.br|com.bz|com.co|com.cu|com.cy|com.do|com.ec|com.eg|com.et|com.fj|com.gh|com.gi|com.gt|com.hk|com.jm|com.kh|com.kw|com.lb|com.ly|com.mm|com.mt|com.mx|com.my|com.na|com.nf|com.ng|com.ni|com.np|com.om|com.pa|com.pe|com.pg|com.ph|com.pk|com.pr|com.py|com.qa|com.sa|com.sb|com.sg|com.sl|com.sv|com.tj|com.tr|com.tw|com.ua|com.uy|com.vc|com.vn|co.mz|co.nz|co.th|co.tz|co.ug|co.uk|co.uz|co.ve|co.vi|co.za|co.zm|co.zw|cv|cz|de|dj|dk|dm|dz|ee|es|eu|fi|fm|fr|ga|ge|gg|gl|gm|gp|gr|gy|hk|hn|hr|ht|hu|ie|im|iq|is|it|it.ao|je|jo|kg|ki|kz|la|li|lk|lt|lu|lv|md|me|mg|mk|ml|mn|ms|mu|mv|mw|mx|ne|nl|no|nr|nu|org|pl|pn|ps|pt|ro|rs|ru|rw|sc|se|sh|si|sk|sm|sn|so|sr|st|td|tg|tk|tl|tm|tn|to|tt|us|vg|vn|vu|ws)\/.*/ -!--||google-analytics.com -!--||googleadservices.com -||googleapis.cn -||googleapis.com +||google.com +||google.ac +||google.ad +||google.ae +||google.af +||google.ai +||google.al +||google.am +||google.as +||google.at +||google.az +||google.ba +||google.be +||google.bf +||google.bg +||google.bi +||google.bj +||google.bs +||google.bt +||google.by +||google.ca +||google.cat +||google.cd +||google.cf +||google.cg +||google.ch +||google.ci +||google.cl +||google.cm +||google.co.ao +||google.co.bw +||google.co.ck +||google.co.cr +||google.co.id +||google.co.il +||google.co.in +||google.co.jp +||google.co.ke +||google.co.kr +||google.co.ls +||google.co.ma +||google.co.mz +||google.co.nz +||google.co.th +||google.co.tz +||google.co.ug +||google.co.uk +||google.co.uz +||google.co.ve +||google.co.vi +||google.co.za +||google.co.zm +||google.co.zw +||google.com.af +||google.com.ag +||google.com.ai +||google.com.ar +||google.com.au +||google.com.bd +||google.com.bh +||google.com.bn +||google.com.bo +||google.com.br +||google.com.bz +||google.com.co +||google.com.cu +||google.com.cy +||google.com.do +||google.com.ec +||google.com.eg +||google.com.et +||google.com.fj +||google.com.gh +||google.com.gi +||google.com.gt +||google.com.hk +||google.com.jm +||google.com.kh +||google.com.kw +||google.com.lb +||google.com.ly +||google.com.mm +||google.com.mt +||google.com.mx +||google.com.my +||google.com.na +||google.com.nf +||google.com.ng +||google.com.ni +||google.com.np +||google.com.om +||google.com.pa +||google.com.pe +||google.com.pg +||google.com.ph +||google.com.pk +||google.com.pr +||google.com.py +||google.com.qa +||google.com.sa +||google.com.sb +||google.com.sg +||google.com.sl +||google.com.sv +||google.com.tj +||google.com.tr +||google.com.tw +||google.com.ua +||google.com.uy +||google.com.vc +||google.com.vn +||google.cv +||google.cz +||google.de +||google.dk +||google.dm +||google.dz +||google.ee +||google.es +||google.eu +||google.fi +||google.fm +||google.fr +||google.ga +||google.ge +||google.gg +||google.gl +||google.gm +||google.gp +||google.gr +||google.gy +||google.hk +||google.hn +||google.hr +||google.ht +||google.hu +||google.ie +||google.im +||google.iq +||google.is +||google.it +||google.it.ao +||google.je +||google.jo +||google.kg +||google.ki +||google.kz +||google.la +||google.li +||google.lk +||google.lt +||google.lu +||google.lv +||google.md +||google.me +||google.mg +||google.mk +||google.ml +||google.mn +||google.ms +||google.mu +||google.mv +||google.mw +||google.mx +||google.ne +||google.nl +||google.no +||google.nr +||google.nu +||google.org +||google.pl +||google.pn +||google.ps +||google.pt +||google.ro +||google.rs +||google.ru +||google.rw +||google.sc +||google.se +||google.sh +||google.si +||google.sk +||google.sm +||google.sn +||google.so +||google.sr +||google.st +||google.td +||google.tg +||google.tk +||google.tl +||google.tm +||google.tn +||google.to +||google.tt +||google.us +||google.vg +||google.vn +||google.vu +||google.ws ||googleapps.com ||googleartproject.com ||googleblog.com ||googlebot.com -!--||googlecapital.com ||googlechinawebmaster.com ||googlecode.com ||googlecommerce.com @@ -1472,67 +924,45 @@ blogspot.com ||googlehosted.com ||googleideas.com ||googleinsidesearch.com -||googlelabs.com ||googlemail.com ||googlemashups.com ||googlepagecreator.com ||googleplay.com ||googleplus.com -||googlescholar.comUSA +||googlescholar.com ||googlesource.com -!--||googlesyndication.com -!--||googletagmanager.com -!--||googletagservices.com ||googleusercontent.com -.googlevideo.com ||googlevideo.com ||googleweblight.com ||googlezip.net -||groups.google.cn -||grow.google ||gstatic.com -!--||gv.com -||gvt0.com ||gvt1.com -@@||redirector.gvt1.com ||gvt3.com ||gwtproject.org ||html5rocks.com ||iam.soy ||igoogle.com ||itasoftware.com -||lers.google ||like.com ||madewithcode.com ||material.io -||nic.google ||on2.com -||opensource.google ||panoramio.com -||passwords.google ||picasaweb.com ||pki.goog ||plus.codes ||polymer-project.org -||pride.google ||questvisual.com ||admin.recaptcha.net ||api.recaptcha.net ||api-secure.recaptcha.net ||api-verify.recaptcha.net ||redhotlabs.com -||registry.google -||research.google -||safety.google ||savethedate.foo ||schema.org ||shattered.io |http://sipml5.org/ -||sheets.new -||slides.new ||snapseed.com -||stories.google -||sustainability.google ||synergyse.com ||teachparentstech.org ||tensorflow.org @@ -1540,9 +970,9 @@ blogspot.com ||thinkwithgoogle.com ||tiltbrush.com ||translate.goog -||tv.google +||ua5v.com ||urchin.com -!--||www.google +||usercontent.goog ||waveprotocol.org ||waymo.com ||web.dev @@ -1555,9 +985,7 @@ blogspot.com ||withgoogle.com ||withyoutube.com ||x.company -||xn--ngstr-lra8j.com ||youtu.be -.youtube.com ||youtube.com ||youtube-nocookie.com ||youtubeeducation.com @@ -1566,51 +994,32 @@ blogspot.com ||yt.be ||ytimg.com ||zynamics.com - -!!---KickASS--- -!--OFFICIAL URL list at: https://kastatus.com - +!!---Microsoft--- +||copilot.microsoft.com !!---NaughtyAmerica--- ||naughtyamerica.com - !!---NYTimes--- -!--||d1f1eryiqyjs0r.cloudfront.net -!--||d3lar09xbwlsge.cloudfront.net -!--||d3q1qj9jzsu8nw.cloudfront.net -!--||dc8xl0ndzn2cb.cloudfront.net -!--||a1.nyt.com -!--||int.nyt.com -!--||s1.nyt.com -static01.nyt.com -!--||static01.nyt.com -!--||typeface.nyt.com ||nyt.com -nytchina.com -nytcn.me ||nytcn.me ||nytco.com |http://nyti.ms/ -.nytimes.com ||nytimes.com ||nytimg.com -userapi.nytlog.com -cn.nytstyle.com ||nytstyle.com - !!---Steam--- -.steamcommunity.com ||steamcommunity.com -!--steamcommunity.com/profiles/76561198062771609 -!--steamcommunity.com/groups/LibetTibet -!--steamcommunity.com/groups/zhonggong -!--steamcommunity.com/id/CJT_Jackton ||store.steampowered.com - +||api.steampowered.com +||steamstatic.com !!---Telegram--- !!!---Domain--- +||tx.me +||tg.dev +||telega.one ||cdn-telegram.org ||comments.app ||graph.org +||legra.ph ||quiz.directory ||t.me ||updates.tdesktop.com @@ -1618,1194 +1027,579 @@ cn.nytstyle.com ||telegram.me ||telegram.org ||telegram.space -||telegram-cdn.org ||telegramdownload.com ||telegra.ph ||telesco.pe !!!---IP--- - !!---Tiktok--- ||tiktok.com ||tiktokv.com ||tiktokv.us ||tiktokcdn-us.com - +||tiktokcdn.com +||tiktokcdn-eu.com !!---Twitch--- ||jtvnw.net ||ttvnw.net ||twitch.tv ||twitchcdn.net - !!---Twitter/X--- ||periscope.tv -.pscp.tv ||pscp.tv -.t.co ||t.co -.tweetdeck.com ||tweetdeck.com ||twimg.com -.twitpic.com ||twitpic.com -.twitter.com ||twitter.com ||twitter.jp ||vine.co ||x.com !!---Taiwan--- +||twgov.tw +||gov.tw +@@||www.gov.tw ||gov.taipei -.gov.tw -|https://aiss.anws.gov.tw -||archives.gov.tw -||tacc.cwb.gov.tw -||data.gov.tw -||epa.gov.tw -||fa.gov.tw -||fda.gov.tw -||hpa.gov.tw -||immigration.gov.tw -||itaiwan.gov.tw ||li.taipei -||mjib.gov.tw -||moeaic.gov.tw -||mofa.gov.tw -||mol.gov.tw -||mvdis.gov.tw -||nat.gov.tw -||nhi.gov.tw -||npa.gov.tw -||nsc.gov.tw -||ntbk.gov.tw -||ntbna.gov.tw -||ntbt.gov.tw -||ntsna.gov.tw -||pcc.gov.tw -||stat.gov.tw -||taipei.gov.tw -||taiwanjobs.gov.tw -||thb.gov.tw -||tipo.gov.tw -||wda.gov.tw - ||teco-hk.org ||teco-mo.org - -@@||aftygh.gov.tw -@@||aide.gov.tw -@@||tpde.aide.gov.tw -@@||arte.gov.tw -@@||chukuang.gov.tw -@@||cwb.gov.tw -@@||cycab.gov.tw -@@||dbnsa.gov.tw -@@||df.gov.tw -@@||eastcoast-nsa.gov.tw -@@||erv-nsa.gov.tw -@@||grb.gov.tw -@@||gysd.nyc.gov.tw -@@||hchcc.gov.tw -@@||hsinchu-cc.gov.tw -@@||iner.gov.tw -@@||klsio.gov.tw -@@||kmseh.gov.tw -@@||lungtanhr.gov.tw -@@||maolin-nsa.gov.tw -@@||matsu-news.gov.tw -@@||matsu-nsa.gov.tw -@@||matsucc.gov.tw -@@||moe.gov.tw -@@||nankan.gov.tw -@@||ncree.gov.tw -@@||necoast-nsa.gov.tw -@@||siraya-nsa.gov.tw -@@||cromotc.nat.gov.tw -@@||tax.nat.gov.tw -@@||necoast-nsa.gov.tw -@@||ner.gov.tw -@@||nmmba.gov.tw -@@||nmp.gov.tw -@@||nmvttc.gov.tw -@@||northguan-nsa.gov.tw -||npm.gov.tw -@@||nstm.gov.tw -@@||ntdmh.gov.tw -@@||ntl.gov.tw -@@||ntsec.gov.tw -@@||ntuh.gov.tw -@@||nvri.gov.tw -@@||penghu-nsa.gov.tw -@@||post.gov.tw -@@||siraya-nsa.gov.tw -@@||stdtime.gov.tw -@@||sunmoonlake.gov.tw -@@||taitung-house.gov.tw -@@||taoyuan.gov.tw -@@||tphcc.gov.tw -@@||trimt-nsa.gov.tw -@@||vghtpe.gov.tw -@@||vghks.gov.tw -@@||vghtc.gov.tw -@@||wanfang.gov.tw -@@||yatsen.gov.tw -@@||yda.gov.tw - -!--@@||4pppc.gov.tw -!--@@||921.gov.tw -!--@@||dmtip.gov.tw -!--@@||etraining.gov.tw -!--@@||gsn-cert.nat.gov.tw -!--@@||nici.nat.gov.tw -!--@@||hcc.gov.tw -!--@@||hengchuen.gov.tw -!--@@||khcc.gov.tw -!--@@||khms.gov.tw -!--@@||kk.gov.tw -!--@@||klccab.gov.tw -!--@@||klra.gov.tw -!--@@||nmh.gov.tw -!--@@||nmtl.gov.tw -!--@@||pabp.gov.tw -!--@@||pet.gov.tw -!--@@||tchb.gov.tw -!--@@||tcsac.gov.tw -!--@@||tncsec.gov.tw ||kinmen.org.tw - !!---USA--- -|http://www.americorps.gov +||americorps.gov +||dma.mil ||jpl.nasa.gov ||pds.nasa.gov +||pacom.mil +||soc.mil ||solarsystem.nasa.gov iipdigital.usembassy.gov +||uscg.mil ||usfk.mil -||usmc.mil |http://tarr.uspto.gov/ ||tsdr.uspto.gov - !!---V2EX--- ||v2ex.com -!--.v2ex.com -!--Included in above rule: dns.v2ex.com -!--@@|http://v2ex.com -!--@@|http://cdn.v2ex.com -!--@@|http://cn.v2ex.com -!--@@|http://hk.v2ex.com -!--@@|http://i.v2ex.com -!--@@|http://lax.v2ex.com -!--@@|http://neue.v2ex.com -!--@@|http://pagespeed.v2ex.com -!--@@|http://static.v2ex.com -!--@@|http://workspace.v2ex.com -!--@@|http://www.v2ex.com - !!---VOA--- -cn.voa.mobi -tw.voa.mobi ||voacambodia.com -.voachineseblog.com ||voachineseblog.com -.voacantonese.com ||voacantonese.com -voachinese.com ||voachinese.com -voagd.com ||voaindonesia.com -.voanews.com ||voanews.com -voatibetan.com ||voatibetan.com -.voatibetanenglish.com ||voatibetanenglish.com - !!---Wikia--- ||zh.ecdm.wikia.com ||evchk.wikia.com -fq.wikia.com -zh.pttpedia.wikia.com/wiki/%E7%BF%92%E5%8C%85%E5%AD%90%E4%B9%8B%E4%BA%82 -cn.uncyclopedia.wikia.com -zh.uncyclopedia.wikia.com - !-------------Wikipedia Related------------- !!Emergency need only(IP/Port block usage)!! !------0------ -!--||mediawiki.org -!--@@||m.mediawiki.org +||mediawiki.org !------1------ -!--||wikidata.org -!--@@||m.wikidata.org +||wikidata.org !------2------ ||wikimedia.org -!--@@||lists.wikimedia.org -!--@@||m.wikimedia.org -!--@@||phabricator.wikimedia.org -!--@@||upload.wikimedia.org -!--@@||wikitech.wikimedia.org !------3------ -!--||wikibooks.org -!--@@||m.wikibooks.org +||wikibooks.org !------4------ -!--||wikiversity.org -!--@@||m.wikiversity.org +||wikiversity.org !------5------ -!--||wikisource.org -!--@@||m.wikisource.org -|http://zh.wikisource.org +||wikisource.org !------6------ ||zh.wikiquote.org -!--@@||m.wikiquote.org !------7------ -!--||wikinews.org -!--@@||m.wikinews.org -||zh.wikinews.org +||wikinews.org !------8------ -!--||wikivoyage.org -!--@@||m.wikivoyage.org -!--|http://zh.wikivoyage.org +||wikivoyage.org !------9------ -!--||wiktionary.org -!--@@||m.wiktionary.org -!--|http://zh.wiktionary.org -!-----10------ -!--||wikimediafoundation.org -!--@@||m.wikimediafoundation.org +||wiktionary.org !----Main----- -!!--||en.wikipedia.org -!--||wikipedia.org -||ja.wikipedia.org -!!--zh.wikipedia.org -!--||zh.wikipedia.org -!!--||ug.m.wikipedia.org -!!--zh.m.wikipedia.org -!!--|https://zh.m.wikipedia.org -!--@@||m.wikipedia.org -!!--|https://zh.wikipedia.org -!--Other Languages of Wikipedia -!!--wuu.wikipedia.org -!!--|https://wuu.wikipedia.org -!!--zh-yue.wikipedia.org -!!--|https://zh-yue.wikipedia.org -!!! Starting with !! are previous rules replaced by: ||wikipedia.org - +||wmfusercontent.org !!---Yahoo--- -||data.flurry.com -||page.bid.yahoo.com -||tw.bid.yahoo.com +||shopping.yahoo.co.jp ||auctions.yahoo.co.jp -||blogs.yahoo.co.jp ||search.yahoo.co.jp -||buy.yahoo.com.tw -||hk.yahoo.com -||hk.knowledge.yahoo.com -||tw.money.yahoo.com -||hk.myblog.yahoo.com -news.yahoo.com/china-blocks-bbc -||hk.news.yahoo.com -hk.rd.yahoo.com -hk.search.yahoo.com/search -hk.video.news.yahoo.com/video -meme.yahoo.com -!--tw.yahoo.com -tw.answers.yahoo.com -|https://tw.answers.yahoo.com -||tw.knowledge.yahoo.com -||tw.mall.yahoo.com -tw.yahoo.com -||tw.mobi.yahoo.com -tw.myblog.yahoo.com -||tw.news.yahoo.com -pulse.yahoo.com -||search.yahoo.com -upcoming.yahoo.com -video.yahoo.com +||yahoo.com.tw ||yahoo.com.hk -||duckduckgo-owned-server.yahoo.net - +||yahoo.com !------------------Numerics--------------------- +||996.icu +||ipfs.4everland.io +||91dasai.com +||i.111666.best +||1lib.sk +||2047.one +||69shuba.cx +||2049bbs.xyz +||611study.com +||18comic.org ||000webhost.com -.030buy.com -.0rz.tw |http://0rz.tw -1-apple.com.tw ||1-apple.com.tw -.10.tt -.100ke.org -.1000giri.net ||1000giri.net ||10beasts.net -.10conditionsoflove.com ||10musume.com -123rf.com -.12bet.com ||12bet.com -.12vpn.com -.12vpn.net ||12vpn.com ||12vpn.net ||1337x.to -.138.com -141hongkong.com/forum ||141jj.com -.141tube.com ||1688.com.au -.173ng.com ||173ng.com -.177pic.info -.17t17p.com ||18board.com -||18board.info -18onlygirls.com -.18p2p.com -.18virginsex.com -.1949er.org -zhao.1984.city ||zhao.1984.city -1984bbs.com ||1984bbs.com -!--||1984blog.com -.1984bbs.org -||1984bbs.org -.1991way.com ||1991way.com -.1998cdp.org -.1bao.org -|http://1bao.org -.1eew.com -.1mobile.com -|http://*.1mobile.tw ||1point3acres.com ||1pondo.tv -.2-hand.info -.2000fun.com/bbs ||2008xianzhang.info -||2017.hk ||2021hkcharter.com ||2047.name -21andy.com/blog -21sextury.com -.228.net.tw ||233abc.com ||24hrs.ca -24smile.org -2lipstube.com -.2shared.com -30boxes.com -.315lz.com ||32red.com ||36rain.com -.3a5a.com -3arabtv.com -.3boys2girls.com -.3proxy.ru -.3ren.ca -.3tui.net ||404museum.com ||4bluestones.biz -.4chan.com -!--||4chan.org -.4everproxy.com ||4everproxy.com ||4rbtv.com ||4shared.com -taiwannation.50webs.com ||51.ca ||51jav.org -.51luoben.com ||51luoben.com ||5278.cc -.5299.tv -5aimiku.com -5i01.com -.5isotoi5.org -.5maodang.com +||611study.icu ||63i.com -.64museum.org -64tianwang.com -64wiki.com -.66.ca -666kb.com ||6do.news -.6park.com +||6do.world ||6park.com ||6parkbbs.com ||6parker.com ||6parknews.com ||7capture.com -.7cow.com -!--||7-zip.org -.8-d.com |http://8-d.com -85cc.net -.85cc.us |http://85cc.us -|http://85st.com -.881903.com/page/zh-tw/ ||881903.com -.888.com -.888poker.com -89.64.charter.constitutionalism.solutions -89-64.org ||89-64.org ||8964museum.com -.8news.com.tw -.8z1.net ||8z1.net -.9001700.com -|http://908taiwan.org/ ||91porn.com ||91porny.com ||91vps.club -.92ccav.com -.991.com |http://991.com -.99btgc01.com ||99btgc01.com -.99cn.info |http://99cn.info ||9bis.com ||9bis.net ||9news.com.au - !--------------------AA------------------------- -.tibet.a.se -|http://tibet.a.se +||annas-archive.gd +||annas-archive.gl +||annas-archive.pk +||archive-it.org +||aave.com +||arweave.org +||arena.ai +||akile.io +||aljazeera.net +||anuneko.com +||ai.dev +||adguard-vpn.com +||aoxvpn.com +||asianfanfics.com +||amuletmc.com +||abplive.com +||cdn.arstechnica.net +||aomedia.org +||aljazeera.com +||akinator.com +||av01.tv +||acg.rip ||a-normal-day.com -a5.com.ru |http://aamacau.com -!--|http://cdn*.abc.com/ -.abc.com -.abc.net.au ||abc.net.au -.abchinese.com -abclite.net -|https://www.abclite.net -.ablwang.com -.aboluowang.com +||abebooks.co.uk ||aboluowang.com ||about.me -.aboutgfw.com -.abs.edu ||acast.com -.accim.org -.aceros-de-hispania.com -.acevpn.com ||acevpn.com -.acg18.me |http://acg18.me ||acgbox.org ||acgkj.com ||acgnx.se -.acmedia365.com -.acnw.com.au -actfortibet.org -actimes.com.au -activpn.com ||activpn.com ||aculo.us ||addictedtocoffee.de ||addyoutube.com -.adelaidebbs.com/bbs -.adpl.org.hk |http://adpl.org.hk -.adult-sex-games.com ||adult-sex-games.com -adultfriendfinder.com -adultkeep.net/peepshow/members/main.htm ||advanscene.com ||advertfan.com -.ae.org ||aei.org ||aenhancers.com ||af.mil -.afantibbs.com |http://afantibbs.com ||afr.com -.ai-kan.net -||ai-kan.net -ai-wen.net -.aiph.net +||aiosearch.com ||aiph.net -.airasia.com ||airconsole.com |http://download.aircrack-ng.org -.airvpn.org ||airvpn.org -.aisex.com ||ait.org.tw -aiweiwei.com -.aiweiweiblog.com ||aiweiweiblog.com ||www.ajsands.com - !!---Akamai--- -a248.e.akamai.net ||a248.e.akamai.net - -rfalive1.akacast.akamaistream.net -voa-11.akacast.akamaistream.net - -!!--403 -||abematv.akamaized.net -||linear-abematv.akamaized.net -||vod-abematv.akamaized.net - |https://fbcdn*.akamaihd.net/ -!--||fbexternal-a.akamaihd.net -!--||fbstatic-a.akamaihd.net -!--|https://igcdn*.akamaihd.net -rthklive2-lh.akamaihd.net - -.akademiye.org/ug |http://akademiye.org/ug ||akiba-online.com ||akow.org -.al-islam.com -||al-qimmah.net ||alabout.com -.alanhou.com |http://alanhou.com -.alarab.qa ||alasbarricadas.org -alexlur.org ||alforattv.net -.alhayat.com -.alicejapan.co.jp -aliengu.com ||alive.bar ||alkasir.com ||all4mom.org ||allconnected.co -.alldrawnsex.com ||alldrawnsex.com -.allervpn.com ||allfinegirls.com -.allgirlmassage.com -allgirlsallowed.org -.allgravure.com -alliance.org.hk -.allinfa.com ||allinfa.com -.alljackpotscasino.com ||allmovie.com -||almasdarnews.com -.alphaporno.com ||alternate-tools.com -alternativeto.net/software -alvinalexander.com -alwaysdata.com ||alwaysdata.com ||alwaysdata.net -.alwaysvpn.com ||alwaysvpn.com ||am730.com.hk -ameblo.jp ||ameblo.jp -www1.american.edu/ted/ice/tibet ||americangreencard.com ||amiblockedornot.com -.amigobbs.net -.amitabhafoundation.us |http://amitabhafoundation.us -.amnesty.org ||amnesty.org ||amnesty.org.hk -.amnesty.tw -.amnestyusa.org ||amnestyusa.org -.amnyemachen.org -.amoiist.com -.amtb-taipei.org -androidplus.co/apk -.andygod.com |http://andygod.com -annatam.com/chinese ||anchor.fm ||anchorfree.com !--GHS ||ancsconf.org ||andfaraway.net ||android-x86.org -angelfire.com/hi/hayashi +||androidapksfree.com ||angularjs.org -animecrazy.net -aniscartujo.com ||aniscartujo.com ||anobii.com ||anonfiles.com -.anonymitynetwork.com -.anonymizer.com -.anonymouse.org ||anonymouse.org -anontext.com -.anpopo.com -.answering-islam.org |http://www.antd.org ||anthonycalzadilla.com -.anti1984.com -antichristendom.com -.antiwave.net |http://antiwave.net -.anyporn.com -.anysex.com |http://anysex.com -.ao3.org ||ao3.org ||aobo.com.au -.aofriend.com |http://aofriend.com -.aofriend.com.au -.aojiao.org ||aomiwang.com -video.ap.org ||apat1989.org -.apetube.com ||apiary.io -.apigee.com ||apigee.com ||apk.support -||apk-dl.com ||apkcombo.com -.apkmonk.com/app ||apkmonk.com ||apkplz.com ||apkpure.com ||apkpure.net -.aplusvpn.com -!--||appannie.com +||appadvice.com ||appbrain.com -.appdownloader.net/Android -.appledaily.com ||appledaily.com -appledaily.com.hk -||appledaily.com.hk -appledaily.com.tw ||appledaily.com.tw -.appshopper.com |http://appshopper.com ||appsocks.net ||appsto.re -.aptoide.com ||aptoide.com ||archives.gov -.archive.fo ||archive.fo -.archive.is +||archive.vn ||archive.is -.archive.li ||archive.li ||archive.md ||archive.org ||archive.ph -archive.today -|https://archive.today +||archive.today ||archiveofourown.com ||archiveofourown.org -.arctosia.com -|http://arctosia.com +||arctosia.com ||areca-backup.org -.arethusa.su ||arethusa.su ||arlingtoncemetery.mil -||army.mil -.art4tibet1998.org -artofpeacefoundation.org -artsy.net ||asacp.org -asdfg.jp/dabr -asg.to -.asia-gaming.com -.asiaharvest.org ||asiaharvest.org ||asianage.com ||asianews.it -|http://japanfirst.asianfreeforum.com/ ||asiansexdiary.com -||asianwomensfilm.de ||asiaone.com -.asiatgp.com -.asiatoday.us +||ask.com ||askstudent.com -.askynz.net ||askynz.net ||aspi.org.au ||aspistrategist.org.au ||assembla.com ||astrill.com ||atc.org.au -.atchinese.com |http://atchinese.com -atgfw.org -.atlaspost.com -||atlaspost.com -||atdmt.com -.atlanta168.com ||atlanta168.com -.atnext.com ||atnext.com ||audacy.com -ice.audionow.com -.av.com ||av.movie -.av-e-body.com -avaaz.org ||avaaz.org -!--||avast.com -.avbody.tv -.avcity.tv -.avcool.com -.avdb.in ||avdb.in -.avdb.tv ||avdb.tv -.avfantasy.com ||avg.com -.avgle.com ||avgle.com ||avidemux.org ||avoision.com -.avyahoo.com ||axios.com ||axureformac.com -.azerbaycan.tv -azerimix.com ||azirevpn.com !--boxun.azurewebsites.net doesn't exist. -boxun*.azurewebsites.net ||boxun*.azurewebsites.net - !--------------------BB------------------------- +||bittorrent.com +||help.byspotify.com +||bitbaby.com +||bettergpt.chat +||bt4gprx.com +||bt4g.org +||betterhash.net +||binance.org +||bitget.com +||blackmagicdesign.com +||bearteach.com +||btbtt.me +||btbtt.co +||btbit.net +||betaclouds.net +||blocktempo.com +||blockcast.it +||www.bing.com +||bangumi.moe ||b-ok.cc -forum.baby-kingdom.com ||babylonbee.com -babynet.com.hk -backchina.com ||backchina.com -.backpackers.com.tw/forum -backtotiananmen.com ||bad.news -.badiucao.com ||badiucao.com -.badjojo.com -badoo.com |http://*2.bahamut.com.tw ||baidu.jp -.baijie.org ||baijie.org ||bailandaily.com ||baixing.me ||baizhi.org -||bakgeekhome.tk -.banana-vpn.com ||banana-vpn.com ||band.us ||bandcamp.com -.bandwagonhost.com ||bandwagonhost.com -.bangbrosnetwork.com -.bangchen.net |http://bangchen.net ||bangkokpost.com ||bangyoulater.com -bannedbook.org ||bannedbook.org -.bannednews.org -.baramangaonline.com |http://baramangaonline.com -.barenakedislam.com ||barnabu.co.uk ||barton.de -.bastillepost.com ||bastillepost.com -bayvoice.net ||bayvoice.net -dajusha.baywords.com ||bbchat.tv ||bb-chat.tv -.bbg.gov -.bbkz.com/forum -.bbnradio.org -bbs-tw.com -.bbsdigest.com/thread -||bbsfeed.com -bbsland.com -.bbsmo.com -.bbsone.com -bbtoystore.com -.bcast.co.nz -.bcc.com.tw/board -.bcchinese.net -.bcmorning.com -bdsmvideos.net -.beaconevents.com -.bebo.com ||bebo.com -.beevpn.com ||beevpn.com -.behindkink.com ||beijing1989.com ||beijing2022.art -beijingspring.com ||beijingspring.com -.beijingzx.org -|http://beijingzx.org -.belamionline.com -.bell.wiki |http://bell.wiki -bemywife.cc -beric.me ||berlinerbericht.de -.berlintwitterwall.com ||berlintwitterwall.com -.berm.co.nz -.bestforchina.org -||bestforchina.org -.bestgore.com -.bestpornstardb.com ||bestvpn.com -.bestvpnanalysis.com -.bestvpnserver.com -.bestvpnservice.com -.bestvpnusa.com +||bestvpnanalysis.com +||bestvpnforchina.net +||bestvpnserver.com +||bestvpnservice.com +||bestvpnusa.com ||bet365.com -.betfair.com ||betternet.co -.bettervpn.com ||bettervpn.com -.bettween.com ||bettween.com ||betvictor.com -.bewww.net -.beyondfirewall.com ||bfnn.org ||bfsh.hk -.bgvpn.com ||bgvpn.com -.bianlei.com -@@||bianlei.com -biantailajiao.com -biantailajiao.in -.biblesforamerica.org -|http://biblesforamerica.org -.bic2011.org +||biblesforamerica.org +||vpl.bibliocommons.com ||biedian.me -bigfools.com ||bigjapanesesex.com -.bignews.org ||bignews.org -.bigsound.org ||bild.de -.biliworld.com |http://biliworld.com -|http://billypan.com/wiki -.binux.me -ai.binwang.me/couplet -.bit.do |http://bit.do -.bit.ly |http://bit.ly -!--||bitbucket.org ||bitchute.com ||bitcointalk.org -.bitshare.com ||bitshare.com -bitsnoop.com -.bitvise.com ||bitvise.com -bizhat.com ||bl-doujinsouko.com -.bjnewlife.org -.bjs.org -bjzc.org ||bjzc.org -.blacklogic.com -.blackvpn.com +||blacked.com ||blackvpn.com -blewpass.com -tor.blingblingsquad.net -.blinkx.com ||blinkx.com -blinw.com -.blip.tv -||blip.tv/ -||blockcast.it -.blockcn.com +||blip.tv ||blockcn.com ||blockedbyhk.com ||blockless.com ||blog.de -.blog.jp |http://blog.jp -@@||jpush.cn -.blogcatalog.com ||blogcatalog.com ||blogcity.me -.blogger.com ||blogger.com -blogimg.jp -||blog.kangye.org -.bloglines.com ||bloglines.com ||bloglovin.com -rconversation.blogs.com -blogtd.net -.blogtd.org |http://blogtd.org ||bloodshed.net -!--403 -||assets.bwbx.io - +||bootstrapcdn.com ||bloomfortune.com -blueangellive.com ||blubrry.com -.bmfinn.com -.bnews.co -||bnews.co +||bmdru.com ||bnext.com.tw ||bnrmetal.com -boardreader.com/thread ||boardreader.com -.bod.asia ||bod.asia -.bodog88.com -.bolehvpn.net ||bolehvpn.net -bonbonme.com -.bonbonsex.com -.bonfoundation.org -.bongacams.com ||boobstagram.com ||book.com.tw ||bookdepository.com -bookepub.com ||books.com.tw +||bookwalker.com.tw ||borgenmagazine.com ||botanwang.com -.bot.nu -.bowenpress.com ||bowenpress.com ||app.box.com -dl.box.net ||dl.box.net -.boxpn.com ||boxpn.com -boxun.com ||boxun.com -.boxun.tv ||boxun.tv -boxunblog.com -||boxunblog.com -.boxunclub.com -boyangu.com -.boyfriendtv.com -.boysfood.com ||br.st -.brainyquote.com/quotes/authors/d/dalai_lama -||brandonhutchinson.com ||braumeister.org ||brave.com -.bravotube.net ||bravotube.net -.brazzers.com ||brazzers.com ||breached.to -.break.com ||break.com -breakgfw.com ||breakgfw.com -breaking911.com -.breakingtweets.com ||breakingtweets.com ||breakwall.net -briian.com/6511/freegate -.briefdream.com/%E7%B4%A0%E6%A3%BA ||brill.com -brizzly.com ||brizzly.com -||brkmd.com -broadbook.com -.broadpressinc.com ||broadpressinc.com -bbs.brockbbs.com ||brookings.edu -brucewang.net -.brutaltgp.com ||brutaltgp.com ||bsky.app +||bsky.network ||bsky.social ||bt95.com -.btaia.com -.btbtav.com ||btdig.com -||btdigg.org -.btku.me +||btguard.com ||btku.me ||btku.org -.btspread.com -.btsynckeys.com -.budaedu.org ||budaedu.org -.buddhanet.com.tw/zfrop/tibet ||buffered.com ||bullguard.com -.bullog.org ||bullog.org -.bullogger.com ||bullogger.com ||bumingbai.net ||bunbunhk.com -.busayari.com |http://busayari.com ||business-humanrights.org -.businessinsider.com/bing-could-be-censoring-search-results-2014 -.businessinsider.com/china-banks-preparing-for-debt-implosion-2014 -.businessinsider.com/hong-kong-activists-defy-police-tear-gas-as-protests-continue-overnight-2014 -.businessinsider.com/internet-outages-reported-in-north-korea-2014 -.businessinsider.com/iphone-6-is-approved-for-sale-in-china-2014 -.businessinsider.com/nfl-announcers-surface-tablets-2014 -.businessinsider.com/panama-papers -.businessinsider.com/umbrella-man-hong-kong-2014 -|http://www.businessinsider.com.au/* -.businesstoday.com.tw ||businesstoday.com.tw -.busu.org/news |http://busu.org/news -busytrade.com -.buugaa.com -.buzzhand.com -.buzzhand.net -.buzzorange.com ||buzzorange.com ||buzzsprout.com ||bvpn.com ||bwh1.net -bwsj.hk -||bx.tl ||bypasscensorship.org - !--------------------CC------------------------- +||cia.gov +||claude.com +||www.clashverge.dev +||clementine-player.org +||backend-v2.crixet.com +||cchostvps.xyz +||canva.com +||chatpdf.com +||chat.com +||ctinets.com +||covenantswatch.org.tw +||cpu-monkey.com +||coffeemanga.to +||ctinews.com +||cachefly.com +||cachefly.net +||cutout.pro +||cixiaoya.club +||campaign-archive.com +||chinauncensored.tv +||catbox.moe +||crosswall.org +||clipconverter.cc +||zh-hans.cfsh99.com +||colacloud.net +||ci-en.jp ||c-span.org -.c-spanvideo.org ||c-spanvideo.org ||c-est-simple.com -.c100tibet.org ||cableav.tv ||cablegatesearch.net -.cachinese.com -.cacnw.com |http://cacnw.com -.cactusvpn.com ||cactusvpn.com -.cafepress.com -.cahr.org.tw -.caijinglengyan.com -||caijinglengyan.com -.calameo.com/books ||calendarz.com -.calgarychinese.ca -.calgarychinese.com -.calgarychinese.net -|http://blog.calibre-ebook.com -falun.caltech.edu -.its.caltech.edu/~falun/ -.cam4.com -.cam4.jp -.cam4.sg -.camfrog.com ||camfrog.com ||campaignforuyghurs.org ||cams.com -.cams.org.sg -canadameet.com -.canalporno.com |http://bbs.cantonese.asia/ -!--http://www.cantonese.asia/action-bbs.html -.canyu.org ||canyu.org -.cao.im -.caobian.info ||caobian.info -caochangqing.com ||caochangqing.com -.cap.org.hk ||cap.org.hk -.carabinasypistolas.com -cardinalkungfoundation.org +||caoporn.us ||posts.careerengine.us -carmotorshow.com ||carrd.co -ss.carryzhou.com -.cartoonmovement.com ||cartoonmovement.com -.casadeltibetbcn.org -.casatibet.org.mx |http://casatibet.org.mx -.cari.com.my ||cari.com.my ||caribbeancom.com -.casinoking.com -.casinoriva.com +||carousell.com.hk ||catch22.net -.catchgod.com |http://catchgod.com -||catfightpayperview.xxx -.catholic.org.hk ||catholic.org.hk -catholic.org.tw ||catholic.org.tw -.cathvoice.org.tw ||cato.org ||cattt.com -.cbc.ca +||caus.com ||cbc.ca -.cbsnews.com/video -.cbtc.org.hk ||southpark.cc.com -!-.ccc.de -!-||ccc.de ||cccat.cc ||cccat.co -.ccdtr.org -||ccdtr.org -.cchere.com +||ccfd.org.tw ||cchere.com -.ccim.org -.cclife.ca -cclife.org ||cclife.org -cclifefl.org ||cclifefl.org -.ccthere.com ||ccthere.com ||ccthere.net -.cctmweb.net -.cctongbao.com/article/2078732 -ccue.ca -ccue.com -.ccvoice.ca -.ccw.org.tw -.cgdepot.org |http://cgdepot.org ||cdbook.org -.cdcparty.com -.cdef.org ||cdef.org ||cdig.info -cdjp.org ||cdjp.org -!--.cdn-apple.com -!--||cdn-apple.com -.cdnews.com.tw -cdp1989.org -cdp1998.org ||cdp1998.org -cdp2006.org ||cdp2006.org -.cdpa.url.tw ||cdpeu.org ||cdpuk.co.uk -||cdpusa.org -||cdpweb.org ||cdpweb.org ||cdpwu.org ||cdw.com @@ -2814,184 +1608,80 @@ cdp2006.org ||cenews.eu ||centerforhumanreprod.com ||centralnation.com -.centurys.net |http://centurys.net -.cfhks.org.hk -.cfos.de ||cfr.org -.cftfc.com -.cgst.edu -.change.org ||change.org -.changp.com ||changp.com -.changsa.net -|http://changsa.net ||channelnewsasia.com -.chapm25.com +||chanworld.org +||chaos.social +||character.ai ||chatgpt.com -.chaturbate.com ||chaturbate.com -.chuang-yen.org ||checkgfw.com -chengmingmag.com -.chenguangcheng.com +||chengmingmag.com ||chenguangcheng.com -.chenpokong.com ||chenpokong.com -.chenpokong.net -|http://chenpokong.net ||chenpokongvip.com ||cherrysave.com -.chhongbi.org -chicagoncmtv.com -|http://chicagoncmtv.com -.china-week.com -china101.com +||chhongbi.org +||china-week.com ||china101.com ||china18.org ||china21.com -china21.org ||china21.org -.china5000.us -chinaaffairs.org +||china5000.us ||chinaaffairs.org -||chinaaid.me -chinaaid.us -chinaaid.org -chinaaid.net +||chinaaid.us +||chinaaid.org ||chinaaid.net -chinacomments.org -||chinacomments.org -.chinachange.org ||chinachange.org -chinachannel.hk ||chinachannel.hk -.chinacitynews.be -.chinadialogue.net -.chinadigitaltimes.net +||chinademocrats.org +||chinadialogue.net ||chinadigitaltimes.net -.chinaelections.org ||chinaelections.org -.chinaeweekly.com -||chinaeweekly.com ||chinafile.com ||chinafreepress.org -.chinagate.com -chinageeks.org -chinagfw.org ||chinagfw.org -.chinagonet.com -.chinagreenparty.org -||chinagreenparty.org -.chinahorizon.org ||chinahorizon.org -.chinahush.com -.chinainperspective.com -||chinainterimgov.org -chinalaborwatch.org -chinalawtranslate.com -.chinapost.com.tw/taiwan/national/national-news -chinaxchina.com/howto -chinalawandpolicy.com -.chinamule.com ||chinamule.com -chinamz.org -.chinanewscenter.com |https://chinanewscenter.com -.chinapress.com.my ||chinapress.com.my -.china-review.com.ua |http://china-review.com.ua -.chinarightsia.org -chinasmile.net/forums -chinasocialdemocraticparty.com ||chinasocialdemocraticparty.com -chinasoul.org ||chinasoul.org -.chinasucks.net ||chinatopsex.com -.chinatown.com.au -chinatweeps.com -chinaway.org -.chinaworker.info ||chinaworker.info -chinayouth.org.hk -chinayuanmin.org -||chinayuanmin.org -.chinese-hermit.net -chinese-leaders.org -chinese-memorial.org -.chinesedaily.com +||chinese-memorial.org ||chinesedailynews.com -.chinesedemocracy.com ||chinesedemocracy.com ||chinesegay.org -.chinesen.de ||chinesen.de -.chinesenews.net.au/ -.chinesepen.org +||chinesenews.net.au ||chineseradioseattle.com -.chinesetalks.net/ch ||chineseupress.com -.chingcheong.com ||chingcheong.com -.chinman.net |http://chinman.net -chithu.org ||cnnews.chosun.com -.chrdnet.com |http://chrdnet.com -.christianfreedom.org ||christianfreedom.org -christianstudy.com ||christianstudy.com -christusrex.org/www1/sdc -.chubold.com -chubun.com ||christiantimes.org.hk -.chrlawyers.hk ||chrlawyers.hk -.churchinhongkong.org/b5/index.php -|http://churchinhongkong.org/b5/index.php -.chushigangdrug.ch -.cienen.com -.cineastentreff.de -.cipfg.org -||circlethebayfortibet.org ||cirosantilli.com -.citizencn.com ||citizencn.com ||citizenlab.ca ||citizenlab.org -||citizenscommission.hk -.citizenlab.org -citizensradio.org -.city365.ca |http://city365.ca -city9x.com ||citypopulation.de -.citytalk.tw/event -.civicparty.hk ||civicparty.hk -.civildisobediencemovement.org -civilhrfront.org ||civilhrfront.org -.civiliangunner.com -.civilmedia.tw ||civilmedia.tw -psiphon.civisec.org ||civitai.com -.ck101.com ||ck101.com -.clarionproject.org/news/islamic-state-isis-isil-propaganda ||classicalguitarblog.net -.clb.org.hk -clearharmony.net -clearwisdom.net ||clinica-tibet.ru -.clipfish.de -cloakpoint.com ||app.cloudcone.com ||cloudflare-ipfs.com ||club1069.com @@ -2999,1681 +1689,673 @@ cloakpoint.com ||cmegroup.com ||cmi.org.tw |http://www.cmoinc.org -cmp.hku.hk -hkupop.hku.hk ||cmule.com -||cmule.org ||cms.gov |http://vpn.cmu.edu |http://vpn.sv.cmu.edu -.cn6.eu -.cna.com.tw ||cna.com.tw -.cnabc.com -.cnd.org ||cnd.org -download.cnet.com -.cnex.org.cn -.cnineu.com -wiki.cnitter.com -.cnn.com/video -.cnpolitics.org ||cnpolitics.org -.cn-proxy.com |http://cn-proxy.com -.cnproxy.com -blog.cnyes.com -news.cnyes.com ||coat.co.jp -.cochina.co -||cochina.co ||cochina.org -.code1984.com/64 -|http://goagent.codeplex.com ||codeshare.io ||codeskulptor.org +||cofacts.tw ||conoha.jp |http://tosh.comedycentral.com -comefromchina.com ||comefromchina.com -.comic-mega.me -commandarms.com ||commentshk.com -.communistcrimes.org ||communistcrimes.org ||communitychoicecu.com ||comparitech.com ||compileheart.com -||conoha.jp -.contactmagazine.net -.convio.net -.coobay.com ||cool18.com -.coolaler.com ||coolaler.com -coolder.com ||coolder.com ||coolloud.org.tw -.coolncute.com ||coolstuffinc.com -corumcollege.com -.cos-moe.com |http://cos-moe.com -.cosplayjav.pl |http://cosplayjav.pl -.cotweet.com ||cotweet.com -.coursehero.com ||coursehero.com -cpj.org ||cpj.org -.cq99.us |http://cq99.us -crackle.com ||crackle.com -.crazys.cc -.crazyshit.com ||crazyshit.com ||crchina.org -crd-net.org -creaders.net ||creaders.net -.creadersnet.com ||cristyli.com ||croxyproxy.com -.crocotube.com |http://crocotube.com -.crossthewall.net -||crossthewall.net -.crossvpn.net ||crossvpn.net ||crucial.com ||blog.cryptographyengineering.com -csdparty.com ||csdparty.com ||csis.org ||csmonitor.com ||csuchen.de -.csw.org.uk -.ct.org.tw +||csw.org.uk ||ct.org.tw -.ctao.org -.ctfriend.net -.ctitv.com.tw +||ctitv.com.tw ||ctowc.org -.cts.com.tw ||cts.com.tw ||ctwant.com -|http://library.usc.cuhk.edu.hk/ -|http://mjlsh.usc.cuhk.edu.hk/ -.cuhkacs.org/~benng -.cuihua.org -||cuihua.org -.cuiweiping.net +|http://library.usc.cuhk.edu.hk +|http://mjlsh.usc.cuhk.edu.hk ||cuiweiping.net ||culture.tw -.cumlouder.com ||cumlouder.com ||curvefish.com ||cusp.hk -.cusu.hk -||cusu.hk -.cutscenes.net ||cutscenes.net -.cw.com.tw ||cw.com.tw |http://forum.cyberctm.com -cyberghostvpn.com ||cyberghostvpn.com ||cynscribe.com -cytode.us ||ifan.cz.cc ||mike.cz.cc ||nic.cz.cc - !--------------------DD------------------------- -.d-fukyu.com +||dns.sb +||doh.sb +||dot.sb +||download.dappcdn.com +||dazn.com +||darmau.co +||dockerstatus.com +||doom9.org +||dweb.link +||docker.io +||disneyplus.com +||ddex.io +||d.cash +||doubiyunbackup.com +||cloud.dify.ai |http://d-fukyu.com -cl.d0z.net -.d100.net ||d100.net -.d2bay.com |http://d2bay.com -.dabr.co.uk ||dabr.co.uk -dabr.eu -dabr.mobi ||dabr.mobi ||dabr.me -dadazim.com ||dadazim.com -.dadi360.com -.dafabet.com -dafagood.com -dafahao.com -.dafoh.org -.daftporn.com -.dagelijksestandaard.nl -.daidostup.ru |http://daidostup.ru -.dailidaili.com -||dailidaili.com ||dailymail.co.uk -.dailymotion.com ||dailymotion.com ||dailysabah.com -daiphapinfo.net -.dajiyuan.com ||dajiyuan.de -dajiyuan.eu -dalailama.com -.dalailama.mn |http://dalailama.mn -.dalailama.ru ||dalailama.ru -dalailama80.org -.dalailama-archives.org -.dalailamacenter.org |http://dalailamacenter.org -dalailamafellows.org -.dalailamafilm.com -.dalailamafoundation.org -.dalailamahindi.com -.dalailamainaustralia.org -.dalailamajapanese.com -.dalailamaprotesters.info -.dalailamaquotes.org -.dalailamatrust.org -.dalailamavisit.org.nz -.dalailamaworld.com ||dalailamaworld.com -dalianmeng.org ||dalianmeng.org -.daliulian.org ||daliulian.org -.danke4china.net ||danke4china.net -daolan.net -darktoy.net ||darrenliuwei.com -||dastrassi.org -||daum.net -.david-kilgour.com +||dashlane.com |http://david-kilgour.com -daxa.cn ||daxa.cn -cn.dayabook.com -.daylife.com/topic/dalai_lama ||db.tt ||dbgjd.com ||dcard.tw -dcmilitary.com ||ddc.com.tw -.ddhw.info -||de-sci.org -.de-sci.org ||deadhouse.org ||deadline.com ||deepai.org ||decodet.co - -!--Origin:cdn-i30$_ -!--Exception: Homepage access without rst -!--Keyword is $_ -.definebabe.com - ||delcamp.net -delicious.com/GFWbookmark -.democrats.org -||democrats.org -.demosisto.hk ||demosisto.hk ||desc.se ||dessci.com -.destroy-china.jp ||deutsche-welle.de ||deviantart.com ||deviantart.net ||devio.us ||devpn.com ||devv.ai -||dfas.mil -dfn.org -dharmakara.net -.dharamsalanet.com -.diaoyuislands.org ||diaoyuislands.org -.difangwenge.org |http://digiland.tw/ -||digitalnomadsproject.org -.diigo.com ||diigo.com -||dilber.se -||furl.net -.dipity.com ||directcreative.com -!--||discogs.com -!--@@||cdn.discogs.com -.discuss.com.hk ||discuss.com.hk -.discuss4u.com -disp.cc -.disqus.com +||disp.cc ||disqus.com -.dit-inc.us ||dit-inc.us -.dizhidizhi.com +||diyin.org ||dizhuzhishang.com -djangosnippets.org -.djorz.com -||djorz.com ||dl-laby.jp ||dlive.tv ||dlsite.com ||dlyoutube.com ||dmc.nico ||dmcdn.net -.dnscrypt.org ||dnscrypt.org ||dns2go.com ||dnssec.net -doctorvoice.org - -!--DogFartNetwork -.dogfartnetwork.com/tour -gloryhole.com - -.dojin.com -.dok-forum.net ||dolc.de ||dolf.org.hk -||dollf.com -.domain.club.tw -.domaintoday.com.au -chinese.donga.com -dongtaiwang.com ||dongtaiwang.com -.dongtaiwang.net ||dongtaiwang.net -.dongyangjing.com -|http://danbooru.donmai.us -.dontfilter.us -||dontmovetochina.com -.dorjeshugden.com -.dotplane.com +||danbooru.donmai.us +||doosho.com +||doourbest.org ||dotplane.com ||dotsub.com -.dotvpn.com ||dotvpn.com -.doub.io ||doub.io ||doublethinklab.org ||dougscripts.com -||douhokanko.net ||doujincafe.com -dowei.org |https://bartender.dowjones.com -dphk.org -dpp.org.tw ||dpp.org.tw ||dpr.info ||dragonsprings.org -!--||draw.io -.dreamamateurs.com -.drepung.org ||drgan.net -.drmingxia.org -|http://drmingxia.org ||dropbooks.tv ||dropbox.com -||api.dropboxapi.com -||notify.dropboxapi.com +||dropboxapi.com ||dropboxusercontent.com -drsunacademy.com -.drtuber.com -.dscn.info |http://dscn.info -.dstk.dk |http://dstk.dk ||dtiblog.com ||dtic.mil -.dtwang.org -.duanzhihu.com -.duckdns.org -|http://duckdns.org -.duckduckgo.com ||duckduckgo.com -.duckload.com/download ||duckmylife.com -.duga.jp |http://duga.jp -.duihua.org ||duihua.org ||duihuahrjournal.org -.dunyabulteni.net -.duoweitimes.com -||duoweitimes.com -duping.net ||duplicati.com -dupola.com -dupola.net -.dushi.ca ||duyaoss.com ||dvorak.org -.dw.com ||dw.com ||dw.de -.dw-world.com ||dw-world.com -.dw-world.de |http://dw-world.de -www.dwheeler.com -dwnews.com ||dwnews.com -dwnews.net ||dwnews.net -xys.dxiong.com ||dynawebinc.com ||dysfz.cc -.dzze.com - !--------------------EE------------------------- +||economist.com +||e621.net +||edx-cdn.org +||everipedia.org +||epochtimes.com.tw +||etherscan.com +||elconfidencial.com ||e-classical.com.tw ||e-gold.com -.e-gold.com -.e-hentai.org ||e-hentai.org -.e-hentaidb.com |http://e-hentaidb.com -e-info.org.tw -.e-traderland.net/board -.e-zone.com.hk/discuz |http://e-zone.com.hk/discuz -.e123.hk ||e123.hk -.earlytibet.com |http://earlytibet.com -.earthcam.com -.earthvpn.com ||earthvpn.com -eastern-ark.com -.easternlightning.org -.eastturkestan.com +||eastasiaforum.org |http://www.eastturkistan.net/ -.eastturkistan-gov.org -.eastturkistancc.org -.eastturkistangovernmentinexile.us ||eastturkistangovernmentinexile.us -.easyca.ca -.easypic.com ||fnc.ebc.net.tw ||news.ebc.net.tw -.ebony-beauty.com -ebookbrowse.com -ebookee.com ||ecfa.org.tw -ushuarencity.echainhost.com ||ecimg.tw -ecministry.net -.economist.com -bbs.ecstart.com -edgecastcdn.net ||edgecastcdn.net -/twimg\.edgesuite\.net\/\/?appledaily/ -edicypages.com -.edmontonchina.cn -.edmontonservice.com -edoors.com -.edubridge.com ||edubridge.com -.edupro.org ||eevpn.com -efcc.org.hk -.efukt.com |http://efukt.com ||eic-av.com ||eireinikotaerukai.com -.eisbb.com -.eksisozluk.com ||eksisozluk.com -electionsmeter.com ||elgoog.im -.ellawine.org -.elpais.com ||elpais.com -.eltondisney.com -.emaga.com/info/3407 -emilylau.org.hk -.emanna.com/chineseTraditional -bitc.bme.emory.edu/~lzhou/blogs -.empfil.com -.emule-ed2k.com |http://emule-ed2k.com -.emulefans.com |http://emulefans.com -.emuparadise.me -.enanyang.my -!--.enanyang.my/news/20170502/%E7%BE%8E%E5%9B%BD%E4%B9%8B%E9%9F%B3%E5%A4%A7%E5%9C%B0%E9%9C%87%E3%80%8A%E8%8B%B9%E6%9E%9C%E3%80%8B%E7%8B%AC%E5%AE%B6 ||encrypt.me ||enewstree.com -.enfal.de ||chinese.engadget.com -||engagedaily.org -englishforeveryone.org ||englishfromengland.co.uk -englishpen.org -.enlighten.org.tw ||entermap.com -||app.evozi.com -.episcopalchurch.org -.epochhk.com ||epochhk.com -epochtimes-bg.com ||epochtimes-bg.com -epochtimes-romania.com ||epochtimes-romania.com -epochtimes.co.il ||epochtimes.co.il -epochtimes.co.kr ||epochtimes.co.kr -epochtimes.com ||epochtimes.com -.epochtimes.cz ||epochtimes.de ||epochtimes.fr -||epochtimes.ie ||epochtimes.it ||epochtimes.jp ||epochtimes.ru ||epochtimes.se ||epochtimestr.com -.epochweek.com ||epochweek.com ||epochweekly.com -.eporner.com -.equinenow.com -erabaru.net -.eracom.com.tw -.eraysoft.com.tr -.erepublik.com -.erights.net +||eporner.com ||erights.net -.erktv.com -|http://erktv.com ||ernestmandel.org ||erodaizensyu.com ||erodoujinlog.com ||erodoujinworld.com ||eromanga-kingdom.com ||eromangadouzin.com -.eromon.net |http://eromon.net -.eroprofile.com -.eroticsaloon.net -.eslite.com ||eslite.com -!--.eslite.com/product -!--.eslite.com/Search_BW.aspx?q -wiki.esu.im/%E8%9B%A4%E8%9B%A4%E8%AF%AD%E5%BD%95 -||esu.dog -.etaa.org.au -.etadult.com -etaiwannews.com ||etizer.org ||etokki.com ||etsy.com -!--.ettoday.net -.ettoday.net/news/20151216/614081 -etvonline.hk -.eu.org -||eu.org -.eucasino.com -.eulam.com -.eurekavpt.com ||eurekavpt.com -.euronews.com ||euronews.com -eeas.europa.eu/delegations/china/press_corner/all_news/news/2015/20150716_zh -eeas.europa.eu/statements-eeas/2015/151022 ||apps.evozi.com ||evschool.net -||exblog.jp -||blog.exblog.co.jp -@@||www.exblog.jp -.exchristian.hk ||exchristian.hk |http://blog.excite.co.jp ||exhentai.org ||exmormon.org ||expatshield.com -.expecthim.com ||expecthim.com -experts-univers.com ||exploader.net -.expressvpn.com ||expressvpn.com -.extremetube.com -eyevio.jp ||eyevio.jp -.eyny.com ||eyny.com -.ezpc.tk/category/soft -.ezpeer.com - !--------------------FF------------------------- +||freedom.gov +||hyperbeam.com +||flowgpt.com +||forefront.ai +||flexclip.com +||freegpt.tech +||freegpt.es +||feedly.com +||fuckccp.xyz +||fuckccp.com +||furrybar.com +||forbes.com +||financialexpress.com +||fast.com +||factchecklab.org +||ft.com +||fuchsia.dev +||freess.org +||fril.jp +||free.com.tw +||froth.zone +||fanbox.cc +||free.bg +||f-droid.org ||facebookquotes4u.com -.faceless.me ||faceless.me |http://facesoftibetanselfimmolators.info ||facesofnyfw.com ||factpedia.org -.faith100.org |http://faith100.org - -!--Enhancement: -!--http://faithfuleye.com.detail.website/ -!--http://faithfuleye.com.ipaddress.com/ -.faithfuleye.com - ||faiththedog.info -.fakku.net ||fallenark.com -.falsefire.com ||falsefire.com -falun-co.org -falunart.org ||falunasia.info |http://falunau.org -.falunaz.net -falundafa.org -falundafa-dc.org ||falundafa-florida.org ||falundafa-nc.org ||falundafa-pa.net -||falundafa-sacramento.org -falun-ny.net ||falundafaindia.org -falundafamuseum.org -.falungong.club -.falungong.de -falungong.org.uk ||falunhr.org -faluninfo.de -faluninfo.net -.falunpilipinas.net -||falunworld.net -familyfed.org -.fangeming.com ||fanglizhi.info ||fangong.org -fangongheike.com ||fanhaolou.com -.fanqiang.tk -fanqianghou.com ||fanqianghou.com -.fanqiangzhe.com ||fanqiangzhe.com ||fantv.hk -fapdu.com -faproxy.com -!--.farxian.com -.fawanghuihui.org -fanqiangyakexi.net -fail.hk ||famunion.com -.fan-qiang.com -.fangbinxing.com -||fangbinxing.com -fangeming.com -.fangmincn.org -||fangmincn.org -.fanhaodang.com ||fanqiang.network ||fanswong.com -.fanyue.info -.farwestchina.com - !--Fastly -en.favotter.net -!--||rnw.global.ssl.fastly.net -!--|https://*global.ssl.fastly.net/ -nytimes.map.fastly.net +||en.favotter.net +||global.ssl.fastly.net +||freetls.fastly.net ||nytimes.map.fastly.net ||fast.wistia.com - ||fastestvpn.com ||fastssh.com ||faststone.org -favstar.fm ||favstar.fm -faydao.com/weblog ||faz.net -.fc2.com -.fc2china.com -.fc2cn.com ||fc2cn.com -fc2blog.net |http://uygur.fc2web.com/ -video.fdbox.com -.fdc64.de -.fdc64.org -.fdc89.jp -||fourface.nodesnoop.com -!--feedbooks.mobi ||feeder.co ||feelssh.com -feer.com -.feifeiss.com |http://feitianacademy.org -.feitian-california.org ||feixiaohao.com ||feministteacher.com -.fengzhenghu.com ||fengzhenghu.com -.fengzhenghu.net ||fengzhenghu.net -.fevernet.com |http://ff.im -fffff.at -fflick.com -.ffvpn.com -fgmtv.net -.fgmtv.org -.fhreports.net |http://fhreports.net -.figprayer.com ||figprayer.com -.fileflyer.com ||fileflyer.com |http://feeds.fileforum.com -.files2me.com -.fileserve.com/file -fillthesquare.org -filmingfortibet.org -.filthdump.com -.finchvpn.com ||finchvpn.com -!--findbook.tw -findmespot.com ||findyoutube.com ||findyoutube.net -.fingerdaily.com -finler.net -.firearmsworld.net |http://firearmsworld.net ||relay.firefox.com -.fireofliberty.org +||fireofliberty.info ||fireofliberty.org -.firetweet.io ||firetweet.io +||open.firstory.me ||firstpost.com ||firstrade.com ||fish.audio -!--||flagfox.net -.flagsonline.it -fleshbot.com -.fleursdeslettres.com |http://fleursdeslettres.com -||flgg.us ||flgjustice.org - -!--||farm6.staticflickr.com -!--.flickr.com/photos/46231077@N06 -!--.flickr.com/groups/aiweiwei -!--.flickr.com/photos/digitalboy100 -!--.flickr.com/photos/fzhenghu -!--.flickr.com/photos/lonelyfox -!--flickr.com/photos/vanvan/529925157 -!--.flickr.com/photos/winterkanal -!--.flickr.com/photos/zola ||flickr.com ||staticflickr.com - -flickrhivemind.net -.flickriver.com -.fling.com ||flipkart.com ||flog.tw -.flyvpn.com +||flowhongkong.net ||flyvpn.com |http://cn.fmnnow.com -fofldfradio.org -blog.foolsmountain.com -.forum4hk.com -fangong.forums-free.com -pioneer-worker.forums-free.com -!--foursquare.com -!--|http://4sq.com |https://ss*.4sqi.net -video.foxbusiness.com |http://foxgay.com ||fringenetwork.com ||flecheinthepeche.fr -.fochk.org ||fochk.org ||focustaiwan.tw -.focusvpn.com ||fofg.org -.fofg-europe.net -.fooooo.com ||fooooo.com ||foreignaffairs.com -.fotile.me +||fountmedia.io ||fourthinternational.org -||foxdie.us ||foxsub.com -foxtang.com -.fpmt.org |http://fpmt.org -.fpmt.tw -.fpmt-osel.org ||fpmtmexico.org -fqok.org ||fqrouter.com +||frank2019.me ||franklc.com -.freakshare.com |http://freakshare.com -||free4u.com.ar -free-gate.org -.free-hada-now.org -free-proxy.cz -.free.fr/adsl -kineox.free.fr -tibetlibre.free.fr -||freealim.com -whitebear.freebearblog.org ||freebrowser.org -.freechal.com -.freedomchina.info -||freedomchina.info -.freedomhouse.org ||freedomhouse.org -.freedomsherald.org ||freedomsherald.org -.freefq.com -.freefuckvids.com -.freegao.com ||freegao.com -freeilhamtohti.org ||freekazakhs.org -.freekwonpyong.org -||saveliuxiaobo.com -.freelotto.com ||freelotto.com -freeman2.com -.freeopenvpn.com -freemoren.com -freemorenews.com -freemuse.org/archives/789 -freenet-china.org -freenewscn.com -cn.freeones.com -.freeoz.org/bbs ||freeoz.org ||freessh.us -free4u.com.ar -.free-ssh.com -||free-ssh.com ||freebeacon.com -.freechina.news -||freechinaforum.org ||freechinaweibo.com -.freedomcollection.org/interviews/rebiya_kadeer -.freeforums.org ||freenetproject.org -.freeoz.org -.freetibet.net ||freetibet.org -.freetibetanheroes.org |http://freetibetanheroes.org ||freetribe.me -.freeviewmovies.com -.freevpn.me |http://freevpn.me ||freewallpaper4.me -.freewebs.com -.freewechat.com ||freewechat.com -freeweibo.com ||freeweibo.com -.freexinwen.com -.freeyoutubeproxy.net -||freeyoutubeproxy.net -friendfeed.com -friendfeed-media.com/e99a4ebe2fb4c1985c2a58775eb4422961aa5a2e -friends-of-tibet.org -.friendsoftibet.org +||freezhihu.org +||friendfeed.com +||friends-of-tibet.org ||friendsoftibet.org -freechina.net -|http://www.zensur.freerk.com/ -freevpn.nl -freeyellow.com -hk.frienddy.com/hk -|http://adult.friendfinder.com/ -.fring.com +|http://www.zensur.freerk.com +|http://adult.friendfinder.com ||fring.com -.fromchinatousa.net ||frommel.net -.frontlinedefenders.org ||frontlinedefenders.org -.frootvpn.com ||frootvpn.com ||fscked.org -.fsurf.com -.ftv.com.tw ||ftv.com.tw ||ftvnews.com.tw -fucd.com -.fuckcnnic.net -||fuckcnnic.net -fuckgfw.org -.fulione.com |https://fulione.com ||fullerconsideration.com -fulue.com -.funf.tw -funp.com -.fuq.com -.furhhdl.org +||fullservicegame.com ||furinkan.com -.futurechinaforum.org ||futuremessage.org -.fux.com -.fuyin.net -.fuyindiantai.org -.fuyu.org.tw ||fw.cm -.fxcm-chinese.com ||fxcm-chinese.com -fzh999.com -fzh999.net -fzlm.com - !--------------------GG------------------------- -.g6hentai.com +||gmgn.ai +||grokipedia.com +||gfwbao.com +||greatfirevpn.com +||garudalinux.org +||about.gitlab.com +||gitlab.net +|http://gmp4.com +||getsession.org +||gdaily.org +||gfwatch.org +||go-to-zlibrary.se +||gitbook.io |http://g6hentai.com ||g-queen.com ||gab.com ||gabocorp.com -.gaeproxy.com -.gaforum.org -.gagaoolala.com ||gagaoolala.com -.galaxymacau.com ||galenwu.com -.galstars.net ||game735.com -gamebase.com.tw -gamejolt.com |http://wiki.gamerp.jp ||gamer.com.tw -.gamer.com.tw -.gamez.com.tw ||gamez.com.tw -.gamousa.com -.gaoming.net ||gaoming.net -ganges.com ||ganjing.com ||ganjingworld.com -.gaopi.net |http://gaopi.net -.gaozhisheng.org -.gaozhisheng.net -gardennetworks.com -||gardennetworks.org -!--IP of Garden Network -72.52.81.22 ||gartlive.com -||gate-project.com ||gather.com -.gatherproxy.com -gati.org.tw -.gaybubble.com -.gaycn.net -.gayhub.com ||gaymap.cc -.gaymenring.com -.gaytube.com -!--||gaytube.com ||images-gaytube.com -.gaywatch.com |http://gaywatch.com -.gazotube.com ||gazotube.com ||gcc.org.hk -||gclooney.com ||gclubs.com ||gcmasia.com -.gcpnews.com |http://gcpnews.com -.gdbt.net/forum -gdzf.org ||geek-art.net -geekerhome.com/2010/03/xixiang-project-cross-gfw -||geekheart.info -.gekikame.com -|http://gekikame.com -.gelbooru.com +||gekikame.com |http://gelbooru.com ||generated.photos ||genius.com -!--||genuitec.com -.geocities.co.jp -.geocities.com/SiliconValley/Circuit/5683/download.html -hk.geocities.com -geocities.jp ||geph.io -.gerefoundation.org ||getastrill.com -.getchu.com -.getcloak.com ||getcloak.com ||getfoxyproxy.org -.getfreedur.com ||getgom.com -.geti2p.net ||geti2p.net -getiton.com -.getjetso.com/forum -.getlantern.org ||getlantern.org ||getmalus.com -.getsocialscope.com ||getsync.com ||gettr.com -gfbv.de -.gfgold.com.hk -.gfsale.com ||gfsale.com -gfw.org.ua -.gfw.press ||gfw.press ||gfw.report -.ggssl.com ||ggssl.com -!--||ghost.org -.ghostpath.com ||ghostpath.com ||ghut.org -.giantessnight.com |http://giantessnight.com -.gifree.com ||giga-web.jp -tw.gigacircle.com -|http://cn.giganews.com/ -gigporno.ru ||girlbanker.com -.git.io ||git.io |http://softwaredownload.gitbooks.io ||raw.githack.com - !---GitHub--- ||github.blog ||github.com -!--github.com/getlantern -!--|https://gist.github.com -!--http://cthlo.github.io/hktv -!--hahaxixi.github.io -!--|https://hahaxixi.github.io -!--||haoel.github.io -!--|http://onionhacker.github.io -!--||rg3.github.io -!--||sikaozhe1997.github.io -!--||sodatea.github.io -!--||terminus2049.github.io -!--||toutyrater.github.io -!--wsgzao.github.io -!--|https://wsgzao.github.io -.github.io +||githubcopilot.com ||github.io ||githubusercontent.com ||githubassets.com - -.gizlen.net ||gizlen.net -.gjczz.com ||gjczz.com -globaljihad.net -globalmediaoutreach.com -globalmuseumoncommunism.org +||glarity.app +||globaljihad.net ||globalrescue.net -.globaltm.org -.globalvoicesonline.org ||globalvoicesonline.org ||globalvpn.net -.glock.com -gluckman.com/DalaiLama ||gmgard.com -||gmhz.org |http://www.gmiddle.com |http://www.gmiddle.net -.gmll.org ||suche.gmx.net ||gnci.org.hk ||gnews.org -go-pki.com ||goagent.biz -||goagentplus.com -gobet.cc ||godaddy.com -godfootsteps.org ||godfootsteps.org -godns.work -godsdirectcontact.co.uk -.godsdirectcontact.org -godsdirectcontact.org.tw -.godsimmediatecontact.com ||gofundme.com -.gogotunnel.com ||gohappy.com.tw -.gokbayrak.com -.goldbet.com ||goldbetsports.com ||golden-ages.org ||goldeneyevault.com -.goldenfrog.com ||goldenfrog.com -.goldjizz.com -|http://goldjizz.com -.goldstep.net ||goldwave.com -gongmeng.info -gongm.in -gongminliliang.com -.gongwt.com -|http://gongwt.com -blog.goo.ne.jp/duck-tail_2009 -.gooday.xyz +||gongm.in ||gooday.xyz ||goodhope.school -.goodreads.com +||goodnewsnetwork.org ||goodreads.com -.goodreaders.com ||goodreaders.com -.goodtv.com.tw -.goodtv.tv ||goofind.com -.googlesile.com -.gopetition.com ||gopetition.com -.goproxing.net ||goreforum.com -.gotrusted.com +||gotquestions.org ||gotrusted.com ||gotw.ca ||grammaly.com -grandtrial.org -.graphis.ne.jp ||graphis.ne.jp ||graphql.org ||gravatar.com -greatfirewall.biz -||greatfirewallofchina.net -.greatfirewallofchina.org ||greatfirewallofchina.org -||greenfieldbookstore.com.hk -.greenparty.org.tw ||greenpeace.org -.greenreadings.com/forum -great-firewall.com -great-roc.org -greatroc.org -greatzhonghua.org -.greenpeace.com.tw -.greenvpn.net +||greasyfork.org ||greenvpn.net -.greenvpn.org ||grindr.com ||ground.news -||grotty-monday.com -gs-discuss.com ||gsearch.media ||gtricks.com -guancha.org -guaneryu.com -.guardster.com -.gun-world.net -gunsandammo.com ||gutteruncensored.com ||gvm.com.tw ||gwins.org -.gzm.tv ||gzone-anime.info - -!-------------GHS----- -!-||feeds.cbsnews.com -!-||www.chinesealbumart.com -||clementine-player.org -!-||clemesha.org -!-||www.cloudgirlfriend.com -!-||cocoawithlove.com -!-||blog.controlspace.org -!-D -!-||www.dailygyan.com -!-||dailytodo.org -!-||blog.danmarner.com -!-||github.danmarner.com -!-||design-seeds.com -!-||designers-artists.com -!-||mail.diyang.org -!-||blog.doughellmann.com -!-||downforeveryoneorjustme.com -!-||droidsecurity.com -!-||www.dropmocks.com -!-||dumblittleman.com -!-E -echofon.com -!-||echofon.com -!-||epc-jav.com -!-||everdark.info -!-||evhead.com -!-F -!-||facilelogin.com -!-||*.fatduck.org -!-||blog.fdcn.org -!-||fftogo.com -!-||flightsimtalk.com -!-||mclee.foolme.net -!-||www.frienddeck.com -!-||fringespoilers.com -!-||fringetelevision.com -!-||funpea.com -!-G -!-||blog.gatein.org -!-||feeds.gawker.com -!-||geektang.com -!-||geohot.us -!-||getaround.com -!-||gmer.net -!-||www.gmote.org -!-||blog.go2web20.net -!-||google-melange.com -!-||fame.gonzolabs.org -!-||govecn.org -!-||gqueues.com -!-||graphycalc.com -!-||blog.growlforwindows.com -!-H -!-||hcm.com.tw -!-||blog.headius.com -!-||hogbaysoftware.com -!-||blog.hotot.org -!-||feeds.howstuffworks.com -!-||huhaitai.com -!-||blog.humanrightsfirst.org -!-I -!-||site.icu-project.org -!-||igorware.com -!-||ihas1337code.com -!-||inknouveau.com -!-||inote.tw -!-||ironhelmet.com -!-||iwfwcf.com -!-J -!-||blog.jangmt.com -!-||blog.jayfields.com -!-||blog.joint.net -!-||blog.jsquaredjavascript.com -!-||blog.jtbworld.com -!-K -!-||kathyschwalbe.com -!-||tomatovpn.keithmoyer.com -!-||www.keithmoyer.com -!-||kendalvandyke.com -!-||blog.kengao.tw -!-||log.keso.cn -!-||www.khanacademy.org -||www.klip.me -!-||usbloadergx.koureio.net -!-||blog.kowalczyk.info -!-L -!-||labyrinth2.com -!-||larsgeorge.com -!-||blog.lastpass.com -!-||docs.latexlab.org -!-||leanessays.com -!-||blog.lidaobing.info -!-||log.lightory.net -!-||feeds.limi.net -!-||www.liteapplications.com -!-||blog.liukangxu.info -!-||twitter.liukangxu.info -!-||oasisnewsroom.live4ever.us -!-||www.lockergnome.com -!-||locql.com -@@||site.locql.com -!-||feeds.loiclemeur.com -!-||blog.louisgray.com -!-M -!-||madebysofa.com -!-||mademoisellerobot.com -!-||masamixes.com -!-||www.metamuse.net -!-||blog.metasploit.com -!-||milazi.com -!-||www.miniweather.com -!-||twitter.missiu.com -!-||plurktop-button.mmdays.com -!-||feeds.mobileread.com -!-||www.modernizr.com -!-||www.modk.it -!-||mytwishirt.com -!-N -!-||blog.netflix.com -!-||blog.nihilogic.dk -!-||ntlk.org -!-||nvquan.org -!-||nogoodatcoding.com -!-||blog.notdot.net -!-||www.notify.io -!-O -!-||blog.obvious.com -!-||onebigfluke.com -!-||overstimulate.com -!-P -!-||pcgeekblog.com -!-||feeds.pdfchm.net -!-||feeds.people.com -!-||blog.persistent.info -!-||chrome.plantsvszombies.com -!-||portablesoft.org.ru -!-||prasannatech.net -!-||talk.news.pts.org.tw -!-||python-excel.org -!-Q -!-R -!-||r-chart.com -!-||rameshsubramanian.org -!-||rapid.pk -!-||blog.renanse.com -!-||robertmao.com -!-||www.romeo-foxtrot.com -!-S -!-||salmiyuck.com -!-||samsal.com -!-||blog.seeminglee.com -!-||blog.sflow.com -!-||blog.sigfpe.com -!-||simpletext.ws -!-||www.skulpt.org -!-||rss.slashdot.org -!-||snippetsapp.com -!-||w.sns.ly -!-||www.socialnmobile.com -!-||www.socialwhois.com -!-||spiritjb.org -!-||ssbook.com -!-||sshforwarding.com -!-||stationeria.com -||stephaniered.com -!-||sunjidong.net -!-||syniumsoftware.com -@@||download.syniumsoftware.com -!-T -!-||tagxedo.com -!-||blog.tatoeba.org -!-||www.techfob.com -!-||teachparentstech.org -!-||the8pen.com -!-||theiphonewiki.com -!-||blog.thesilentnumber.me -!-||thesponty.com -!-||theultralinx.com -!-||blog.think-async.com -!-||tornadoweb.org -!-||transparentuptime.com -!-||triangulationblog.com -!-||blog.tsunanet.net -!-||en.tuxero.com -!-||twazzup.com -!-||tweetswell.com -!-||twibes.com -!-||art.twgg.org -!-||twivert.com -!-U |http://ub0.cc -!-||jonny.ubuntu-tw.net -!-||blog.umonkey.net -!-V -!-||tp.vbap.com.au -!-||www.virtuousrom.com -!-||blog.visibotech.com -!-W -!-||waveprotocol.org -!-||www.wavesandbox.com -!-||webfee.org.ru -!-||blog.webmproject.org -!-||webupd8.org -!-||www.whatbrowser.org -!-||www.wheredoyougo.net -!-||willhains.com -!-||feeds.wired.com -!-||wisemapping.org -wozy.in -!-||wozy.in/ -!-||blog.wundercounter.com -!-X -!-||xdelta.org -!-||xiaogaozi.org -!-||xilou.us -!-||xzy.org.ru -!-Y -!-||yooper.be -!-||tsong.yunxi.net -!-Z - -gospelherald.com ||gospelherald.com |http://hk.gradconnection.com/ -||grangorz.org -greatfire.org ||greatfire.org -greatfirewallofchina.org -||greatroc.tw -.gts-vpn.com -|http://gts-vpn.com ||gtv.org ||gtv1.org -.gu-chu-sum.org |http://gu-chu-sum.org -.guaguass.com |http://guaguass.com -.guaguass.org -|http://guaguass.org -.guangming.com.my -guishan.org ||guishan.org -.gumroad.com ||gumroad.com ||gunsamerica.com -guruonline.hk |http://gvlib.com -.gyalwarinpoche.com -.gyatsostudio.com - !--------------------HH------------------------- -.h528.com -.h5dm.com -.h5galgame.me +||v2.hysteria.network +||hembed.com +||www.hoyolab.com +||hbomax.com +||hicairo.com +||herominers.com +||hinet.net +||hindustantimes.com +||hanime1.me +||halktv.com.tr +||haiwaikan.com +||home.saxo +||hoy.tv ||h-china.org -.h-moe.com |http://h-moe.com -h1n1china.org -.hacken.cc/bbs -.hacker.org ||hackmd.io ||hackthatphone.net -hahlo.com ||haijiao.com ||hakkatv.org.tw -.handcraftedsoftware.org |http://bbs.hanminzu.org/ -.hanunyi.com -.hao.news/news -|http://ae.hao123.com -|http://ar.hao123.com -|http://br.hao123.com -|http://en.hao123.com -|http://id.hao123.com -|http://jp.hao123.com -|http://ma.hao123.com -|http://mx.hao123.com -|http://sa.hao123.com -|http://th.hao123.com -|http://tw.hao123.com -|http://vn.hao123.com -|http://hk.hao123img.com -|http://ld.hao123img.com -||happy-vpn.com -.haproxy.org ||hardsextube.com -.harunyahya.com -|http://harunyahya.com -bbs.hasi.wang -have8.com -@@||haygo.com -.hclips.com -||hdlt.me +||b.hatena.ne.jp ||hdtvb.net -.hdzog.com |http://hdzog.com ||ordns.he.net ||heartyit.com -.heavy-r.com -.hec.su |http://hec.su -.hecaitou.net ||hecaitou.net -.hechaji.com ||hechaji.com ||heeact.edu.tw -.hegre-art.com |http://hegre-art.com -||cdn.helixstudios.net -||helplinfen.com -||helpuyghursnow.org +||helixstudios.net ||helloandroid.com ||helloqueer.com -.helloss.pw -hellotxt.com -||hellotxt.com -.hentai.to -.hellouk.org/forum/lofiversion -.helpeachpeople.com ||helpeachpeople.com ||helpster.de -.helpzhuling.org -hentaitube.tv -.hentaivideoworld.com - -!###########--Heroku--########## -!--||getcloudapp.com -!--||cl.ly -!--@@||f.cl.ly -!--EC2 DNS Poisoned ||id.heroku.com ||herokuapp.com - ||heqinglian.net ||heritage.org -||heungkongdiscuss.com -.hexieshe.com ||hexieshe.com ||hexieshe.xyz -!--Google employee within Google IP ||hexxeh.net ||heyuedi.com -app.heywire.com -.heyzo.com -.hgseav.com -.hhdcb3office.org -.hhthesakyatrizin.org -hi-on.org.tw ||hiccears.com -hidden-advent.org ||hidden-advent.org -hidecloud.com/blog/2008/07/29/fuck-beijing-olympics.html ||hide.me -.hidein.net -.hideipvpn.com ||hideipvpn.com -.hideman.net ||hideman.net -hideme.nl ||hidemy.name -.hidemyass.com ||hidemyass.com -hidemycomp.com ||hidemycomp.com -.hihiforum.com -.hihistory.net -||hihistory.net -.higfw.com -highpeakspureearth.com ||highrockmedia.com ||hiitch.com ||hikinggfw.org -.hilive.tv -.himalayan-foundation.org ||himalayan-foundation.org -himalayanglacier.com -.himemix.com ||himemix.com -.himemix.net -times.hinet.net -.hitomi.la |http://hitomi.la -.hiwifi.com -@@||hiwifi.com -hizbuttahrir.org -hizb-ut-tahrir.info -hizb-ut-tahrir.org -.hjclub.info -.hk-pub.com/forum |http://hk-pub.com -.hk01.com ||hk01.com -.hk32168.com -||hk32168.com ||hkacg.com ||hkacg.net -.hkatvnews.com -hkbc.net -.hkbf.org -.hkbookcity.com ||hkbookcity.com ||hkchronicles.com -.hkchurch.org -hkci.org.hk -.hkcmi.edu ||hkcnews.com ||hkcoc.com -||hkctu.org.hk -hkday.net -.hkdailynews.com.hk/china.php ||hkdc.us -hkdf.org -.hkej.com -.hkepc.com/forum/viewthread.php?tid=1153322 ||hket.com ||hkfaa.com -hkfreezone.com -hkfront.org -m.hkgalden.com |https://m.hkgalden.com -.hkgreenradio.org/home ||hkgpao.com -.hkheadline.com*blog -.hkheadline.com/instantnews -hkhkhk.com -hkhrc.org.hk -hkhrm.org.hk -||hkip.org.uk -1989report.hkja.org.hk -hkjc.com -.hkjp.org -.hklft.com -.hklts.org.hk ||hklts.org.hk ||hkmap.live ||hkopentv.com ||hkpeanut.com -hkptu.org -.hkreporter.com ||hkreporter.com -|http://hkupop.hku.hk/ -.hkusu.net -||hkusu.net -.hkvwet.com -.hkwcc.org.hk -||hkzone.org -.hmonghot.com -|http://hmonghot.com -.hmv.co.jp/ -hnjhj.com ||hnjhj.com -.hnntube.com ||hojemacau.com.mo ||hola.com ||hola.org -holymountaincn.com -holyspiritspeaks.org ||holyspiritspeaks.org -||derekhsu.homeip.net -.homeperversion.com |http://homeservershow.com |http://old.honeynet.org/scans/scan31/sub/doug_eric/spam_translation.html -.hongkongfp.com ||hongkongfp.com -hongmeimei.com ||hongzhi.li ||honven.xyz -.hootsuite.com ||hootsuite.com ||hoover.org -.hopedialogue.org -|http://hopedialogue.org -.hopto.org -.hornygamer.com -.hornytrip.com |http://hornytrip.com ||horrorporn.com ||hostloc.com ||hotair.com -.hotav.tv -.hotels.cn -hotfrog.com.tw -hotgoo.com -.hotpornshow.com -hotpot.hk -.hotshame.com ||hotspotshield.com ||hottg.com -.hotvpn.com ||hotvpn.com -||hougaige.com ||howtoforge.com ||hoxx.com ||hpjav.com -.hqcdp.org ||hqcdp.org ||hqjapanesesex.com -hqmovies.com -.hrcir.com -.hrcchina.org -.hrea.org -.hrichina.org ||hrichina.org ||hrntt.org -.hrtsea.com -.hrw.org ||hrw.org -hrweb.org ||hsex.men ||hsjp.net ||hsselite.com ||hst.net.tw -.hstern.net -.hstt.net -.htkou.net ||htkou.net -.hua-yue.net -.huaglad.com ||huaglad.com -.huanghuagang.org ||huanghuagang.org -.huangyiyu.com -.huaren.us ||huaren.us -.huaren4us.com -.huashangnews.com |http://huashangnews.com -bbs.huasing.org -huaxia-news.com -huaxiabao.org -huaxin.ph ||huayuworld.org ||huffingtonpost.com ||huffpost.com @@ -4681,144 +2363,77 @@ huaxin.ph ||hugoroy.eu ||huhaitai.com ||huhamhire.com -.huhangfei.com ||huhangfei.com -huiyi.in -.hulkshare.com ||humanparty.me ||humanrightspressawards.org ||hung-ya.com -||hungerstrikeforaids.org ||huping.net -hurgokbayrak.com -.hurriyet.com.tr -.hut2.ru ||hutianyi.net -hutong9.net -huyandex.com -.hwadzan.tw ||hwayue.org.tw -||hwinfo.com ||hxwk.org -hxwq.org ||hyperrate.com -ebook.hyread.com.tw +||hypothes.is ||ebook.hyread.com.tw - !--------------------II------------------------- -||i1.hk +||investing.com +||idcflare.com +||interseclab.org +||ipify.org +||itiger.com +||itch.io +||infura.io +||president.ir +||gov.ir +||irna.ir +||arvanstorage.ir +||irangov.ir +||india.com +||indiatoday.in +||invidio.us +||improd.works +||illawarramercury.com.au +||imago-images.com ||i2p2.de -||i2runner.com ||i818hk.com -.i-cable.com -.i-part.com.tw -.iamtopone.com -iask.ca ||iask.ca -iask.bz -||iask.bz -.iav19.com ||iavian.net -ibiblio.org/pub/packages/ccic -||ibit.am -.iblist.com -||iblogserv-f.net -ibros.org -|http://cn.ibtimes.com -.ibvpn.com ||ibvpn.com -icams.com ||icedrive.net -.icij.org ||icij.org ||icl-fi.org -.icoco.com ||icoco.com - -!--38.103.165.50 ||furbo.org -!--||iconfactory.com -||warbler.iconfactory.net - ||iconpaper.org -!-- Google Pages ||icu-project.org -w.idaiwan.com/forum -idemocracy.asia -.identi.ca ||identi.ca ||idiomconnection.com |http://www.idlcoyote.com -.idouga.com -.idreamx.com -forum.idsam.com -.idv.tw -.ieasy5.com -|http://ieasy5.com -.ied2k.net -.ienergy1.com -||iepl.us +||idope.se ||ift.tt -ifanqiang.com -.ifcss.org ||ifcss.org -ifjc.org -.ift.tt |http://ift.tt ||ifreewares.com ||igcd.net -.igfw.net ||igfw.net -.igfw.tech -||igfw.tech -.igmg.de -||ignitedetroit.net -.igotmail.com.tw ||igvita.com -||ihakka.net -.ihao.org/dz5 ||iicns.com -.ikstar.com ||ilhamtohtiinstitute.org ||illusionfactory.com ||ilove80.be -||im.tv -@@||myvlog.im.tv ||im88.tw ||imgchili.net -.imageab.com -.imagefap.com ||imagefap.com ||imageflea.com ||imageglass.org ||imageshack.us ||imagevenue.com ||imagezilla.net -.imb.org |http://imb.org - !--IMDB -|http://www.imdb.com/name/nm0482730 -.imdb.com/title/tt0819354 -.imdb.com/title/tt1540068 -.imdb.com/title/tt4908644 - -.img.ly ||img.ly ||imgasd.com -.imgur.com ||imgur.com -.imkev.com ||imkev.com -.imlive.com -.immoral.jp -impact.org.au -impp.mn -|http://tech2.in.com/video/ -in99.org -in-disguise.com -.incapdns.net -.incloak.com ||incloak.com ||incredibox.fr ||independent.co.uk @@ -4826,17 +2441,13 @@ in-disguise.com ||indiandefensenews.in ||indianarrative.com ||timesofindia.indiatimes.com -.indiemerch.com ||indiemerch.com ||info-graf.fr -website.informer.com ||inherit.live ||initiativesforchina.org ||inkbunny.net ||inkui.com ||inmediahk.net -||inmediahk.net -||innermongolia.org ||inoreader.com ||inote.tw ||insecam.org @@ -4846,1398 +2457,709 @@ website.informer.com ||institut-tibetain.org ||interactivebrokers.com ||internet.org -internetdefenseleague.org ||internetfreedom.org -!--||interpol.int ||internetpopculture.com -.inthenameofconfuciusmovie.com ||inthenameofconfuciusmovie.com -inxian.com ||inxian.com -ipalter.com -!--||ipcf.org.tw +||ipdefenseforum.com ||ipfire.org ||iphone4hongkong.com -||iphonehacks.com ||iphonetaiwan.org ||iphonix.fr ||ipicture.ru -.ipjetable.net ||ipjetable.net -.ipobar.com/read.php? -ipoock.com/img -.iportal.me |http://iportal.me ||ippotv.com -.ipredator.se ||ipredator.se -.iptv.com.tw ||iptvbin.com ||ipvanish.com -iredmail.org -chinese.irib.ir -||ironbigfools.compython.net ||ironpython.net -.ironsocket.com ||ironsocket.com -.is.gd -.islahhaber.net -.islam.org.hk +||ishr.ch |http://islam.org.hk -.islamawareness.net/Asia/China -.islamhouse.com ||islamhouse.com -.islamicity.com -.islamicpluralism.org -.islamtoday.net -.isaacmao.com ||isaacmao.com ||isgreat.org ||ismaelan.com -.ismalltits.com ||ismprofessional.net -isohunt.com ||israbox.com -.issuu.com ||issuu.com -.istars.co.nz -oversea.istarshine.com ||oversea.istarshine.com -blog.istef.info/2007/10/21/myentunnel -.istiqlalhewer.com -.istockphoto.com -isunaffairs.com -isuntv.com ||isupportuyghurs.org -itaboo.info -||itaboo.info ||italiatibet.org ||itemfix.com -ithelp.ithome.com.tw ||itshidden.com -.itsky.it -.itweet.net |http://itweet.net -.iu45.com -.iuhrdf.org ||iuhrdf.org -.iuksky.com -.ivacy.com ||ivacy.com -.iverycd.com ||ivonblog.com -.ivpn.net ||ivpn.net ||iwara.tv ||ixquick.com -.ixxx.com -.iyouport.com ||iyouport.com ||iyouport.org -.izaobao.us -||gmozomg.izihost.org -.izles.net -.izlesem.org - !--------------------JJ------------------------- +||jhelab.org +||justmysockscn.com +||justmysocks.net +||jav321.com +||javdb.com +||jifangge.com ||j.mp ||jable.tv ||blog.jackjia.com -jamaat.org ||jamestown.org ||jamyangnorbu.com ||jan.ai -.jandyx.com -||janwongphoto.com ||japan-whores.com -.jav.com -.jav101.com -.jav2be.com -||jav2be.com -.jav68.tv -.javakiba.org -|http://javakiba.org -.javbus.com +||japanhdv.com +||javakiba.org ||javbus.com +||javfinder.ai ||javfor.me -.javhd.com -.javhip.com -.javmobile.net -|http://javmobile.net -.javmoo.com -.javseen.com -|http://javseen.com -jbtalks.cc -jbtalks.com -jbtalks.my -.jdwsy.com -jeanyim.com -||jfqu36.club -||jfqu37.xyz +||javmobile.net +||javseen.com ||jgoodies.com -.jiangweiping.com ||jiangweiping.com ||jiaoyou8.com -||jichangtj.com -.jiehua.cz ||hk.jiepang.com ||tw.jiepang.com -jieshibaobao.com -.jigglegifs.com -56cun04.jigsy.com -jigong1024.com -daodu14.jigsy.com -specxinzl.jigsy.com -wlcnew.jigsy.com -.jihadology.net |http://jihadology.net -jinbushe.org -||jinbushe.org -.jingsim.org -zhao.jinhai.de -jingpin.org ||jingpin.org -jinpianwang.com -.jinroukong.com -ac.jiruan.net +||jinrizhiyi.news ||jitouch.com -.jizzthis.com -jjgirls.com -.jkb.cc |http://jkb.cc -jkforum.net ||jma.go.jp -research.jmsc.hku.hk/social -weiboscope.jmsc.hku.hk -.jmscult.com +||jmsc.hku.hk |http://jmscult.com ||joachims.org -||jobso.tv -.sunwinism.joinbbs.net ||joinclubhouse.com ||jornaldacidadeonline.com.br -.journalchretien.net ||journalofdemocracy.org -.joymiihub.com -.joyourself.com -jpopforum.net ||jsdelivr.net ||fiddle.jshell.net -.jubushoushen.com -||jubushoushen.com -!--Doamin parking -.juhuaren.com ||juliereyc.com ||junauza.com -.june4commemoration.org -.junefourth-20.net -||junefourth-20.net ||bbs.junglobal.net -.juoaa.com |http://juoaa.com -justfreevpn.com ||justhost.ru -.justicefortenzin.org -justpaste.it ||justmysocks1.net -justtristan.com -juyuange.org -juziyue.com ||juziyue.com -||jwmusic.org -@@||music.jwmusic.org ||cdn.jwplayer.com -.jyxf.net - !--------------------KK------------------------- -||k-doujin.net +||kingkong.com.tw +||kanald.com.tr +||kpkuang.org ||ka-wai.com ||kadokawa.co.jp -.kagyu.org ||kagyu.org.za -.kagyumonlam.org -.kagyunews.com.hk -.kagyuoffice.org ||kagyuoffice.org ||kagyuoffice.org.tw -.kaiyuan.de -.kakao.com ||kakao.com -.kalachakralugano.org -.kankan.today -.kannewyork.com ||kannewyork.com -.kanshifang.com ||kanshifang.com ||kantie.org -kanzhongguo.com -kanzhongguo.eu -.kaotic.com ||kaotic.com ||karayou.com -karkhung.com -.karmapa.org -.karmapa-teachings.org ||kawase.com -.kba-tx.org -.kcoolonline.com -.kebrum.com ||kebrum.com -.kechara.com -.keepandshare.com/visit/visit_page.php?i=688154 -!--||keepvid.com -.keezmovies.com -.kendincos.net -.kenengba.com ||kenengba.com -||keontech.net -.kepard.com ||kepard.com -wiki.keso.cn/Home ||keycdn.com -.khabdha.org -.khmusic.com.tw ||kichiku-doujinko.com -.kik.com ||kik.com -bbs.kimy.com.tw -.kindleren.com |http://kindleren.com |http://www.kindleren.com -.kingdomsalvation.org ||kingdomsalvation.org -kinghost.com -!--.kingstone.com.tw/book/ ||kingstone.com.tw -.kink.com -.kinokuniya.com ||kinokuniya.com -killwall.com ||killwall.com +||kindle4rss.com ||kinmen.travel -.kir.jp -.kissbbao.cn |http://kiwi.kz ||kk-whys.co.jp -!--||kmt.org.tw -.kmuh.org.tw -.knowledgerush.com/kr/encyclopedia ||knowyourmeme.com -.kobo.com ||kobo.com -.kobobooks.com ||kobobooks.com -||kodingen.com -@@||www.kodingen.com ||kompozer.net -.konachan.com ||konachan.com -.kone.com ||koolsolutions.com -.koornk.com ||koornk.com ||koranmandarin.com -.korenan2.com ||kqes.net |http://gojet.krtco.com.tw -.ksdl.org -.ksnews.com.tw ||ktzhk.com -.kui.name/event +||kuaichedao.co ||kukuku.uk -kun.im -.kurashsultan.com ||kurtmunger.com -kusocity.com ||kwcg.ca -||kwok7.com -.kwongwah.com.my ||kwongwah.com.my -.kxsw.life ||kxsw.life -.kyofun.com -kyohk.net -||kyoyue.com -.kyzyhello.com -||kyzyhello.com -.kzeng.info +||kzaobao.com ||kzeng.info - !--------------------LL------------------------- -la-forum.org -ladbrokes.com +||lovart.ai +||library-access.sk +||linux.do +||lmarena.ai +||lexica.art +||luckymobile.ca +||ludepress.com +||lingualeo.com +||ldplayer.tw +||ldplayer.net +||ltn.com.tw +||litenews.hk +||www.lorenzetti.com.br +||linktr.ee ||labiennale.org -.lagranepoca.com ||lagranepoca.com ||lala.im -.lalulalu.com -.lama.com.tw ||lama.com.tw -.lamayeshe.com -|http://lamayeshe.com -|http://www.lamenhu.com -.lamnia.co.uk +||lamayeshe.com ||lamnia.co.uk -lamrim.com ||landofhope.tv -.lanterncn.cn -|http://lanterncn.cn -.lantosfoundation.org -.laod.cn -|http://laod.cn -laogai.org ||laogai.org ||laogairesearch.org -laomiu.com -.laoyang.info -|http://laoyang.info -||laptoplockdown.com -.laqingdan.net ||laqingdan.net ||larsgeorge.com -.lastcombat.com |http://lastcombat.com ||lastfm.es -latelinenews.com ||lausan.hk ||le-vpn.com -.leafyvpn.net ||leafyvpn.net ||ledger.com -leeao.com.cn/bbs/forum.php -!--||leecheukyan.org -lefora.com ||left21.hk -.legalporno.com -.legsjapan.com -|http://leirentv.ca -leisurecafe.ca ||lematin.ch -.lemonde.fr ||lenwhite.com -||leorockwell.com -lerosua.org -||lerosua.org -blog.lester850.info ||lesoir.be -.letou.com -letscorp.net ||letscorp.net -||ocsp.int-x3.letsencrypt.org -||ss.levyhsu.com -!69.16.175.42 -||cdn.assets.lfpcontent.com -.lhakar.org |http://lhakar.org -.lhasocialwork.org -.liangyou.net ||liangyou.net -.lianyue.net ||liaowangxizang.net -.liaowangxizang.net ||liberal.org.hk ||libertysculpturepark.com ||libertytimes.com.tw -blogs.libraryinformationtechnology.com/jxyz ||libredd.it ||lighten.org.tw ||lightnovel.cn -limiao.net -linkuswell.com -abitno.linpie.com/use-ipv6-to-fuck-gfw +||lilaoshibushinilaoshi.com ||line.me ||line-apps.com -.linglingfa.com ||lingvodics.com -.link-o-rama.com |http://link-o-rama.com ||linkedin.com -.linkideo.com -||api.linksalpha.com -||apidocs.linksalpha.com -||www.linksalpha.com -||help.linksalpha.com ||linux.org.hk -linuxtoy.org/archives/installing-west-chamber-on-ubuntu -.lionsroar.com -.lipuman.com ||liquidvpn.com ||greatfire.us7.list-manage.com ||listennotes.com ||listentoyoutube.com -listorious.com -.liu-xiaobo.org -||liudejun.com -.liuhanyu.com -.liujianshu.com -||liujianshu.com -.liuxiaobo.net ||liuxiaobo.net -liuxiaotong.com ||liuxiaotong.com -.livedoor.jp -.liveleak.com ||liveleak.com ||livemint.com -livestream.com ||livestream.com -||livingonline.us ||livingstream.com ||livevideo.com -.livevideo.com -.liwangyang.com -lizhizhuangbi.com -lkcn.net ||chat.lmsys.org -||lncn.org -.load.to -.lobsangwangyal.com -.localdomain.ws ||localdomain.ws -localpresshk.com ||lockestek.com -logbot.net -||logiqx.com -secure.logmein.com ||secure.logmein.com ||logos.com.hk -.londonchinese.ca -.longhair.hk -longmusic.com ||longtermly.net ||lookpic.com -.looktoronto.com |http://looktoronto.com -.lotsawahouse.org/tibetan-masters/fourteenth-dalai-lama -.lotuslight.org.hk -.lotuslight.org.tw -hkreporter.loved.hk -!--403? -||lpsg.com -||lrfz.com -.lrip.org ||lrip.org -.lsd.org.hk ||lsd.org.hk -lsforum.net -.lsm.org ||lsm.org -.lsmchinese.org ||lsmchinese.org -.lsmkorean.org ||lsmkorean.org -.lsmradio.com/rad_archives -.lsmwebcast.com -.ltn.com.tw -||ltn.com.tw ||luckydesigner.space -.luke54.com -.luke54.org -.lupm.org ||lupm.org ||lushstories.com -luxebc.com -lvhai.org ||lvhai.org ||lvv2.com -.lyfhk.net |http://lyfhk.net ||lzjscript.com -.lzmtnews.org ||lzmtnews.org - !--------------------MM------------------------- -http://*.m-team.cc -!--m-team.cc/forum -.macrovpn.com -macts.com.tw +||mistral.ai +||manus.im +||meee.com.tw +||mosavi.io +||dcs-spotify.megaphone.fm +||mij.rip +||mji.rip +||mjj.rip +||mcusercontent.com +||metamask.io +||missav.ws +||news.mt.co.kr +||musixmatch.com +||mergersandinquisitions.com +||m.moegirl.org +||myjs.tw +||mercari.com +||mercari.jp +||mirror.xyz +||mywife.cc +||c.mi.com +||missav.com +||madou.club +||mahjongsoul.com +||mangabz.com ||mad-ar.ch ||madrau.com ||madthumbs.com -||magic-net.info -mahabodhi.org -my.mail.ru -.maiplus.com |http://maiplus.com -.maizhong.org -makkahnewspaper.com -.mamingzhe.com -manicur4ik.ru +||mangmang.run ||manyvoices.news -.maplew.com -|http://maplew.com ||marc.info -marguerite.su -||martincartoons.com -maskedip.com -.maiio.net -.mail-archive.com -.malaysiakini.com ||makemymood.com -.manchukuo.net -.maniash.com -|http://maniash.com -.mansion.com -.mansionpoker.com -!--||marines.mil -!--markmail.org*message ||martau.com -|http://blog.martinoei.com -.martsangkagyuofficial.org +||blog.martinoei.com |http://martsangkagyuofficial.org -maruta.be/forget -.marxist.com ||marxist.net -.marxists.org/chinese -!--||mashable.com +||marxists.org ||matainja.com -||mathable.io -||mathiew-badimon.com ||matrix.org -||matsushimakaede.com ||matters.town -||maturejp.com -mayimayi.com -.maxing.jp -.mcaf.ee |http://mcaf.ee ||mcadforums.com -mcfog.com -mcreasite.com -.md-t.org ||md-t.org ||meansys.com -.media.org.hk -.mediachinese.com ||mediachinese.com -.mediafire.com/? -.mediafire.com/download -.mediafreakcity.com ||mediafreakcity.com -.medium.com ||medium.com -.meetav.com -||meetup.com -mefeedia.com -jihadintel.meforum.org ||mega.co.nz ||mega.io ||mega.nz +||megalodon.jp ||megaproxy.com -||megarotic.com -megavideo.com ||megurineluka.com ||meizhong.blog ||meizhong.report -.meltoday.com -.memehk.com ||memehk.com -memorybbs.com -.memri.org -.memrijttm.org +||memes.tw ||mercdn.net -.mercyprophet.org ||mercyprophet.org -||mergersandinquisitions.org -.meridian-trust.org ||meridian-trust.org -.meripet.biz -||meripet.biz -.meripet.com ||meripet.com ||merit-times.com.tw -meshrep.com -.mesotw.com/bbs -metacafe.com/watch +||wiki.metacubex.one ||metafilter.com ||meteorshowersonline.com ||metro.taipei -.metrohk.com.hk/?cmd=detail&categoryID=2 ||metrolife.ca -.metroradio.com.hk -|http://metroradio.com.hk +||metroradio.com.hk ||mewe.com -meyou.jp -.meyul.com ||mgoon.com ||mgstage.com ||mh4u.org -mhradio.org -|http://michaelanti.com -||michaelmarketl.com -|http://bbs.mikocon.com -.microvpn.com -|http://microvpn.com -middle-way.net -.mihk.hk/forum -.mihr.com -mihua.org -!--IP +||bbs.mikocon.com +||microvpn.com +||mihua.org +||mikanani.me ||mikesoltys.com -.milph.net -|http://milph.net -.milsurps.com -mimiai.net -.mimivip.com -.mimivv.com -.mindrolling.org |http://mindrolling.org ||mingdemedia.org -.minghui.or.kr -|http://minghui.or.kr -minghui.org +||minghui.or.kr ||minghui.org -minghui-a.org -minghui-b.org -minghui-school.org -.mingjinglishi.com +||minghui-school.org ||mingjinglishi.com -mingjingnews.com +||mingjingnews.com ||mingjingtimes.com -.mingpao.com ||mingpao.com -.mingpaocanada.com -.mingpaomonthly.com -|http://mingpaomonthly.com -mingpaonews.com -.mingpaony.com -.mingpaosf.com -.mingpaotor.com -.mingpaovan.com -.mingshengbao.com -.minhhue.net -.miniforum.org -.ministrybooks.org -.minzhuhua.net -||minzhuhua.net -minzhuzhanxian.com -minzhuzhongguo.org +||mingpaocanada.com +||mingpaomonthly.com +||mingpaonews.com +|http://mingpaony.com +|http://mingpaosf.com +||mingshengbao.com +||minhhue.net +||ministrybooks.org +||minzhuzhongguo.org ||miroguide.com -mirrorbooks.com +||mirrorbooks.com ||mirrormedia.mg -.mist.vip ||thecenter.mit.edu ||scratch.mit.edu -.mitao.com.tw -.mitbbs.com ||mitbbs.com -mitbbsau.com -.mixero.com ||mixero.com ||mixi.jp -mixpod.com -.mixx.com ||mixx.com ||mizzmona.com -.mk5000.com -.mlcool.com +||mlc.ai ||mlzs.work -.mm-cg.com ||mmaaxx.com -.mmmca.com -mnewstv.com ||mobatek.net -.mobile01.com ||mobile01.com ||mobileways.de -.mobypicture.com |http://moby.to ||mod.io ||modernchinastudies.org ||moeerolibrary.com -wiki.moegirl.org -.mofaxiehui.com -.mofos.com +||moeshare.cc ||mog.com ||mohu.rocks -molihua.org ||momoshop.com.tw ||mondex.org ||money-link.com.tw |http://www.monlamit.org ||moon.fm -.moonbbs.com ||moonbbs.com ||moptt.tw +||moneydj.com ||monica.im ||monitorchina.org ||monocloud.me -bbs.morbell.com ||morningsun.org -||moroneta.com -.motherless.com |http://motherless.com -motor4ik.ru -.mousebreaker.com -!--||movabletype.com -.movements.org ||movements.org ||moviefap.com ||www.moztw.org -.mp3buscador.com ||mpettis.com -.mpfinance.com ||mpfinance.com -.mpinews.com ||mpinews.com -mponline.hk -.mqxd.org -|http://mqxd.org -mrtweet.com ||mrtweet.com -news.hk.msn.com -news.msn.com.tw -msguancha.com -.mswe1.org |http://mswe1.org ||mthruf.com ||mubi.com -muchosucko.com ||multiply.com -multiproxy.org -multiupload.com -.mullvad.net ||mullvad.net -.mummysgold.com -.murmur.tw -|http://murmur.tw -.musicade.net -.muslimvideo.com ||muzi.com ||muzi.net ||mx981.com -.my-formosa.com -.my-proxy.com -.my-private-network.co.uk ||my-private-network.co.uk -forum.my903.com -.myactimes.com/actimes -||myanniu.com -.myaudiocast.com ||myaudiocast.com -.myav.com.tw/bbs -.mybbs.us -.myca168.com -.mycanadanow.com ||bbs.mychat.to -||mychinamyhome.com -.mychinamyhome.com -.mychinanet.com -.mychinanews.com ||mychinanews.com -.mychinese.news ||mycnnews.com ||mykomica.org -mycould.com/discuz -.myeasytv.com ||myeclipseide.com -.myforum.com.hk -||myforum.com.hk -||myforum.com.uk -.myfreecams.com -.myfreepaysite.com -.myfreshnet.com -.myiphide.com ||myiphide.com -forum.mymaji.com -mymediarom.com/files/box ||mymoe.moe -||mymusic.net.tw ||myparagliding.com ||mypopescu.com -myradio.hk/podcast -.myreadingmanga.info -mysinablog.com -.myspace.com -!--.blogs.myspace.com -!--||blogs.myspace.com -!--vids.myspace.com/index.cfm?fuseaction=vids. -!--viewmorepics.myspace.com ||myspacecdn.com -.mytalkbox.com -.mytizi.com - !--------------------NN------------------------- +||assets.nxtrace.org +||nephobox.com +||namu.wiki +||nirsoft.net +||naver.com +||maven.neoforged.net +||nftstorage.link +||newindianexpress.com +||news18.com +||bbs.naixi.net +||nikke.hotcool.tw +||nikke-kr.com +||nikke-jp.com +||nikke-en.com +||netlify.app +||nightswatch.top +||nbyy.tv +||newthuhole.com ||naacoalition.org -old.nabble.com ||naitik.net -.nakido.com ||nakido.com -.nakuz.com/bbs ||nalandabodhi.org ||nalandawest.org -.namgyal.org -namgyalmonastery.org -||namsisi.com -.nanyang.com ||nanyang.com -.nanyangpost.com ||nanyangpost.com -.nanzao.com -!--.nanzao.com/sc/china/20223 -!--.nanzao.com/sc/hk-macau-tw -.naol.ca -.naol.cc -uighur.narod.ru -.nat.moe ||nat.moe -cyberghost.natado.com ||national-lottery.co.uk ||nationalawakening.org ||nationalinterest.org -news.nationalgeographic.com/news/2014/06/140603-tiananmen-square ||nationalreview.com -.nationsonline.org/oneworld/tibet ||line.naver.jp ||navyfamily.navy.mil ||navyreserve.navy.mil -||nko.navy.mil ||usno.navy.mil -naweeklytimes.com ||nbcnews.com -.nbtvpn.com |http://nbtvpn.com -nccwatch.org.tw -.nch.com.tw -.ncn.org ||nchrd.org ||ncn.org ||etools.ncol.com -.nde.de ||ndi.org -.ndr.de -.ned.org ||nekoslovakia.net ||neowin.net -||nepusoku.com -||net-fits.pro ||netalert.me -!--bbsnew.netbig.com -bbs.netbig.com -.netbirds.com -netcolony.com -bolin.netfirms.com ||netflav.com ||netme.cc ||netsarang.com -netsneak.com -.network54.com -networkedblogs.com -.networktunnel.net -neverforget8964.org -new-3lunch.net -.new-akiba.com -.new96.ca -.newcenturymc.com |http://newcenturymc.com -newcenturynews.com ||newchen.com -.newchen.com -.newgrounds.com ||newhighlandvision.com -newipnow.com -.newlandmagazine.com.au ||newmitbbs.com -.newnews.ca -news100.com.tw -newschinacomment.org -.newscn.org -||newscn.org -newspeak.cc/story -.newsancai.com +||news1.kr ||newsancai.com -.newsdetox.ca -.newsdh.com +||newsblur.com ||newsmax.com ||newstamago.com ||newstapa.org ||newstatesman.com -newstarnet.com ||newsweek.com -.newtaiwan.com.tw -newtalk.tw ||newtalk.tw ||newyorker.com -newyorktimes.com ||nexon.com -.next11.co.jp ||nextdigital.com.hk -.nextmag.com.tw - -!--hk*.nextmedia.com -!--tw*.nextmedia.com -!--static*.nextmedia.com -.nextmedia.com - ||nexton-net.jp ||nexttv.com.tw -.nfjtyd.com ||co.ng.mil ||nga.mil -ngensis.com -||ngodupdongchung.com -.nhentai.net -|http://nhentai.net -.nhk-ondemand.jp -.nicovideo.jp/watch +||nhentai.net ||nicovideo.jp -||nighost.org -av.nightlife141.com -ninecommentaries.com -.ninjacloak.com ||ninjaproxy.ninja -nintendium.com -taiwanyes.ning.com -usmgtcg.ning.com/forum ||niusnews.com ||njactb.org -njuice.com -||njuice.com ||nlfreevpn.com ||nmsl.website ||nnews.eu - -!--no-ip.com#NOIP -.ddns.net/ -.gooddns.info ||gotdns.ch -.maildns.xyz -.no-ip.org -.opendn.xyz -.servehttp.com -sytes.net -.whodns.xyz -.zapto.org |http://dynupdate.no-ip.com/ - ||nobel.se -!--.nobelprize.org -!--|http://nobelprize.org -nobelprize.org/nobel_prizes/peace/laureates/1989 -nobelprize.org/nobel_prizes/peace/laureates/2010 -nobodycanstop.us -||nobodycanstop.us +||nodeseek.com ||nokogiri.org ||nokola.com -noodlevpn.com -.norbulingka.org -nordvpn.com ||nordvpn.com +||nos.nl ||notepad-plus-plus.org -||novelasia.com -.news.now.com -|http://news.now.com -!--|http://news.now.com/home* -news.now.com%2Fhome ||nownews.com -.nowtorrents.com -.noypf.com -||noypf.com ||npa.go.jp -.npnt.me |http://npnt.me -.nps.gov -.nradio.me |http://nradio.me -.nrk.no ||nrk.no -.ntd.tv ||ntd.tv -.ntdtv.com ||ntdtv.com ||ntdtv.com.tw -.ntdtv.co.kr -ntdtv.ca -ntdtv.org -ntdtv.ru -ntdtvla.com -.ntrfun.com ||cbs.ntu.edu.tw ||media.nu.nl -.nubiles.net ||nuexpo.com -.nukistream.com ||nurgo-software.com ||nutaku.net ||nutsvpn.work -.nuvid.com ||nvdst.com -nuzcom.com -.nvquan.org -.nvtongzhisheng.org |http://nvtongzhisheng.org -.nwtca.org |http://nyaa.eu ||nyaa.si ||nybooks.com -.nydus.ca -nylon-angel.com -nylonstockingsonline.com ||nypost.com -!--nysingtao.com -.nzchinese.com -||nzchinese.net.nz - !--------------------OO------------------------- +||osmand.net +||oklink.com +||okcoin.com +||opencritic.com +||ooni.io +||ooni.org +||files.oaiusercontent.com +||octocaptcha.com +||oojj.de +||onevps.com +||onedrive.com +||olelive.com ||oann.com -observechina.net -.obutu.com -ocaspro.com -occupytiananmen.com -oclp.hk -.ocreampies.com ||october-review.org ||odysee.com -offbeatchina.com ||officeoftibet.com |http://ofile.org ||ogaoga.org -twtr2src.ogaoga.org -.ogate.org ||ogate.org -www2.ohchr.org/english/bodies/cat/docs/ngos/II_China_41.pdf ||ohmyrss.com -.oikos.com.tw/v4 -.oiktv.com -oizoblog.com -.ok.ru ||ok.ru -.okayfreedom.com ||okayfreedom.com ||okk.tw -|http://filmy.olabloga.pl/player -old-cat.net ||olevod.com ||olumpo.com -.olympicwatch.org ||omct.org -omgili.com ||omnitalk.com ||omnitalk.org ||omny.fm -cling.omy.sg -forum.omy.sg -news.omy.sg -showbiz.omy.sg ||on.cc ||onedrive.live.com ||onion.city ||onion.ly -.onlinecha.com ||onlineyoutube.com ||onlygayvideo.com -.onlytweets.com |http://onlytweets.com -onmoon.net -onmoon.com -.onthehunt.com |http://onthehunt.com -.oopsforum.com -open.com.hk -openallweb.com -opendemocracy.net ||opendemocracy.net -.openervpn.in -openid.net ||openid.net -.openleaks.org ||openleaks.org ||openstreetmap.org ||opentech.fund -openvpn.net ||openvpn.net ||openwebster.com -.openwrt.org.cn -@@||openwrt.org.cn -my.opera.com/dahema -||demo.opera-mini.net -.opus-gaming.com |http://opus-gaming.com -www.orchidbbs.com -.organcare.org.tw -organharvestinvestigation.net -.orgasm.com -.orgfree.com ||oricon.co.jp ||orient-doll.com -orientaldaily.com.my ||orientaldaily.com.my -!--orientaldaily.on.cc ||orn.jp -t.orzdream.com -||t.orzdream.com -tui.orzdream.com -||orzistic.org ||osfoora.com -.otnd.org -||otnd.org ||otto.de ||ourdearamy.com -oursogo.com -.oursteps.com.au ||oursteps.com.au -.oursweb.net ||ourtv.hk -xinqimeng.over-blog.com ||overcast.fm ||overdaily.org ||overplay.net -share.ovi.com/media ||ovpn.com |http://owl.li |http://ht.ly |http://htl.li |http://mash.to -www.owind.com ||owltail.com ||oxfordscholarship.com |http://www.oxid.it -oyax.com -oyghan.com/wps -.ozchinese.com/bbs ||ow.ly -bbs.ozchinese.com -.ozvoice.org ||ozvoice.org -.ozxw.com -.ozyoyo.com - !--------------------PP------------------------- +||prompthero.com +||pdst.fm +||static.pocketcasts.com +||partnerstack.xyz +||podwise.ai +||picsart.com +||images.prismic.io +||api.palworldgame.com +||pewresearch.org +||privacyguides.org +||pancakeswap.finance +||img.picgo.net +||pornmate.com +||puredns.org +||polymarket.com +||pandafan.pub +||proxz.com +||potatso.com +||pendrivelinux.com +||paimon.moe +||photonmedia.net +||points-media.com +||pkuanvil.com ||pachosting.com -.pacificpoker.com -.packetix.net ||pacopacomama.com -.padmanet.com ||page.link -page2rss.com -||pagodabox.com -.palacemoon.com -forum.palmislife.com ||eriversoft.com -.paldengyal.com -paljorpublications.com -.paltalk.com -!--||pangci.net ||pandapow.co -.pandapow.net -.pandavpn-jp.com ||pandavpn-jp.com ||pandavpnpro.com -.panluan.net -||panluan.net ||pao-pao.net -paper.li -paperb.us -.paradisehill.cc -.paradisepoker.com ||parler.com ||parsevideo.com -.partycasino.com -.partypoker.com -.passion.com ||passion.com -.passiontimes.hk -pastebin.com -.pastie.org ||pastie.org ||blog.pathtosharepoint.com ||patreon.com +||patreonusercontent.com ||pawoo.net -pbs.org/wgbh/pages/frontline/tankman -pbs.org/wgbh/pages/frontline/tibet -video.pbs.org - -!--Pbwiki -pbwiki.com +||pbs.org ||pbworks.com ||developers.box.net ||wiki.oauth.net ||wiki.phonegap.com ||wiki.jqueryui.com - ||pbxes.com ||pbxes.org -pcdvd.com.tw ||pcgamestorrents.com -.pchome.com.tw ||pcij.org -.pcstore.com.tw ||pct.org.tw -pdetails.com ||pdproxy.com ||peace.ca -peacefire.org -peacehall.com -||peacehall.com -|http://pearlher.org -.peeasian.com ||peing.net -.pekingduck.org ||pekingduck.org -.pemulihan.or.id |http://pemulihan.or.id ||pen.io -penchinese.com -||penchinese.net -.penchinese.net ||blog.pentalogic.net -.penthouse.com ||pentoy.hk -.peoplebookcafe.com -.peoplenews.tw ||peoplenews.tw -.peopo.org ||peopo.org -.percy.in -.perfectgirls.net ||perfect-privacy.com ||perplexity.ai -.persecutionblog.com -.persiankitty.com -phapluan.org -.phayul.com ||phayul.com -philborges.com ||phncdn.com ||photodharma.net ||photofocus.com -||phuquocservices.com ||picacomiccn.com -.picidae.net ||img*.picturedip.com -picturesocial.com +||picuki.com +||pigav.com ||pin-cong.com -.pin6.com ||pin6.com -.ping.fm ||ping.fm ||pinimg.com -.pinkrod.com ||pinoy-n.com -||pinterest.at -||pinterest.ca -||pinterest.co.kr -||pinterest.co.uk -.pinterest.com ||pinterest.com ||pinterest.com.mx +||pinterest.com.au +||pinterest.co.uk +||pinterest.cl +||pinterest.ca +||pinterest.at ||pinterest.de -||pinterest.dk +||pinterest.es ||pinterest.fr +||pinterest.ie +||pinterest.it ||pinterest.jp -||pinterest.nl +||pinterest.nz +||pinterest.ph +||pinterest.pt ||pinterest.se -.pipii.tv -.piposay.com -piraattilahti.org -.piring.com ||pixeldrain.com ||pixelqi.com ||css.pixnet.in ||pixnet.net -.pixnet.net -.pk.com +||pkqjiasu.com ||placemix.com -!--.planetsuzy.org -|http://pictures.playboy.com +||play-asia.com ||playboy.com -.playboyplus.com ||playboyplus.com ||player.fm -.playno1.com ||playno1.com ||playpcesor.com -plays.com.tw ||plexvpn.pro -||m.plixi.com -plm.org.hk -plunder.com -.plurk.com ||plurk.com -.plus28.com -.plusbb.com -.pmatehunter.com ||pmatehunter.com -.pmates.com ||po2b.com -pobieramy.top -!--||pocoo.org ||podbean.com ||podictionary.com ||poe.com -.pokerstars.com ||pokerstars.com ||pokerstars.net ||zh.pokerstrategy.com ||politicalchina.org -||politicalconsultation.org -.politiscales.net ||poloniex.com ||polymerhk.com -.popo.tw -!--||popularpages.net ||popvote.hk ||popxi.click -.popyard.com ||popyard.org -.porn.com -.porn2.com -.porn5.com -.pornbase.org -.pornerbros.com ||pornhd.com -.pornhost.com -.pornhub.com ||pornhub.com -.pornhubdeutsch.net |http://pornhubdeutsch.net -||pornmm.net -.pornoxo.com -.pornrapidshare.com ||pornrapidshare.com -.pornsharing.com |http://pornsharing.com -.pornsocket.com -.pornstarclub.com +||pornstarbyface.com ||pornstarclub.com -.porntube.com -.porntubenews.com -.porntvblog.com ||porntvblog.com -.pornvisit.com -.portablevpn.nl ||poskotanews.com -.post01.com -.post76.com ||post76.com -.post852.com ||post852.com -postadult.com -.postimg.org ||potvpn.com ||pourquoi.tw ||powercx.com -.powerphoto.org ||www.powerpointninja.com +||ppy.sh ||presidentlee.tw ||cdn.printfriendly.com -.pritunl.com -provpnaccounts.com ||provpnaccounts.com -.proxfree.com ||proxfree.com -proxyanonimo.es -.proxynetwork.org.uk ||proxynetwork.org.uk -||pts.org.tw -.pttvan.org -pubu.com.tw -puffinbrowser.com -pureinsight.org -.pushchinawall.com -.putty.org +||pubu.com.tw +||puffinbrowser.com +||pureinsight.org ||putty.org - !-------------Posterous----- ||calebelston.com ||blog.fizzik.com @@ -6246,1050 +3168,524 @@ pureinsight.org ||vatn.org ||ventureswell.com ||whereiswerner.com - -.power.com ||power.com -powerapple.com ||powerapple.com -||abc.pp.ru -heix.pp.ru ||prayforchina.net -||premeforwindows7.com +||prcleader.org ||presentationzen.com ||prestige-av.com -.prisoneralert.com ||pritunl.com ||privacybox.de -.private.com/home +||private.com ||privateinternetaccess.com -privatepaste.com ||privatepaste.com -privatetunnel.com ||privatetunnel.com ||privatevpn.com ||privoxy.org ||procopytips.com ||project-syndicate.org -||proton.me -provideocoalition.com ||prosiben.de -proxifier.com ||proxomitron.info -.proxpn.com ||proxpn.com -.proxylist.org.uk -||proxylist.org.uk -.proxypy.net -||proxypy.net -proxyroad.com -.proxytunnel.net -!--403 maybe -||proyectoclubes.com -prozz.net -psblog.name -||psblog.name ||pshvpn.com ||psiphon.ca -.psiphon3.com ||psiphon3.com -.psiphontoday.com ||pstatic.net ||pt.im -.ptt.cc ||ptt.cc ||pttgame.com -.puffstore.com -.puuko.com -||pullfolio.com -.punyu.com/puny +||main-ecnpaper-economist.content.pugpig.com +||pullfolio.co ||pureconcepts.net -||pureinsight.org ||purepdf.com ||purevpn.com -.purplelotus.org -.pursuestar.com ||pursuestar.com -||nitter.pussthecat.org -.pussyspace.com -.putihome.org -.putlocker.com/file -pwned.com ||pximg.net -python.com -.python.com.tw ||python.com.tw -pythonhackers.com/p -ss.pythonic.life - !--------------------QQ------------------------- -.qanote.com -||qanote.com +|http://qmp4.com +||qianmo.tw ||qbittorrent.org ||qgirl.com.tw ||qianbai.tw ||qiandao.today +||qianglie.com ||qiangwaikan.com -.qi-gong.me ||qi-gong.me -!--#921 ||qiangyou.org -.qidian.ca -.qienkuen.org -||qienkuen.org ||qiwen.lu -qixianglu.cn -bbs.qmzdd.com -.qkshare.com -qoos.com ||qoos.com ||efksoft.com ||qstatus.com -||qtweeter.com ||qtrac.eu -.quannengshen.org -||quannengshen.org -quantumbooter.net -||quitccp.net -.quitccp.net ||quitccp.org -.quitccp.org -.quora.com/Chinas-Future -.quran.com |http://quran.com -.quranexplorer.com -qusi8.net -.qvodzy.org -nemesis2.qx.net/pages/MyEnTunnel -qxbbs.org - !--------------------RR------------------------- +||radiojar.com +||radio.co +||rustdesk.com +||rentry.co +||radmin-vpn.com +||rule34video.com +||r10s.jp +||rakuten.co.jp ||r0.ru ||radio-canada.ca ||radio-en-ligne.fr ||rael.org -radicalparty.org ||radio.garden ||radioaustralia.net.au -.radiohilight.net ||radiohilight.net ||radioline.co -opml.radiotime.com ||radiovaticana.org ||radiovncr.com ||raggedbanner.com ||raidcall.com.tw -.raidtalk.com.tw -.rainbowplan.org/bbs |https://raindrop.io/ -.raizoji.or.jp |http://raizoji.or.jp -rangwang.biz -rangzen.net -rangzen.org |http://blog.ranxiang.com/ -ranyunfei.com -||ranyunfei.com -.rapbull.net !--|http://rapidgator.net/ ||rapidmoviez.com -rapidvpn.com ||rapidvpn.com ||rarbgprx.org -.raremovie.cc -|http://raremovie.cc -.raremovie.net -|http://raremovie.net ||rationalwiki.org ||rawgit.com ||rawgithub.com -!--.rayfme.com/bbs -||razyboard.com -rcinet.ca -.read100.com -.readingtimes.com.tw +||rcinet.ca +||reabble.com ||readingtimes.com.tw ||readmoo.com -.readydown.com |http://readydown.com -.realcourage.org -.realitykings.com +||realcourage.org ||realitykings.com -.realraptalk.com -.realsexpass.com ||reason.com -.recordhistory.org -.recovery.org.tw |http://online.recoveryversion.org ||recoveryversion.com.tw ||red-lang.org -redballoonsolidarity.org ||redbubble.com -.redchinacn.net -|http://redchinacn.net -redchinacn.org -redtube.com -referer.us +||redchinacn.net ||referer.us ||reflectivecode.com -relaxbbs.com -.relay.com.tw -.releaseinternational.org +||blog.reimu.net ||religionnews.com -religioustolerance.org -renminbao.com ||renminbao.com -.renyurenquan.org ||renyurenquan.org |http://certificate.revocationcheck.com -subacme.rerouted.org ||resilio.com -.reuters.com ||reuters.com ||reutersmedia.net -.revleft.com ||resistchina.org -retweetist.com ||retweetrank.com -!--connectedchina.reuters.com -!--|http://www.reuters.com/news/video -revver.com -.rfa.org ||rfa.org -.rfachina.com -.rfamobile.org -rfaweb.org ||rferl.org -.rfi.fr ||rfi.fr ||rfi.my -!--.rhcloud.com -!--Edgecast -|http://vds.rightster.com/ -.rigpa.org -.rileyguide.com ||riku.me -.ritouki.jp ||ritter.vg -.rlwlw.com ||rlwlw.com ||rmbl.ws -.rmjdw.com -.rmjdw132.info -.roadshow.hk -.roboforex.com ||robustnessiskey.com -!--||roc-taiwan.org +||rocket.chat ||rocket-inc.net -|http://www2.rocketbbs.com/11/bbs.cgi?id=5mus -|http://www2.rocketbbs.com/11/bbs.cgi?id=freemgl -!--||rocmp.org ||rojo.com ||ronjoneswriter.com ||rolfoundation.org ||rolia.net ||rolsociety.org -.roodo.com -.rosechina.net -.rotten.com ||rou.video -.rsf.org ||rsf.org -.rsf-chinese.org ||rsf-chinese.org -.rsgamen.org ||rsshub.app ||phosphation13.rssing.com -.rssmeme.com ||rssmeme.com ||rtalabel.org -.rthk.hk ||rthk.hk -.rthk.org.hk ||rthk.org.hk -.rti.org.tw ||rti.org.tw ||rti.tw -.rtycminnesota.org -.ruanyifeng.com/blog*some_ways_to_break_the_great_firewall -rukor.org ||rule34.xxx ||rumble.com -.runbtx.com -.rushbee.com ||rusvpn.com -.ruten.com.tw ||ruten.com.tw ||rutracker.net -rutube.ru -.ruyiseek.com -.rxhj.net +||rutracker.org |http://rxhj.net - !--------------------SS------------------------- -.s1s1s1.com +||shitjournal.org +||spacex.com +||stephaniered.com +||simianx.ai +||steamladder.com +||sora.com +||lt.sntp.uk +||solscan.io +||sina.com.hk +||swapspace.co +||storry.tv +||standard.co.uk +||sagernet.org +||simplex.chat +||soundon.fm +||ssrtool.com +||ssrshare.us +||secure.shadowsocks.nu +||synapse.org +||south-plus.net +||silvergatebank.com +||share-videos.se +||cdn.statically.io +||slides.com +||suno.com +||sydney.bing.com +||sehuatang.org +||singlelogin.se +||suno.ai +||syosetu.com ||s-cute.com -.s-dragon.org -||s1heng.com |http://www.s4miniarchive.com -||s8forum.com -cdn1.lp.saboom.com ||sacks.com -sacom.hk ||sacom.hk ||sadpanda.us ||safechat.com ||safeguarddefenders.com -.safervpn.com ||safervpn.com -.saintyculture.com |http://saintyculture.com -.saiq.me -||saiq.me ||sakuralive.com -.sakya.org -.salvation.org.hk ||salvation.org.hk -.samair.ru/proxy/type-01 -.sambhota.org ||cn.sandscotaicentral.com ||sankakucomplex.com ||sankei.com -.sanmin.com.tw -sapikachu.net -savemedia.com +||sanmin.com.tw ||savethesounds.info -.savetibet.de ||savetibet.de -savetibet.fr -savetibet.nl -.savetibet.org ||savetibet.org -savetibet.ru -.savetibetstore.org ||savetibetstore.org ||saveuighur.org -savevid.com -||say2.info -.sbme.me |http://sbme.me -.sbs.com.au/yourlanguage -.scasino.com -|http://www.sciencemag.org/content/344/6187/953 -.sciencenets.com -.scmp.com ||scmp.com -.scmpchinese.com ||scramble.io -.scribd.com ||scribd.com ||scriptspot.com ||search.com -.searchtruth.com ||searx.me ||seattlefdc.com -.secretchina.com ||secretchina.com ||secretgarden.no -.secretsline.biz ||secretsline.biz ||secureservercdn.net ||securetunnel.com -securityinabox.org |https://securityinabox.org -.securitykiss.com ||securitykiss.com ||seed4.me -news.seehua.com -seesmic.com +||news.seehua.com ||seevpn.com ||seezone.net -sejie.com -.sendspace.com +||sehuatang.net ||sensortower.com -|http://tweets.seraph.me/ -sesawe.net ||sesawe.net -.sesawe.org ||sethwklein.net -.setn.com -.settv.com.tw -forum.setty.com.tw -.sevenload.com +||setn.com ||sevenload.com -.sex.com ||sex.com -.sex-11.com ||sex3.com ||sex8.cc -.sexandsubmission.com -.sexbot.com -.sexhu.com -.sexhuang.com -sexinsex.net ||sexinsex.net -.sextvx.com - -!--IP of SexInSex -67.220.91.15 -67.220.91.18 -67.220.91.23 - |http://*.sf.net -.sfileydy.com ||sfshibao.com -.sftindia.org -.sftuk.org ||sftuk.org ||shadeyouvpn.com -shadow.ma -.shadowsky.xyz -.shadowsocks.asia ||www.shadowsocks.com -.shadowsocks.com ||shadowsocks.com.hk -.shadowsocks.org ||shadowsocks.org -||shadowsocks-r.com |http://cn.shafaqna.com ||shahit.biz -.shambalapost.com -.shambhalasun.com -.shangfang.org -||shangfang.org -shapeservices.com -.sharebee.com ||sharecool.org -!--||sharkdolphin.com -sharpdaily.com.hk -||sharpdaily.com.hk -.sharpdaily.hk -.sharpdaily.tw -.shat-tibet.com -sheikyermami.com -.shellfire.de ||shellfire.de -.shenshou.org -shenyun.com -shenyunperformingarts.org ||shenyunperformingarts.org ||shenyunshop.com -shenzhoufilm.com ||shenzhoufilm.com ||shenzhouzhengdao.org -||sherabgyaltsen.com -.shiatv.net -.shicheng.org -shinychan.com -shipcamouflage.com -.shireyishunjian.com -.shitaotv.org ||shixiao.org ||shizhao.org -shizhao.org -shkspr.mobi/dabr ||shodanhq.com ||shooshtime.com -.shop2000.com.tw ||shopee.tw -.shopping.com -.showhaotu.com -.showtime.jp ||showwe.tw -.shutterstock.com ||shutterstock.com -ch.shvoong.com -.shwchurch.org ||shwchurch.org -.shwchurch3.com |http://shwchurch3.com -.siddharthasintent.org ||sidelinesnews.com -.sidelinessportseatery.com ||signal.org -.sijihuisuo.club -.sijihuisuo.com -.silkbook.com ||simbolostwitter.com -simplecd.org ||simplecd.org -@@||simplecd.me -simpleproductivityblog.com -bbs.sina.com/ -bbs.sina.com%2F -blog.sina.com.tw -dailynews.sina.com/ -dailynews.sina.com%2F -forum.sina.com.hk -home.sina.com -||magazines.sina.com.tw -news.sina.com.hk -news.sina.com.tw -news.sinchew.com.my -.sinchew.com.my/node/ -.sinchew.com.my/taxonomy/term -.singaporepools.com.sg +||simplecd.me ||singaporepools.com.sg -.singfortibet.com -.singpao.com.hk -singtao.com ||singtao.com -news.singtao.ca -.singtaousa.com ||singtaousa.com -!--||cdp.sinica.edu.tw -sino-monthly.com ||sinoca.com ||sinocast.com -sinocism.com -sinomontreal.ca -.sinonet.ca -.sinopitt.info -.sinoants.com ||sinoants.com ||sinoinsider.com -.sinoquebec.com -.sierrafriendsoftibet.org -sis.xxx ||sis001.com -sis001.us -.site2unblock.com -||site90.net -.sitebro.tw ||sitekreator.com -||siteks.uk.to ||sitemaps.org -.sjrt.org -|http://sjrt.org -||sjum.cn ||sketchappsources.com ||skimtube.com ||lab.skk.moe ||skybet.com -|http://users.skynet.be/reves/tibethome.html -.skyking.com.tw -bbs.skykiwi.com |http://www.skype.com/intl/ |http://www.skype.com/zh-Hant ||skyvegas.com -.xskywalker.com ||xskywalker.com ||skyxvpn.com -m.slandr.net -.slaytizle.com -.sleazydream.com ||sleazyfork.org ||slheng.com ||slideshare.net -forum.slime.com.tw -.slinkset.com ||slickvpn.com -.slutload.com ||smartdnsproxy.com -.smarthide.com ||app.smartmailcloud.com -smchbooks.com -.smh.com.au/world/death-of-chinese-playboy-leaves-fresh-scratches-in-party-paintwork-20120903-25a8v -smhric.org -.smith.edu/dalailama -.smyxy.org -!--TODO-no-homepage -||snapchat.com -.snaptu.com -||snaptu.com +||smh.com.au +||smn.news ||sndcdn.com -sneakme.net -snowlionpub.com -home.so-net.net.tw/yisa_tsai -||soc.mil ||socialblade.com -.socks-proxy.net ||socks-proxy.net -.sockscap64.com ||sockslist.net -.socrec.org |http://socrec.org -.sod.co.jp -.softether.org ||softether.org -.softether-download.com ||softether-download.com ||cdn.softlayer.net ||sogclub.com -sohcradio.com ||sohcradio.com -.sokmil.com ||sorting-algorithms.com -.sostibet.org -.soumo.info ||soup.io -@@||static.soup.io -.sobees.com ||sobees.com -socialwhale.com -.softether.co.jp ||softwarebychuck.com -blog.sogoo.org -soh.tw ||soh.tw -sohfrance.org ||sohfrance.org -chinese.soifind.com -sokamonline.com ||solana.com -.solidaritetibet.org -.solidfiles.com ||somee.com -.songjianjun.com ||songjianjun.com -.sonicbbs.cc -.sonidodelaesperanza.org -.sopcast.com -.sopcast.org ||nakedsecurity.sophos.com -.sorazone.net ||sos.org -bbs.sou-tong.org -.soubory.com +||sosad.fun |http://soubory.com -.soul-plus.net -.soulcaliburhentai.net ||soulcaliburhentai.net ||soundcloud.com -!--|https://soundcloud.com/punkgod -.soundofhope.kr -soundofhope.org ||soundofhope.org -||soupofmedia.com -!--.sourceforge.net -!-|http://sourceforge.net -|http://sourceforge.net/p*/shadowsocksgui/ -.sourcewadio.com ||south-plus.org -southnews.com.tw -sowers.org.hk -||wlx.sowiki.net +||southmongolia.org +||southnews.com.tw +||sowers.org.hk ||spankbang.com -.spankingtube.com -.spankwire.com +||spatial.io ||spb.com ||speakerdeck.com +||speedcat.me ||speedify.com -spem.at ||spencertipping.com ||spendee.com ||spicevpn.com -.spideroak.com ||spideroak.com -.spike.com -.spotflux.com ||spotflux.com ||spreaker.com -.spring4u.info ||spring4u.info ||springwood.me ||sproutcore.com -||sproxy.info ||squirrelvpn.com -||srocket.us -.ss-link.com ||ss-link.com -.ssglobal.co/wp |http://ssglobal.co -.ssglobal.me -||ssh91.com -.sspro.ml -|http://sspro.ml -.ssrshare.com ||ssrshare.com -||sss.camp -!--|http://cdn.sstatic.net/ ||sstm.moe ||sstmlt.moe -sstmlt.net ||sstmlt.net -|http://stackoverflow.com/users/895245 -.stage64.hk -||stage64.hk ||standupfortibet.org ||standwithhk.org -stanford.edu/group/falun -usinfo.state.gov -||statueofdemocracy.org -.starfishfx.com -.starp2p.com ||starp2p.com -.startpage.com ||startpage.com -.startuplivingchina.com |http://startuplivingchina.com ||static-economist.com ||stboy.net ||stc.com.sa ||steel-storm.com -.steganos.com ||steganos.com -.steganos.net -.stepchina.com -!--||stepmania.com -ny.stgloballink.com -hd.stheadline.com/news/realtime -sthoo.com ||sthoo.com -.stickam.com -stickeraction.com/sesawe -.stileproject.com -.sto.cc -.stoporganharvesting.org +||stitcher.com ||storagenewsletter.com -.storm.mg ||storm.mg -.stoptibetcrisis.net ||stoptibetcrisis.net -||storify.com ||storj.io -.stormmediagroup.com ||stoweboyd.com ||straitstimes.com -stranabg.com ||straplessdildo.com ||streamable.com ||streamate.com ||streamingthe.net -streema.com/tv/NTDTV_Chinese -cn.streetvoice.com/article -cn.streetvoice.com/diary -cn2.streetvoice.com -tw.streetvoice.com -.strikingly.com ||strongvpn.com -.strongwindpress.com -.student.tw/db ||studentsforafreetibet.org ||stumbleupon.com -stupidvideos.com ||substack.com -.successfn.com -panamapapers.sueddeutsche.de -.sugarsync.com +||subhd.tv ||sugarsync.com -.sugobbs.com ||sugumiru18.com ||suissl.com -summify.com -.sumrando.com ||sumrando.com -sun1911.com ||sundayguardianlive.com -.sunporno.com ||sunmedia.ca ||sunporno.com -.sunskyforum.com -.sunta.com.tw -.sunvpn.net -.suoluo.org -.superfreevpn.com -.supervpn.net ||supervpn.net -.superzooi.com |http://superzooi.com -.suppig.net -.suprememastertv.com |http://suprememastertv.com -.surfeasy.com ||surfeasy.com -.surfeasy.com.au |http://surfeasy.com.au ||surfshark.com ||surrenderat20.net -.svsfx.com -.swissinfo.ch ||swissinfo.ch -.swissvpn.net ||swissvpn.net -switchvpn.net ||switchvpn.net -.sydneytoday.com ||sydneytoday.com -.sylfoundation.org ||sylfoundation.org ||syncback.com -sysresccd.org -.sytes.net -blog.syx86.com/2009/09/puff -blog.syx86.cn/2009/09/puff -.szbbs.net -.szetowah.org.hk - !--------------------TT------------------------- +||tor.eff.org +||tails.net +||bbc.pdn.tritondigital.com +||terobox.com +||temu.com +||trustwallet.com +||tap.io +||taptap.io +||talkatone.com +||tanks.gg +||thehansindia.com +||rtm.tnt-ea.com +||tellapart.com +||threads.com +||tg-me.com +||twkan.com +||tunein.streamguys1.com +||tou.tv +||tinyurl.com +||textnow.com +||token.im +||tokenlon.im +||tardigrade.io +||torrentgalaxy.to +||tomp3.cc +||tukaani.org +||thetatoken.org +||typeset.io +||thechasernews.co.uk +||hole.thu.monster +||thuhole.com ||t-g.com -.t35.com -.t66y.com ||t66y.com ||esg.t91y.com -.taa-usa.org |http://taa-usa.org -.taaze.tw ||taaze.tw |http://www.tablesgenerator.com/ -tabtter.jp -.tacem.org -.taconet.com.tw ||taedp.org.tw -.tafm.org -.tagwa.org.au -tagwalk.com ||tagwalk.com -tahr.org.tw -.taipeisociety.org ||taipeisociety.org ||taipeitimes.com ||taisounds.com -.taiwanbible.com -.taiwancon.com -.taiwandaily.net -||taiwandaily.net -.taiwandc.org -!--||taiwanembassy.org ||taiwanhot.net -.taiwanjustice.com -taiwankiss.com -taiwannation.com -taiwannation.com.tw ||taiwanncf.org.tw ||taiwannews.com.tw |http://www.taiwanonline.cc/ -!--||taiwantoday.tw -taiwantp.net ||taiwantt.org.tw -taiwanus.net -taiwanyes.com -taiwan-sex.com -.talk853.com -.talkboxapp.com ||talkboxapp.com -.talkcc.com ||talkcc.com -.talkonly.net ||talkonly.net -||tamiaode.tk ||tanc.org -tangben.com -.tangren.us -.taoism.net |http://taoism.net -.taolun.info -||taolun.info -.tapatalk.com ||tapatalk.com -blog.taragana.com -.tascn.com.au ||taup.net -|http://www.taup.org.tw -.taweet.com ||taweet.com -.tbcollege.org ||tbcollege.org -.tbi.org.hk -.tbicn.org -.tbjyt.org -||tbpic.info -.tbrc.org -tbs-rainbow.org -.tbsec.org ||tbsec.org -tbskkinabalu.page.tl -.tbsmalaysia.org -.tbsn.org ||tbsn.org -.tbsseattle.org -.tbssqh.org |http://tbssqh.org -tbswd.org -.tbtemple.org.uk -.tbthouston.org -.tccwonline.org -.tcewf.org -tchrd.org -tcnynj.org -||tcpspeed.co -.tcpspeed.com -||tcpspeed.com -.tcsofbc.org -.tcsovi.org -.tdm.com.mo -teamamericany.com -||techspot.com -!--OVH ||techviz.net ||teck.in -.teeniefuck.net -teensinasia.com ||tehrantimes.com -.telecomspace.com -||telegraph.co.uk -.tenacy.com ||tenor.com ||tenzinpalmo.com -.tew.org ||tew.org +||tfc-taiwan.org.tw ||tfiflve.com -.thaicn.com ||theatlantic.com ||theatrum-belli.com ||cn.theaustralian.com.au -theblemish.com ||thebcomplex.com ||theblaze.com -.thebobs.com ||thebobs.com -.thechinabeat.org ||thechinacollection.org -|http://www.thechinastory.org/yearbooks/yearbook-2012/ ||theconversation.com -.thedalailamamovie.com |http://thedalailamamovie.com ||thediplomat.com ||thedw.us ||theepochtimes.com -!--||thefreeland.club -thefrontier.hk/tf ||theguardian.com ||thegay.com |http://thegioitinhoc.vn/ -.thegly.com -.thehots.info -thehousenews.com +||thehindu.com ||thehun.net -.theinitium.com ||theinitium.com -||themoviedb.org -.thenewslens.com ||thenewslens.com -.thepiratebay.org ||thepiratebay.org -!--||thepiratebay.se -.theporndude.com ||theporndude.com ||theportalwiki.com ||theprint.in ||threadreaderapp.com -thereallove.kr -therock.net.nz ||thesaturdaypaper.com.au ||thestandnews.com -thetibetcenter.org -thetibetconnection.org -.thetibetmuseum.org -.thetibetpost.com ||thetibetpost.com -!--Tor -||thetinhat.com -thetrotskymovie.com ||thetvdb.com -thevivekspot.com ||thewgo.org -.theync.com +||thewirechina.com |http://theync.com -.thinkingtaiwan.com ||thinkingtaiwan.com -.thisav.com -|http://thisav.com -.thlib.org +||thirdmill.org +||thisav.com ||thomasbernhard.org -.thongdreams.com -threatchaos.com ||throughnightsfire.com -.thumbzilla.com ||thywords.com -.thywords.com.tw -tiananmenmother.org -.tiananmenduizhi.com ||tiananmenduizhi.com ||tiananmenuniv.com ||tiananmenuniv.net ||tiandixing.org -.tianhuayuan.com -.tianlawoffice.com ||tianti.io -tiantibooks.org ||tiantibooks.org -tianyantong.org.cn -.tianzhu.org -.tibet.at -tibet.ca -.tibet.com ||tibet.com -tibet.fr -.tibet.net ||tibet.net ||tibet.nu -.tibet.org ||tibet.org -.tibet.sk ||tibet.org.tw ||tibet.to -.tibet-envoy.eu ||tibet-envoy.eu -.tibet-foundation.org -.tibet-house-trust.co.uk ||tibet-initiative.de -.tibet-munich.de -.tibet3rdpole.org |http://tibet3rdpole.org -tibetaction.net ||tibetaction.net -.tibetaid.org -tibetalk.com -.tibetan.fr -tibetan-alliance.org -.tibetanarts.org -.tibetanbuddhistinstitute.org ||tibetanbuddhistinstitute.org ||tibetancommunity.org ||tibetanentrepreneurs.org ||tibetanhealth.org -.tibetanjournal.com -.tibetanlanguage.org -.tibetanliberation.org ||tibetanliberation.org -.tibetcollection.com -.tibetanaidproject.org -.tibetancommunityuk.net |http://tibetancommunityuk.net -tibetanculture.org -tibetanfeministcollective.org -.tibetanpaintings.com -.tibetanphotoproject.com -.tibetanpoliticalreview.org -.tibetanreview.net |http://tibetansports.org -.tibetanwomen.org |http://tibetanwomen.org -.tibetanyouth.org -.tibetanyouthcongress.org ||tibetanyouthcongress.org -.tibetcharity.dk -tibetcharity.in -.tibetchild.org -.tibetcity.com ||tibetcorps.org ||tibetexpress.net ||tibetfocus.com ||tibetfund.org -.tibetgermany.com ||tibetgermany.de -.tibethaus.com -.tibetheritagefund.org ||tibethouse.jp ||tibethouse.org ||tibethouse.us -.tibetinfonet.net -.tibetjustice.org -.tibetkomite.dk ||tibetmuseum.org ||tibetnetwork.org ||tibetoffice.ch -tibetoffice.eu ||tibetoffice.org ||tibetonline.com ||tibetoffice.com.au @@ -7297,7 +3693,6 @@ tibetoffice.eu ||tibetoralhistory.org ||tibetpolicy.eu ||tibetrelieffund.co.uk -||tibetsites.com ||tibetsociety.com ||tibetsun.com ||tibetsupportgroup.org @@ -7306,625 +3701,305 @@ tibetoffice.eu ||tibettimes.net ||tibettruth.com ||tibetwrites.org -.ticket.com.tw -.tigervpn.com ||tigervpn.com -.timdir.com |http://timdir.com -.time.com |http://time.com -!--.time.com/time/time100/leaders/profile/rebel -!--.time.com/time/specials/packages/article/0,28804 -!--.time.com/time/magazine ||timesnownews.com -.timsah.com ||timtales.com ||blog.tiney.com -tintuc101.com -.tiny.cc -|http://tiny.cc -tinychat.com +||tingtalk.me +||tiny.cc +||tinychat.com ||tinypaste.com ||tipas.net -.tistory.com ||tkcs-collins.com -.tmagazine.com ||tmagazine.com -.tmdfish.com |http://tmi.me -.tmpp.org |http://tmpp.org -.tnaflix.com ||tnaflix.com -.tngrnow.com -.tngrnow.net -.tnp.org |http://tnp.org -.to-porno.com ||to-porno.com -togetter.com -.tokyo-247.com -.tokyo-hot.com +||togetter.com ||tokyo-porn-tube.com ||tokyocn.com -tw.tomonews.net -.tongil.or.kr -.tono-oka.jp -tonyyan.net -.toodoc.com -toonel.net -top81.ws -.topnews.in -.toppornsites.com |http://toppornsites.com -.torguard.net +||toptoon.net ||torguard.net ||top.tv -.topshareware.com -.topsy.com ||topsy.com ||toptip.ca -tora.to -.torcn.com ||torlock.com -.torproject.org ||torproject.org ||torrentkitty.tv -torrentprivacy.com ||torrentprivacy.com |http://torrentproject.se ||torrenty.org -||torrentz.eu ||tortoisesvn.net ||torvpn.com ||totalvpn.com -.toutiaoabc.com -towngain.com -toypark.in -toytractorshow.com -.tparents.org -.tpi.org.tw ||tpi.org.tw ||tradingview.com ||transparency.org ||treemall.com.tw -trendsmap.com ||trendsmap.com -.trialofccp.org -||trialofccp.org -.trimondi.de/SDLE -.trouw.nl ||trouw.nl -.trt.net.tr ||trt.net.tr -trtc.com.tw -.truebuddha-md.org ||truebuddha-md.org -trulyergonomic.com -.truth101.co.tv -||truth101.co.tv -.truthontour.org -||truthontour.org ||truthsocial.com -.truveo.com -.tsctv.net -.tsemtulku.com -tsquare.tv -.tsu.org.tw -tsunagarumon.com -!--|http://www.tsuru-bird.net/ -.tsctv.net ||tt1069.com -.tttan.com ||tttan.com ||ttv.com.tw -tu8964.com -.tubaholic.com -.tube.com -tube8.com ||tube8.com -.tube911.com ||tube911.com -.tubecup.com -.tubegals.com -.tubeislam.com |http://tubeislam.com -.tubestack.com ||tubewolf.com -.tuibeitu.net -tuidang.net -.tuidang.org ||tuidang.org -.tuidang.se -bbs.tuitui.info -.tumutanzi.com |http://tumutanzi.com ||tumview.com -.tunein.com |http://tunein.com ||tunnelbear.com ||tunnelblick.net -.tunnelr.com ||tunnelr.com ||tunsafe.com -tuitwit.com -.turansam.org -.turbobit.net ||turbobit.net -.turbohide.com ||turbohide.com ||turkistantimes.com -.tushycash.com |http://tushycash.com -||app.tutanota.com -.tuvpn.com ||tuvpn.com |http://tuzaijidi.com |http://*.tuzaijidi.com -.tw01.org |http://tw01.org - +||use.typekit.net !---Tumblr--- -.tumblr.com ||tumblr.com -!--@@||assets.tumblr.com -!--@@||data.tumblr.com -!--@@||media.tumblr.com -!--@@||static.tumblr.com -!--@@||www.tumblr.com ||lecloud.net -|http://cosmic.monar.ch ||slutmoonbeam.com |http://blog.soylent.com - -.tv.com |http://tv.com -tvants.com -forum.tvb.com -news.tvb.com/list/world -news.tvb.com/local -news.tvbs.com.tw -.tvboxnow.com -|http://tvboxnow.com/ -tvider.com -.tvmost.com.hk -.tvplayvideos.com +||mytvsuper.com +||tvbanywhere.com +||akamai.tvb.com +||inews-api.tvb.com +||news.tvbs.com.tw +||tvboxnow.com ||tvunetworks.com -.tw-blog.com |https://tw-blog.com -.tw-npo.org -.twaitter.com -twapperkeeper.com ||twapperkeeper.com ||twaud.io -.twaud.io -.twavi.com -.twbbs.net.tw -twbbs.org -twbbs.tw ||twblogger.com -tweepmag.com -.tweepml.org ||tweepml.org -.tweetbackup.com ||tweetbackup.com -tweetboard.com ||tweetboard.com -.tweetboner.biz -||tweetboner.biz -.tweetcs.com |http://tweetcs.com |http://deck.ly -!-- Operation discontinued -!--||tweete.net -!--m.tweete.net -||mtw.tl ||tweetedtimes.com -!-- Operation discontinued -!--tweetmeme.com -||tweetmylast.fm -tweetphoto.com ||tweetphoto.com -||tweetrans.com -tweetree.com ||tweetree.com -.tweettunnel.com ||tweettunnel.com ||tweetwally.com -tweetymail.com ||twelve.today -.tweez.net |http://tweez.net ||twftp.org ||twgreatdaily.com -twibase.com -.twibble.de ||twibble.de -twibbon.com ||twibs.com -.twicountry.org |http://twicountry.org -twicsy.com -.twiends.com |http://twiends.com -.twifan.com |http://twifan.com -twiffo.com ||twiffo.com -.twilightsex.com -twilog.org -twimbow.com -||twindexx.com -twipple.jp ||twipple.jp ||twip.me -twishort.com ||twishort.com -twistar.cc ||twister.net.co -||twisterio.com -twisternow.com -twistory.net -twitbrowser.net -||twitcause.com -||twitgether.com ||twiggit.org -twitgoo.com -twitiq.com ||twitiq.com -.twitlonger.com ||twitlonger.com |http://tl.gd/ -twitmania.com -twitoaster.com ||twitoaster.com ||twitonmsn.com -!--Same IP -.twit2d.com -||twit2d.com -.twitstat.com ||twitstat.com -||firstfivefollowers.com -||retweeteffect.com -||tweeplike.me ||tweepguide.com -||turbotwitter.com -.twitvid.com -||twitvid.com |http://twt.tl -twittbot.net ||ads-twitter.com ||twttr.com ||twitter4j.org -.twittercounter.com ||twittercounter.com -twitterfeed.com -.twittergadget.com ||twittergadget.com -.twitterkr.com ||twitterkr.com ||twittermail.com ||twitterrific.com -twittertim.es ||twittertim.es -twitthat.com ||twitturk.com -.twitturly.com ||twitturly.com -.twitzap.com -twiyia.com -||twstar.net -.twtkr.com |http://twtkr.com -.twnorth.org.tw ||twreporter.org -twskype.com -twtrland.com -twurl.nl -.twyac.org -||twyac.org -.txxx.com -.tycool.com ||tycool.com - !--typepad ||typepad.com -@@||www.typepad.com -@@||static.typepad.com ||blog.expofutures.com -||legaltech.law.com -||blogs.tampabay.com ||contests.twilio.com -!-lawprofessors.typepad.com/china_law_prof ||typora.io - !--------------------UU------------------------- -.u9un.com +||uniswap.org +||up.audio +||udomain.hk +||upbit.com +||demo.unlock-music.dev ||u9un.com -.ubddns.org |http://ubddns.org ||uberproxy.net -.uc-japan.org ||uc-japan.org -.srcf.ucam.org/salon/ |http://china.ucanews.com/ -||ucdc1998.org |http://hum*.uchicago.edu/faculty/ywang/history -||uderzo.it -.udn.com ||udn.com ||udn.com.tw -udnbkk.com/bbs ||uforadio.com.tw -ufreevpn.com -.ugo.com !--ghs ||uhdwallpapers.org ||uhrp.org -.uighur.nl ||uighur.nl -uighurbiz.net -.ulike.net -ukcdp.co.uk -ukliferadio.co.uk -||ukliferadio.co.uk -ultravpn.fr +||ultrasurf.us +||ultravpn.com ||ultravpn.fr -ultraxs.com -umich.edu/~falun ||unblock.cn.com -.unblocker.yt -unblock-us.com ||unblock-us.com -.unblockdmm.com |http://unblockdmm.com ||unblocksit.es -uncyclomedia.org -.uncyclopedia.hk/wiki |http://uncyclopedia.hk -!--uncyclopedia.info |http://uncyclopedia.tw -underwoodammo.com ||underwoodammo.com ||unholyknight.com -.uni.cc ||cldr.unicode.org -.unification.net -.unification.org.tw ||unirule.cloud -.unitedsocialpress.com -.unix100.com ||unknownspace.org -.unodedos.com -unpo.org ||unstable.icu -.untraceable.us -|http://untraceable.us +||unwire.hk ||uocn.org -tor.updatestar.com ||upghsbc.com -.upholdjustice.org -.upload4u.info -uploaded.net/file -|http://uploaded.net/file -|http://uploaded.to/file -.uploadstation.com/file -.upmedia.mg ||upmedia.mg -.upornia.com |http://upornia.com ||uproxy.org ||uptodown.com -.upwill.org -ur7s.com ||urbandictionary.com ||urbansurvival.com -myshare.url.com.tw/ ||urlborg.com ||urlparser.com -us.to ||usacn.com -.usaip.eu ||usaip.eu ||uscnpm.org ||uscardforum.com ||usma.edu -.usocctn.com ||ustibetcommittee.org -.ustream.tv ||ustream.tv -usus.cc -.utopianpal.com ||utopianpal.com -.uu-gg.com -.uvwxyz.xyz +||uujiasu.com ||uvwxyz.xyz -.uwants.com ||uwants.com -.uwants.net -uyghur.co.uk -|http://uyghur-j.org +||uyghur-j.org ||uyghuraa.org ||uyghuramerican.org ||uyghurbiz.org -||uyghurcanadian.ca ||uyghurcongress.org ||uyghurpen.org -||uyghurpress.com ||uyghurstudies.org ||uyghurtribunal.com -uygur.org |http://uymaarip.com/ - !--------------------VV------------------------- +||vimeocdn.com +||vpsxb.net +||vilanet.me +||vewas.net +||v2.help +||vocaroo.com +||vern.cc ||v2fly.org -.v2ray.com ||v2ray.com ||v2raycn.com -||v2raytech.com ||valeursactuelles.com -.van001.com -.van698.com -.vanemu.cn -.vanilla-jp.com -.vanpeople.com -vansky.com +||vansky.com ||vaticannews.va ||vcf-online.org ||vcfbuilder.org -.vegasred.com -.velkaepocha.sk -.venbbs.com -.venchina.com -.venetianmacao.com ||venetianmacao.com -veoh.com ||vercel.app -mysite.verizon.net -vermonttibet.org -.versavpn.com -||versavpn.com ||verybs.com -.vft.com.tw -.viber.com ||viber.com -.vica.info -.victimsofcommunism.org ||victimsofcommunism.org ||vid.me ||vidble.com -videobam.com ||videobam.com -.videodetective.com -.videomega.tv ||videomega.tv -.videomo.com -videopediaworld.com -.videopress.com -.vidinfo.org/video -vietdaikynguyen.com -.vijayatemple.org ||vilavpn.com -vimeo.com ||vimeo.com ||vimperator.org ||vincnd.com ||vinniev.com -|http://www.lib.virginia.edu/area-studies/Tibet/tibet.html -.virtualrealporn.com ||virtualrealporn.com -visibletweets.com -|http://ny.visiontimes.com -.vital247.org ||viu.com -.vivahentai4u.net ||vivaldi.com -.vivatube.com -.vivthomas.com ||vivthomas.com -.vjav.com ||vjav.com -.vjmedia.com.hk -.vllcs.org |http://vllcs.org ||vmixcore.com ||vnet.link -.vocativ.com -vocn.tv ||vocus.cc ||voicettank.org -.vot.org ||vot.org -.vovo2000.com |http://vovo2000.com -.voxer.com ||voxer.com -.voy.com ||vpn.ac -.vpn4all.com +||vpn.net ||vpn4all.com -.vpnaccount.org |http://vpnaccount.org -.vpnaccounts.com ||vpnaccounts.com -.vpncomparison.org -.vpncup.com ||vpncup.com -vpnbook.com -.vpncoupons.com |http://vpncoupons.com -.vpndada.com ||vpndada.com -.vpnfan.com -vpnfire.com -.vpnfires.biz -.vpnforgame.net ||vpnforgame.net ||vpngate.jp -.vpngate.net ||vpngate.net -.vpngratis.net -vpnhq.com ||vpnhub.com -.vpnmaster.com ||vpnmaster.com -.vpnmentor.com ||vpnmentor.com -.vpninja.net ||vpninja.net -.vpnintouch.com -||vpnintouch.net -vpnjack.com ||vpnjack.com -.vpnpick.com ||vpnpick.com ||vpnpop.com ||vpnpronet.com -.vpnreactor.com +||vpnproxymaster.com ||vpnreactor.com ||vpnreviewz.com -.vpnsecure.me ||vpnsecure.me -.vpnshazam.com ||vpnshazam.com -.vpnshieldapp.com ||vpnshieldapp.com -.vpnsp.com -.vpntraffic.com -.vpntunnel.com ||vpntunnel.com -.vpnuk.info ||vpnuk.info ||vpnunlimitedapp.com -.vpnvip.com ||vpnvip.com -.vpnworldwide.com -.vporn.com ||vporn.com -.vpser.net -@@||vpser.net -vraiesagesse.net ||vrchat.com -.vrmtr.com +||vrporn.com ||vtunnel.com ||vuku.cc - !--------------------WW------------------------- -lists.w3.org/archives/public -||w3schools.com +||wispbyte.com +||walletconnect.org +||wallzhihu.com +||wikis.tw +||weights.com +||wikiunblocked.org +||websdr.org +||wikipedia.com +||wxw.moe +||wxw.cat +||walletconnect.com +|https://w3s.link/ipfs +||work2icu.org +||wikiless.funami.tech ||waffle1999.com -.wahas.com -.waigaobu.com -waikeung.org/php_wind -.wailaike.net ||wainao.me -.waiwaier.com -|http://waiwaier.com ||wallmama.com -wallornot.org ||wallpapercasa.com -.wallproxy.com -@@||wallproxy.com.cn ||wallsttv.com ||waltermartin.com ||waltermartin.org @@ -7932,164 +4007,85 @@ wallornot.org ||wanderinghorse.net ||wangafu.net ||wangjinbo.org -.wangjinbo.org -wanglixiong.com -.wango.org ||wango.org -wangruoshui.net -www.wangruowang.org ||want-daily.com -wapedia.mobi/zhsimp ||warroom.org ||waselpro.com -.watchinese.com +||watchinese.com ||watchout.tw -.wattpad.com ||wattpad.com -.makzhou.warehouse333.com -washeng.net -.watch8x.com ||watchmygf.net ||wav.tv +||waybig.com ||wd.bible -.wdf5.com ||wealth.com.tw -.wearehairy.com -.wearn.com ||wearn.com |http://hkcoc.weather.com.hk ||hudatoriq.web.id ||web2project.net -webbang.net -.webevader.org -.webfreer.com -weblagu.com -.webjb.org -.webrush.net -webs-tv.net -.websitepulse.com/help/testtools.china-test |http://www.websnapr.com -.webwarper.net |http://webwarper.net -webworkerdaily.com ||wechatlawsuit.com -.weekmag.info ||wefightcensorship.org -.wefong.com -weiboleak.com -.weihuo.org ||weijingsheng.org -.weiming.info ||weiming.info -weiquanwang.org |http://weisuo.ws -.welovecock.com ||welt.de -.wemigrate.org |http://wemigrate.org -wengewang.com ||wengewang.org -.wenhui.ch -|http://trans.wenweipo.com/gb/ -.wenxuecity.com ||wenxuecity.com -.wenyunchao.com ||wenyunchao.com -.westca.com ||westca.com ||westernwolves.com -.westkit.net ||westpoint.edu -.westernshugdensociety.org -wetpussygames.com -.wetplace.com -wexiaobo.org -||wexiaobo.org -wezhiyong.org ||wezone.net -.wforum.com -||wforum.com/ -.whatblocked.com +||wforum.com ||whatblocked.com -.wheatseeds.org ||wheelockslatin.com -.whippedass.com -!--|http://who.is/ -.whoer.net ||whoer.net -whotalking.com -whylover.com ||whyx.org ||wikileaks.ch ||wikileaks.com ||wikileaks.de ||wikileaks.eu ||wikileaks.lu -.wikileaks.org ||wikileaks.org ||wikileaks.pl -.wikileaks-forum.com -wildammo.com -.williamhill.com +||wilsoncenter.org ||collateralmurder.com ||collateralmurder.org -wikilivres.info/wiki/%E9%9B%B6%E5%85%AB%E5%AE%AA%E7%AB%A0 ||wikimapia.org -.wikiwand.com ||wikiwand.com -||wikiwiki.jp ||casino.williamhill.com ||sports.williamhill.com ||vegas.williamhill.com ||willw.net -||windowsphoneme.com -.windscribe.com ||windscribe.com -||community.windy.com ||wingy.site -.winning11.com -winwhispers.info ||wionews.com ||wiredbytes.com ||wiredpen.com ||wireguard.com -!--||wireshark.org -.wisdompubs.org -.wisevid.com ||wisevid.com ||whispersystems.org -.witnessleeteaching.com -.witopia.net -.wjbk.org +||witopia.net ||wjbk.org ||wmflabs.org ||wn.com -.wnacg.com -.wnacg.org -.wo.tc +||wnacg.com +||wnacg.org +||wo.tc ||woeser.com -.wokar.org ||wokar.org -wolfax.com ||wolfax.com ||wombo.ai ||woolyss.com -woopie.jp ||woopie.jp -woopie.tv ||woopie.tv ||workatruna.com -.workerdemo.org.hk -.workerempowerment.org -||workers.dev -||workersthebig.net -.worldcat.org -worldjournal.com -.worldvpn.net +||workerempowerment.org ||worldvpn.net - ||videopress.com -.wordpress.com |http://*.wordpress.com ||chenshan20042005.wordpress.com ||chinaview.wordpress.com @@ -8106,142 +4102,71 @@ worldjournal.com ||wo3ttt.wordpress.com ||sujiatun.wordpress.com ||xijie.wordpress.com +||ifreechina.wordpress.com ||wp.com - -!-||wormsculptor.com -.wow.com -.wow-life.net -||wowlegacy.ml ||wowporn.com ||wowgirls.com -.wowrk.com -woxinghuiguo.com -.woyaolian.org |http://woyaolian.org -.wpoforum.com ||wpoforum.com -.wqyd.org -||wqyd.org -wrchina.org -wretch.cc ||writesonic.com -.wsj.com ||wsj.com -.wsj.net ||wsj.net -.wsjhk.com -.wtbn.org -.wtfpeople.com -wuerkaixi.com ||wufafangwen.com ||wufi.org.tw -||wuguoguang.com -wujie.net -wujieliulan.com ||wujieliulan.com -wukangrui.net ||wuw.red -||wuyanblog.com -.wwitv.com ||wwitv.com -wzyboy.im/post/160 - !--------------------XX------------------------- +||xdaforums.com +||xcancel.com +||www.xicons.org +||x.ai +||xt.com +||xt.pub ||x.co -.x-berry.com ||x-berry.com ||x-art.com ||x-wall.org -x1949x.com -x365x.com -xanga.com +||x3guide.com ||xbabe.com -.xbookcn.com ||xbookcn.com ||xcafe.in ||xcity.jp -.xcritic.com -|http://cdn*.xda-developers.com -.xerotica.com -destiny.xfiles.to/ubbthreads -.xfm.pp.ru -.xgmyd.com +||xerotica.com +||xfxssr.me ||xgmyd.com -xhamster.com ||xhamster.com -.xianba.net -.xianchawang.net -.xianjian.tw |http://xianjian.tw -.xianqiao.net -.xiaobaiwu.com -.xiaochuncnjp.com -.xiaod.in -.xiaohexie.com ||xiaolan.me ||xiaoma.org ||xiaohexie.com ||xiaxiaoqiang.net -xiezhua.com -.xihua.es -forum.xinbao.de/forum -.xing.com |http://xing.com ||xinjiangpolicefiles.org -.xinmiao.com.hk ||xinmiao.com.hk -xinsheng.net -xinshijue.com -xinhuanet.org -|http://xinyubbs.net -.xiongpian.com -.xiuren.org -||xixicui.icu -xizang-zhiye.org -xjp.cc ||xjp.cc ||xjtravelguide.com -xlfmtalk.com -||xlfmwz.info ||xml-training-guide.com -xmovies.com ||xnxx.com -!--||xnxx-cdn.com -xpdo.net ||xpud.org -.xrentdvd.com -.xskywalker.net ||xtube.com -blog.xuite.net -vlog.xuite.net -xuzhiyong.net ||xuchao.org -xuchao.net ||xuchao.net -xvideo.cc -.xvideos.com ||xvideos.com ||xvideos-cdn.com ||xvideos.es ||xvbelink.com ||xvinlink.com -.xkiwi.tk/ ||xsden.info -.xxbbx.com -.xxlmovies.com ||xxx.com -.xxx.xxx |http://xxx.xxx -.xxxfuckmom.com ||xxxx.com.au -.xxxymovies.com |http://xxxymovies.com -xys.org -xysblogs.org -xyy69.com -xyy69.info - !--------------------YY------------------------- +||yfsp.tv +||youmind.com +||yangzhi.org +||storage.yandex.net ||y2mate.com ||yadi.sk ||yakbutterblues.com @@ -8250,273 +4175,129 @@ xyy69.info ||yande.re ||disk.yandex.com ||disk.yandex.ru -.yanghengjun.com -yangjianli.com -.yasni.co.uk ||yasni.co.uk -!--||yasukuni.or.jp -.yayabay.com/forum +||yasukuni.or.jp ||news.ycombinator.com -.ydy.com -.yeahteentube.com ||yeahteentube.com ||yecl.net ||yeelou.com ||yeeyi.com -yegle.net ||yegle.net -.yes.xxx ||yes123.com.tw ||yesasia.com ||yesasia.com.hk -.yes-news.com |http://yes-news.com -.yespornplease.com ||yespornplease.com |http://yeyeclub.com -!--yfrog.com ||yhcw.net -.yibada.com -.yibaochina.com -.yidio.com +||yibaochina.com ||yidio.com -||yigeni.com -yilubbs.com ||s.yimg.com -||xa.yimg.com -.yingsuoss.com -.yipub.com ||yipub.com -yinlei.org/mt -.yizhihongxing.com ||yizhihongxing.com -.yobt.com -.yobt.tv ||yobt.tv -.yogichen.org ||yogichen.org -.yolasite.com -.yomiuri.co.jp -yong.hu -.yorkbbs.ca ||you.com ||youxu.info -.youjizz.com ||youjizz.com -.youmaker.com ||youmaker.com -.youngpornvideos.com -youngspiration.hk -.youpai.org ||youpai.org -.your-freedom.net ||yourepeat.com -.yourprivatevpn.com -||yourprivatevpn.com -.yousendit.com ||yousendit.com -||youthforfreechina.org -.youthnetradio.org/tmit/forum -blog.youthwant.com.tw -me.youthwant.com.tw -share.youthwant.com.tw -topic.youthwant.com.tw -.youporn.com ||youporn.com -.youporngay.com ||youporngay.com -.yourlisten.com ||yourlisten.com -.yourlust.com ||yourlust.com -youshun12.com -.youtubecn.com -youversion.com ||youversion.com -ytht.net -yuanming.net -.yuanzhengtang.org -.yulghun.com ||yulghun.com ||yunchao.net -.yuvutu.com +||yunomi.tokyo ||yvesgeleyn.com -.ywpw.com/forums/history/post/A0/p0/html/227 -yx51.net -.yyii.org ||yyii.org ||yyjlymb.xyz ||yysub.net -.yzzk.com ||yzzk.com - !--------------------ZZ------------------------- +||zaochenbao.com +||z-library.ec +||z-library.sk +||z-lib.fm +||z-lib.gd +||z-lib.gl +||z-lib.fo +||zodgame.xyz +||zhongzidi.com +||zooqle.com ||z-lib.io ||z-lib.org -zacebook.com -.zalmos.com ||zalmos.com -||zannel.com -.zaobao.com -||zaobao.com -||zaobao.com.sg -.zaozon.com ||zdnet.com.tw -.zello.com ||zello.com -.zengjinyan.org -.zenmate.com ||zenmate.com ||zenmate.com.ru ||zerohedge.com ||zeronet.io -||zeutch.com -!--www.zfreet.com/post/usejump-browns.html -.zfreet.com -.zgsddh.com -zgzcjj.net -.zhanbin.net -||zhanbin.net -.zhangboli.net ||zhangtianliang.com ||zhanlve.org -zhenghui.org -.zhengjian.org ||zhengjian.org -zhengwunet.org -zhenlibu.info -||zhenlibu.info -.zhenlibu1984.com -||zhenlibu1984.com +||zhengwunet.org |http://zhenxiang.biz -.zhinengluyou.com -zhongguo.ca -|http://zhongguorenquan.org -zhongguotese.net +|http://zhongguo.ca ||zhongguotese.net -||zhongmeng.org -.zhoushuguang.com -||zhreader.com -.zhuangbi.me -||zhuangbi.me -.zhuanxing.cn ||zhuatieba.com -zhuichaguoji.org ||zhuichaguoji.org ||zi.media -|http://book.zi5.me -.ziddu.com/download ||zillionk.com -.zinio.com ||zinio.com -.ziporn.com -.zippyshare.com -.zkaip.com -||zkaip.com -realforum.zkiz.com -!--||zlib.net +||zmedia.com.tw ||zmw.cn -.zodgame.us -zomobo.net -.zonaeuropa.com ||zonaeuropa.com ||zonghexinwen.com -.zonghexinwen.net ||zoogvpn.com ||zootool.com -.zoozle.net ||zophar.net -writer.zoho.com ||zorrovpn.com ||zpn.im ||zspeeder.me -.zsrhao.com -.zuo.la ||zuo.la ||zuobiao.me -.zuola.com ||zuola.com ||zvereff.com ||zyxel.com -.zynaima.com -zyzc9.com -.zzcartoon.com !##############General List End################# - !###########Supplemental List Start############# -!-----------------URL Keywords------------------ -64memo -aHR0cHM6Ly95ZWNsLm5ldA -freenet -.google.*/falun -phobos.apple.com*/video -q=freedom -q%3Dfreedom -remembering_tiananmen_20_years -search*safeweb -q=triangle -q%3DTriangle -ultrareach -ultrasurf !#############Supplemental List End############# - !################Whitelist Start################ -@@||aliyun.com -@@||baidu.com -!--@@||bing.com -@@||chinaso.com -@@||chinaz.com -@@|http://nrch.culture.tw/ - -!---Some are powered by GuXiang (BGP), please comment off if -!---you encounter connectivity issues. +@@||firebase-settings.crashlytics.com +@@||cn.investing.com +@@||www.typepad.com +@@||static.typepad.com +@@||ci.android.com +@@||crl.pki.goog +@@||g2.gstatic.com +@@||g1.gstatic.com +@@||g0.gstatic.com +@@||checkin.gstatic.com +@@||i.pki.goog +@@||c.pki.goog +@@||o.pki.goog @@||adservice.google.com -!--ISP cache works sometimes, verified at drpeng + gehua. @@||dl.google.com -!--@@||kh.google.com -!--@@||khm.google.com -!--@@||khm0.google.com -!--@@||khm1.google.com -!--@@||khm2.google.com -!--@@||khm3.google.com -!--@@||khmdb.google.com @@||tools.google.com @@||clientservices.googleapis.com +@@||imasdk.googleapis.com @@||fonts.googleapis.com -!--@@||khm.googleapis.com -!--@@||khm0.googleapis.com -!--@@||khm1.googleapis.com -!--@@||khm2.googleapis.com -!--@@||khm3.googleapis.com -!--@@||khmdb.googleapis.com -@@||storage.googleapis.com -!--@@||translate.googleapis.com @@||update.googleapis.com @@||safebrowsing.googleapis.com -@@||cn.gravatar.com -!--@@||connectivitycheck.gstatic.com -!--@@||csi.gstatic.com -!--@@||fonts.gstatic.com -!--@@||ssl.gstatic.com -@@||haosou.com -@@||ip.cn -@@||jike.com -@@|http://translate.google.cn -@@|http://www.google.cn/maps -@@||http2.golang.org -@@||gov.cn +@@||connectivitycheck.gstatic.com +@@||csi.gstatic.com +@@||fonts.gstatic.com +@@||ssl.gstatic.com +@@||www.gstatic.com @@||ocsp.pki.goog -@@||qq.com -@@||sina.cn -@@||sina.com.cn -@@||sogou.com -@@||so.com -@@||soso.com -@@||uluai.com.cn -@@||weibo.com -@@||yahoo.cn -@@||youdao.com -@@||zhongsou.com -@@|http://ime.baidu.jp +@@||www.ampproject.org +@@||cdn.ampproject.org +@@||cdn-china.ampproject.org +@@||redirector.gvt1.com !################Whitelist End################## !---------------------EOF----------------------- diff --git a/packages/gui/extra/scripts/github.script b/packages/gui/extra/scripts/github.script index 51c8d94f9f..3f5a79534f 100644 --- a/packages/gui/extra/scripts/github.script +++ b/packages/gui/extra/scripts/github.script @@ -1,21 +1,21 @@ // ==UserScript== -// @name Github 增强 - 高速下载 +// @name Github Enhancement - High Speed Download // @name:zh-CN Github 增强 - 高速下载 // @name:zh-TW Github 增強 - 高速下載 -// @name:en Github Enhancement - High Speed Download -// @version 2.5.21 +// @name:ru Улучшение GitHub – быстрое скачивание +// @version 2.6.37 // @author X.I.U -// @description 高速下载 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、项目列表单文件快捷下载 (☁)、添加 git clone 命令 +// @description High-speed download of Git Clone/SSH, Release, Raw, Code(ZIP) and other files (Based on public welfare), project list file quick download (☁) // @description:zh-CN 高速下载 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、项目列表单文件快捷下载 (☁) // @description:zh-TW 高速下載 Git Clone/SSH、Release、Raw、Code(ZIP) 等文件 (公益加速)、項目列表單文件快捷下載 (☁) -// @description:en High-speed download of Git Clone/SSH, Release, Raw, Code(ZIP) and other files (Based on public welfare), project list file quick download (☁) +// @description:ru Высокоскоростная загрузка Git Clone/SSH, выпусков, изначальных файлов, кода (ZIP) и других файлов (на основе общественного благосостояния), быстрая загрузка файлов из списка проектов (☁) // @match *://github.com/* -// @match *://hub.incept.pw/* -// @match *://hub.nuaa.cf/* -// @match *://hub.yzuu.cf/* -// @match *://hub.scholar.rr.nu/* +// @match *://hub.whtrys.space/* // @match *://dgithub.xyz/* // @match *://kkgithub.com/* +// @match *://github.site/* +// @match *://github.store/* +// @match *://bgithub.xyz/* // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACEUExURUxpcRgWFhsYGBgWFhcWFh8WFhoYGBgWFiUlJRcVFRkWFhgVFRgWFhgVFRsWFhgWFigeHhkWFv////////////r6+h4eHv///xcVFfLx8SMhIUNCQpSTk/r6+jY0NCknJ97e3ru7u+fn51BOTsPCwqGgoISDg6empmpoaK2srNDQ0FhXV3eXcCcAAAAXdFJOUwCBIZXMGP70BuRH2Ze/LpIMUunHkpQR34sfygAAAVpJREFUOMt1U+magjAMDAVb5BDU3W25b9T1/d9vaYpQKDs/rF9nSNJkArDA9ezQZ8wPbc8FE6eAiQUsOO1o19JolFibKCdHGHC0IJezOMD5snx/yE+KOYYr42fPSufSZyazqDoseTPw4lGJNOu6LBXVUPBG3lqYAOv/5ZwnNUfUifzBt8gkgfgINmjxOpgqUA147QWNaocLniqq3QsSVbQHNp45N/BAwoYQz9oUJEiE4GMGfoBSMj5gjeWRIMMqleD/CAzUHFqTLyjOA5zjNnwa4UCEZ2YK3khEcBXHjVBtEFeIZ6+NxYbPqWp1DLKV42t6Ujn2ydyiPi9nX0TTNAkVVZ/gozsl6FbrktkwaVvL2TRK0C8Ca7Hck7f5OBT6FFbLATkL2ugV0tm0RLM9fedDvhWstl8Wp9AFDjFX7yOY/lJrv8AkYuz7fuP8dv9izCYH+x3/LBnj9fYPBTpJDNzX+7cAAAAASUVORK5CYII= // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand @@ -23,6 +23,7 @@ // @grant GM_getValue // @grant GM_setValue // @grant GM_notification +// @grant GM_setClipboard // @grant window.onurlchange // @sandbox JavaScript // @license GPL-3.0 License @@ -34,119 +35,193 @@ (function() { 'use strict'; - var backColor = '#ffffff', fontColor = '#888888', menu_rawFast = GM_getValue('xiu2_menu_raw_fast'), menu_rawFast_ID, menu_rawDownLink_ID, menu_gitClone_ID, menu_feedBack_ID; + var menu_rawFast = GM_getValue('xiu2_menu_raw_fast'), menu_rawFast_ID, menu_rawDownLink_ID, menu_gitClone_ID, menu_customUrl_ID, menu_feedBack_ID; const download_url_us = [ - //['https://gh.h233.eu.org/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@X.I.U/XIU2] 提供'], - //['https://gh.api.99988866.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [hunshcn/gh-proxy] 提供'], // 官方演示站用的人太多了 - ['https://gh.ddlc.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@mtr-static-official] 提供'], - //['https://gh2.yanqishui.work/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@HongjieCN] 提供'], // 解析错误 - ['https://dl.ghpig.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [feizhuqwq.com] 提供'], + ['https://gh.h233.eu.org/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@X.I.U/XIU2] 提供'], + //['https://gh.api.99988866.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.com/hunshcn/gh-proxy] 提供'], // 官方演示站用的人太多了 + //['https://ghproxy.1888866.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [WJQSERVER-STUDIO/ghproxy] 提供'],//挂了 + ['https://rapidgit.jjda.de5.net/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [热心网友] 提供'], + ['https://gh.ddlc.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@mtr-static-official] 提供'], // Error 1027 + //['https://gh2.yanqishui.work/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@HongjieCN] 提供'], // 错误 + //['https://dl.ghpig.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [feizhuqwq.com] 提供'], // ERR_SSL_VERSION_OR_CIPHER_MISMATCH //['https://gh.flyinbug.top/gh/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [Mintimate] 提供'], // 错误 - ['https://slink.ltd/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [知了小站] 提供'], - //['https://git.xfj0.cn/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], // 无解析 - ['https://gh.con.sh/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], - //['https://ghps.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], // 提示 blocked - //['https://gh-proxy.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [佚名] 提供'], // 502 + //['https://gh.con.sh/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.con.sh] 提供'], // Suspent due to abuse report. + //['https://ghps.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghps.cc] 提供'], // 提示 blocked + ['https://gh-proxy.org/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh-proxy.com] 提供'], + //['https://hk.gh-proxy.org/https://github.com', '其他', '[中国香港] - 该公益加速源由 [gh-proxy.com] 提供'], + ['https://cdn.gh-proxy.org/https://github.com', '其他', '[Fastly CDN] - 该公益加速源由 [gh-proxy.com] 提供'], + ['https://edgeone.gh-proxy.org/https://github.com', '其他', '[edgeone] - 该公益加速源由 [gh-proxy.com] 提供'], ['https://cors.isteed.cc/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@Lufs\'s] 提供'], - ['https://hub.gitmirror.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供'], - ['https://sciproxy.com/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [sciproxy.com] 提供'], - ['https://ghproxy.cc/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchiii lau] 提供'], - ['https://cf.ghproxy.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchiii lau] 提供'], - ['https://gh.jiasu.in/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], - ['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], - //['https://download.fgit.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 被投诉挂了 - ['https://download.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], - ['https://download.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], - //['https://download.njuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 域名挂了 - ['https://download.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'] - ]; - - const download_url = [ - //['https://download.fastgit.org', '德国', '[德国] - 该公益加速源由 [FastGit] 提供 提示:希望大家尽量多使用前面的美国节点(每次随机 4 个来负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~', 'https://archive.fastgit.org'], // 证书过期 - ['https://mirror.ghproxy.com/https://github.com', '韩国', '[日本、韩国、德国等](CDN 不固定) - 该公益加速源由 [ghproxy] 提供 提示:希望大家尽量多使用前面的美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], - ['https://ghproxy.net/https://github.com', '日本', '[日本 大阪] - 该公益加速源由 [ghproxy] 提供 提示:希望大家尽量多使用前面的美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], - ['https://kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供 提示:希望大家尽量多使用前面的美国节点(每次随机 4 个来负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], - //['https://download.incept.pw', '香港', '[中国香港] - 该公益加速源由 [FastGit 群组成员] 提供 提示:希望大家尽量多使用前面的美国节点(每次随机 4 个来负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'] // ERR_SSL_PROTOCOL_ERROR - ]; - - const clone_url = [ + //['https://hub.gitmirror.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供'], // 域名无解析 + //['https://down.sciproxy.com/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [sciproxy.com] 提供'], // 522 + ['https://ghproxy.it/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchilau] 提供'], + //['https://github.site', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchilau] 提供'], // 挂了 + //['https://github.store', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchilau] 提供'], // 挂了 + //['https://gh.jiasu.in/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 404 + ['https://github.boki.moe/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [blog.boki.moe] 提供'], + //['https://github.moeyy.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [moeyy.cn] 提供'], // 墙了 + ['https://gh-proxy.net/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh-proxy.net] 提供'], + //['https://github.yongyong.online/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.yongyong.online] 提供'], // 空白 + //['https://ghdd.862510.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghdd.862510.xyz] 提供'], // turnstile token missing + ['https://gh.jasonzeng.dev/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.jasonzeng.dev] 提供'], + ['https://gh.monlor.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.monlor.com] 提供'], + ['https://fastgit.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [fastgit.cc] 提供'], + ['https://github.tbedu.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.tbedu.top] 提供'], + //['https://github.geekery.cn/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.geekery.cn] 提供'], // 下载认证信息 用户名:123123 密 码:123123 + ['https://firewall.lxstd.org/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [firewall.lxstd.org] 提供'], + ['https://github.ednovas.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.ednovas.xyz] 提供'], + ['https://ghfile.geekertao.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghfile.geekertao.top] 提供'], + ['https://ghp.keleyaa.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghp.keleyaa.com] 提供'], // Error 1027 + //['https://github.wuzhij.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.wuzhij.com] 提供'], // 404 + ['https://gh.chjina.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.chjina.com] 提供'], + ['https://ghpxy.hwinzniej.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghpxy.hwinzniej.top] 提供'], + ['https://cdn.crashmc.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [cdn.crashmc.com] 提供'], + ['https://git.yylx.win/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [git.yylx.win] 提供'], + ['https://gitproxy.mrhjx.cn/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gitproxy.mrhjx.cn] 提供'], + ['https://ghproxy.cxkpro.top/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghproxy.cxkpro.top] 提供'], + ['https://gh.xxooo.cf/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.xxooo.cf] 提供'], + ['https://github.limoruirui.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.limoruirui.com] 提供'], + ['https://gh.idayer.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.idayer.com] 提供'], // Error 1027 + //['https://gh.zwnes.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.zwnes.xyz] 提供'], // 超时 + ['https://gh.llkk.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh.llkk.cc] 提供'], + ['https://down.npee.cn/?https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [npee社区] 提供'], + ['https://raw.ihtw.moe/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [raw.ihtw.moe] 提供'], + ['https://xget.xi-xu.me/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.com/xixu-me/Xget] 提供'], + //['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], // 证书挂了 + //['https://gh-proxy.ygxz.in/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@一个小站 www.ygxz.in] 提供'], // 被蔷 + ['https://gh.nxnow.top/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [gh.nxnow.top] 提供'], + ['https://gh.zwy.one/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [gh.zwy.one] 提供'], + ['https://ghproxy.monkeyray.net/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [ghproxy.monkeyray.net] 提供'], + ['https://gh.xx9527.cn/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [gh.xx9527.cn] 提供'], + //], download_url = [ // 为了缓解非美国公益节点压力(考虑到很多人无视前面随机的美国节点),干脆也将其加入随机 + //['https://ghproxy.net/https://github.com', '英国', '[英国伦敦] - 该公益加速源由 [ghproxy.net] 提供 提示:希望大家尽量多使用美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], // 挂了 + ['https://ghfast.top/https://github.com', '其他', '[日本、韩国、新加坡、美国、德国等](CDN 不固定) - 该公益加速源由 [ghproxy.link] 提供 提示:希望大家尽量多使用美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], + ['https://wget.la/https://github.com', '其他', '[中国香港、中国台湾、日本、美国等](CDN 不固定) - 该公益加速源由 [ucdn.me] 提供 提示:希望大家尽量多使用美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], + //['https://hub.glowp.xyz/https://github.com', '其他', '[中国香港] - 该公益加速源由 [hub.glowp.xyz] 提供 提示:希望大家尽量多使用美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], + //['https://kkgithub.com', '其他', '[中国香港、日本、韩国、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供 提示:希望大家尽量多使用美国节点(每次随机 负载均衡), 避免流量都集中到亚洲公益节点,减少成本压力,公益才能更持久~'], // 404 + ], clone_url = [ ['https://gitclone.com', '国内', '[中国 国内] - 该公益加速源由 [GitClone] 提供 - 缓存:有 - 首次比较慢,缓存后较快'], - ['https://kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供 - 缓存:无(或时间很短)'], - ['https://hub.incept.pw', '香港', '[中国香港、美国] - 该公益加速源由 [FastGit 群组成员] 提供'], - ['https://mirror.ghproxy.com/https://github.com', '韩国', '[日本、韩国、德国等](CDN 不固定) - 该公益加速源由 [ghproxy] 提供 - 缓存:无(或时间很短)'], - //['https://gh-proxy.com/https://github.com', '韩国', '[韩国] - 该公益加速源由 [ghproxy] 提供 - 缓存:无(或时间很短)'], - ['https://githubfast.com', '韩国', '[韩国] - 该公益加速源由 [Github Fast] 提供 - 缓存:无(或时间很短)'], - ['https://ghproxy.net/https://github.com', '日本', '[日本 大阪] - 该公益加速源由 [ghproxy] 提供 - 缓存:无(或时间很短)'], - ['https://github.moeyy.xyz/https://github.com', '新加坡', '[新加坡、中国香港、日本等](CDN 不固定) - 该公益加速源由 [Moeyy] 提供 - 缓存:无(或时间很短)'], - //['https://slink.ltd/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [知了小站] 提供'] // 暂无必要 - //['https://hub.gitmirror.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供'], // 暂无必要 - //['https://sciproxy.com/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [sciproxy.com] 提供'], // 暂无必要 - //['https://ghproxy.cc/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要 - //['https://cf.ghproxy.cc/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要 - //['https://gh.jiasu.in/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 暂无必要 - //['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], // 暂无必要 - //['https://hub.fgit.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 被投诉挂了 - //['https://hub.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要 - //['https://hub.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要 - //['https://hub.njuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 域名挂了 - //['https://hub.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要 - //['https://hub.0z.gs', '美国', '[美国 Cloudflare CDN]'], // 域名无解析 - //['https://hub.shutcm.cf', '美国', '[美国 Cloudflare CDN]'] // 连接超时 - ]; - - const clone_ssh_url = [ + //['https://kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供'], // 超时 + //['https://gitdl.cn/https://github.com', '香港', '[中国香港] - 该公益加速源由 [gitdl] 提供'], // 输出文件错误 + //['https://gitproxy.click/https://github.com', '香港', '[中国 香港] - 该公益加速源由 [gitproxy.click] 提供'], + //['https://cdn.moran233.xyz/https://github.com', '香港', '[中国 香港] - 该公益加速源由 [cdn.moran233.xyz] 提供'], + //['https://hub.glowp.xyz/https://github.com', '香港', '[中国香港] - 该公益加速源由 [hub.glowp.xyz] 提供'], + ['https://wget.la/https://github.com', '香港', '[中国香港、中国台湾、日本、美国等](CDN 不固定) - 该公益加速源由 [ucdn.me] 提供'], + ['https://hk.gh-proxy.org/https://github.com', '香港', '[中国香港] - 该公益加速源由 [gh-proxy.com] 提供'], + ['https://ghfast.top/https://github.com', '韩国', '[日本、韩国、新加坡、美国、德国等](CDN 不固定) - 该公益加速源由 [ghproxy] 提供'], + //['https://gh.catmak.name/https://raw.githubusercontent.com', '韩国', '[韩国 首尔] - 该公益加速源由 [gh.catmak.name] 提供'], + ['https://githubfast.com', '韩国', '[韩国] - 该公益加速源由 [Github Fast] 提供'], + //['https://ghproxy.net/https://github.com', '日本', '[日本 大阪] - 该公益加速源由 [ghproxy.net] 提供'], // 挂了 + //['https://proxy.yaoyaoling.net/https://github.com', '日本', '[日本 东京] - 该公益加速源由 [proxy.yaoyaoling.net] 提供'], + //['https://g.blfrp.cn/https://github.com', '日本', '[日本 东京] - 该公益加速源由 [g.blfrp.cn] 提供'], + //['https://raw.bgithub.xyz', '荷兰', '[荷兰] - 该公益加速源由 [bgithub.xyz] 提供 - 缓存:有'], + //['https://ghproxy.1888866.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [WJQSERVER-STUDIO/ghproxy] 提供'], + //['https://github.moeyy.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [moeyy.cn] 提供'], // 墙了 + //['https://gh-proxy.net/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh-proxy.net] 提供'], + //['https://rapidgit.jjda.de5.net/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [热心网友] 提供'], + //['https://github.yongyong.online/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.yongyong.online] 提供'], + //['https://ghdd.862510.xyz/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghdd.862510.xyz] 提供'], + //['https://hub.gitmirror.com/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供'], // 域名无解析 + //['https://gh-proxy.org/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh-proxy.com] 提供'], + //['https://cdn.gh-proxy.org/https://github.com', '其他', '[Fastly CDN] - 该公益加速源由 [gh-proxy.com] 提供'], + //['https://edgeone.gh-proxy.org/https://github.com', '其他', '[edgeone] - 该公益加速源由 [gh-proxy.com] 提供'], + //['https://ghproxy.it/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchilau] 提供'], + //['https://github.site', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchilau] 提供'], // 挂了 + //['https://github.store', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchilau] 提供'], // 挂了 + //['https://gh.jiasu.in/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 404 + //['https://github.boki.moe/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [blog.boki.moe] 提供'], + //['https://raw.ihtw.moe/github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [raw.ihtw.moe] 提供'], + //['https://xget.xi-xu.me/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.com/xixu-me/Xget] 提供'], + //['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], + //['https://gh-proxy.ygxz.in/https://github.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@一个小站 www.ygxz.in] 提供'], // 被蔷 + //['https://hub.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 证书到期 + ], clone_ssh_url = [ ['ssh://git@ssh.github.com:443/', 'Github 原生', '[日本、新加坡等] - Github 官方提供的 443 端口的 SSH(依然是 SSH 协议),适用于限制访问 22 端口的网络环境'], - ['git@ssh.fastgit.org:', '香港', '[中国 香港] - 该公益加速源由 [FastGit] 提供'] + //['git@ssh.fastgit.org:', '香港', '[中国 香港] - 该公益加速源由 [FastGit] 提供'], // 挂了 //['git@git.zhlh6.cn:', '美国', '[美国 洛杉矶]'] // 挂了 - ]; - - const raw_url = [ - ['https://raw.githubusercontent.com', 'Github 原生', '[日本 东京]'], - ['https://raw.kkgithub.com', '香港', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供 - 缓存:无(或时间很短)'], - ['https://mirror.ghproxy.com/https://raw.githubusercontent.com', '韩国', '[日本、韩国、德国等](CDN 不固定) - 该公益加速源由 [ghproxy] 提供 - 缓存:无(或时间很短)'], - //['https://gh-proxy.com/https://raw.githubusercontent.com', '韩国 2', '[韩国] - 该公益加速源由 [ghproxy] 提供 - 缓存:无(或时间很短)'], - ['https://ghproxy.net/https://raw.githubusercontent.com', '日本 1', '[日本 大阪] - 该公益加速源由 [ghproxy] 提供 - 缓存:无(或时间很短)'], - ['https://fastly.jsdelivr.net/gh', '日本 2', '[日本 东京] - 该公益加速源由 [JSDelivr CDN] 提供 - 缓存:有 - 不支持大小超过 50 MB 的文件 - 不支持版本号格式的分支名(如 v1.2.3)'], - ['https://fastraw.ixnic.net', '日本 3', '[日本 大阪] - 该公益加速源由 [FastGit 群组成员] 提供 - 缓存:无(或时间很短)'], + ], raw_url = [ + ['https://raw.githubusercontent.com', 'Github 原生', '[日本 东京] - 缓存:无(或很短)'], + //['https://raw.kkgithub.com', '香港 1', '[中国香港、日本、新加坡等] - 该公益加速源由 [help.kkgithub.com] 提供 - 缓存:有'], // 超时 + //['https://jsd.proxy.aks.moe/gh', '香港 2', '[中国 香港] - 该公益加速源由 [cdn.akass.cn] 提供'], // 证书错误 + ['https://wget.la/https://raw.githubusercontent.com', '香港 1', '[中国香港、中国台湾、日本、美国等](CDN 不固定) - 该公益加速源由 [ucdn.me] 提供 - 缓存:无(或很短)'], + ['https://hk.gh-proxy.org/https://raw.githubusercontent.com', '香港 2', '[中国香港] - 该公益加速源由 [gh-proxy.com] 提供 - 缓存:有(官方注明 2 小时)'], + ['https://hub.glowp.xyz/https://raw.githubusercontent.com', '香港 3', '[中国香港] - 该公益加速源由 [hub.glowp.xyz] 提供 - 缓存:有'], + //['https://gitproxy.click/https://raw.githubusercontent.com', '香港', '[中国 香港] - 该公益加速源由 [gitproxy.click] 提供'], // 输出错误 + //['https://cdn.moran233.xyz/https://raw.githubusercontent.com', '香港', '[中国 香港] - 该公益加速源由 [cdn.moran233.xyz] 提供'], // 404 + //['https://gitdl.cn/https://raw.githubusercontent.com', '香港 3', '[中国香港] - 该公益加速源由 [gitdl] 提供 - 缓存:有'], // 输出文件错误 + ['https://ghfast.top/https://raw.githubusercontent.com', '韩国', '[日本、韩国、新加坡、美国、德国等](CDN 不固定) - 该公益加速源由 [ghproxy.link] 提供 - 缓存:无(或很短)'], + ['https://gh.catmak.name/https://raw.githubusercontent.com', '韩国', '[韩国 首尔] - 该公益加速源由 [gh.catmak.name] 提供'], + //['https://ghproxy.net/https://raw.githubusercontent.com', '日本 1', '[日本 大阪] - 该公益加速源由 [ghproxy.net] 提供 - 缓存:有(约 10 分钟)'], // 挂了 + ['https://fastly.jsdelivr.net/gh', '日本 1', '[日本 东京] - 该公益加速源由 [JSDelivr CDN] 提供 - 缓存:有 - 不支持大小超过 50 MB 的文件 - 不支持版本号格式的分支名(如 v1.2.3)'], + ['https://cdn.gh-proxy.org/https://raw.githubusercontent.com', '日本 2', '[Fastly CDN] - 该公益加速源由 [gh-proxy.com] 提供 - 缓存:有'], + //['https://jsdelivr.pai233.top/gh', '日本 3', '[日本 东京](Vercel Anycast) - 该公益加速源由 [blog.pai233.top] 提供 - 缓存:有'], // This deployment is temporarily paused + //['https://proxy.yaoyaoling.net/https://raw.githubusercontent.com', '日本', '[日本 东京] - 该公益加速源由 [proxy.yaoyaoling.net] 提供'], // 空白 + ['https://g.blfrp.cn/https://raw.githubusercontent.com', '日本 3', '[日本 东京] - 该公益加速源由 [g.blfrp.cn] 提供'], + //['https://raw.bgithub.xyz', '荷兰', '[荷兰] - 该公益加速源由 [bgithub.xyz] 提供 - 缓存:有'], //['https://gcore.jsdelivr.net/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [JSDelivr CDN] 提供 - 缓存:有 - 不支持大小超过 50 MB 的文件 - 不支持版本号格式的分支名(如 v1.2.3)'], // 变成 美国 Cloudflare CDN 了 - ['https://cdn.jsdelivr.us/gh', '其他 1', '[韩国、美国、马来西亚、罗马尼亚等](CDN 不固定) - 该公益加速源由 [@ayao] 提供 - 缓存:有'], - //['https://jsdelivr.b-cdn.net/gh', '其他 2', '[中国香港、台湾、日本、新加坡等](CDN 不固定) - 该公益加速源由 [@rttwyjz] 提供 - 缓存:有'], - ['https://github.moeyy.xyz/https://raw.githubusercontent.com', '其他 3', '[新加坡、中国香港、日本等](CDN 不固定) - 缓存:无(或时间很短)'], - ['https://raw.cachefly.998111.xyz', '其他 4', '[新加坡、日本、印度等](Anycast CDN 不固定) - 该公益加速源由 [@XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX0] 提供 - 缓存:有(约 12 小时)'], - //['https://raw.incept.pw', '香港', '[中国香港、美国] - 该公益加速源由 [FastGit 群组成员] 提供 - 缓存:无(或时间很短)'], // ERR_SSL_PROTOCOL_ERROR - //['https://ghproxy.cc/https://raw.githubusercontent.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要 - //['https://cf.ghproxy.cc/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchiii lau] 提供'], // 暂无必要 - //['https://gh.jiasu.in/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 暂无必要 - //['https://dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供'], // 暂无必要 - //['https://raw.fgit.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供 - 缓存:无(或时间很短)'], // 被投诉挂了 - //['https://raw.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要 - //['https://raw.scholar.rr.nu', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 暂无必要 - //['https://raw.njuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供 - 缓存:无(或时间很短)'], // 域名挂了 - //['https://raw.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供 - 缓存:无(或时间很短)'], // 暂无必要 - //['https://raw.gitmirror.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供 - 缓存:有'], // 暂无必要 - //['https://cdn.54188.cf/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [PencilNavigator] 提供 - 缓存:有'], // 暂无必要 - //['https://raw.fastgit.org', '德国', '[德国] - 该公益加速源由 [FastGit] 提供 - 缓存:无(或时间很短)'], // 挂了 - //['https://git.yumenaka.net/https://raw.githubusercontent.com', '美国', '[美国 圣何塞] - 缓存:无(或时间很短)'], // 连接超时 - ]; - - const svg = [ + //['https://jsdelivr.b-cdn.net/gh', '其他', '[中国香港、中国台湾、日本、新加坡等](CDN 不固定) - 该公益加速源由 [@rttwyjz] 提供 - 缓存:有'], // 疑似 SNI 阻断 + //['https://xget.xi-xu.me/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.com/xixu-me/Xget] 提供'], + //['https://ghproxy.1888866.xyz/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [WJQSERVER-STUDIO/ghproxy] 提供'], + //['https://github.moeyy.xyz/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [moeyy.cn] 提供 - 缓存:有(约 10 分钟)'], // 墙了 + //['https://gh-proxy.net/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh-proxy.net] 提供'], + //['https://rapidgit.jjda.de5.net/https://github.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [热心网友] 提供'], + //['https://github.yongyong.online/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [github.yongyong.online] 提供'], + //['https://ghdd.862510.xyz/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [ghdd.862510.xyz] 提供'], + //['https://raw.cachefly.998111.xyz', '其他 4', '[新加坡、日本、印度等](Anycast CDN 不固定) - 该公益加速源由 [@XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX0] 提供 - 缓存:有(约 12 小时)'], // 证书到期 + //['https://ghproxy.it/https://raw.githubusercontent.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@yionchilau] 提供 - 缓存:无(或很短)'], + //['https://raw.github.site', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchilau] 提供 - 缓存:无(或很短)'], // 挂了 + //['https://raw.github.store', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@yionchilau] 提供 - 缓存:无(或很短)'], // 挂了 + //['https://gh.jiasu.in/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [@0-RTT] 提供'], // 404 + //['https://github.boki.moe/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [blog.boki.moe] 提供 - 缓存:无(或很短)'], + //['https://gh-proxy.org/https://raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [gh-proxy.com] 提供 - 缓存:有'], + //['https://cdn.gh-proxy.org/https://raw.githubusercontent.com', '其他', '[Fastly CDN] - 该公益加速源由 [gh-proxy.com] 提供'], + //['https://edgeone.gh-proxy.org/https://raw.githubusercontent.com', '其他', '[edgeone] - 该公益加速源由 [gh-proxy.com] 提供'], + //['https://cdn.githubraw.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [githubraw.com] 提供 - 缓存:有(几乎永久)'], + //['https://raw.dgithub.xyz', '美国', '[美国 西雅图] - 该公益加速源由 [dgithub.xyz] 提供 - 缓存:无(或很短)'], + //['https://gh-proxy.ygxz.in//https://raw.githubusercontent.com', '美国', '[美国 洛杉矶] - 该公益加速源由 [@一个小站 www.ygxz.in] 提供 - 缓存:无(或很短)'], // 被蔷 + //['https://raw.nuaa.cf', '美国', '[美国 洛杉矶] - 该公益加速源由 [FastGit 群组成员] 提供'], // 证书到期 + //['https://raw.yzuu.cf', '美国', '[美国 纽约] - 该公益加速源由 [FastGit 群组成员] 提供'], // 证书到期 + //['https://hub.gitmirror.com/raw.githubusercontent.com', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [GitMirror] 提供 - 缓存:无(或很短)'], // 域名无解析 + //['https://jsdelivr-cdn.pencilnavrp.990989.xyz/gh', '美国', '[美国 Cloudflare CDN] - 该公益加速源由 [PencilNavigator] 提供 - 缓存:有'], + //['https://git.yumenaka.net/https://raw.githubusercontent.com', '美国', '[美国 圣何塞]'], // 连接超时 + ], svg = [ '' - ], style = ['padding:0 6px; margin-right: -1px; border-radius: 2px; background-color: var(--XIU2-back-Color); border-color: rgba(27, 31, 35, 0.1); font-size: 11px; color: var(--XIU2-font-Color);']; + ], style = ['padding:0 6px; margin-right: -1px; border-radius: 2px; background-color: var(--XIU2-background-color); border-color: var(--borderColor-default); font-size: 11px; color: var(--XIU2-font-color);']; if (menu_rawFast == null){menu_rawFast = 1; GM_setValue('xiu2_menu_raw_fast', 1)}; if (GM_getValue('menu_rawDownLink') == null){GM_setValue('menu_rawDownLink', true)}; if (GM_getValue('menu_gitClone') == null){GM_setValue('menu_gitClone', true)}; + // 如果自定义加速源不存在或为空则忽略,如果自定义加速源地址存在,则添加到 raw_url、clone_url 数组中 + if (GM_getValue('custom_raw_url')) {raw_url.splice(1, 0, [GM_getValue('custom_raw_url'), '自定义', '[由你自定义的 Raw 加速源] 提示:点击浏览器右上角 Tampermonkey 扩展图标 - [ #️⃣ 自定义加速源 ] 即可轮流设置 Raw、Git Clone、Release/Code(ZIP) 的自定义加速源地址(留空代表不设置)。'])}; + if (GM_getValue('custom_clone_url')) {clone_url.unshift([GM_getValue('custom_clone_url'), '自定义', '[由你自定义的 Git Clone 加速源] 提示:点击浏览器右上角 Tampermonkey 扩展图标 - [ #️⃣ 自定义加速源 ] 即可轮流设置 Raw、Git Clone、Release/Code(ZIP) 的自定义加速源地址(留空代表不设置)。'])}; registerMenuCommand(); // 注册脚本菜单 function registerMenuCommand() { // 如果反馈菜单ID不是 null,则删除所有脚本菜单 - if (menu_feedBack_ID) {GM_unregisterMenuCommand(menu_rawFast_ID); GM_unregisterMenuCommand(menu_rawDownLink_ID); GM_unregisterMenuCommand(menu_gitClone_ID); GM_unregisterMenuCommand(menu_feedBack_ID); menu_rawFast = GM_getValue('xiu2_menu_raw_fast');} + if (menu_feedBack_ID) {GM_unregisterMenuCommand(menu_rawFast_ID); GM_unregisterMenuCommand(menu_rawDownLink_ID); GM_unregisterMenuCommand(menu_gitClone_ID); GM_unregisterMenuCommand(menu_customUrl_ID); GM_unregisterMenuCommand(menu_feedBack_ID); menu_rawFast = GM_getValue('xiu2_menu_raw_fast');} // 避免在减少 raw 数组后,用户储存的数据大于数组而报错 if (menu_rawFast > raw_url.length - 1) menu_rawFast = 0 - menu_rawDownLink_ID = GM_registerMenuCommand(`${GM_getValue('menu_rawDownLink')?'✅':'❌'} 项目列表单文件快捷下载 (☁)`, function(){if (GM_getValue('menu_rawDownLink') == true) {GM_setValue('menu_rawDownLink', false); GM_notification({text: `已关闭「项目列表单文件快捷下载 (☁)」功能\n(点击刷新网页后生效)`, timeout: 3500, onclick: function(){location.reload();}});} else {GM_setValue('menu_rawDownLink', true); GM_notification({text: `已开启「项目列表单文件快捷下载 (☁)」功能\n(点击刷新网页后生效)`, timeout: 3500, onclick: function(){location.reload();}});}registerMenuCommand();}, {title: "点击开关「项目列表单文件快捷下载 (☁)」功能"}); - if (GM_getValue('menu_rawDownLink')) menu_rawFast_ID = GM_registerMenuCommand(`      ${['0️⃣','1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟'][menu_rawFast]} [ ${raw_url[menu_rawFast][1]} ] 加速源 (☁) - 点击切换`, menu_toggle_raw_fast, {title: "点击切换「项目列表单文件快捷下载 (☁)」功能的加速源"}); - menu_gitClone_ID = GM_registerMenuCommand(`${GM_getValue('menu_gitClone')?'✅':'❌'} 添加 git clone 命令`, function(){if (GM_getValue('menu_gitClone') == true) {GM_setValue('menu_gitClone', false); GM_notification({text: `已关闭「添加 git clone 命令」功能`, timeout: 3500});} else {GM_setValue('menu_gitClone', true); GM_notification({text: `已开启「添加 git clone 命令」功能`, timeout: 3500});}registerMenuCommand();}, {title: "点击开关「添加 git clone 命令」功能"}); - menu_feedBack_ID = GM_registerMenuCommand('💬 反馈问题 & 功能建议', function () {GM_openInTab('https://github.com/XIU2/UserScript', {active: true,insert: true,setParent: true});GM_openInTab('https://greasyfork.org/zh-CN/scripts/412245/feedback', {active: true,insert: true,setParent: true});}, {title: "点击前往反馈问题或提出建议"}); + if (GM_getValue('menu_rawDownLink')) menu_rawFast_ID = GM_registerMenuCommand(`${['0️⃣','1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟'][menu_rawFast]} [ ${raw_url[menu_rawFast][1]} ] 加速源 (☁) - 点击切换`, menu_toggle_raw_fast); + menu_rawDownLink_ID = GM_registerMenuCommand(`${GM_getValue('menu_rawDownLink')?'✅':'❌'} 项目列表单文件快捷下载 (☁)`, function(){if (GM_getValue('menu_rawDownLink') == true) {GM_setValue('menu_rawDownLink', false); GM_notification({text: `已关闭 [项目列表单文件快捷下载 (☁)] 功能\n(点击刷新网页后生效)`, timeout: 3500, onclick: function(){location.reload();}});} else {GM_setValue('menu_rawDownLink', true); GM_notification({text: `已开启 [项目列表单文件快捷下载 (☁)] 功能\n(点击刷新网页后生效)`, timeout: 3500, onclick: function(){location.reload();}});}registerMenuCommand();}); + menu_gitClone_ID = GM_registerMenuCommand(`${GM_getValue('menu_gitClone')?'✅':'❌'} 添加 git clone 命令`, function(){if (GM_getValue('menu_gitClone') == true) {GM_setValue('menu_gitClone', false); GM_notification({text: `已关闭 [添加 git clone 命令] 功能\n(点击刷新网页后生效)`, timeout: 3500, onclick: function(){location.reload();}});} else {GM_setValue('menu_gitClone', true); GM_notification({text: `已开启 [添加 git clone 命令] 功能\n(点击刷新网页后生效)`, timeout: 3500, onclick: function(){location.reload();}});}registerMenuCommand();}); + menu_customUrl_ID = GM_registerMenuCommand(`#️⃣ 自定义加速源`, function () { + // 定义三种自定义加速源的键名和描述 + const customKeys = [ + { key: 'custom_raw_url', desc: 'Raw 加速源', placeholder: 'https://example.com/https://raw.githubusercontent.com' }, + { key: 'custom_clone_url', desc: 'Git Clone 加速源', placeholder: 'https://example.com/https://github.com' }, + { key: 'custom_download_url', desc: 'Release/Code(ZIP) 加速源', placeholder: 'https://example.com/https://github.com' } + ]; + // 递归弹出输入框 + function promptCustomUrl(index = 0) { + if (index >= customKeys.length) {GM_notification({ text: '自定义加速源设置已完成!\n(点击刷新网页后生效)', timeout: 3500, onclick: function () { location.reload(); } });return;} + const { key, desc, placeholder } = customKeys[index]; + let current = GM_getValue(key, ''); + let input = prompt(`请输入自定义${desc}地址:\n- 当前:\n${current || '(未设置)'}\n\n- 示例:\n${placeholder}\n\n- 留空为不设置\n- 点击 [确定] 保存 并 继续设置下一个\n- 点击 [取消] 不保存 并 终止后续设置`,current); + if (input !== null) {GM_setValue(key, input.trim());promptCustomUrl(index + 1);}// 如果用户点击 取消 按钮,则不再继续弹出 + } + promptCustomUrl(); + }); + menu_feedBack_ID = GM_registerMenuCommand('💬 反馈 & 建议 [Github]', function () {window.GM_openInTab('https://github.com/XIU2/UserScript', {active: true,insert: true,setParent: true});window.GM_openInTab('https://greasyfork.org/zh-CN/scripts/412245/feedback', {active: true,insert: true,setParent: true});}); } // 切换加速源 @@ -188,17 +263,16 @@ for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { if (target.nodeType !== 1) return - if (target.tagName === 'DIV' && target.parentElement.id === '__primerPortalRoot__') { + if (target.tagName === 'DIV' && target.parentElement && target.parentElement.id === '__primerPortalRoot__') { + addGitClone(target); + addGitCloneSSH(target); addDownloadZIP(target); - if (addGitClone(target) === false) return; - if (addGitCloneSSH(target) === false) return; - } else if (target.tagName === 'DIV' && target.className.indexOf('Box-sc-') !== -1) { + //setTimeout(()=>{addDownloadZIP(target)}, 300); + } else if (target.tagName === 'DIV' && target.className.indexOf('LocalTab-module__') != -1) { if (target.querySelector('input[value^="https:"]')) { - addGitCloneClear('.XIU2-GCS'); - if (addGitClone(target) === false) return; + addGitCloneClear('.XIU2-GCS'); addGitClone(target); } else if (target.querySelector('input[value^="git@"]')) { - addGitCloneClear('.XIU2-GC'); - if (addGitCloneSSH(target) === false) return; + addGitCloneClear('.XIU2-GC'); addGitCloneSSH(target); } else if (target.querySelector('input[value^="gh "]')) { addGitCloneClear('.XIU2-GC, .XIU2-GCS'); } @@ -211,12 +285,17 @@ observer.observe(document, { childList: true, subtree: true }); - // download_url 随机 4 个美国加速源 + // download_url 随机几个美国加速源 function get_New_download_url() { - //return download_url_us.concat(download_url) // 全输出调试用 - let shuffled = download_url_us.slice(0), i = download_url_us.length, min = i - 4, temp, index; + //return download_url_us // 全输出调试用 + let minnum = 6; // 随机输出几个美国加速源 + if (GM_getValue('custom_download_url')) {minnum = 5;} // 如果有自定义加速源,则只随机输出 5 个美国加速源 + let shuffled = download_url_us.slice(0), i = download_url_us.length, min = i - minnum, temp, index; while (i-- > min) {index = Math.floor((i + 1) * Math.random()); temp = shuffled[index]; shuffled[index] = shuffled[i]; shuffled[i] = temp;} - return shuffled.slice(min).concat(download_url); // 随机洗牌 download_url_us 数组并取前 4 个,然后将其合并至 download_url 数组 + // 如果有自定义加速源,则将其添加到随机数组的开头 + if (GM_getValue('custom_download_url')) {return [[GM_getValue('custom_download_url'), '自定义', '[由你自定义的 Release/Code(ZIP) 加速源地址] 提示:点击浏览器右上角 Tampermonkey 扩展图标 - [ #️⃣ 自定义加速源 ] 即可轮流设置 Raw、Git Clone、Release/Code(ZIP) 的自定义加速源地址(留空代表不设置)。']].concat(shuffled.slice(min));} + return shuffled.slice(min) // 随机洗牌 download_url_us 数组并取前几个,然后将其合并至 download_url 数组 + // 为了缓解非美国公益节点压力(考虑到很多人无视前面随机的美国节点),干脆也将其加入随机 } // Release @@ -224,6 +303,7 @@ let html = document.querySelectorAll('.Box-footer'); if (html.length == 0 || location.pathname.indexOf('/releases') == -1) return let divDisplay = 'margin-left: -90px;', new_download_url = get_New_download_url(); if (document.documentElement.clientWidth > 755) {divDisplay = 'margin-top: -3px;margin-left: 8px;display: inherit;';}; // 调整小屏幕时的样式 + html[0].appendChild(document.createElement('style')).textContent = '@media (min-width: 768px) {.Box-footer li.Box-row>div>span.color-fg-muted {min-width: 27px !important;}}'; for (const current of html) { if (current.querySelector('.XIU2-RS')) continue current.querySelectorAll('li.Box-row a').forEach(function (_this) { @@ -236,7 +316,7 @@ } else { url = new_download_url[i][0] + href[1] } - _html += `${new_download_url[i][1]}` + _html += `${new_download_url[i][1]}` } _this.parentElement.nextElementSibling.insertAdjacentHTML('beforeend', _html + ''); }); @@ -246,11 +326,14 @@ // Download ZIP function addDownloadZIP(target) { - let html = target.querySelector('ul[class^=List__ListBox-sc-] ul[class^=List__ListBox-sc-]>li:last-child');if (!html) return - let href_script = document.querySelector('react-partial[partial-name=repos-overview]>script[data-target="react-partial.embeddedData"]'), - href_slice = href_script.textContent.slice(href_script.textContent.indexOf('"zipballUrl":"')+14), - href = href_slice.slice(0, href_slice.indexOf('"')), - url = '', _html = '', new_download_url = get_New_download_url(); + const html = target.querySelector('ul[class^=prc-ActionList-ActionList-]>li:last-child');if (!html) return + let href = html.querySelector('a[href^="/"][href$=".zip"]');if (!href || !href.getAttribute('href')) return + href = href.getAttribute('href'); + //const href_script = document.querySelector('react-partial[partial-name=repos-overview]>script[data-target="react-partial.embeddedData"]');if (!href_script) return + //const href = JSON.parse(href_script.textContent).props.initialPayload.overview.codeButton.local.platformInfo.zipballUrl + /*let href_slice = href_script.textContent.slice(href_script.textContent.indexOf('"zipballUrl":"')+14), + href = href_slice.slice(0, href_slice.indexOf('"')),*/ + let url = '', _html = '', new_download_url = get_New_download_url(); // 克隆原 Download ZIP 元素,并定位 标签 let html_clone = html.cloneNode(true), @@ -266,7 +349,9 @@ url = new_download_url[i][0] + href } html_clone_a.href = url - html_clone_a.setAttribute('title', new_download_url[i][2].replaceAll(' ','\n')) + html_clone_a.setAttribute('title', new_download_url[i][2].replaceAll(' ','\n') + '\n\n提示:如果不想要点击链接在前台打开空白新标签页(一闪而过影响体验),\n可以 [鼠标中键] 或 [Ctrl+鼠标左键] 点击加速链接即可在后台打开新标签页!'); + html_clone_a.setAttribute('target', '_blank'); + html_clone_a.setAttribute('rel', 'noreferrer noopener nofollow'); html_clone_span.textContent = 'Download ZIP ' + new_download_url[i][1] _html += html_clone.outerHTML } @@ -280,12 +365,14 @@ // Git Clone function addGitClone(target) { - let html = target.querySelector('input[value^="https:"]');if (!html) return - if (!html.nextElementSibling) return false; + let html = target.querySelector('input[value^="https:"]:not([title])');if (!html) return let href_split = html.value.split(location.host)[1], html_parent = '
', url = '', _html = '', _gitClone = ''; - html.nextElementSibling.hidden = true; // 隐藏右侧复制按钮(考虑到能直接点击复制,就不再重复实现复制按钮事件了) + if (html.nextElementSibling) html.nextElementSibling.hidden = true; // 隐藏右侧复制按钮(考虑到能直接点击复制,就不再重复实现复制按钮事件了) + if (html.parentElement.nextElementSibling.tagName === 'P'){ + html.parentElement.nextElementSibling.textContent += ' (↑点击文字自动复制)' + } if (GM_getValue('menu_gitClone')) {_gitClone='git clone '; html.value = _gitClone + html.value; html.setAttribute('value', html.value);} // 克隆原 Git Clone 元素 let html_clone = html.cloneNode(true); @@ -300,17 +387,23 @@ _html += html_parent + html_clone.outerHTML + '
' } html.parentElement.insertAdjacentHTML('afterend', _html); + if (html.parentElement.parentElement.className.indexOf('XIU2-GCP') === -1){ + html.parentElement.parentElement.classList.add('XIU2-GCP') + html.parentElement.parentElement.addEventListener('click', (e)=>{if (e.target.tagName === 'INPUT') {GM_setClipboard(e.target.value);}}) + } } // Git Clone SSH function addGitCloneSSH(target) { - let html = target.querySelector('input[value^="git@"]');if (!html) return - if (!html.nextElementSibling) return false; + let html = target.querySelector('input[value^="git@"]:not([title])');if (!html) return let href_split = html.value.split(':')[1], html_parent = '
', url = '', _html = '', _gitClone = ''; html.nextElementSibling.hidden = true; // 隐藏右侧复制按钮(考虑到能直接点击复制,就不再重复实现复制按钮事件了) + if (html.parentElement.nextElementSibling.tagName === 'P'){ + html.parentElement.nextElementSibling.textContent += ' (↑点击自动复制)' + } if (GM_getValue('menu_gitClone')) {_gitClone='git clone '; html.value = _gitClone + html.value; html.setAttribute('value', html.value);} // 克隆原 Git Clone SSH 元素 let html_clone = html.cloneNode(true); @@ -321,6 +414,10 @@ _html += html_parent + html_clone.outerHTML + '
' } html.parentElement.insertAdjacentHTML('afterend', _html); + if (html.parentElement.parentElement.className.indexOf('XIU2-GCP') === -1){ + html.parentElement.parentElement.classList.add('XIU2-GCP') + html.parentElement.parentElement.addEventListener('click', (e)=>{if (e.target.tagName === 'INPUT') {GM_setClipboard(e.target.value);}}) + } } @@ -337,7 +434,7 @@ } else { url = raw_url[i][0] + href2; } - _html += `
${raw_url[i][1].replace(/ \d/,'')}` + _html += `${raw_url[i][1].replace(/ \d/,'')}` } if (document.querySelector('.XIU2-RF')) document.querySelectorAll('.XIU2-RF').forEach((e)=>{e.remove()}) html.insertAdjacentHTML('afterend', _html); @@ -382,7 +479,7 @@ url = raw_url[menu_rawFast][0] + href2; } - fileElm.insertAdjacentHTML('afterend', ``); + fileElm.insertAdjacentHTML('afterend', ``); // 绑定鼠标事件 trElm.onmouseover = mouseOverHandler; trElm.onmouseout = mouseOutHandler; @@ -436,7 +533,7 @@ function colorMode() { let style_Add; if (document.getElementById('XIU2-Github')) {style_Add = document.getElementById('XIU2-Github')} else {style_Add = document.createElement('style'); style_Add.id = 'XIU2-Github'; style_Add.type = 'text/css';} - backColor = '#ffffff'; fontColor = '#888888'; + let backColor = '#ffffff', fontColor = '#888888'; if (document.lastElementChild.dataset.colorMode === 'dark') { // 如果是夜间模式 if (document.lastElementChild.dataset.darkTheme === 'dark_dimmed') { @@ -454,7 +551,7 @@ } } - document.lastElementChild.appendChild(style_Add).textContent = `.XIU2-RS a {--XIU2-back-Color: ${backColor}; --XIU2-font-Color: ${fontColor};}`; + document.lastElementChild.appendChild(style_Add).textContent = `.XIU2-RS a {--XIU2-background-color: ${backColor}; --XIU2-font-color: ${fontColor};}`; } @@ -478,4 +575,4 @@ window.dispatchEvent(new Event('urlchange')) }); } -})(); +})(); \ No newline at end of file diff --git a/packages/gui/extra/scripts/tampermonkey.script b/packages/gui/extra/scripts/tampermonkey.script index 297e2baf13..e9933868a7 100644 --- a/packages/gui/extra/scripts/tampermonkey.script +++ b/packages/gui/extra/scripts/tampermonkey.script @@ -1,9 +1,9 @@ /** * 篡改猴(Tampermonkey)| 油猴(Greasemonkey)浏览器脚本扩展 * - * @version 0.1.4 - * @since 2024-04-24 17:06 - * @author 王良 + * @version 0.1.8 + * @since 2024-09-27 14:24 + * @author 王良 / wangliang181230 * @authorHomePage https://wangliang1024.cn * @remark 当前脚本为仿照的版本,并非篡改猴插件的源码,仅供学习参考。 * @description 篡改猴 (Tampermonkey) 是拥有 超过 1000 万用户 的最流行的浏览器扩展之一。 它适用于 Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。 @@ -16,7 +16,7 @@ */ 'use strict'; (function () { - const version = "0.1.4"; + const version = "0.1.8"; const PRE = "DS-Tampermonkey:"; // 前缀 const MENU_ID_PRE = PRE + "menu-"; const icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwEAQAAACtm+1PAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAACqjSMyAAAACXBIWXMAAABIAAAASABGyWs+AAAL+UlEQVRo3tWaeXRUdZbHP7+qkI1ABIQJryoJEAkYaAJECBEEGhAIgiw2DAfBAUEE9MjixtjSKNotyICI4CggckCddsSWRZYASqvIIltQo0ADQdaEfctGSH3nj9/L0ggIkjPY95yqd+q9V/fe77339373e6vgX1zMtS4+2CRuzo+fDOl4IbfDY4H2DR7P3xjWMXAS8AKhV/lSDhC4htIwIOgq1y6BJwJCU/NWeb7PmBEem9a0wZ/fjX7/ob1DbgjA042jeq8YNtGcm/vQhqIkc8C8BYwBugHRYO4AfK6ztwHhZbRdAgqAY0Cwe+0MUAmo7DpfbFXAUaACEAH6EfgBWAlMBj0N3n2Kifx8fkqXPmM16ZWsj34RQPfHW7b7Ln5h14L0qNEmF8wa4EMww4BBwO/cVxVXQSzQyDoAwCmgEDjkRjsCOO7eH+m+ijNwAtgCOu1+5wdgB5AGehF4AtQKFA4hfbNeq/dpj54rntxU+6oAUucnZ+4cvDb24udhxlQHkwPmWzCfAqPAdASSgKZALOAB8lynzpeC+kU542aj2PolIBvYCnwPWgn8F2ggqDaoks1O8IA8JdT+/U/LVpeCKAHw7JdRvT8K2Z5SEIga7ekGxg8m2I3+h8AIMN2Be4B/c6NYFbsWKrnnKlwnAICTbpkVuZkoAHJtRvgYNBt4DNQcFAH6BgKfQGi/rNf63NVkwysf23IqWU4r908eUrA9qqMZCaYumBgwZ8HE2WhwAsxPQJQLOxg4dwMOX0sCQD6QBewHXQQzF7t+agLVLDhTE/I7Ro1e8fHkBjDgI9wiYGjleJ1e8OAQcxxMHpiKYM6DOQRkgFkF5mNguFs+ES6A8hKP62wd4A9gpoBZCXwDZjeYLDBeMGH2eGr4g0Me/Uu8SgBsWT7oYFFLc8BsAbMPWARmLTAPzNtA+3J09nolGcwsYCmY1e4r3fpVVGQObJk/6CC4a6Dec5u75cy+a4lZAp7XwbS09W+WAm1ugfNlZTOoLSgYtAECQ0HdoOKMLffv6tNsqQGIy8tNK5gc1tHzdzCvgycROOlG33uLAQBMAIVAYB3oKQi0hNBZeav2ZIZ3MgD+YxLHwPMteJ4Dc7d9fPIft9pzV74EJYDWQOAJCNwBpMAhjzFBAIoFTyGwDlvvtbl6q3ArpKnrU8D1LwwCOfaSB7CbUR0wrYB6QDTXvyn9f0gEUB+IAxMPph4lpe0peb8d+yirjI2+51Z7fZnEAzWAi4DARJQFgOt8ODYbxah/S1LJ9S/BftSpywEUS/GZ87fa4yv4FcBmAC4rIYALoMP2SB621f0tyUlK2w3ARNpjKbU4DJwF9mF7nJ3Yvue3IAHXn8PYDJTZmyyAO7HE4htsNxgNrAXa3mrPXdmI7XQPWu4gbMNHpeISKgDtA/5he3H9D2gBMBnYe4udzwTNxwb0K2ADtpz22Mt2I9sBZiOWynmwZfQB6Elsds5cQXEFoDWYRKAZkIJlYNcjF4E1rnM7sFk/XOZ6dWxnGopldqNAfwJ5gRXYdXpbGQAUArtBH9iNQt+6IOq6yi64qLdi6aEHSyODQJlgHgFqgRkKPIjdDK/EtncCfwO95epfC9qELZFdWHJTD0twggE/lgt0Bx4GkoFlwBNAw7IAjgGrgSO2WTLtQR2xpP1zYCkozY3KfjeC9cH0AMbacjN/diMWBQwC0xlIxNLFjaB04A17jw5jWdd/A31AS7HszmApa0MwDwG9XBAAna1P6gtMB2LKAtgBLALWgb52P2cBM0EfAgdsO8s2Sp8GB+zRTLftt9bZVsQsB1MdNARogt18QkFBbsksBDUCloDO2PLUBrfGC90sNLCVQD83q52xu3APoLGbsbvLAvjJdfwkmA6ge62zmgC0A02C0FZH61Wo/NXSwN93j/TGFY7iKadn0YKmEfmfNn05sMc71FRzo5hnCbh5Abt7XrJAtQuYB/orqIJ9kngWFM0Kq7p1Fp02nQ85e2J6Ye8K09QwrlGR03ZR7taau0wyaB6YUW7J3g0aCyoAllsABsAZI5k8MIuAVECgiUBvqBy57U/+/uOc9/ulBWoUFT12eVmPuD1mzNf/O2r5mbTH+xbtqzDe+MB4wKQBA9yIvgNqbw3rMHiHFr5YpcOMvya3nXL07e8On71cp+Sd2WF423sOtX7pyIVZKZ1YCmacLVvmgpJAOXBkvrErzRkk+RpKvvck30rJN1CKbqxJLd4av/zEXu94rkN6fpGYUefA7lzfD1J0uBS9V4rpKsXcL0VvlKKN5NshxUXuzu31XWLG9eg8X+gdn7Lw+Qf8ay4d9A2UfFsl3weSL1Zyequ0V3CSJOeY5Ksl+YIl3zqldNj+ZNL1GCkrwx5w+t8xdl9f3zLJ/5EUXUuKbij5P5B8f5PqpO/OfXSr0/9G9baIGzTJH6sUXxXJd5fkHJactmUBJEjOF5KzR3JGS4mb36l9o0ZKMtE/MSPWc/EF3wzJ30DyJ0u+uVKs5+ILve67vshfSRrXebO/80fJ2S05cyXnzrIAkiWns+R0k2qHnh4xNify+K81BJD41NT6zhbJ11/yDZOcdVKjFVPr34zOVzZEHq8z8vQI51HJSZWcOJWOVbiIXc5VIHz/7MMTK56tfjPGfv/AtC7e3UWzNBQ0DLyzi2a1/n5al5vR+Z8pZ6tXSp59mJPuiaNlLjqhkvO25CRKqUEtsm7GULHET9iU5HSVnJ5S3ZWbbng9XUl6V26R5bSSnOGSE1w2AwHgAgS9pzbvPp0+rTyMEbvpPEHYjnHxlj7loXL+7vRpwTvUhndKz5USmskQ1klNo17Jn1gexkKmnJhOEWDAk52dWB46w6LyJyr1RGTZc/9EKQMdysOMlcIHKkyjMhAJ6h9abgQ1aHrl6T8D4HGZV+5mT9WZg/2Ly8OQmR3/OhWwVPVS3B/KQ+fsbf7F+eNCFgB4WpYBENIobxW1QQWMe29O6xY3ayinuXfmxUP3dOMiEASXftd2yDGvd+bN6p2T0GqlljGOJhDSLm9VCYCg7IwZ+IDmcKb5oEkSvW7GUI9xnTx5y2ruYiQwBPK+rDHnoS+6PHIzOiV6nev8cD2aAfEQdCJjRgmAME9aU7YDSXA+tMOU7kXtdv5aQydjvOMPLnrpCCFg+oJ5GIiDzLUvvHT8OvuqK8n9Ge12nj9x70haAeshOHV5egmAxOR3o70TFUMeBBpBxt45jQe/GPnZrzGUmvF88rmkphPM8zb6DLat9fk6TSd0cZ5P/jU6H5kZ+dkP++c0DrQHvOD9UDF3jZj/z8FI3LUgzWnhbmjtpfht6yOe2Vot9kYMtdv3ZJL/a03yNZB8CyV/TclfR/KtlnxNJH+6JrX77MaaxGc2V4uNL1wf4YRJzhuS01JquG7u9J/dOOLO28/VSTs61WkjOW9KTjMpbsS+fve93S7hl9bEmOzI402i5r3sm64UX5jke1Xy15D8IZK/tuT3u616fck3RymNd817ecxz1+63JHp1ndEuIW7kvn5OqOSMcX0yR6c+nF7tyk+1rtuSM2vNzQ04vSRnquQMsRFM6PB5RkpSv+w3sksfsdnvhY7tYlpkNYh5tXuttadHOMNsm+vrK/nSJf89kj9W8reWfEWSL1/yDZKcfMl5Vqq1+PSIhJ9e7d45p0XWkezQscV6Z67wL05Z3i+7/oDVY/yxkjNScmbayNf6KjfQLT85s6zPP5sddItKzszYtOiT/HujRtMP+yvhbCAfzB5eCvcFThX9JXfnpXMVcy+9ab6gCpbI/xHwgXkOO1kYAOZ2SkaUeh9LUydiJ3/PAJnWg6C2ahN0R064d0Z4/dwfPVVViXHchx3XFADzIOzHrNca1O3Rc8nma/zQXSyj766xYvWrE/9xNn/g5KKB5gDDgRDsiOVyWYqljanYHyISwDRyQQTc7xViJx/bXG78FZCOHQ50u0xfDnawnAmsB+9Cxdy2aN7THbuPrTul3rHUy81f888egyvFzdn67KCDuUvaP1aUkrg9f2RYRzW77KYooKJr2IudQlTFznN87j2nsO3vaezgOBc7vj/Fz4ZmZj2Ezs1b5T20o0nIv69Kar5m/vh3pl/9zx7/8vJ/39rPvCQFiBIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMDEtMjlUMjM6NDM6MjcrMDE6MDDuWSV6AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTAxLTI5VDIzOjQzOjI3KzAxOjAwnwSdxgAAAFl0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9qYW5iL1Byb2pla3RlL3RhbXBlcm1vbmtleS9yZWwvaW1hZ2VzL2luY2x1ZGVzL3RhbXBlcm1vbmtleS5zdmf3en/XAAAAAElFTkSuQmCC"; @@ -47,7 +47,7 @@ const api = {}; // 监听页面关闭事件,用于关闭最后一个通知 - window.addEventListener('beforeunload', function(event) { + window.addEventListener("beforeunload", function (event) { api.closeLastNotification(); }); @@ -62,16 +62,16 @@ options = options || {}; // 创建一个新的 diff --git a/packages/gui/src/view/api.js b/packages/gui/src/view/api.js index 9ff814a6a8..66e59c7a6b 100644 --- a/packages/gui/src/view/api.js +++ b/packages/gui/src/view/api.js @@ -1,16 +1,20 @@ import { ipcRenderer, shell } from 'electron' import lodash from 'lodash' -import path from 'node:path' +import path from 'path' let inited = false let apiObj = null export function apiInit (app) { const invoke = (api, args) => { return ipcRenderer.invoke('apiInvoke', [api, args]).catch((e) => { - app.$notification.error({ - message: 'Api invoke error', - description: e.message, - }) + const notification = app.config.globalProperties.$notification + if (notification) { + notification.error({ + message: 'Api invoke error', + description: e.message, + }) + } + throw e }) } const send = (channel, message) => { diff --git a/packages/gui/src/view/components/JsonEditor.vue b/packages/gui/src/view/components/JsonEditor.vue new file mode 100644 index 0000000000..7e6475dd91 --- /dev/null +++ b/packages/gui/src/view/components/JsonEditor.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/packages/gui/src/view/components/container.vue b/packages/gui/src/view/components/container.vue index e4ef1413a9..3700109795 100644 --- a/packages/gui/src/view/components/container.vue +++ b/packages/gui/src/view/components/container.vue @@ -1,7 +1,9 @@