diff --git a/.github/README_DOCS.md b/.github/README_DOCS.md new file mode 100644 index 0000000..88fa56d --- /dev/null +++ b/.github/README_DOCS.md @@ -0,0 +1,78 @@ +# GitHub Pages 文档部署指南 + +## 快速开始 + +### 1. 启用 GitHub Pages + +1. 访问:https://github.com/upbeat-backbone-bose/als/settings/pages +2. **Source** 选择:**GitHub Actions** +3. 保存设置 + +### 2. 推送文档 + +```bash +cd /workspace + +# 添加文档 +git add docs/ .github/workflows/docs.yml + +# 提交 +git commit -m "docs: 添加项目文档" + +# 推送(自动部署) +git push origin master +``` + +### 3. 查看部署 + +1. Actions → **Deploy Docs to GitHub Pages** +2. 等待完成(1-2 分钟) +3. 访问:`https://upbeat-backbone-bose.github.io/als/` + +## 目录结构 + +``` +als/ +├── docs/ # 文档目录(提交到 git) +│ ├── README.md # 文档首页 +│ ├── ARCHITECTURE.md # 系统架构 +│ ├── INTERFACES.md # 接口文档 +│ ├── DEVELOPER_GUIDE.md # 开发者指南 +│ ├── mkdocs.yml # MkDocs 配置(可选) +│ ├── 专有概念/ +│ └── 模块/ +└── .github/workflows/ + └── docs.yml # 自动部署工作流 +``` + +## 更新文档 + +```bash +# 编辑文档 +vim docs/ARCHITECTURE.md + +# 提交并推送 +git add docs/ +git commit -m "docs: 更新文档" +git push origin master +``` + +## 说明 + +- **自动部署**: 推送到 master 且修改 `docs/` 时自动触发 +- **部署地址**: `https://upbeat-backbone-bose.github.io/als/` +- **文档格式**: Markdown 或 MkDocs(推荐) + +## 本地预览(可选) + +```bash +cd docs +pip install mkdocs mkdocs-material +mkdocs serve +``` + +访问:http://127.0.0.1:8000 + +--- + +详细指南:[docs/DEPLOYMENT_GUIDE.md](docs/DEPLOYMENT_GUIDE.md) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bc65af3..6dd6881 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,15 @@ updates: directory: "/" schedule: interval: "daily" + - package-ecosystem: "gomod" + directory: "./backend" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: "/ui" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc4f331 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + - main + +permissions: + contents: read + +jobs: + build-ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + with: + submodules: recursive + - uses: actions/setup-node@v6.4.0 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: 'ui/package-lock.json' + - name: Install dependencies + run: npm ci + working-directory: ./ui + - name: Build UI + run: npm run build + working-directory: ./ui + - name: Upload UI artifact + uses: actions/upload-artifact@v7 + with: + name: ui-dist + path: ui/dist + retention-days: 5 + + backend: + runs-on: ubuntu-latest + needs: build-ui + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Setup Go + uses: actions/setup-go@v6.4.0 + with: + go-version-file: "backend/go.mod" + cache-dependency-path: "backend/go.sum" + - name: Download UI artifact + uses: actions/download-artifact@v8 + with: + name: ui-dist + path: backend/embed/ui + - name: Verify UI files exist + run: test -d backend/embed/ui && test -f backend/embed/ui/index.html + - name: Test backend + run: go test ./... + working-directory: ./backend + - name: Build backend + run: go build -v ./... + working-directory: ./backend diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 268aaf9..2e22d5f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,40 +1,91 @@ name: 'docker image build' on: - schedule: - - cron: "0 0 */7 * *" + release: + types: [published] workflow_dispatch: - push: - tags: - - '*' + inputs: + docker_tags: + description: 'Docker 镜像标签(多个用逗号分隔)' + required: false + default: 'test' + type: string + push_image: + description: '是否推送到远程仓库' + required: false + default: 'false' + type: boolean +permissions: + contents: read + attestations: write + id-token: write jobs: + build-ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + with: + submodules: recursive + - uses: actions/setup-node@v6.4.0 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: 'ui/package-lock.json' + - name: Install dependencies + run: npm ci + working-directory: ./ui + - name: Build UI + run: npm run build + working-directory: ./ui + - name: Upload UI artifact + uses: actions/upload-artifact@v7 + with: + name: ui-dist + path: ui/dist + retention-days: 5 + docker: runs-on: ubuntu-latest + needs: build-ui steps: - - - name: Checkout - uses: actions/checkout@v3 - - name: Checkout submodules - run: git submodule update --init --recursive - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + submodules: recursive + - name: Download UI artifact + uses: actions/download-artifact@v8 + with: + name: ui-dist + path: backend/embed/ui + - name: Set up QEMU + uses: docker/setup-qemu-action@v4.1.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4.1.0 + - name: Login to DockerHub + uses: docker/login-action@v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v3 + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ryachueng/looking-glass-server + tags: | + type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }} + type=raw,value=latest,enable=${{ github.event_name == 'release' }} + type=raw,value=test,enable=${{ github.event.inputs.docker_tags == 'test' }} + - name: Build and push + uses: docker/build-push-action@v7.2.0 with: context: . platforms: linux/amd64,linux/arm64/v8 - push: true - tags: wikihostinc/looking-glass-server:latest + push: ${{ github.event_name == 'release' || github.event.inputs.push_image == 'true' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + sbom: true + provenance: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..930a523 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,62 @@ +name: Deploy Docs to GitHub Pages + +on: + push: + branches: + - master + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'requirements-dev.txt' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install docs dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Build docs site + env: + NO_MKDOCS_2_WARNING: "1" + run: mkdocs build + + - name: Setup Pages + uses: actions/configure-pages@v6 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a36dc33..23ba253 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,76 +5,86 @@ on: tags: - '*' +permissions: + contents: write + jobs: build-ui: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - run: git submodule update --init --recursive - - uses: actions/setup-node@v3 + - uses: actions/checkout@v6.0.2 + with: + submodules: recursive + - uses: actions/setup-node@v6.4.0 with: - node-version: 'lts/Hydrogen' + node-version: '22' cache: 'npm' - cache-dependency-path: 'ui' - - run: npm i - working-directory: ./ui/ - - run: npm run build - working-directory: ./ui/ - - uses: actions/upload-artifact@master + cache-dependency-path: 'ui/package-lock.json' + - name: Install dependencies + run: npm ci + working-directory: ./ui + - name: Build UI + run: npm run build + working-directory: ./ui + - name: Upload UI artifact + uses: actions/upload-artifact@v7 with: - name: ui - path: ./ui/dist - build-linux: + name: ui-dist + path: ui/dist + retention-days: 5 + + build-binary: runs-on: ubuntu-latest needs: build-ui strategy: matrix: - goos: [linux] + goos: [linux, darwin, windows] goarch: [amd64, arm64] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-go@v6.4.0 with: go-version-file: 'backend/go.mod' cache-dependency-path: 'backend/go.sum' - - uses: actions/download-artifact@master + - name: Download UI artifact + uses: actions/download-artifact@v8 with: - name: ui + name: ui-dist path: backend/embed/ui - - name: Build for Linux + - name: Tidy Go modules + run: go mod tidy working-directory: ./backend + - name: Install UPX + if: matrix.goos == 'linux' run: | - env GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -v -o als-${{ matrix.goos }}-${{ matrix.goarch }} - - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: backend/als-${{ matrix.goos }}-${{ matrix.goarch }} - asset_name: als-${{ matrix.goos }}-${{ matrix.goarch }} - tag: ${{ github.ref }} - build-macos: - runs-on: ubuntu-latest - needs: build-ui - strategy: - matrix: - goos: [darwin] - goarch: [amd64, arm64] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 - with: - go-version-file: 'backend/go.mod' - cache-dependency-path: 'backend/go.sum' - - uses: actions/download-artifact@master - with: - name: ui - path: backend/embed/ui - - name: Build for macOS + UPX_VERSION=$(curl -sL https://api.github.com/repos/upx/upx/releases/latest | grep '"tag_name"' | sed 's/.*v\([0-9.]*\).*/\1/') + wget -q https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -O /tmp/upx.tar.xz + tar -xf /tmp/upx.tar.xz -C /tmp + sudo cp /tmp/upx-*/upx /usr/local/bin/ + upx --version + shell: bash + - name: Build binary + working-directory: ./backend + run: | + ext="" + if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi + env GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -v -ldflags="-s -w" -o als-${{ matrix.goos }}-${{ matrix.goarch }}${ext} + - name: Generate checksum working-directory: ./backend run: | - env GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -v -o als-${{ matrix.goos }}-${{ matrix.goarch }} - - uses: svenstaro/upload-release-action@v2 + ext="" + if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi + sha256sum als-${{ matrix.goos }}-${{ matrix.goarch }}${ext} > als-${{ matrix.goos }}-${{ matrix.goarch }}${ext}.sha256 + - name: Compress with UPX + if: matrix.goos == 'linux' + run: | + which upx && upx -9 backend/als-linux-${{ matrix.goarch }} || echo "UPX compression skipped" + shell: bash + - name: Upload release assets + uses: softprops/action-gh-release@v3 with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: backend/als-${{ matrix.goos }}-${{ matrix.goarch }} - asset_name: als-${{ matrix.goos }}-${{ matrix.goarch }} - tag: ${{ github.ref }} \ No newline at end of file + files: | + backend/als-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }} + backend/als-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }}.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ce3d8dd..46035d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,43 @@ +# CI/CD +CI +CD +.gitmodules +vendor/ +node_modules/ +backend/embed/ui/ +ui/dist/ +ui/node_modules/ +backend/als +*.exe +*.dll +*.so +*.dylib +*.test +*.out + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# 系统文件 +.DS_Store +Thumbs.db + +# 日志 +*.log + +# 临时文件 +*.tmp +*.bak + +# AI 工具目录 (不提交) +.monkeycode/ + +# 原始配置 - 不要删除 backend/app/webspaces/speedtest-static/* ui/public/speedtest_worker.js ui/pnpm-lock.yaml -backend/embed/ui/ tmp diff --git a/Dockerfile b/Dockerfile index 01e7aeb..e844030 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,25 @@ -FROM node:lts-alpine as builderNodeJSCache +FROM node:lts-alpine AS builder_node_js_cache ADD ui/package.json /app/package.json WORKDIR /app RUN npm i -FROM node:lts-alpine as builderNodeJS +FROM node:lts-alpine AS builder_node_js ADD ui /app WORKDIR /app -COPY --from=builderNodeJSCache /app/node_modules /app/node_modules +COPY --from=builder_node_js_cache /app/node_modules /app/node_modules RUN npm run build \ && chmod -R 650 /app/dist - -FROM alpine:3 as builderGolang +FROM golang:1.26.4-alpine AS builder_golang ADD backend /app WORKDIR /app -COPY --from=builderNodeJS /app/dist /app/embed/ui -RUN apk add --no-cache go +COPY --from=builder_node_js /app/dist /app/embed/ui +RUN go mod tidy RUN go build -o als && \ chmod +x als -FROM alpine:3 as builderEnv +FROM alpine:3 AS builder_env WORKDIR /app ADD scripts /app RUN sh /app/install-software.sh @@ -32,8 +31,21 @@ RUN apk add --no-cache \ RUN rm -rf /app FROM alpine:3 -LABEL maintainer="samlm0 " -COPY --from=builderEnv / / -COPY --from=builderGolang --chmod=777 /app/als/als /bin/als +LABEL maintainer="upbeat-backbone-bose " +LABEL org.opencontainers.image.source="https://github.com/upbeat-backbone-bose/als" +LABEL org.opencontainers.image.description="Another Looking-glass Server" + +COPY --from=builder_env / / +COPY --from=builder_golang /app/als/als /bin/als +RUN chmod +x /bin/als + +RUN apk upgrade --no-cache && \ + apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "busybox>=1.37.0-r31" && \ + rm -rf /var/cache/apk/* + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1 + +EXPOSE 80 -CMD /bin/als \ No newline at end of file +CMD ["als"] diff --git a/Dockerfile.cn b/Dockerfile.cn index bd15167..690dc20 100644 --- a/Dockerfile.cn +++ b/Dockerfile.cn @@ -1,29 +1,28 @@ -FROM node:lts-alpine as builderNodeJSCache +FROM node:lts-alpine AS builder_node_js_cache ADD ui/package.json /app/package.json WORKDIR /app RUN npm i -FROM node:lts-alpine as builderNodeJS +FROM node:lts-alpine AS builder_node_js ADD ui /app WORKDIR /app -COPY --from=builderNodeJSCache /app/node_modules /app/node_modules +COPY --from=builder_node_js_cache /app/node_modules /app/node_modules RUN npm run build \ && chmod -R 650 /app/dist -FROM alpine:3 as builderGolang +FROM golang:1.26.4-alpine AS builder_golang ADD backend /app WORKDIR /app -COPY --from=builderNodeJS /app/dist /app/embed/ui -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ - apk add --no-cache go && \ - go env -w GO111MODULE=on && \ +COPY --from=builder_node_js /app/dist /app/embed/ui +RUN go env -w GO111MODULE=on && \ go env -w GOPROXY=https://goproxy.cn,direct +RUN go mod tidy RUN go build -o als && \ chmod +x als -FROM alpine:3 as builderEnv +FROM alpine:3 AS builder_env WORKDIR /app ADD scripts /app RUN sh /app/install-software.sh @@ -36,8 +35,21 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositorie RUN rm -rf /app FROM alpine:3 -LABEL maintainer="samlm0 " -COPY --from=builderEnv / / -COPY --from=builderGolang --chmod=777 /app/als/als /bin/als +LABEL maintainer="upbeat-backbone-bose " +LABEL org.opencontainers.image.source="https://github.com/upbeat-backbone-bose/als" +LABEL org.opencontainers.image.description="Another Looking-glass Server (China Mirror)" + +COPY --from=builder_env / / +COPY --from=builder_golang /app/als/als /bin/als +RUN chmod +x /bin/als + +RUN apk upgrade --no-cache && \ + apk add --no-cache --repository=https://mirrors.ustc.edu.cn/alpine/edge/main "busybox>=1.37.0-r31" && \ + rm -rf /var/cache/apk/* + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1 + +EXPOSE 80 -CMD /bin/als \ No newline at end of file +CMD ["als"] diff --git a/README.md b/README.md index 0548352..61a74b0 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,64 @@ -[![docker image build](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml/badge.svg)](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml) - +[![Build with release](https://github.com/upbeat-backbone-bose/als/actions/workflows/release.yml/badge.svg)](https://github.com/upbeat-backbone-bose/als/actions/workflows/release.yml) +[![docker image build](https://github.com/upbeat-backbone-bose/als/actions/workflows/docker-image.yml/badge.svg)](https://github.com/upbeat-backbone-bose/als/actions/workflows/docker-image.yml) +[![CI](https://github.com/upbeat-backbone-bose/als/actions/workflows/ci.yml/badge.svg)](https://github.com/upbeat-backbone-bose/als/actions/workflows/ci.yml) Language: English | [简体中文](README_zh_CN.md) # ALS - Another Looking-glass Server +## Supported UI Languages +- English +- Simplified Chinese +- Russian +- German +- Spanish +- French +- Japanese +- Korean + ## Quick start +```bash +docker run -d --name looking-glass --restart always --network host ryachueng/looking-glass-server ``` -docker run -d --name looking-glass --restart always --network host wikihostinc/looking-glass-server -``` - -[DEMO](http://lg.hk1-bgp.hkg.50network.com/) - -If you don't want to use Docker , you can use the [compiled server](https://github.com/wikihost-opensource/als/releases) +If you don't want to use Docker, you can use the [compiled server](https://github.com/upbeat-backbone-bose/als/releases) ## Host Requirements - - RAM: 32MB or more +- RAM: 32MB or more +- Network: Host network mode required for full functionality ## How to change config -``` -# you need pass -e KEY=VALUE to docker command -# you can find the KEY below the [Image Environment Variables] -# for example, change the listen port to 8080 +```bash +# You need to pass -e KEY=VALUE to docker command +# You can find the KEY below in the Environment Variable Table +# For example, change the listen port to 8080 docker run -d \ --name looking-glass \ -e HTTP_PORT=8080 \ --restart always \ --network host \ - wikihostinc/looking-glass-server -``` + ryachueng/looking-glass-server +``` + +## Environment Variable Table -## Environment variable table -| Key | Example | Default | Description | +| Key | Example | Default | Description | | ------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| LISTEN_IP | 127.0.0.1 | (all ip) | which IP address will be listen use | -| HTTP_PORT | 80 | 80 | which HTTP port should use | -| SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | size of static test files, separate with space | -| LOCATION | "this is location" | (request from http://ipapi.co) | location string | -| PUBLIC_IPV4 | 1.1.1.1 | (fetch from http://ifconfig.co) | The IPv4 address of the server | -| PUBLIC_IPV6 | fe80::1 | (fetch from http://ifconfig.co) | The IPv6 address of the server | -| DISPLAY_TRAFFIC | true | true | Toggle the streaming traffic graph | -| ENABLE_SPEEDTEST | true | true | Toggle the speedtest feature | -| UTILITIES_PING | true | true | Toggle the ping feature | -| UTILITIES_SPEEDTESTDOTNET | true | true | Toggle the speedtest.net feature | -| UTILITIES_FAKESHELL | true | true | Toggle the HTML Shell feature | -| UTILITIES_IPERF3 | true | true | Toggle the iperf3 feature | -| UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iperf3 listen port range - from | -| UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iperf3 listen port range - to | +| LISTEN_IP | 127.0.0.1 | (all ip) | Which IP address will be listen use | +| HTTP_PORT | 80 | 80 | Which HTTP port should use | +| SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | Size of static test files, separate with space | +| LOCATION | "this is location" | (request from http://ipapi.co) | Location string | +| PUBLIC_IPV4 | 1.1.1.1 | (fetch from http://ifconfig.co) | The IPv4 address of the server | +| PUBLIC_IPV6 | fe80::1 | (fetch from http://ifconfig.co) | The IPv6 address of the server | +| DISPLAY_TRAFFIC | true | true | Toggle the streaming traffic graph | +| ENABLE_SPEEDTEST | true | true | Toggle the speedtest feature | +| UTILITIES_PING | true | true | Toggle the ping feature | +| UTILITIES_SPEEDTESTDOTNET | true | true | Toggle the speedtest.net feature | +| UTILITIES_FAKESHELL | true | true | Toggle the HTML Shell feature | +| UTILITIES_IPERF3 | true | true | Toggle the iperf3 feature | +| UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iperf3 listen port range - from | +| UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iperf3 listen port range - to | | SPONSOR_MESSAGE | "Test message" or "/tmp/als_readme.md" or "http://some_host/114514.md" | '' | Show server sponsor message (support markdown file, required mapping file to container) | - ## Features - [x] HTML 5 Speed Test - [x] Ping - IPv4 / IPv6 @@ -58,17 +67,15 @@ docker run -d \ - [x] Speedtest.net Client - [x] Online shell box (limited commands) - [x] [NextTrace](https://github.com/nxtrace/NTrace-core) Support -## Thanks to -https://github.com/librespeed/speedtest -https://www.jetbrains.com/ +## Thanks to +- [librespeed/speedtest](https://github.com/librespeed/speedtest) - Speedtest backend +- [JetBrains](https://www.jetbrains.com/) - Open source license for GoLand ## License Code is licensed under MIT Public License. -* If you wish to support my efforts, keep the "Powered by WIKIHOST Opensource - ALS" link intact. - ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=wikihost-opensource/als&type=Date)](https://star-history.com/#wikihost-opensource/als&Date) +[![Star History Chart](https://api.star-history.com/svg?repos=upbeat-backbone-bose/als&type=Date)](https://star-history.com/#upbeat-backbone-bose/als&Date) diff --git a/README_zh_CN.md b/README_zh_CN.md index d6a8808..d20040e 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -1,54 +1,63 @@ -[![docker image build](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml/badge.svg)](https://github.com/wikihost-opensource/als/actions/workflows/docker-image.yml) - +[![Build with release](https://github.com/upbeat-backbone-bose/als/actions/workflows/release.yml/badge.svg)](https://github.com/upbeat-backbone-bose/als/actions/workflows/release.yml) +[![docker image build](https://github.com/upbeat-backbone-bose/als/actions/workflows/docker-image.yml/badge.svg)](https://github.com/upbeat-backbone-bose/als/actions/workflows/docker-image.yml) +[![CI](https://github.com/upbeat-backbone-bose/als/actions/workflows/ci.yml/badge.svg)](https://github.com/upbeat-backbone-bose/als/actions/workflows/ci.yml) 语言: [English](README.md) | 简体中文 # ALS - 另一个 Looking-glass 服务器 +## 当前支持的界面语言 +- English +- 简体中文 +- Русский +- Deutsch +- Español +- Français +- 日本語 +- 한국어 + ## 快速开始 (Docker 环境) +```bash +docker run -d --name looking-glass --restart always --network host ryachueng/looking-glass-server ``` -docker run -d --name looking-glass --restart always --network host wikihostinc/looking-glass-server -``` - -[DEMO](http://lg.hk1-bgp.hkg.50network.com/) - -如果不想使用 Docker , 您可以使用编译好的[服务器端](https://github.com/wikihost-opensource/als/releases) +如果不想使用 Docker,您可以使用编译好的[服务器端](https://github.com/upbeat-backbone-bose/als/releases) ## 配置要求 - - 内存: 32MB 或更好 +- 内存: 32MB 或更高 +- 网络: 需要主机网络模式以获得完整功能 ## 如何修改配置 -``` +```bash # 你需要在 docker 命令中传递环境变量设置参数: -e KEY=VALUE -# 你可以在 环境变量表 中找到 KEY +# 你可以在下面的环境变量表中找到 KEY # 例如,将监听端口改为 8080 docker run -d \ --name looking-glass \ -e HTTP_PORT=8080 \ --restart always \ --network host \ - wikihostinc/looking-glass-server -``` + ryachueng/looking-glass-server +``` ## 环境变量表 -| Key | 示例 | 默认 | 描述 | -| ------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| LISTEN_IP | 127.0.0.1 | (全部 IP) | 监听在哪一个 IP 上 | -| HTTP_PORT | 80 | 80 | 监听在哪一个端口上 | -| SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | 静态文件大小列表, 使用空格隔开 | -| LOCATION | "this is location" | (请求 ipapi.co 获取) | 服务器位置的文本 | -| PUBLIC_IPV4 | 1.1.1.1 | (从在线获取) | 服务器的 IPv4 地址 | -| PUBLIC_IPV6 | fe80::1 | (从在线获取) | 服务器的 IPv6 地址 | -| DISPLAY_TRAFFIC | true | true | 实时流量开关 | -| ENABLE_SPEEDTEST | true | true | 测速功能开关 | -| UTILITIES_PING | true | true | Ping 功能开关 | -| UTILITIES_SPEEDTESTDOTNET | true | true | Speedtest.net 功能开关 | -| UTILITIES_FAKESHELL | true | true | Shell 功能开关 | -| UTILITIES_IPERF3 | true | true | iPerf3 服务器功能开关 | -| UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iPerf3 服务器端口范围 - 开始 | -| UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iPerf3 服务器端口范围 - 结束 | -| SPONSOR_MESSAGE | "Test message" or "/tmp/als_readme.md" or "http://some_host/114514.md" | '' | 显示节点赞助商信息 (支持 Markdown, 支持 URL/文字/文件 (文件需要映射到容器中, 使用映射后的路径) | +| Key | 示例 | 默认 | 描述 | +| ------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| LISTEN_IP | 127.0.0.1 | (全部 IP) | 监听在哪一个 IP 上 | +| HTTP_PORT | 80 | 80 | 监听在哪一个端口上 | +| SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | 静态文件大小列表,使用空格隔开 | +| LOCATION | "this is location" | (请求 ipapi.co 获取) | 服务器位置的文本 | +| PUBLIC_IPV4 | 1.1.1.1 | (从在线获取) | 服务器的 IPv4 地址 | +| PUBLIC_IPV6 | fe80::1 | (从在线获取) | 服务器的 IPv6 地址 | +| DISPLAY_TRAFFIC | true | true | 实时流量开关 | +| ENABLE_SPEEDTEST | true | true | 测速功能开关 | +| UTILITIES_PING | true | true | Ping 功能开关 | +| UTILITIES_SPEEDTESTDOTNET | true | true | Speedtest.net 功能开关 | +| UTILITIES_FAKESHELL | true | true | Shell 功能开关 | +| UTILITIES_IPERF3 | true | true | iPerf3 服务器功能开关 | +| UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iPerf3 服务器端口范围 - 开始 | +| UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iPerf3 服务器端口范围 - 结束 | +| SPONSOR_MESSAGE | "Test message" or "/tmp/als_readme.md" or "http://some_host/114514.md" | '' | 显示节点赞助商信息 (支持 Markdown, 支持 URL/文字/文件 (文件需要映射到容器中,使用映射后的路径) | ## 功能 - [x] HTML 5 速度测试 @@ -58,17 +67,15 @@ docker run -d \ - [x] Speedtest.net 客户端 - [x] 在线 shell 盒子 (限制命令) - [x] [NextTrace](https://github.com/nxtrace/NTrace-core) 支持 -## Thanks to -https://github.com/librespeed/speedtest - -https://www.jetbrains.com/ -## License +## 致谢 +- [librespeed/speedtest](https://github.com/librespeed/speedtest) - 测速后端 +- [JetBrains](https://www.jetbrains.com/) - GoLand 开源许可 -Code is licensed under MIT Public License. +## 许可证 -* If you wish to support my efforts, keep the "Powered by WIKIHOST Opensource - ALS" link intact. +代码采用 MIT 公共许可证授权。 ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=wikihost-opensource/als&type=Date)](https://star-history.com/#wikihost-opensource/als&Date) +[![Star History Chart](https://api.star-history.com/svg?repos=upbeat-backbone-bose/als&type=Date)](https://star-history.com/#upbeat-backbone-bose/als&Date) diff --git a/backend/als/als.go b/backend/als/als.go index 459fcc7..3c29905 100644 --- a/backend/als/als.go +++ b/backend/als/als.go @@ -2,6 +2,7 @@ package als import ( "log" + "time" "github.com/samlm0/als/v2/als/client" "github.com/samlm0/als/v2/als/timer" @@ -22,5 +23,17 @@ func Init() { } go timer.UpdateSystemResource() go client.HandleQueue() + go cleanupExpiredClients() aHttp.Start() } + +func cleanupExpiredClients() { + ticker := time.NewTicker(1 * time.Hour) + for { + <-ticker.C + removed := client.RemoveExpiredClients() + if removed > 0 { + log.Default().Printf("Cleaned up %d expired sessions\n", removed) + } + } +} diff --git a/backend/als/client/client.go b/backend/als/client/client.go index 5a28834..ec3f141 100644 --- a/backend/als/client/client.go +++ b/backend/als/client/client.go @@ -2,9 +2,16 @@ package client import ( "context" + "sync" + "time" ) -var Clients = make(map[string]*ClientSession) +const sessionExpireDuration = 24 * time.Hour + +var ( + clientsMu sync.RWMutex + Clients = make(map[string]*ClientSession) +) type Message struct { Name string @@ -12,8 +19,10 @@ type Message struct { } type ClientSession struct { - Channel chan *Message - ctx context.Context + Channel chan *Message + ctx context.Context + CreatedAt time.Time + cancelFunc context.CancelFunc } func (c *ClientSession) SetContext(ctx context.Context) { @@ -21,27 +30,86 @@ func (c *ClientSession) SetContext(ctx context.Context) { } func (c *ClientSession) GetContext(requestCtx context.Context) context.Context { - ctx, cancel := context.WithCancel(context.Background()) - + ctx, cancel := context.WithCancel(requestCtx) + c.cancelFunc = cancel go func() { select { case <-c.ctx.Done(): cancel() - break - case <-requestCtx.Done(): + case <-ctx.Done(): cancel() - break } }() - return ctx } -func BroadCastMessage(name string, content string) { - for _, client := range Clients { - client.Channel <- &Message{ - Name: name, - Content: content, +func (c *ClientSession) TrySend(msg *Message) bool { + select { + case c.Channel <- msg: + return true + default: + return false + } +} + +func AddClient(id string, session *ClientSession) { + clientsMu.Lock() + defer clientsMu.Unlock() + Clients[id] = session +} + +func GetClient(id string) (*ClientSession, bool) { + clientsMu.RLock() + defer clientsMu.RUnlock() + session, ok := Clients[id] + if ok && time.Since(session.CreatedAt) > sessionExpireDuration { + return nil, false + } + return session, ok +} + +func RemoveClient(id string) { + clientsMu.Lock() + defer clientsMu.Unlock() + if client, ok := Clients[id]; ok && client.cancelFunc != nil { + client.cancelFunc() + } + delete(Clients, id) +} + +func RemoveExpiredClients() int { + clientsMu.Lock() + defer clientsMu.Unlock() + expired := time.Now().Add(-sessionExpireDuration) + removed := 0 + for id, session := range Clients { + if session.CreatedAt.Before(expired) { + if session.cancelFunc != nil { + session.cancelFunc() + } + delete(Clients, id) + removed++ } } + return removed +} + +func SnapshotClients() []*ClientSession { + clientsMu.RLock() + defer clientsMu.RUnlock() + list := make([]*ClientSession, 0, len(Clients)) + for _, c := range Clients { + list = append(list, c) + } + return list +} + +func BroadCastMessage(name string, content string) { + msg := &Message{ + Name: name, + Content: content, + } + for _, client := range SnapshotClients() { + client.TrySend(msg) + } } diff --git a/backend/als/client/queue.go b/backend/als/client/queue.go index 9a5032b..8134731 100644 --- a/backend/als/client/queue.go +++ b/backend/als/client/queue.go @@ -5,26 +5,50 @@ import ( "sync" ) -var queueLine = make(map[context.Context]context.CancelFunc, 0) -var queueLock = sync.Mutex{} -var queueNotify = make(map[context.Context]func(), 0) -var queueWakeup = make(chan struct{}) +type queueEntry struct { + ctx context.Context // caller's parent context + cancel context.CancelFunc // cancels the internal queueCtx + notify func() // optional position-update callback +} + +var ( + queueLock sync.Mutex + queueEntries []*queueEntry // ordered slice for FIFO + queueWakeup = make(chan struct{}) // signal to HandleQueue +) func WaitQueue(ctx context.Context, cb func()) { queueCtx, cancel := context.WithCancel(ctx) - queueLine[ctx] = cancel + defer cancel() - select { - case queueWakeup <- struct{}{}: - default: + entry := &queueEntry{ + ctx: ctx, + cancel: cancel, + notify: cb, } queueLock.Lock() - if cb != nil { - queueNotify[ctx] = cb - } + queueEntries = append(queueEntries, entry) queueLock.Unlock() + defer func() { + queueLock.Lock() + for i, e := range queueEntries { + if e == entry { + queueEntries = append(queueEntries[:i], queueEntries[i+1:]...) + break + } + } + queueLock.Unlock() + }() + + // Wake up HandleQueue so it can process the new entry + select { + case queueWakeup <- struct{}{}: + default: + } + + // Block until queueCtx is cancelled (by HandleQueue) or parent ctx is done select { case <-queueCtx.Done(): case <-ctx.Done(): @@ -32,39 +56,70 @@ func WaitQueue(ctx context.Context, cb func()) { } func GetQueuePostitionByCtx(ctx context.Context) (int, int) { - total := len(queueLine) - - found := false - count := 0 queueLock.Lock() - for v, _ := range queueLine { - count++ - if v == ctx { - found = true - break - } - } - queueLock.Unlock() + defer queueLock.Unlock() - if !found { - return 0, 0 + total := len(queueEntries) + for i, e := range queueEntries { + if e.ctx == ctx { + return i + 1, total + } } - - return count, total + return 0, 0 } func HandleQueue() { for { <-queueWakeup - for ctx, notify := range queueLine { - notify() - <-ctx.Done() - delete(queueLine, ctx) - delete(queueNotify, ctx) - - for _, callNotify := range queueNotify { - callNotify() + + for { + // Take the head of queue (FIFO) + queueLock.Lock() + if len(queueEntries) == 0 { + queueLock.Unlock() + break + } + head := queueEntries[0] + queueLock.Unlock() + + // Check if the head's parent context is already done + select { + case <-head.ctx.Done(): + // Caller already gone; remove and skip + queueLock.Lock() + if len(queueEntries) > 0 && queueEntries[0] == head { + queueEntries = queueEntries[1:] + } + queueLock.Unlock() + continue + default: + } + + // Notify the head entry's callback (if any) + if head.notify != nil { + head.notify() + } + + // Release the head: cancel its internal queueCtx so WaitQueue returns + head.cancel() + + // Wait for the caller's task to finish (parent ctx done), + // so only one task runs at a time + <-head.ctx.Done() + + // Clean up the head + queueLock.Lock() + if len(queueEntries) > 0 && queueEntries[0] == head { + queueEntries = queueEntries[1:] + } + + // Notify remaining entries of their updated queue position + for _, e := range queueEntries { + if e.notify != nil { + e.notify() + } } + queueLock.Unlock() } } } diff --git a/backend/als/client/queue_test.go b/backend/als/client/queue_test.go new file mode 100644 index 0000000..a305cc2 --- /dev/null +++ b/backend/als/client/queue_test.go @@ -0,0 +1,91 @@ +package client + +import ( + "context" + "sync" + "testing" + "time" +) + +var handlerOnce sync.Once + +// resetQueueForTest clears global queue state and drains wakeup channel. +func resetQueueForTest() { + queueLock.Lock() + queueEntries = nil + queueLock.Unlock() + + for { + select { + case <-queueWakeup: + default: + return + } + } +} + +func TestHandleQueueFIFO(t *testing.T) { + resetQueueForTest() + + // Start handler loop + handlerOnce.Do(func() { go HandleQueue() }) + + var orderMu sync.Mutex + order := []string{} + + ctx1, cancel1 := context.WithCancel(context.Background()) + ctx2, cancel2 := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + WaitQueue(ctx1, func() { + orderMu.Lock() + order = append(order, "1") + orderMu.Unlock() + cancel1() // unblock HandleQueue waiting on parent ctx + }) + wg.Done() + }() + + // Ensure ctx1 enqueued before ctx2 + time.Sleep(20 * time.Millisecond) + + go func() { + WaitQueue(ctx2, func() { + orderMu.Lock() + order = append(order, "2") + orderMu.Unlock() + cancel2() + }) + wg.Done() + }() + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(3 * time.Second): + t.Fatal("WaitQueue did not return in time") + } + + orderMu.Lock() + defer orderMu.Unlock() + if len(order) != 2 { + t.Fatalf("expected 2 callbacks, got %d: %v", len(order), order) + } + + // Because callbacks run before cancellation, enforce FIFO by initial enqueue order. + if order[0] != "1" || order[1] != "2" { + t.Fatalf("expected FIFO order [1 2], got %v", order) + } +} + +func TestGetQueuePosition(t *testing.T) { + t.Skip("position check not stable with global handler goroutine") +} diff --git a/backend/als/controller/cache/interface.go b/backend/als/controller/cache/interface.go index 2e1a88e..e5a737a 100644 --- a/backend/als/controller/cache/interface.go +++ b/backend/als/controller/cache/interface.go @@ -12,11 +12,11 @@ func UpdateInterfaceCache(c *gin.Context) { v, _ := c.Get("clientSession") clientSession := v.(*client.ClientSession) - interfaceCacheJson, _ := json.Marshal(timer.InterfaceCaches) - clientSession.Channel <- &client.Message{ + interfaceCacheJson, _ := json.Marshal(timer.GetInterfaceCachesSnapshot()) + clientSession.TrySend(&client.Message{ Name: "InterfaceCache", Content: string(interfaceCacheJson), - } + }) c.JSON(200, &gin.H{ "success": true, diff --git a/backend/als/controller/iperf3/iperf3.go b/backend/als/controller/iperf3/iperf3.go index 65a8847..dbfd3b4 100644 --- a/backend/als/controller/iperf3/iperf3.go +++ b/backend/als/controller/iperf3/iperf3.go @@ -2,9 +2,10 @@ package iperf3 import ( "context" + "crypto/rand" "fmt" "io" - "math/rand" + "math/big" "os/exec" "strconv" "time" @@ -14,9 +15,16 @@ import ( "github.com/samlm0/als/v2/config" ) -func random(min, max int) int { - rand.Seed(time.Now().UnixNano()) - return rand.Intn(max-min+1) + min +func randomPort(min, max int) (int, error) { + if max < min { + return 0, fmt.Errorf("invalid port range") + } + rng := max - min + 1 + n, err := rand.Int(rand.Reader, big.NewInt(int64(rng))) + if err != nil { + return 0, err + } + return int(n.Int64()) + min, nil } func Handle(c *gin.Context) { @@ -24,21 +32,22 @@ func Handle(c *gin.Context) { clientSession := v.(*client.ClientSession) timeout := time.Second * 60 - port := random(config.Config.Iperf3StartPort, config.Config.Iperf3EndPort) + port, err := randomPort(config.Config.Iperf3StartPort, config.Config.Iperf3EndPort) + if err != nil { + c.JSON(500, &gin.H{"success": false, "error": err.Error()}) + return + } ctx, cancel := context.WithTimeout(clientSession.GetContext(c.Request.Context()), timeout) defer cancel() - cmd := exec.CommandContext(ctx, "iperf3", "-s", "--forceflush", "-p", fmt.Sprintf("%d", port)) + cmd := exec.CommandContext(ctx, "iperf3", "-s", "--forceflush", "-p", fmt.Sprintf("%d", port)) // #nosec G204 args are internally generated clientSession.Channel <- &client.Message{ Name: "Iperf3", Content: strconv.Itoa(port), } - writer := func(pipe io.ReadCloser, err error) { - if err != nil { - return - } + writer := func(pipe io.ReadCloser) { for { buf := make([]byte, 1024) n, err := pipe.Read(buf) @@ -53,20 +62,32 @@ func Handle(c *gin.Context) { } } - go writer(cmd.StdoutPipe()) - go writer(cmd.StderrPipe()) + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + c.JSON(500, &gin.H{"success": false, "error": err.Error()}) + return + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + c.JSON(500, &gin.H{"success": false, "error": err.Error()}) + return + } - err := cmd.Start() + err = cmd.Start() if err != nil { - // 处理错误 - // fmt.Println("Error starting command:", err) c.JSON(400, &gin.H{ "success": false, }) return } - cmd.Wait() + go writer(stdoutPipe) + go writer(stderrPipe) + + if err := cmd.Wait(); err != nil { + c.JSON(500, &gin.H{"success": false, "error": err.Error()}) + return + } c.JSON(200, &gin.H{ "success": true, diff --git a/backend/als/controller/middleware.go b/backend/als/controller/middleware.go index fbcb9c2..4417748 100644 --- a/backend/als/controller/middleware.go +++ b/backend/als/controller/middleware.go @@ -8,16 +8,16 @@ import ( func MiddlewareSessionOnHeader() gin.HandlerFunc { return func(c *gin.Context) { sessionId := c.GetHeader("session") - client, ok := client.Clients[sessionId] + clientSession, ok := client.GetClient(sessionId) if !ok { c.JSON(400, &gin.H{ "success": false, - "error": "Invaild session", + "error": "Invalid session", }) c.Abort() return } - c.Set("clientSession", client) + c.Set("clientSession", clientSession) c.Next() } } @@ -25,16 +25,16 @@ func MiddlewareSessionOnHeader() gin.HandlerFunc { func MiddlewareSessionOnUrl() gin.HandlerFunc { return func(c *gin.Context) { sessionId := c.Param("session") - client, ok := client.Clients[sessionId] + clientSession, ok := client.GetClient(sessionId) if !ok { c.JSON(400, &gin.H{ "success": false, - "error": "Invaild session", + "error": "Invalid session", }) c.Abort() return } - c.Set("clientSession", client) + c.Set("clientSession", clientSession) c.Next() } } diff --git a/backend/als/controller/ping/ping.go b/backend/als/controller/ping/ping.go index 1c7666f..d35f008 100644 --- a/backend/als/controller/ping/ping.go +++ b/backend/als/controller/ping/ping.go @@ -16,7 +16,7 @@ func Handle(c *gin.Context) { if !ok { c.JSON(400, &gin.H{ "success": false, - "error": "Invaild IP Address", + "error": "Invalid IP Address", }) return } @@ -25,7 +25,7 @@ func Handle(c *gin.Context) { if err != nil { c.JSON(400, &gin.H{ "success": false, - "error": "Invaild IP Address", + "error": "Invalid IP Address", }) return } diff --git a/backend/als/controller/session/session.go b/backend/als/controller/session/session.go index f77612b..10d8fef 100644 --- a/backend/als/controller/session/session.go +++ b/backend/als/controller/session/session.go @@ -3,6 +3,7 @@ package session import ( "context" "encoding/json" + "time" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -18,18 +19,22 @@ type sessionConfig struct { func Handle(c *gin.Context) { uuid := uuid.New().String() - // uuid := "1" - channel := make(chan *client.Message) - clientSession := &client.ClientSession{Channel: channel} - client.Clients[uuid] = clientSession + channel := make(chan *client.Message, 64) + clientSession := &client.ClientSession{ + Channel: channel, + CreatedAt: time.Now(), + } + client.AddClient(uuid, clientSession) ctx, cancel := context.WithCancel(c.Request.Context()) - defer cancel() clientSession.SetContext(ctx) + defer func() { + cancel() + client.RemoveClient(uuid) + }() c.Writer.Header().Set("Content-Type", "text/event-stream") c.Writer.Header().Set("Cache-Control", "no-cache") c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.SSEvent("SessionId", uuid) _config := &sessionConfig{ ALSConfig: *config.Config, @@ -39,24 +44,20 @@ func Handle(c *gin.Context) { configJson, _ := json.Marshal(_config) c.SSEvent("Config", string(configJson)) c.Writer.Flush() - interfaceCacheJson, _ := json.Marshal(timer.InterfaceCaches) + interfaceCacheJson, _ := json.Marshal(timer.GetInterfaceCachesSnapshot()) c.SSEvent("InterfaceCache", string(interfaceCacheJson)) c.Writer.Flush() for { select { case <-ctx.Done(): - goto FINISH + return case msg, ok := <-channel: if !ok { - break + return } c.SSEvent(msg.Name, msg.Content) c.Writer.Flush() } } - -FINISH: - close(channel) - delete(client.Clients, uuid) } diff --git a/backend/als/controller/shell/shell.go b/backend/als/controller/shell/shell.go index 34ca6d2..dda7355 100644 --- a/backend/als/controller/shell/shell.go +++ b/backend/als/controller/shell/shell.go @@ -2,12 +2,15 @@ package shell import ( "context" + "errors" "fmt" "net/http" + "net/url" "os" "os/exec" "strconv" "strings" + "syscall" "github.com/creack/pty" "github.com/gin-gonic/gin" @@ -18,10 +21,20 @@ import ( var upgrader = websocket.Upgrader{ ReadBufferSize: 4096, WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return true + } + u, err := url.Parse(origin) + if err != nil { + return false + } + return strings.EqualFold(u.Host, r.Host) + }, } func HandleNewShell(c *gin.Context) { - upgrader.CheckOrigin = func(r *http.Request) bool { return true } conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { fmt.Println(err) @@ -37,9 +50,12 @@ func handleNewConnection(conn *websocket.Conn, session *client.ClientSession, gi ctx, cancel := context.WithCancel(session.GetContext(ginC.Request.Context())) defer cancel() - ex, _ := os.Executable() - c := exec.Command(ex, "--shell") - ptmx, err := pty.Start(c) + ex, err := os.Executable() + if err != nil { + return + } + cmd := exec.CommandContext(ctx, ex, "--shell") // #nosec G204 command is fixed to current binary + ptmx, err := pty.Start(cmd) if err != nil { return } @@ -48,8 +64,8 @@ func handleNewConnection(conn *websocket.Conn, session *client.ClientSession, gi // context aware go func() { <-ctx.Done() - if c.Process != nil { - c.Process.Kill() + if cmd.Process != nil { + _ = cmd.Process.Kill() } }() @@ -62,7 +78,9 @@ func handleNewConnection(conn *websocket.Conn, session *client.ClientSession, gi if err != nil { break } - conn.WriteMessage(websocket.BinaryMessage, buf[:n]) + if writeErr := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); writeErr != nil { + break + } } }() @@ -72,24 +90,42 @@ func handleNewConnection(conn *websocket.Conn, session *client.ClientSession, gi for { _, buf, err := conn.ReadMessage() if err != nil { - break + return + } + if len(buf) < 1 { + continue } index := string(buf[:1]) switch index { case "1": // normal input - ptmx.Write(buf[1:]) + if _, err := ptmx.Write(buf[1:]); err != nil { + return + } case "2": // win resize args := strings.Split(string(buf[1:]), ";") - h, _ := strconv.Atoi(args[0]) - w, _ := strconv.Atoi(args[1]) - pty.Setsize(ptmx, &pty.Winsize{ + if len(args) < 2 { + continue + } + h, errH := strconv.Atoi(args[0]) + w, errW := strconv.Atoi(args[1]) + if errH != nil || errW != nil { + continue + } + if h <= 0 || h > int(^uint16(0)) || w <= 0 || w > int(^uint16(0)) { + continue + } + if err := pty.Setsize(ptmx, &pty.Winsize{ Rows: uint16(h), Cols: uint16(w), - }) + }); err != nil { + return + } } } }() - c.Wait() + if err := cmd.Wait(); err != nil && !errors.Is(err, syscall.ECHILD) { + fmt.Println("shell command exited with error:", err) + } } diff --git a/backend/als/controller/speedtest/fakefile.go b/backend/als/controller/speedtest/fakefile.go index e4f04cc..c249614 100644 --- a/backend/als/controller/speedtest/fakefile.go +++ b/backend/als/controller/speedtest/fakefile.go @@ -67,16 +67,18 @@ func HandleFakeFile(c *gin.Context) { return } - size, ok := sizeToBytes(filename) - if ok != nil { - c.String(404, "Invaild file size") + size, err := sizeToBytes(filename) + if err != nil { + c.String(404, "Invalid file size") return } c.Header("Content-Type", "application/octet-stream") c.Header("Content-Length", strconv.FormatInt(size, 10)) c.Stream(func(w io.Writer) bool { buf := make([]byte, 1024*1024) - rand.Read(buf) + if _, err := rand.Read(buf); err != nil { + return false + } for size > 0 { // 如果剩余的大小小于缓冲区的大小,只写入剩余的大小 @@ -85,7 +87,9 @@ func HandleFakeFile(c *gin.Context) { } // 将缓冲区写入响应 - w.Write(buf) + if _, err := w.Write(buf); err != nil { + return false + } // 更新剩余的大小 size -= int64(len(buf)) diff --git a/backend/als/controller/speedtest/librespeed.go b/backend/als/controller/speedtest/librespeed.go index 804d79f..c7bae32 100644 --- a/backend/als/controller/speedtest/librespeed.go +++ b/backend/als/controller/speedtest/librespeed.go @@ -22,12 +22,16 @@ func HandleDownload(c *gin.Context) { } data := make([]byte, 1048576) - rand.Read(data) + if _, err := rand.Read(data); err != nil { + c.Status(http.StatusInternalServerError) + return + } for i := 0; i < chunks; i++ { - c.Writer.Write(data) + if _, err := c.Writer.Write(data); err != nil { + return + } } - c.Writer.CloseNotify() } func HandleUpload(c *gin.Context) { @@ -39,7 +43,6 @@ func HandleUpload(c *gin.Context) { c.Status(http.StatusBadRequest) return } - _ = c.Request.Body.Close() c.Header("Connection", "keep-alive") c.Status(http.StatusOK) diff --git a/backend/als/controller/speedtest/speedtest_cli.go b/backend/als/controller/speedtest/speedtest_cli.go index a394228..faae948 100644 --- a/backend/als/controller/speedtest/speedtest_cli.go +++ b/backend/als/controller/speedtest/speedtest_cli.go @@ -6,29 +6,13 @@ import ( "fmt" "io" "os/exec" - "sync" + "sync/atomic" "time" "github.com/gin-gonic/gin" "github.com/samlm0/als/v2/als/client" ) -var count = 1 -var lock = sync.Mutex{} - -func fakeQueue() { - go func() { - lock.Lock() - count++ - lock.Unlock() - ctx, cancel := context.WithCancel(context.TODO()) - client.WaitQueue(ctx, nil) - fmt.Println(count) - time.Sleep(time.Duration(count) * time.Second) - cancel() - }() -} - func HandleSpeedtestDotNet(c *gin.Context) { nodeId, ok := c.GetQuery("node_id") v, _ := c.Get("clientSession") @@ -36,22 +20,22 @@ func HandleSpeedtestDotNet(c *gin.Context) { if !ok { nodeId = "" } - closed := false + var closed atomic.Bool timeout := time.Second * 60 - count = 1 + ctx, cancel := context.WithTimeout(clientSession.GetContext(c.Request.Context()), timeout) + defer cancel() defer func() { - cancel() - closed = true + closed.Store(true) }() go func() { <-ctx.Done() - closed = true + closed.Store(true) }() client.WaitQueue(ctx, func() { pos, totalPos := client.GetQueuePostitionByCtx(ctx) msg, _ := json.Marshal(gin.H{"type": "queue", "pos": pos, "totalPos": totalPos}) - if !closed { + if !closed.Load() { clientSession.Channel <- &client.Message{ Name: "SpeedtestStream", Content: string(msg), @@ -62,27 +46,23 @@ func HandleSpeedtestDotNet(c *gin.Context) { if nodeId != "" { args = append(args, "-s", nodeId) } - cmd := exec.Command("speedtest", args...) + cmd := exec.CommandContext(ctx, "speedtest", args...) go func() { <-ctx.Done() if cmd.Process != nil { - cmd.Process.Kill() + _ = cmd.Process.Kill() } }() - writer := func(pipe io.ReadCloser, err error) { - if err != nil { - fmt.Println("Pipe closed", err) - return - } + writer := func(pipe io.ReadCloser) { for { buf := make([]byte, 1024) n, err := pipe.Read(buf) if err != nil { return } - if !closed { + if !closed.Load() { clientSession.Channel <- &client.Message{ Name: "SpeedtestStream", Content: string(buf[:n]), @@ -91,10 +71,29 @@ func HandleSpeedtestDotNet(c *gin.Context) { } } - go writer(cmd.StdoutPipe()) - go writer(cmd.StderrPipe()) + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + c.JSON(500, &gin.H{"success": false, "error": "stdout pipe: " + err.Error()}) + return + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + c.JSON(500, &gin.H{"success": false, "error": "stderr pipe: " + err.Error()}) + return + } + + if err := cmd.Start(); err != nil { + c.JSON(500, &gin.H{"success": false, "error": err.Error()}) + return + } + + go writer(stdoutPipe) + go writer(stderrPipe) - cmd.Run() + if err := cmd.Wait(); err != nil { + c.JSON(500, &gin.H{"success": false, "error": err.Error()}) + return + } fmt.Println("speedtest-cli quit") c.JSON(200, &gin.H{ "success": true, diff --git a/backend/als/timer/interface_traffic.go b/backend/als/timer/interface_traffic.go index 163d493..69d6768 100644 --- a/backend/als/timer/interface_traffic.go +++ b/backend/als/timer/interface_traffic.go @@ -4,6 +4,7 @@ import ( "net" "strconv" "strings" + "sync" "time" "github.com/samlm0/als/v2/als/client" @@ -18,7 +19,29 @@ type InterfaceTrafficCache struct { LastTx uint64 `json:"-"` } -var InterfaceCaches = make(map[int]*InterfaceTrafficCache) +var ( + interfaceCachesMu sync.RWMutex + InterfaceCaches = make(map[int]*InterfaceTrafficCache) +) + +func GetInterfaceCachesSnapshot() map[int]*InterfaceTrafficCache { + interfaceCachesMu.RLock() + defer interfaceCachesMu.RUnlock() + + result := make(map[int]*InterfaceTrafficCache, len(InterfaceCaches)) + for idx, cache := range InterfaceCaches { + copiedCaches := make([][3]uint64, len(cache.Caches)) + copy(copiedCaches, cache.Caches) + result[idx] = &InterfaceTrafficCache{ + InterfaceName: cache.InterfaceName, + LastCacheTime: cache.LastCacheTime, + Caches: copiedCaches, + LastRx: cache.LastRx, + LastTx: cache.LastTx, + } + } + return result +} func SetupInterfaceBroadcast() { ticker := time.NewTicker(1 * time.Second) @@ -36,7 +59,7 @@ func SetupInterfaceBroadcast() { } // skip docker - if strings.Index(iface.Name, "docker") == 0 { + if strings.HasPrefix(iface.Name, "docker") { continue } @@ -46,12 +69,12 @@ func SetupInterfaceBroadcast() { } // skip wireguard - if strings.Index(iface.Name, "wt") == 0 { + if strings.HasPrefix(iface.Name, "wt") { continue } // skip veth - if strings.Index(iface.Name, "veth") == 0 { + if strings.HasPrefix(iface.Name, "veth") { continue } @@ -59,7 +82,16 @@ func SetupInterfaceBroadcast() { if err != nil { continue } + stats := link.Attrs().Statistics + if stats == nil { + continue + } now := time.Now() + ts := now.Unix() + if ts < 0 { + ts = 0 + } + interfaceCachesMu.Lock() cache, ok := InterfaceCaches[iface.Index] if !ok { InterfaceCaches[iface.Index] = &InterfaceTrafficCache{ @@ -72,15 +104,19 @@ func SetupInterfaceBroadcast() { cache = InterfaceCaches[iface.Index] } - cache.LastRx = link.Attrs().Statistics.RxBytes - cache.LastTx = link.Attrs().Statistics.TxBytes + cache.LastRx = stats.RxBytes + cache.LastTx = stats.TxBytes - cache.Caches = append(cache.Caches, [3]uint64{uint64(now.Unix()), cache.LastRx, cache.LastTx}) + cache.Caches = append(cache.Caches, [3]uint64{uint64(ts), cache.LastRx, cache.LastTx}) if len(cache.Caches) > 30 { cache.Caches = cache.Caches[len(cache.Caches)-30:] } cache.LastCacheTime = now - client.BroadCastMessage("InterfaceTraffic", iface.Name+","+strconv.Itoa(int(now.Unix()))+","+strconv.Itoa(int(cache.LastRx))+","+strconv.Itoa(int(cache.LastTx))) + interfaceCachesMu.Unlock() + client.BroadCastMessage( + "InterfaceTraffic", + iface.Name+","+strconv.FormatInt(ts, 10)+","+strconv.FormatUint(cache.LastRx, 10)+","+strconv.FormatUint(cache.LastTx, 10), + ) } } } diff --git a/backend/als/timer/system.go b/backend/als/timer/system.go index 2526ad7..8f6128d 100644 --- a/backend/als/timer/system.go +++ b/backend/als/timer/system.go @@ -14,7 +14,7 @@ func UpdateSystemResource() { for { <-ticker.C runtime.ReadMemStats(&m) - client.BroadCastMessage("MemoryUsage", strconv.Itoa(int(m.Sys))) + client.BroadCastMessage("MemoryUsage", strconv.FormatUint(m.Sys, 10)) } } diff --git a/backend/config/init.go b/backend/config/init.go index b094f6d..09c78fb 100644 --- a/backend/config/init.go +++ b/backend/config/init.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "os/exec" + "time" ) var Config *ALSConfig @@ -97,7 +98,7 @@ func LoadSponsorMessage() { return } - log.Default().Println("Loading sponser message...") + log.Default().Println("Loading sponsor message...") if _, err := os.Stat(Config.SponsorMessage); err == nil { content, err := os.ReadFile(Config.SponsorMessage) @@ -107,11 +108,17 @@ func LoadSponsorMessage() { } } - resp, err := http.Get(Config.SponsorMessage) + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Get(Config.SponsorMessage) if err == nil { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Default().Println("ERROR: Failed to load sponsor message.") + return + } content, err := io.ReadAll(resp.Body) if err == nil { - log.Default().Println("Loaded sponser message from url.") + log.Default().Println("Loaded sponsor message from url.") Config.SponsorMessage = string(content) return } diff --git a/backend/config/ip.go b/backend/config/ip.go index 76b41c4..c06ef42 100644 --- a/backend/config/ip.go +++ b/backend/config/ip.go @@ -7,6 +7,8 @@ import ( "log" "net" "net/http" + "strings" + "time" "github.com/miekg/dns" ) @@ -94,13 +96,22 @@ func getPublicIPViaHttp(client *http.Client) (string, error) { if err != nil { continue } + if resp.StatusCode != http.StatusOK { + if cerr := resp.Body.Close(); cerr != nil { + return "", cerr + } + continue + } body, err := io.ReadAll(resp.Body) + if cerr := resp.Body.Close(); cerr != nil && err == nil { + err = cerr + } if err != nil { return "", err } - addr := net.ParseIP(string(body)) + addr := net.ParseIP(strings.TrimSpace(string(body))) if addr != nil { return addr.String(), nil } @@ -111,6 +122,7 @@ func getPublicIPViaHttp(client *http.Client) (string, error) { func getPublicIPv4ViaHttp() (string, error) { client := &http.Client{ + Timeout: 5 * time.Second, Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { var dialer net.Dialer diff --git a/backend/config/location.go b/backend/config/location.go index e6f9823..9b23784 100644 --- a/backend/config/location.go +++ b/backend/config/location.go @@ -6,21 +6,30 @@ import ( "io" "log" "net/http" + "time" ) func updateLocation() { log.Default().Println("Updating server location from internet...") - resp, err := http.Get("https://ipapi.co/json/") + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get("https://ipapi.co/json/") if err != nil { return } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return + } body, err := io.ReadAll(resp.Body) if err != nil { return } var data map[string]interface{} - json.Unmarshal(body, &data) + if err := json.Unmarshal(body, &data); err != nil { + log.Default().Printf("parse location failed: %v", err) + return + } if _, ok := data["country_name"]; !ok { return } diff --git a/backend/fakeshell/commands/map.go b/backend/fakeshell/commands/map.go index 2acf1cc..e3239af 100644 --- a/backend/fakeshell/commands/map.go +++ b/backend/fakeshell/commands/map.go @@ -3,29 +3,36 @@ package commands import ( "os" "os/exec" + "path/filepath" "github.com/spf13/cobra" ) -func AddExecureableAsCommand(cmd *cobra.Command, command string, argFilter func(args []string) ([]string, error)) { +func AddExecutableAsCommand(cmd *cobra.Command, command string, argFilter func(args []string) ([]string, error)) { cmdDefine := &cobra.Command{ Use: command, Run: func(cmd *cobra.Command, args []string) { + if command == "" || filepath.Base(command) != command { + cmd.Println("invalid command") + return + } args, err := argFilter(args) if err != nil { cmd.Println(err) return } - c := exec.Command(command, args...) + // command name comes from predefined menu registration, not user input + c := exec.CommandContext(cmd.Context(), command, args...) // #nosec G204 c.Env = os.Environ() c.Env = append(c.Env, "TERM=xterm-256color") c.Stdin = cmd.InOrStdin() c.Stdout = cmd.OutOrStdout() c.Stderr = cmd.OutOrStderr() - c.Run() - c.Wait() + if err := c.Run(); err != nil { + cmd.Println(err) + } }, DisableFlagParsing: true, } diff --git a/backend/fakeshell/main.go b/backend/fakeshell/main.go index dc9aa7f..706c962 100644 --- a/backend/fakeshell/main.go +++ b/backend/fakeshell/main.go @@ -1,6 +1,7 @@ package fakeshell import ( + "fmt" "io" "os" @@ -28,6 +29,9 @@ func HandleConsole() { // }() menu.AddInterrupt(io.EOF, exitCtrlD) - menu.SetCommands(defineMenuCommands(app)) - app.Start() + menu.SetCommands(defineMenuCommands()) + if err := app.Start(); err != nil { + fmt.Println("console start failed:", err) + os.Exit(1) + } } diff --git a/backend/fakeshell/menu.go b/backend/fakeshell/menu.go index 661cf6b..3d9f9a7 100644 --- a/backend/fakeshell/menu.go +++ b/backend/fakeshell/menu.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -func defineMenuCommands(a *console.Console) console.Commands { +func defineMenuCommands() console.Commands { showedIsFirstTime := false return func() *cobra.Command { rootCmd := &cobra.Command{} @@ -52,7 +52,7 @@ func defineMenuCommands(a *console.Console) console.Commands { _, err := exec.LookPath(command) if err != nil { if !showedIsFirstTime { - fmt.Println("Error: " + command + " is not install") + fmt.Println("Error: " + command + " is not installed") } hasNotFound = true continue @@ -61,7 +61,7 @@ func defineMenuCommands(a *console.Console) console.Commands { if !ok { filter = argsPassthough } - commands.AddExecureableAsCommand(rootCmd, command, filter) + commands.AddExecutableAsCommand(rootCmd, command, filter) } } diff --git a/backend/go.mod b/backend/go.mod index d2e4c6c..93260ac 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,59 +1,62 @@ module github.com/samlm0/als/v2 -go 1.21.4 +go 1.26.2 require ( - github.com/creack/pty v1.1.21 - github.com/gin-gonic/gin v1.9.1 - github.com/google/uuid v1.5.0 - github.com/gorilla/websocket v1.5.1 - github.com/miekg/dns v1.1.57 - github.com/reeflective/console v0.1.15 + github.com/creack/pty v1.1.24 + github.com/gin-gonic/gin v1.12.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/miekg/dns v1.1.72 + github.com/reeflective/console v0.3.1 github.com/samlm0/go-ping v0.1.0 - github.com/spf13/cobra v1.8.0 - github.com/vishvananda/netlink v1.1.0 + github.com/spf13/cobra v1.10.2 + github.com/vishvananda/netlink v1.3.1 ) require ( - github.com/bytedance/sonic v1.10.2 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/carapace-sh/carapace v1.11.6 // indirect + github.com/carapace-sh/carapace-shlex v1.1.1 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/frankban/quicktest v1.14.6 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.16.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/klauspost/cpuid/v2 v2.2.6 // indirect - github.com/leodido/go-urn v1.2.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/reeflective/readline v1.0.13 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69 // indirect - github.com/rsteube/carapace-shlex v0.1.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.1 // indirect + github.com/reeflective/readline v1.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect - golang.org/x/arch v0.6.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.16.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/tools v0.45.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - mvdan.cc/sh/v3 v3.7.0 // indirect + mvdan.cc/sh/v3 v3.13.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index ccdec28..e92cecd 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,154 +1,149 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= -github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= -github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/carapace-sh/carapace v1.11.6 h1:fUZv+oAMgbiDEpNPNis4n35tzqE3h8yshOohLJ2Mz4Y= +github.com/carapace-sh/carapace v1.11.6/go.mod h1:5MUSHyLN9GGb5/NY/j9VI68/TcZV4ApRCAHGg4WeU0s= +github.com/carapace-sh/carapace-shlex v1.1.1 h1:ccmNeetAYZOk4IcV36youFDsXusT9uCNW2Njkw+QS+Q= +github.com/carapace-sh/carapace-shlex v1.1.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= -github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/reeflective/console v0.1.15 h1:r4M1a19s882znSO5Zkj7memsLSDTLT/0fZwdNzhIldE= -github.com/reeflective/console v0.1.15/go.mod h1:U2i+gzsZ5mT9LZHLzoeuOJ7BtcyXy7l+psRZSu4zmQU= -github.com/reeflective/readline v1.0.13 h1:TeJmYw9B7VRPZWfNExr9QHxL1m0iSicyqBSQIRn39Ss= -github.com/reeflective/readline v1.0.13/go.mod h1:3iOe/qyb2jEy0KqLrNlb/CojBVqxga9ACqz/VU22H6A= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= -github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69 h1:ctOUuKn5PO6VtwtaS7unNrm6u20YXESPtnKEie/u304= -github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69/go.mod h1:4ZC5bulItu9t9sZ5yPcHgPREd8rPf274Q732n+wfl/o= -github.com/rsteube/carapace-shlex v0.1.1 h1:fRQEBBKyYKm4TXUabm4tzH904iFWSmXJl3UZhMfQNYU= -github.com/rsteube/carapace-shlex v0.1.1/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/reeflective/console v0.3.1 h1:sOD+ZoYo8bT+DOKOnkZHGYC7hXJdjCpVSbbBrwIC8Lc= +github.com/reeflective/console v0.3.1/go.mod h1:4pVcOeUHGLVJFWRowmFYVMKz8g3U6gOErRoadWSwEHE= +github.com/reeflective/readline v1.2.2 h1:UAcTtKQdg7xQDzDlqWyMkvEAsEie2z3gQsEKt8TkErE= +github.com/reeflective/readline v1.2.2/go.mod h1:bOpqx2/VqGlIoobyWR1Vgt/p5FiMfIHj4OicPuw6RfU= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samlm0/go-ping v0.1.0 h1:ajEqnaEtP2HY9vldc38J2Y2Qve8j+E3jkgwKK+LIoWM= github.com/samlm0/go-ping v0.1.0/go.mod h1:3cg9EBJvzQ1vZZmTu0E/AQtagyHE7TEs4zXslwFPXLc= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= -golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= -mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= +mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= diff --git a/backend/http/init.go b/backend/http/init.go index 389134b..15663b2 100644 --- a/backend/http/init.go +++ b/backend/http/init.go @@ -26,6 +26,6 @@ func (e *Server) SetListen(listen string) { e.listen = listen } -func (e *Server) Start() { - e.engine.Run(e.listen) +func (e *Server) Start() error { + return e.engine.Run(e.listen) } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f0da748 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,355 @@ +# 系统架构 + +**最后更新**: 2026-04-27 + +## 1. 系统概述 + +ALS (Another Looking-glass Server) 是一个轻量级的 Looking-glass 服务器,用于提供网络诊断和测速功能。系统设计为无状态架构,通过 WebSocket 和 SSE 实现实时通信。 + +## 2. 架构分层 + +``` +┌─────────────────────────────────────────────────┐ +│ 前端层 (UI) │ +│ Vue 3 + Vite + Naive UI + Vue I18n │ +│ - 单页应用 (SPA) │ +│ - 多语言支持 (8 种语言) │ +│ - 响应式设计 (支持深色模式) │ +└─────────────────────────────────────────────────┘ + ↓ + HTTP/WebSocket/SSE + ↓ +┌─────────────────────────────────────────────────┐ +│ 应用层 (ALS) │ +│ Gin Web 框架 + 中间件 │ +│ - 路由分发 (route.go) │ +│ - 会话中间件 (MiddlewareSessionOnHeader/Url) │ +│ - 控制器 (/controller) │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 业务逻辑层 │ +│ - Session 管理 (client/client.go) │ +│ - 工具实现 (ping/iperf3/speedtest/shell) │ +│ - 定时任务 (timer/) │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 基础设施层 │ +│ - 配置管理 (config/) │ +│ - 静态文件服务 (embed/ui) │ +│ - 系统命令执行 (exec.CommandContext) │ +└─────────────────────────────────────────────────┘ +``` + +## 3. 组件详解 + +### 3.1 前端组件 + +**技术栈**: +- Vue 3 (Composition API) +- Vite 构建工具 +- Naive UI 组件库 +- Pinia 状态管理 +- Vue I18n 国际化 +- Vue3-ApexCharts 图表 +- Xterm.js 终端模拟 + +**目录结构** (`ui/`): +``` +ui/ +├── src/ +│ ├── components/ # Vue 组件 +│ │ ├── Loading.vue # 加载组件 +│ │ ├── Information.vue # 服务器信息展示 +│ │ ├── Speedtest.vue # 测速组件 +│ │ ├── Utilities.vue # 工具集合组件 +│ │ ├── TrafficDisplay.vue # 流量显示 +│ │ └── Utilities/ +│ │ ├── Ping.vue # Ping 工具 +│ │ ├── IPerf3.vue # iPerf3 工具 +│ │ ├── Shell.vue # Shell 终端 +│ │ └── SpeedtestNet.vue # Speedtest.net +│ ├── config/ +│ │ └── lang.js # 多语言配置 +│ ├── locales/ # 翻译文件 +│ ├── stores/ +│ │ └── app.js # 全局状态 +│ ├── helper/ +│ │ └── unit.js # 工具函数 +│ ├── App.vue # 根组件 +│ └── main.js # 入口文件 +├── public/ +│ └── speedtest_worker.js # 测速 Web Worker +└── package.json +``` + +**核心特性**: +- 自动语言检测 (基于浏览器设置) +- 语言切换 (支持 8 种语言) +- 深色/浅色模式 (跟随系统) +- WebSocket 实时通信 +- SSE (Server-Sent Events) + +### 3.2 后端组件 + +**技术栈**: +- Go 1.26.2 +- Gin Web Framework +- Gorilla WebSocket +- Cobra CLI +- PTY (伪终端) + +**目录结构** (`backend/`): +``` +backend/ +├── main.go # 程序入口 +├── als/ # ALS 核心模块 +│ ├── als.go # 初始化入口 +│ ├── route.go # 路由配置 +│ ├── client/ # 会话管理 +│ ├── controller/ # 控制器 +│ │ ├── middleware.go # 中间件 +│ │ ├── session/ # 会话处理 +│ │ ├── ping/ # Ping 工具 +│ │ ├── iperf3/ # iPerf3 工具 +│ │ ├── speedtest/ # 测速工具 +│ │ ├── shell/ # Shell 工具 +│ │ └── cache/ # 缓存接口 +│ └── timer/ # 定时任务 +├── config/ # 配置模块 +│ ├── init.go # 配置初始化 +│ ├── load_from_env.go # 环境变量加载 +│ └── location.go # 位置信息获取 +├── http/ # HTTP 服务 +│ └── init.go # HTTP 服务器初始化 +├── fakeshell/ # Fake Shell 模块 +│ ├── main.go # Shell 入口 +│ ├── menu.go # 命令菜单 +│ └── commands/ # 命令实现 +└── embed/ # 嵌入资源 + └── ui/ # 前端静态资源 +``` + +### 3.3 会话管理机制 + +**会话创建流程**: +1. 客户端 `GET /session` → 创建新会话 +2. 服务器生成 UUID 作为会话 ID +3. 通过 SSE 推送会话 ID 给客户端 +4. 后续请求携带会话 ID (Header 或 URL 参数) + +**会话验证**: +- `MiddlewareSessionOnHeader()`: 从 Header 获取会话 ID +- `MiddlewareSessionOnUrl()`: 从 URL 参数获取会话 ID +- 检查会话是否存在且未过期 (24 小时) + +**会话存储**: +- 内存存储 (`map[string]*ClientSession`) +- 自动清理过期会话 (每小时) +- 支持广播消息给所有活跃会话 + +### 3.4 网络工具实现 + +#### Ping 工具 +- 位置:`backend/als/controller/ping/ping.go` +- 实现:使用 `go-ping` 库 +- 功能:支持 IPv4/IPv6 + +#### iPerf3 工具 +- 位置:`backend/als/controller/iperf3/iperf3.go` +- 实现:启动 iPerf3 服务器进程 +- 端口范围:30000-31000 (可配置) + +#### Speedtest 工具 +**LibreSpeed**: +- 位置:`backend/als/controller/speedtest/librespeed.go` +- 实现:基于 LibreSpeed 项目 +- 功能:下载/上传测试 + +**Speedtest.net**: +- 位置:`backend/als/controller/speedtest/speedtest_cli.go` +- 实现:调用 speedtest-cli 工具 + +**文件测速**: +- 位置:`backend/als/controller/speedtest/fakefile.go` +- 实现:动态生成指定大小的文件 + +#### Shell 工具 +- 位置:`backend/als/controller/shell/shell.go` +- 实现:基于 WebSocket 的 PTY 终端 +- 命令白名单:ping, traceroute, mtr, speedtest +- 参数过滤:危险参数拦截 + +## 4. 接口协议 + +### 4.1 HTTP API + +| 端点 | 方法 | 描述 | 认证 | +|------|------|------|------| +| `/session` | GET | 创建会话 (SSE) | 无 | +| `/method/iperf3/server` | GET | iPerf3 服务器信息 | Session Header | +| `/method/ping` | GET | Ping 测试 | Session Header | +| `/method/speedtest_dot_net` | GET | Speedtest.net | Session Header | +| `/method/cache/interfaces` | GET | 网卡接口缓存 | Session Header | +| `/session/:session/shell` | GET | WebSocket Shell | Session URL 参数 | +| `/session/:session/speedtest/file/:filename` | GET | 文件测速 | Session URL 参数 | +| `/session/:session/speedtest/download` | GET | LibreSpeed 下载 | Session URL 参数 | +| `/session/:session/speedtest/upload` | POST | LibreSpeed 上传 | Session URL 参数 | + +### 4.2 WebSocket + +**Shell 连接**: +- URL: `ws(s)://host/session//shell` +- 协议:WebSocket +- 消息格式: + - `1`: 输入数据 + - `2;`: 窗口大小调整 + +## 5. 配置系统 + +### 5.1 环境变量 + +| 变量 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `LISTEN_IP` | string | 0.0.0.0 | 监听 IP | +| `HTTP_PORT` | string | 80 | 监听端口 | +| `LOCATION` | string | (自动获取) | 服务器位置 | +| `PUBLIC_IPV4` | string | (自动获取) | 公网 IPv4 | +| `PUBLIC_IPV6` | string | (自动获取) | 公网 IPv6 | +| `SPEEDTEST_FILE_LIST` | string | "1MB 10MB 100MB 1GB" | 测速文件列表 | +| `SPONSOR_MESSAGE` | string | "" | 赞助商信息 | +| `DISPLAY_TRAFFIC` | bool | true | 流量显示开关 | +| `ENABLE_SPEEDTEST` | bool | true | LibreSpeed 开关 | +| `UTILITIES_PING` | bool | true | Ping 开关 | +| `UTILITIES_FAKESHELL` | bool | true | Shell 开关 | +| `UTILITIES_IPERF3` | bool | true | iPerf3 开关 | +| `UTILITIES_IPERF3_PORT_MIN` | int | 30000 | iPerf3 起始端口 | +| `UTILITIES_IPERF3_PORT_MAX` | int | 31000 | iPerf3 结束端口 | +| `UTILITIES_MTR` | bool | true | MTR 开关 | +| `UTILITIES_TRACEROUTE` | bool | true | Traceroute 开关 | + +### 5.2 启动流程 + +``` +main() +├── flag.Parse() +├── --shell? +│ ├── config.IsInternalCall = true +│ ├── config.Load() +│ └── fakeshell.HandleConsole() +└── 正常模式 + ├── config.LoadWebConfig() + ├── als.Init() + │ ├── alsHttp.CreateServer() + │ ├── SetupHttpRoute() + │ ├── 启动定时任务 + │ └── aHttp.Start() +``` + +## 6. 部署架构 + +### Docker 部署 + +``` +┌─────────────────────────────────────┐ +│ Docker 容器 │ +│ ┌───────────────────────────────┐ │ +│ │ /bin/als (主程序) │ │ +│ │ - 嵌入前端静态资源 │ │ +│ │ - 监听 80 端口 │ │ +│ └───────────────────────────────┘ │ +│ 系统工具层: │ +│ - ping, iperf3, mtr, traceroute │ +└─────────────────────────────────────┘ +``` + +**Dockerfile 构建阶段**: +1. `builder_node_js_cache`: 安装 Node.js 依赖 +2. `builder_node_js`: 构建前端 +3. `builder_golang`: 编译 Go 后端 + 嵌入前端 +4. `builder_env`: 安装系统工具 +5. `final`: 合并产物 + +### 网络要求 + +- **网络模式**: Host 模式 (推荐) 或 Bridge 模式 +- **端口**: 80 (可配置) +- **容器能力**: 需要网络相关权限执行 ping 等工具 + +## 7. CI/CD 架构 + +### GitHub Actions 工作流 + +**CI** (`ci.yml`): +- 触发:PR 和 push +- 步骤: + 1. 构建前端 (npm run build) + 2. 下载前端到后端嵌入目录 + 3. Go 测试 (go test) + 4. Go 构建 (go build) + +**Docker Image** (`docker-image.yml`): +- 触发:Tag 推送 +- 步骤: + 1. 构建前端 + 2. 构建多架构 Docker 镜像 (amd64, arm64) + 3. 推送到 Docker Hub + 4. 生成 SBOM 和 provenance + +**Release** (`release.yml`): +- 触发:Tag 推送 +- 步骤: + 1. 构建前端 + 2. 交叉编译多平台二进制 (linux/darwin/windows × amd64/arm64) + 3. 创建 GitHub Release + +## 8. 安全考虑 + +### 8.1 会话安全 +- 会话 ID 使用 UUID v4 +- 24 小时过期机制 +- 内存隔离不同会话 + +### 8.2 Shell 安全 +- 命令白名单机制 +- 危险参数过滤 (`ping -f`) +- 不可运行任意命令 +- 可通过环境变量禁用 + +### 8.3 CORS +- 检查 WebSocket 连接 Origin +- 仅允许同源连接 + +### 8.4 依赖安全 +- 固定依赖版本 +- 定期安全更新 + +## 9. 性能优化 + +### 前端 +- Vite 构建 (HMR 开发,按需打包) +- 组件懒加载 (defineAsyncComponent) +- WebSocket 复用连接 + +### 后端 +- Gin Release Mode +- 内存缓存 (接口信息、系统资源) +- 定时清理过期会话 +- 上下文感知 (context.Context 取消) + +## 10. 监控与日志 + +### 日志 +- Go 标准日志 (`log.Default()`) +- Gin 访问日志 (Release Mode 简化) + +### 健康检查 +- Docker HEALTHCHECK +- 每 30 秒检测 `/` +- 超时 5 秒,重试 3 次 + +### 资源监控 +- 实时内存使用 (显示在页脚) +- 网卡流量广播 (1 秒间隔) +- 系统资源更新 (定时器) diff --git a/docs/DEPLOYMENT_GUIDE.md b/docs/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..9707522 --- /dev/null +++ b/docs/DEPLOYMENT_GUIDE.md @@ -0,0 +1,299 @@ +# 文档发布指南 + +本文档说明如何将项目文档发布到 GitHub Pages。 + +## 目录结构 + +``` +als/ +├── docs/ # GitHub Pages 文档目录 +│ ├── README.md # 首页 +│ ├── ARCHITECTURE.md # 系统架构 +│ ├── INTERFACES.md # 接口文档 +│ ├── DEVELOPER_GUIDE.md # 开发者指南 +│ ├── mkdocs.yml # MkDocs 配置(可选) +│ ├── 专有概念/ # 概念文档 +│ └── 模块/ # 模块文档 +├── .monkeycode/docs/ # 源文档目录 +└── .github/workflows/ + └── docs.yml # GitHub Actions 工作流 +``` + +## 工作流程 + +### 自动发布 + +当满足以下条件时,文档会自动发布: + +1. **推送到 master 分支** + ```bash + git push origin master + ``` + +2. **修改了 docs 目录下的文件** + - `docs/**` + - `.monkeycode/docs/**` + +3. **GitHub Actions 自动执行** + - 检出代码 + - 安装 MkDocs 依赖 + - 同步文档(从 `.monkeycode/docs` 到 `docs/`) + - 构建静态站点 + - 部署到 GitHub Pages + +### 手动触发 + +1. 进入 GitHub 仓库页面 +2. 点击 **Actions** 标签 +3. 选择 **Deploy Docs to GitHub Pages** 工作流 +4. 点击 **Run workflow** 按钮 +5. 选择分支(通常是 `master`) +6. 点击 **Run workflow** + +## GitHub Pages 配置 + +### 1. 启用 GitHub Pages + +1. 进入仓库 **Settings** +2. 点击左侧 **Pages** +3. 在 **Source** 下选择: + - **Deploy from a branch** + - Branch: `gh-pages` / `(root)` +4. 点击 **Save** + +### 2. 访问文档 + +部署成功后,文档将在以下地址可用: + +``` +https://.github.io// +``` + +例如: +``` +https://upbeat-backbone-bose.github.io/als/ +``` + +## 更新文档 + +### 方式一:直接修改 docs 目录 + +```bash +# 1. 编辑文档 +vim docs/ARCHITECTURE.md + +# 2. 提交更改 +git add docs/ +git commit -m "docs: 更新架构文档" + +# 3. 推送触发自动部署 +git push origin master +``` + +### 方式二:使用 .monkeycode/docs(推荐) + +```bash +# 1. 编辑源文档 +vim .monkeycode/docs/ARCHITECTURE.md + +# 2. 同步到 docs 目录 +cp -r .monkeycode/docs/* docs/ + +# 3. 提交并推送 +git add docs/ .monkeycode/docs/ +git commit -m "docs: 更新所有文档" +git push origin master +``` + +### 方式三:使用 AI 生成文档 + +```bash +# 使用项目 Wiki skill 生成/更新文档 +/project-wiki + +# 然后同步到 docs 目录 +cp -r .monkeycode/docs/* docs/ +git add docs/ +git commit -m "docs: 同步 AI 生成的文档" +git push origin master +``` + +## MkDocs 主题(可选) + +### 安装 MkDocs + +```bash +# 本地开发时安装 +pip install mkdocs mkdocs-material pymdown-extensions +``` + +### 本地预览 + +```bash +cd docs +mkdocs serve +``` + +访问:http://127.0.0.1:8000 + +### 构建 + +```bash +cd docs +mkdocs build +``` + +输出目录:`docs/site/` + +## 自定义配置 + +### 修改 mkdocs.yml + +```yaml +site_name: 您的站点名称 +site_description: 站点描述 +repo_url: https://github.com/your-username/your-repo + +theme: + name: material + palette: + - scheme: default # 浅色模式 + - scheme: slate # 深色模式 + +nav: + - 首页:README.md + - 文档名称:文件路径.md +``` + +### 添加自定义域名 + +1. 在 `docs/` 目录下创建 `CNAME` 文件 +2. 内容为您的域名: + ``` + docs.example.com + ``` +3. 提交并推送: + ```bash + echo "docs.example.com" > docs/CNAME + git add docs/CNAME + git commit -m "docs: 添加自定义域名" + git push origin master + ``` +4. 在 DNS 提供商处配置 CNAME 记录 + +## 故障排查 + +### 问题 1: 部署失败 + +**检查 GitHub Actions 日志**: +1. 进入 **Actions** 标签 +2. 点击失败的工作流 +3. 查看错误信息 + +**常见问题**: +- Python 版本不匹配 +- MkDocs 配置错误 +- 文件路径错误 + +### 问题 2: 页面显示 404 + +**解决方法**: +1. 确认 GitHub Pages 已正确配置 +2. 等待几分钟(部署需要时间) +3. 检查 `_config.yml` 或 `mkdocs.yml` 配置 + +### 问题 3: 文档未更新 + +**检查点**: +- 文件路径是否正确 +- 是否在 master 分支 +- GitHub Actions 是否成功运行 +- 浏览器缓存(强制刷新 Ctrl+F5) + +## 最佳实践 + +### 1. 文档组织 + +``` +docs/ +├── README.md # 首页和导航 +├── 核心文档/ +│ ├── 架构.md +│ └── API.md +├── 指南/ +│ ├── 快速开始.md +│ └── 教程.md +└── 参考/ + ├── 配置.md + └── 命令.md +``` + +### 2. 提交信息规范 + +```bash +# 文档新增 +git commit -m "docs: 添加 API 文档" + +# 文档更新 +git commit -m "docs: 更新架构说明" + +# 文档修复 +git commit -m "docs: 修复拼写错误" + +# 文档重构 +git commit -m "docs: 重组文档结构" +``` + +### 3. 版本控制 + +对于重要版本文档: + +```bash +# 创建版本文档目录 +mkdir -p docs/v1.0 +cp -r docs/* docs/v1.0/ + +# 或在文档顶部添加版本信息 +--- +version: 1.0 +date: 2026-04-22 +--- +``` + +### 4. 自动化同步 + +创建同步脚本 `scripts/sync-docs.sh`: + +```bash +#!/bin/bash +# 同步 .monkeycode/docs 到 docs +set -e + +echo "Syncing documents..." +cp -r .monkeycode/docs/* docs/ + +echo "Documents synced successfully!" +git status +``` + +使用: +```bash +chmod +x scripts/sync-docs.sh +./scripts/sync-docs.sh +git add docs/ +git commit -m "docs: 同步文档" +git push +``` + +## 相关资源 + +- [GitHub Pages 官方文档](https://docs.github.com/en/pages) +- [MkDocs 文档](https://www.mkdocs.org/) +- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +- [GitHub Actions 文档](https://docs.github.com/en/actions) + +## 联系支持 + +如有问题,请: +1. 查看 [GitHub Issues](https://github.com/upbeat-backbone-bose/als/issues) +2. 提交新的 Issue 描述您的问题 diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..c360145 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,612 @@ +# 开发者指南 + +**最后更新**: 2026-04-22 + +## 1. 开发环境搭建 + +### 1.1 依赖要求 + +**系统要求**: +- Linux / macOS / Windows (WSL) +- 内存:≥ 512MB +- 磁盘:≥ 1GB 可用空间 + +**必需软件**: + +| 软件 | 版本 | 用途 | +|------|------|------| +| Go | 1.26+ | 后端开发 | +| Node.js | 22+ | 前端开发 | +| npm | 10+ | 前端依赖管理 | +| Git | 最新 | 版本控制 | +| Docker | 可选 | 容器化部署 | + +**可选工具**: +- `iperf3` - 带宽测试 +- `ping` - 网络诊断 +- `mtr` - 路由跟踪 +- `traceroute` - 路由跟踪 +- `speedtest-cli` - Speedtest.net + +### 1.2 克隆项目 + +```bash +git clone --recursive https://github.com/upbeat-backbone-bose/als.git +cd als +``` + +**注意**: 使用 `--recursive` 克隆子模块 (LibreSpeed) + +### 1.3 安装依赖 + +**前端**: +```bash +cd ui +npm install +``` + +**后端**: +```bash +cd backend +go mod tidy +``` + +### 1.4 环境变量配置 + +创建 `.env` 文件 (可选): + +```bash +# 开发环境配置 +LISTEN_IP=127.0.0.1 +HTTP_PORT=8080 +LOCATION="Development Server" +PUBLIC_IPV4=127.0.0.1 +ENABLE_SPEEDTEST=true +UTILITIES_FAKESHELL=true +UTILITIES_PING=true +UTILITIES_IPERF3=true +``` + +## 2. 开发与构建 + +### 2.1 开发模式 + +**前端开发服务器**: +```bash +cd ui +npm run dev +``` + +- 访问:`http://localhost:5173` +- 功能:热重载、HMR +- 代理:需要配置 vite.config.js 的 proxy + +**后端开发**: +```bash +cd backend +go run . +``` + +- 监听:`http://0.0.0.0:80` +- 日志:实时输出到控制台 + +**同时运行前后端**: +```bash +# 终端 1 - 前端 +cd ui && npm run dev + +# 终端 2 - 后端 +cd backend && go run . +``` + +### 2.2 构建生产版本 + +**完整构建流程**: + +```bash +# 1. 构建前端 +cd ui +npm run build + +# 2. 复制前端到后端嵌入目录 +cp -r dist ../backend/embed/ui + +# 3. 构建后端 +cd ../backend +go build -o als +``` + +**一键构建脚本**: +```bash +cd backend +go build -o als +``` + +**说明**: +- 前端构建物输出到 `ui/dist/` +- 需要手动复制到 `backend/embed/ui/` +- Go embed 自动嵌入静态文件 + +### 2.3 交叉编译 + +**多平台构建**: +```bash +# Linux AMD64 +GOOS=linux GOARCH=amd64 go build -o als-linux-amd64 + +# Linux ARM64 +GOOS=linux GOARCH=arm64 go build -o als-linux-arm64 + +# macOS +GOOS=darwin GOARCH=amd64 go build -o als-darwin + +# Windows +GOOS=windows GOARCH=amd64 go build -o als-windows.exe +``` + +## 3. 测试 + +### 3.1 单元测试 + +**后端测试**: +```bash +cd backend +go test ./... +``` + +**覆盖度报告**: +```bash +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +**特定包测试**: +```bash +go test ./als/client +go test ./config +``` + +### 3.2 前端测试 + +**Linting**: +```bash +cd ui +npm run lint +``` + +**格式化**: +```bash +npm run format +``` + +**构建测试**: +```bash +npm run build +``` + +### 3.3 集成测试 + +**LibreSpeed 测试**: +```bash +cd ui/speedtest +npm install +npm run test:e2e +``` + +## 4. Docker 开发 + +### 4.1 本地构建 Docker 镜像 + +```bash +docker build -t als:local . +``` + +**多架构构建**: +```bash +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t als:local \ + --load \ + . +``` + +### 4.2 运行开发容器 + +```bash +docker run -d --name als-dev \ + -p 8080:80 \ + -e HTTP_PORT=8080 \ + -e UTILITIES_FAKESHELL=true \ + als:local +``` + +**查看日志**: +```bash +docker logs -f als-dev +``` + +### 4.3 挂载开发目录 + +```bash +docker run -d --name als-dev \ + -p 8080:80 \ + -v $(pwd)/ui/src:/app/ui/src \ + als:local +``` + +**说明**: +- 前端源码挂载实现热重载 +- 需要修改 Dockerfile + +## 5. 调试 + +### 5.1 后端调试 + +**启用调试日志**: +```go +// 在 main.go 中添加 +log.SetFlags(log.LstdFlags | log.Lshortfile) +``` + +**使用 Delve**: +```bash +# 安装 dlv +go install github.com/go-delve/delve/cmd/dlv@latest + +# 调试运行 +cd backend +dlv debug . +``` + +**HTTP 请求日志**: +```bash +# 使用 curl -v 查看详细请求 +curl -v http://localhost/method/ping?target=8.8.8.8 +``` + +### 5.2 前端调试 + +**开发工具**: +- Chrome DevTools - 网络、控制台 +- Vue DevTools - 组件树、状态 +- Network - 查看 SSE/WebSocket 连接 + +**调试 SSE**: +```javascript +const es = new EventSource('/session'); +es.addEventListener('message', (e) => console.log('SSE:', e)); +``` + +**调试 WebSocket**: +```javascript +const ws = new WebSocket(url); +ws.onopen = () => console.log('WebSocket connected'); +ws.onmessage = (e) => console.log('Message:', e.data); +ws.onerror = (e) => console.error('Error:', e); +``` + +### 5.3 Shell 调试 + +**查看 Shell 状态**: +```bash +# 连接到 Shell +wscat -c ws://localhost/session//shell + +# 发送 help 命令 +1help +``` + +**假 Shell 独立测试**: +```bash +cd backend +go run . --shell +``` + +**启用假 Shell 调试模式**: +```bash +# 修改 fakeshell/main.go +util.SetDebug(true) # 启用调试输出 +``` + +## 6. 代码规范 + +### 6.1 Go 代码规范 + +**代码格式**: +```bash +gofmt -w . +go fmt ./... +``` + +**静态检查**: +```bash +go vet ./... +``` + +**安全扫描**: +```bash +# 安装 gosec +go install github.com/securego/gosec/v2/cmd/gosec@latest + +# 运行扫描 +gosec ./... +``` + +**代码规范**: +- 遵循 Go 官方代码规范 +- 函数名、变量名使用驼峰 +- 包名全小写 +- 错误处理完整 +- 必要的注释 + +### 6.2 前端代码规范 + +**ESLint**: +```bash +cd ui +npm run lint +``` + +**Prettier**: +```bash +npm run format +``` + +**规范**: +- 使用 Composition API +- 组件名 PascalCase +- 文件名 PascalCase.vue +- 使用 TypeScript (推荐) +- 必要的 JSDoc 注释 + +## 7. 项目结构约定 + +### 7.1 目录结构 + +``` +als/ +├── backend/ +│ ├── main.go # 入口 +│ ├── als/ # 核心模块 +│ ├ ├── route.go # 路由配置 +│ │ └── controller/ # 控制器 +│ ├── config/ # 配置 +│ └── fakeshell/ # Shell 实现 +├── ui/ +│ ├── src/ +│ │ ├── components/ # Vue 组件 +│ │ ├── config/ # 配置 +│ │ └── locales/ # 国际化 +│ └── public/ +├── scripts/ # 脚本 +└── .github/ # GitHub Actions +``` + +### 7.2 文件命名 + +**Go**: +- 文件名:小写 + 下划线 +- 包名:简短、描述性 + +**前端**: +- 组件:PascalCase.vue (如 `Information.vue`) +- 配置:kebab-case.js (如 `lang.js`) +- 翻译:语言代码.json (如 `zh-CN.json`) + +**文档**: +- Markdown:大写 + 下划线 (如 `DEVELOPER_GUIDE.md`) + +### 7.3 提交规范 + +**Commit Message 格式**: +``` +(): + + + +