From 995ba34aa452d5f4548ba83a4703f6c064e2fa7c Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 6 Mar 2025 02:37:07 +0000 Subject: fix: use toLowerCase() to make sure usernameLower matches while compared to request with possibly capitalization --- packages/backend/src/server/ActivityPubServerService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 10dba1660f..765d54bc71 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -826,7 +826,7 @@ export class ActivityPubServerService { const acct = Acct.parse(request.params.acct); const user = await this.usersRepository.findOneBy({ - usernameLower: acct.username, + usernameLower: acct.username.toLowerCase(), host: acct.host ?? IsNull(), isSuspended: false, }); -- cgit v1.2.3-freya From 83c3bb839f50fc24f667611f6852db3b14bd05e6 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:03:52 +0900 Subject: deps: update pnpm to v10 (#15588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "fix(build): corepackのバグの回避 (#15387)" This reverts commit 9c70a4e63130f85d191c5bc16d0a4be5cd1dece2. * deps: update pnpm to v10 * fix broken lockfile * update changelog * fix * fix * Revert "fix" This reverts commit 4abc6c194edc20989f5ec97d343307a4b8c9047d. * fix * fix * attempt to fix docker build * lint fixes * fix: revertしすぎた * detect pnpm version and install it * fix: そもそもpnpmを2回入れる必要がないかも * fix * refactor * fix * refactor: remove unnecessary arg * Update Dockerfile * update pnpm to v10.6.1 * Update Changelog * chore: use node to avoid installing jq --- .devcontainer/devcontainer.json | 4 +- .devcontainer/init.sh | 2 - .github/workflows/api-misskey-js.yml | 7 +- .github/workflows/get-api-diff.yml | 9 +- .github/workflows/lint.yml | 16 +- .github/workflows/locale.yml | 8 +- .github/workflows/on-release-created.yml | 8 +- .github/workflows/storybook.yml | 8 +- .github/workflows/test-backend.yml | 14 +- .github/workflows/test-federation.yml | 8 +- .github/workflows/test-frontend.yml | 14 +- .github/workflows/test-misskey-js.yml | 7 +- .github/workflows/test-production.yml | 6 +- .github/workflows/validate-api-json.yml | 9 +- .npmrc | 1 + CHANGELOG.md | 4 + Dockerfile | 15 +- package.json | 8 +- .../backend/src/server/ActivityPubServerService.ts | 2 +- packages/backend/src/server/FileServerService.ts | 2 +- packages/backend/test-federation/compose.tpl.yml | 3 +- packages/backend/test-federation/compose.yml | 9 +- pnpm-lock.yaml | 367 ++++++++------------- pnpm-workspace.yaml | 37 ++- 24 files changed, 222 insertions(+), 346 deletions(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8dd9d1c704..c506c36f6b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,8 +7,8 @@ "ghcr.io/devcontainers/features/node:1": { "version": "22.11.0" }, - "ghcr.io/devcontainers-extra/features/corepack:1": { - "version": "0.31.0" + "ghcr.io/devcontainers-extra/features/pnpm:2": { + "version": "10.6.1" } }, "forwardPorts": [3000], diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index e02a533c15..216292b082 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -7,8 +7,6 @@ sudo apt-get update sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb git config --global --add safe.directory /workspace git submodule update --init -corepack install -corepack enable pnpm config set store-dir /home/node/.local/share/pnpm/store pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index fdd128be33..1c4bee2095 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -9,10 +9,6 @@ on: paths: - packages/misskey-js/** - .github/workflows/api-misskey-js.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: report: @@ -22,7 +18,8 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.2 - - run: corepack enable + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Setup Node.js uses: actions/setup-node@v4.2.0 diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 2da9647460..3244a39156 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -9,10 +9,6 @@ on: paths: - packages/backend/** - .github/workflows/get-api-diff.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: get-from-misskey: runs-on: ubuntu-latest @@ -34,14 +30,13 @@ jobs: with: ref: ${{ matrix.ref }} submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b1d52e8b3b..361bd697e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,10 +28,6 @@ on: - packages/misskey-reversi/** - packages/shared/eslint.config.js - .github/workflows/lint.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: pnpm_install: runs-on: ubuntu-latest @@ -40,12 +36,12 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile lint: @@ -71,12 +67,12 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Restore eslint cache uses: actions/cache@v4.2.2 @@ -101,12 +97,12 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - run: pnpm --filter misskey-js run build if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }} diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index 2daeaa3bd7..4c0de376d2 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -9,10 +9,6 @@ on: paths: - locales/** - .github/workflows/locale.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: locale_verify: runs-on: ubuntu-latest @@ -22,11 +18,11 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - run: cd locales && node verify.js diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 8e4ad4368b..aa32f2cb3b 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -6,9 +6,6 @@ on: workflow_dispatch: -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: publish-misskey-js: name: Publish misskey-js @@ -26,8 +23,8 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: @@ -36,7 +33,6 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Publish package run: | - corepack enable pnpm i --frozen-lockfile pnpm build pnpm --filter misskey-js publish --access public --no-git-checks --provenance diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 9e5a79faac..9fdbeab913 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -13,9 +13,6 @@ on: # This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master. - master -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: build: # chromatic is not likely to be available for fork repositories, so we disable for fork repositories. @@ -43,14 +40,13 @@ jobs: run: | echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js 20.x uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 2b8092cf45..69652621ca 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -18,10 +18,6 @@ on: - packages/misskey-js/** - .github/workflows/test-backend.yml - .github/misskey/test.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: unit: name: Unit tests (backend) @@ -48,8 +44,8 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Install FFmpeg run: | for i in {1..3}; do @@ -70,7 +66,6 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml @@ -111,14 +106,13 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index 0b71325de3..93588b54b9 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -15,9 +15,6 @@ on: - packages/misskey-js/** - .github/workflows/test-federation.yml -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: test: name: Federation test @@ -29,8 +26,8 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Install FFmpeg run: | for i in {1..3}; do @@ -53,7 +50,6 @@ jobs: cache: 'pnpm' - name: Build Misskey run: | - corepack enable && corepack prepare pnpm i --frozen-lockfile pnpm build - name: Setup diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index e489ebf07c..14a754c190 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -22,10 +22,6 @@ on: - packages/backend/** - .github/workflows/test-frontend.yml - .github/misskey/test.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: vitest: name: Unit tests (frontend) @@ -39,14 +35,13 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml @@ -95,14 +90,13 @@ jobs: # if: ${{ matrix.browser == 'firefox' }} #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Copy Configure run: cp .github/misskey/test.yml .config diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index 05f757acc1..29b6c6172b 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -14,10 +14,6 @@ on: paths: - packages/misskey-js/** - .github/workflows/test-misskey-js.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: test: name: Unit tests (misskey.js) @@ -33,7 +29,8 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.2 - - run: corepack enable + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 56e42213ff..205eae2399 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -9,7 +9,6 @@ on: env: NODE_ENV: production - COREPACK_DEFAULT_TO_LATEST: 0 jobs: production: @@ -24,14 +23,13 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index a8b2402988..f84efa4821 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -12,10 +12,6 @@ on: paths: - packages/backend/** - .github/workflows/validate-api-json.yml - -env: - COREPACK_DEFAULT_TO_LATEST: 0 - jobs: validate-api-json: runs-on: ubuntu-latest @@ -28,8 +24,8 @@ jobs: - uses: actions/checkout@v4.2.2 with: submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.2.0 with: @@ -37,7 +33,6 @@ jobs: cache: 'pnpm' - name: Install Redocly CLI run: npm i -g @redocly/cli - - run: corepack enable - run: pnpm i --frozen-lockfile - name: Check pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml diff --git a/.npmrc b/.npmrc index c42da845b4..16216a4a21 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ engine-strict = true +save-exact = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b00d15dce..8e2877c175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 2025.3.1 +### General +- pnpmをv10に更新 +- Corepackを削除 + ### Client - Feat: 設定の検索を追加(実験的) - Enhance: 設定項目の再配置 diff --git a/Dockerfile b/Dockerfile index 3bc2044396..0aaf402ac0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,6 @@ ARG NODE_VERSION=22.11.0-bookworm FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder -ENV COREPACK_DEFAULT_TO_LATEST=0 - RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ @@ -16,8 +14,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && apt-get install -yqq --no-install-recommends \ build-essential -RUN corepack enable - WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] @@ -33,6 +29,8 @@ COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bu ARG NODE_ENV=production +RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g + RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output @@ -46,14 +44,10 @@ RUN rm -rf .git/ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder -ENV COREPACK_DEFAULT_TO_LATEST=0 - RUN apt-get update \ && apt-get install -yqq --no-install-recommends \ build-essential -RUN corepack enable - WORKDIR /misskey COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] @@ -65,6 +59,8 @@ COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bu ARG NODE_ENV=production +RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g + RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output @@ -72,13 +68,11 @@ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner ARG UID="991" ARG GID="991" -ENV COREPACK_DEFAULT_TO_LATEST=0 RUN apt-get update \ && apt-get install -y --no-install-recommends \ ffmpeg tini curl libjemalloc-dev libjemalloc2 \ && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ - && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ @@ -91,7 +85,6 @@ WORKDIR /misskey # add package.json to add pnpm COPY --chown=misskey:misskey ./package.json ./package.json -RUN corepack install COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules diff --git a/package.json b/package.json index baa25259a0..f1db9fb426 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@9.15.4", + "packageManager": "pnpm@10.6.1", "workspaces": [ "packages/frontend-shared", "packages/frontend", @@ -73,9 +73,15 @@ "eslint": "9.20.1", "globals": "15.15.0", "ncp": "2.0.0", + "pnpm": "10.6.1", "start-server-and-test": "2.0.10" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" + }, + "pnpm": { + "overrides": { + "@aiscript-dev/aiscript-languageserver": "-" + } } } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 8c4b13a40a..20e985aaf2 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -751,7 +751,7 @@ export class ActivityPubServerService { }); // follow - fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => { + fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => { // This may be used before the follow is completed, so we do not // check if the following exists and only check if the follow request exists. diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index bf0a011699..772c37094c 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -497,7 +497,7 @@ export class FileServerService { @bindThis private async downloadAndDetectTypeFromUrl(url: string): Promise< - { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } + { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } > { const [path, cleanup] = await createTemp(); try { diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml index 8b270e58f7..a7e907c3ee 100644 --- a/packages/backend/test-federation/compose.tpl.yml +++ b/packages/backend/test-federation/compose.tpl.yml @@ -17,7 +17,6 @@ services: - ./.config/docker.env environment: - NODE_ENV=production - - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../../../built @@ -82,7 +81,7 @@ services: working_dir: /misskey command: > bash -c " - corepack enable && corepack prepare + npm install -g pnpm pnpm -F backend migrate pnpm -F backend start " diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml index ed39109aab..4df4ced365 100644 --- a/packages/backend/test-federation/compose.yml +++ b/packages/backend/test-federation/compose.yml @@ -9,7 +9,7 @@ services: service: misskey command: > bash -c " - corepack enable && corepack prepare + npm install -g pnpm pnpm -F backend i pnpm -F misskey-js i pnpm -F misskey-reversi i @@ -29,7 +29,6 @@ services: environment: - NODE_ENV=development - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt - - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../package.json @@ -78,7 +77,7 @@ services: working_dir: /misskey entrypoint: > bash -c ' - corepack enable && corepack prepare + npm install -g pnpm pnpm -F misskey-js i --frozen-lockfile pnpm -F backend i --frozen-lockfile exec "$0" "$@" @@ -90,8 +89,6 @@ services: depends_on: redis.test: condition: service_healthy - environment: - - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../package.json @@ -120,7 +117,7 @@ services: working_dir: /misskey command: > bash -c " - corepack enable && corepack prepare + npm install -g pnpm pnpm -F backend i --frozen-lockfile pnpm exec tsc -p ./packages/backend/test-federation node ./packages/backend/test-federation/built/daemon.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fa22d198c..4f57ea7fb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: chokidar: 3.6.0 lodash: 4.17.21 + '@aiscript-dev/aiscript-languageserver': '-' importers: @@ -45,10 +46,6 @@ importers: typescript: specifier: 5.7.3 version: 5.7.3 - optionalDependencies: - '@tensorflow/tfjs-core': - specifier: 4.22.0 - version: 4.22.0(encoding@0.1.13) devDependencies: '@misskey-dev/eslint-plugin': specifier: 2.1.0 @@ -77,9 +74,16 @@ importers: ncp: specifier: 2.0.0 version: 2.0.0 + pnpm: + specifier: 10.6.1 + version: 10.6.1 start-server-and-test: specifier: 2.0.10 version: 2.0.10 + optionalDependencies: + '@tensorflow/tfjs-core': + specifier: 4.22.0 + version: 4.22.0(encoding@0.1.13) packages/backend: dependencies: @@ -437,94 +441,6 @@ importers: xev: specifier: 3.0.2 version: 3.0.2 - optionalDependencies: - '@swc/core-android-arm64': - specifier: 1.3.11 - version: 1.3.11 - '@swc/core-darwin-arm64': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-darwin-x64': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-freebsd-x64': - specifier: 1.3.11 - version: 1.3.11 - '@swc/core-linux-arm-gnueabihf': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-linux-arm64-gnu': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-linux-arm64-musl': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-linux-x64-gnu': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-linux-x64-musl': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-win32-arm64-msvc': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-win32-ia32-msvc': - specifier: 1.10.16 - version: 1.10.16 - '@swc/core-win32-x64-msvc': - specifier: 1.10.16 - version: 1.10.16 - '@tensorflow/tfjs': - specifier: 4.22.0 - version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) - '@tensorflow/tfjs-node': - specifier: 4.22.0 - version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) - bufferutil: - specifier: 4.0.9 - version: 4.0.9 - slacc-android-arm-eabi: - specifier: 0.0.10 - version: 0.0.10 - slacc-android-arm64: - specifier: 0.0.10 - version: 0.0.10 - slacc-darwin-arm64: - specifier: 0.0.10 - version: 0.0.10 - slacc-darwin-universal: - specifier: 0.0.10 - version: 0.0.10 - slacc-darwin-x64: - specifier: 0.0.10 - version: 0.0.10 - slacc-freebsd-x64: - specifier: 0.0.10 - version: 0.0.10 - slacc-linux-arm-gnueabihf: - specifier: 0.0.10 - version: 0.0.10 - slacc-linux-arm64-gnu: - specifier: 0.0.10 - version: 0.0.10 - slacc-linux-arm64-musl: - specifier: 0.0.10 - version: 0.0.10 - slacc-linux-x64-gnu: - specifier: 0.0.10 - version: 0.0.10 - slacc-linux-x64-musl: - specifier: 0.0.10 - version: 0.0.10 - slacc-win32-arm64-msvc: - specifier: 0.0.10 - version: 0.0.10 - slacc-win32-x64-msvc: - specifier: 0.0.10 - version: 0.0.10 - utf-8-validate: - specifier: 6.0.5 - version: 6.0.5 devDependencies: '@jest/globals': specifier: 29.7.0 @@ -682,6 +598,94 @@ importers: simple-oauth2: specifier: 5.1.0 version: 5.1.0 + optionalDependencies: + '@swc/core-android-arm64': + specifier: 1.3.11 + version: 1.3.11 + '@swc/core-darwin-arm64': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-darwin-x64': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-freebsd-x64': + specifier: 1.3.11 + version: 1.3.11 + '@swc/core-linux-arm-gnueabihf': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-linux-arm64-gnu': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-linux-arm64-musl': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-linux-x64-gnu': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-linux-x64-musl': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-win32-arm64-msvc': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-win32-ia32-msvc': + specifier: 1.10.16 + version: 1.10.16 + '@swc/core-win32-x64-msvc': + specifier: 1.10.16 + version: 1.10.16 + '@tensorflow/tfjs': + specifier: 4.22.0 + version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) + '@tensorflow/tfjs-node': + specifier: 4.22.0 + version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) + bufferutil: + specifier: 4.0.9 + version: 4.0.9 + slacc-android-arm-eabi: + specifier: 0.0.10 + version: 0.0.10 + slacc-android-arm64: + specifier: 0.0.10 + version: 0.0.10 + slacc-darwin-arm64: + specifier: 0.0.10 + version: 0.0.10 + slacc-darwin-universal: + specifier: 0.0.10 + version: 0.0.10 + slacc-darwin-x64: + specifier: 0.0.10 + version: 0.0.10 + slacc-freebsd-x64: + specifier: 0.0.10 + version: 0.0.10 + slacc-linux-arm-gnueabihf: + specifier: 0.0.10 + version: 0.0.10 + slacc-linux-arm64-gnu: + specifier: 0.0.10 + version: 0.0.10 + slacc-linux-arm64-musl: + specifier: 0.0.10 + version: 0.0.10 + slacc-linux-x64-gnu: + specifier: 0.0.10 + version: 0.0.10 + slacc-linux-x64-musl: + specifier: 0.0.10 + version: 0.0.10 + slacc-win32-arm64-msvc: + specifier: 0.0.10 + version: 0.0.10 + slacc-win32-x64-msvc: + specifier: 0.0.10 + version: 0.0.10 + utf-8-validate: + specifier: 6.0.5 + version: 6.0.5 packages/frontend: dependencies: @@ -1448,11 +1452,6 @@ packages: '@adobe/css-tools@4.4.0': resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} - '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz': - resolution: {tarball: https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz} - version: 0.1.6 - hasBin: true - '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -1648,10 +1647,6 @@ packages: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.23.5': - resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.24.7': resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} engines: {node: '>=6.9.0'} @@ -1692,10 +1687,6 @@ packages: resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.22.15': - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.24.7': resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} @@ -1716,10 +1707,6 @@ packages: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} - '@babel/helper-simple-access@7.22.5': - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} - engines: {node: '>=6.9.0'} - '@babel/helper-simple-access@7.24.7': resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} @@ -1740,10 +1727,6 @@ packages: resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.23.5': - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.24.7': resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} engines: {node: '>=6.9.0'} @@ -4680,11 +4663,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.14.1: resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} @@ -5073,11 +5051,6 @@ packages: browser-assert@1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.24.4: resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -5154,9 +5127,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -5933,9 +5903,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.4.686: - resolution: {integrity: sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==} - electron-to-chromium@1.5.83: resolution: {integrity: sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==} @@ -8206,9 +8173,6 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -8650,6 +8614,11 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + pnpm@10.6.1: + resolution: {integrity: sha512-QO4Jr0B/qfu1+/uOHLQPu3TArww+EOkiTXtTx2WFKGFbLJJFDnTPrZHjotyv485AUNgL2nHXV3VtLOK2YhPpow==} + engines: {node: '>=18.12'} + hasBin: true + polished@4.2.2: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'} @@ -10356,12 +10325,6 @@ packages: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} - update-browserslist-db@1.0.13: - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -10531,16 +10494,9 @@ packages: vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - vscode-languageserver-textdocument@1.0.11: - resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} - vscode-languageserver-types@3.17.5: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} @@ -10847,14 +10803,6 @@ snapshots: '@adobe/css-tools@4.4.0': {} - '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz': - dependencies: - seedrandom: 3.0.5 - stringz: 2.1.0 - uuid: 9.0.1 - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.11 - '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -11380,8 +11328,6 @@ snapshots: '@babel/highlight': 7.24.7 picocolors: 1.1.1 - '@babel/compat-data@7.23.5': {} - '@babel/compat-data@7.24.7': {} '@babel/core@7.23.5': @@ -11440,9 +11386,9 @@ snapshots: '@babel/helper-compilation-targets@7.22.15': dependencies: - '@babel/compat-data': 7.23.5 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.24.4 lru-cache: 5.1.1 semver: 6.3.1 @@ -11467,10 +11413,6 @@ snapshots: dependencies: '@babel/types': 7.25.6 - '@babel/helper-module-imports@7.22.15': - dependencies: - '@babel/types': 7.25.6 - '@babel/helper-module-imports@7.24.7': dependencies: '@babel/traverse': 7.24.7 @@ -11482,10 +11424,12 @@ snapshots: dependencies: '@babel/core': 7.23.5 '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': dependencies: @@ -11500,10 +11444,6 @@ snapshots: '@babel/helper-plugin-utils@7.22.5': {} - '@babel/helper-simple-access@7.22.5': - dependencies: - '@babel/types': 7.25.6 - '@babel/helper-simple-access@7.24.7': dependencies: '@babel/traverse': 7.24.7 @@ -11521,8 +11461,6 @@ snapshots: '@babel/helper-validator-identifier@7.24.7': {} - '@babel/helper-validator-option@7.23.5': {} - '@babel/helper-validator-option@7.24.7': {} '@babel/helpers@7.23.5': @@ -15118,8 +15056,6 @@ snapshots: acorn@7.4.1: {} - acorn@8.14.0: {} - acorn@8.14.1: {} adm-zip@0.5.10: @@ -15157,7 +15093,6 @@ snapshots: aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7: dependencies: - '@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz vscode-languageclient: 9.0.1 ajv-draft-04@1.0.0(ajv@8.13.0): @@ -15596,13 +15531,6 @@ snapshots: browser-assert@1.2.1: {} - browserslist@4.23.0: - dependencies: - caniuse-lite: 1.0.30001591 - electron-to-chromium: 1.4.686 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) - browserslist@4.24.4: dependencies: caniuse-lite: 1.0.30001695 @@ -15708,11 +15636,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.2: - dependencies: - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -16414,7 +16337,7 @@ snapshots: deep-equal@2.2.0: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 es-get-iterator: 1.1.3 get-intrinsic: 1.2.4 is-arguments: 1.1.1 @@ -16430,7 +16353,7 @@ snapshots: side-channel: 1.0.6 which-boxed-primitive: 1.0.2 which-collection: 1.0.1 - which-typed-array: 1.1.11 + which-typed-array: 1.1.15 deep-extend@0.6.0: optional: true @@ -16447,7 +16370,7 @@ snapshots: dependencies: es-define-property: 1.0.0 es-errors: 1.3.0 - gopd: 1.0.1 + gopd: 1.2.0 define-lazy-prop@2.0.0: {} @@ -16594,8 +16517,6 @@ snapshots: dependencies: jake: 10.8.5 - electron-to-chromium@1.4.686: {} - electron-to-chromium@1.5.83: {} emittery@0.13.1: {} @@ -16741,7 +16662,7 @@ snapshots: es-get-iterator@1.1.3: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 get-intrinsic: 1.2.4 has-symbols: 1.0.3 is-arguments: 1.1.1 @@ -18051,9 +17972,9 @@ snapshots: is-array-buffer@3.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 get-intrinsic: 1.2.4 - is-typed-array: 1.1.10 + is-typed-array: 1.1.13 is-array-buffer@3.0.4: dependencies: @@ -18149,14 +18070,14 @@ snapshots: is-regex@1.1.4: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 has-tostringtag: 1.0.2 is-set@2.0.2: {} is-shared-array-buffer@1.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 is-shared-array-buffer@1.0.3: dependencies: @@ -18182,10 +18103,10 @@ snapshots: is-typed-array@1.1.10: dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-tostringtag: 1.0.2 is-typed-array@1.1.13: @@ -19670,8 +19591,6 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.14: {} - node-releases@2.0.19: {} nodemailer@6.10.0: {} @@ -19776,14 +19695,14 @@ snapshots: object-is@1.1.5: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 object-keys@1.1.1: {} object.assign@4.1.4: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -20114,6 +20033,8 @@ snapshots: pngjs@5.0.0: {} + pnpm@10.6.1: {} + polished@4.2.2: dependencies: '@babel/runtime': 7.23.4 @@ -20705,7 +20626,7 @@ snapshots: regexp.prototype.flags@1.5.0: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 functions-have-names: 1.2.3 @@ -20985,7 +20906,7 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 get-intrinsic: 1.2.4 - gopd: 1.0.1 + gopd: 1.2.0 has-property-descriptors: 1.0.2 set-function-name@2.0.2: @@ -21550,7 +21471,7 @@ snapshots: terser@5.39.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + acorn: 8.14.1 commander: 2.20.3 source-map-support: 0.5.21 @@ -21763,7 +21684,7 @@ snapshots: dependencies: call-bind: 1.0.7 get-intrinsic: 1.2.4 - is-typed-array: 1.1.10 + is-typed-array: 1.1.13 typed-array-buffer@1.0.2: dependencies: @@ -21776,30 +21697,30 @@ snapshots: call-bind: 1.0.7 for-each: 0.3.3 has-proto: 1.0.1 - is-typed-array: 1.1.10 + is-typed-array: 1.1.13 typed-array-byte-length@1.0.1: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-proto: 1.0.3 is-typed-array: 1.1.13 typed-array-byte-offset@1.0.0: dependencies: - available-typed-arrays: 1.0.5 + available-typed-arrays: 1.0.7 call-bind: 1.0.7 for-each: 0.3.3 has-proto: 1.0.1 - is-typed-array: 1.1.10 + is-typed-array: 1.1.13 typed-array-byte-offset@1.0.2: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-proto: 1.0.3 is-typed-array: 1.1.13 @@ -21807,13 +21728,13 @@ snapshots: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - is-typed-array: 1.1.10 + is-typed-array: 1.1.13 typed-array-length@1.0.6: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-proto: 1.0.3 is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 @@ -21941,12 +21862,6 @@ snapshots: untildify@4.0.0: {} - update-browserslist-db@1.0.13(browserslist@4.23.0): - dependencies: - browserslist: 4.23.0 - escalade: 3.1.1 - picocolors: 1.1.1 - update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -22120,14 +22035,8 @@ snapshots: vscode-jsonrpc: 8.2.0 vscode-languageserver-types: 3.17.5 - vscode-languageserver-textdocument@1.0.11: {} - vscode-languageserver-types@3.17.5: {} - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - vscode-uri@3.0.8: {} vue-component-meta@2.0.16(typescript@5.8.2): @@ -22293,10 +22202,10 @@ snapshots: which-typed-array@1.1.11: dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-tostringtag: 1.0.2 which-typed-array@1.1.15: @@ -22304,7 +22213,7 @@ snapshots: available-typed-arrays: 1.0.7 call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-tostringtag: 1.0.2 which@1.3.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d222614eda..68e4f0adc1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,29 @@ packages: - - 'packages/backend' - - 'packages/frontend-shared' - - 'packages/frontend' - - 'packages/frontend-embed' - - 'packages/sw' - - 'packages/misskey-js' - - 'packages/misskey-js/generator' - - 'packages/misskey-reversi' - - 'packages/misskey-bubble-game' + - packages/backend + - packages/frontend-shared + - packages/frontend + - packages/frontend-embed + - packages/sw + - packages/misskey-js + - packages/misskey-js/generator + - packages/misskey-reversi + - packages/misskey-bubble-game +onlyBuiltDependencies: + - '@nestjs/core' + - '@parcel/watcher' + - '@sentry/profiling-node' + - '@swc/core' + - '@tensorflow/tfjs-node' + - bufferutil + - canvas + - core-js + - cypress + - esbuild + - msgpackr-extract + - msw + - nice-napi + - re2 + - sharp + - utf-8-validate + - v-code-diff + - vue-demi -- cgit v1.2.3-freya From a35c2f214b1b1054229f31569f6df4090a7375a5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 21 Feb 2025 22:04:36 -0500 Subject: convert Authorized Fetch to a setting and add support for hybrid mode (essential metadata only) --- .config/ci.yml | 2 - .config/docker_example.yml | 2 - .config/example.yml | 2 - UPGRADE_NOTES.md | 9 + locales/index.d.ts | 52 +++++ .../migration/1740162088574-add_unsignedFetch.js | 35 ++++ packages/backend/src/config.ts | 1 + packages/backend/src/const.ts | 6 + packages/backend/src/core/CacheService.ts | 8 + .../backend/src/core/CreateSystemUserService.ts | 3 +- packages/backend/src/core/InstanceActorService.ts | 10 +- .../src/core/activitypub/ApRendererService.ts | 32 +++ .../backend/src/core/entities/MetaEntityService.ts | 1 + .../backend/src/core/entities/UserEntityService.ts | 1 + packages/backend/src/models/Meta.ts | 11 ++ packages/backend/src/models/User.ts | 14 +- packages/backend/src/models/json-schema/meta.ts | 7 + packages/backend/src/models/json-schema/user.ts | 7 + .../backend/src/server/ActivityPubServerService.ts | 220 ++++++++++++--------- .../backend/src/server/api/endpoints/admin/meta.ts | 12 ++ .../src/server/api/endpoints/admin/update-meta.ts | 10 + .../backend/src/server/api/endpoints/i/update.ts | 10 + packages/backend/test/unit/activitypub.ts | 84 +++++++- packages/frontend/src/pages/admin/index.vue | 7 + packages/frontend/src/pages/admin/security.vue | 25 +++ packages/frontend/src/pages/settings/privacy.vue | 23 +++ packages/misskey-js/src/autogen/types.ts | 11 ++ sharkey-locales/en-US.yml | 15 ++ 28 files changed, 517 insertions(+), 103 deletions(-) create mode 100644 packages/backend/migration/1740162088574-add_unsignedFetch.js (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/.config/ci.yml b/.config/ci.yml index def276ca58..2126f76337 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -243,8 +243,6 @@ signToActivityPubGet: true # When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. # This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". diff --git a/.config/docker_example.yml b/.config/docker_example.yml index f798fd8246..acbaec8023 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -326,8 +326,6 @@ signToActivityPubGet: true # When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. # This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". diff --git a/.config/example.yml b/.config/example.yml index d199544589..e18afd615b 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -369,8 +369,6 @@ signToActivityPubGet: true # When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. # This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index c941de6643..47ac649c31 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -1,5 +1,14 @@ # Upgrade Notes +## 2025.X.X + +### Authorized Fetch + +This version retires the configuration entry `checkActivityPubGetSignature`, which is now replaced with the new "Authorized Fetch" settings under Control Panel/Security. +The database migrations will automatically import the value of this configuration file, but it will never be read again after upgrading. +To avoid confusion and possible mis-configuration, please remove the entry **after** completing the upgrade. +Do not remove it before migration, or else the setting will reset to default (disabled)! + ## 2024.10.0 ### Hellspawns diff --git a/locales/index.d.ts b/locales/index.d.ts index 998d5da5d0..2d51b994a6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12231,6 +12231,58 @@ export interface Locale extends ILocale { */ "quoteUnavailable": string; }; + /** + * Authorized Fetch + */ + "authorizedFetchSection": string; + /** + * Allow unsigned ActivityPub requests: + */ + "authorizedFetchLabel": string; + /** + * This setting controls the behavior when a remote instance or user attempts to access your content without verifying their identity. If disabled, any remote user can access your profile and posts - even one who has been blocked or defederated. + */ + "authorizedFetchDescription": string; + "_authorizedFetchValue": { + /** + * Never + */ + "never": string; + /** + * Always + */ + "always": string; + /** + * Only for essential metadata + */ + "essential": string; + /** + * Use staff recommendation + */ + "staff": string; + }; + "_authorizedFetchValueDescription": { + /** + * Block all unsigned requests. Improves privacy and makes blocks more effective, but is not compatible with some very old or uncommon instance software. + */ + "never": string; + /** + * Allow all unsigned requests. Provides the greatest compatibility with other instances, but reduces privacy and weakens blocks. + */ + "always": string; + /** + * Allow some limited unsigned requests. Provides a hybrid between "Never" and "Always" by exposing only the minimum profile metadata that is required for federation with older software. + */ + "essential": string; + /** + * Use the default value of "{value}" recommended by the instance staff. + */ + "staff": ParameterizedString<"value">; + }; + /** + * The configuration property 'checkActivityPubGetSignature' has been deprecated and replaced with the new Authorized Fetch setting. Please remove it from your configuration file. + */ + "authorizedFetchLegacyWarning": string; } declare const locales: { [lang: string]: Locale; diff --git a/packages/backend/migration/1740162088574-add_unsignedFetch.js b/packages/backend/migration/1740162088574-add_unsignedFetch.js new file mode 100644 index 0000000000..855a3796aa --- /dev/null +++ b/packages/backend/migration/1740162088574-add_unsignedFetch.js @@ -0,0 +1,35 @@ +import { loadConfig } from '../built/config.js'; + +export class AddUnsignedFetch1740162088574 { + name = 'AddUnsignedFetch1740162088574' + + async up(queryRunner) { + // meta.allowUnsignedFetch + await queryRunner.query(`CREATE TYPE "public"."meta_allowunsignedfetch_enum" AS ENUM('never', 'always', 'essential')`); + await queryRunner.query(`ALTER TABLE "meta" ADD "allowUnsignedFetch" "public"."meta_allowunsignedfetch_enum" NOT NULL DEFAULT 'always'`); + + // user.allowUnsignedFetch + await queryRunner.query(`CREATE TYPE "public"."user_allowunsignedfetch_enum" AS ENUM('never', 'always', 'essential', 'staff')`); + await queryRunner.query(`ALTER TABLE "user" ADD "allowUnsignedFetch" "public"."user_allowunsignedfetch_enum" NOT NULL DEFAULT 'staff'`); + + // Special one-time migration: allow unauthorized fetch for instance actor + await queryRunner.query(`UPDATE "user" SET "allowUnsignedFetch" = 'always' WHERE "username" = 'instance.actor' AND "host" IS null`); + + // Special one-time migration: convert legacy config "" to meta setting "" + const config = await loadConfig(); + if (config.checkActivityPubGetSignature) { + // noinspection SqlWithoutWhere + await queryRunner.query(`UPDATE "meta" SET "allowUnsignedFetch" = 'never'`); + } + } + + async down(queryRunner) { + // user.allowUnsignedFetch + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "allowUnsignedFetch"`); + await queryRunner.query(`DROP TYPE "public"."user_allowunsignedfetch_enum"`); + + // meta.allowUnsignedFetch + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowUnsignedFetch"`); + await queryRunner.query(`DROP TYPE "public"."meta_allowunsignedfetch_enum"`); + } +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c571c227a1..61c7fcb6c7 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -200,6 +200,7 @@ export type Config = { customMOTD: string[] | undefined; signToActivityPubGet: boolean; attachLdSignatureForRelays: boolean; + /** @deprecated Use MiMeta.allowUnsignedFetch instead */ checkActivityPubGetSignature: boolean | undefined; logging?: { sql?: { diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index e2c492ff80..50ccecd571 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -70,3 +70,9 @@ https://github.com/sindresorhus/file-type/blob/main/supported.js https://github.com/sindresorhus/file-type/blob/main/core.js https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers */ + +export const instanceUnsignedFetchOptions = ['never', 'always', 'essential'] as const; +export type InstanceUnsignedFetchOption = (typeof instanceUnsignedFetchOptions)[number]; + +export const userUnsignedFetchOptions = ['never', 'always', 'essential', 'staff'] as const; +export type UserUnsignedFetchOption = (typeof userUnsignedFetchOptions)[number]; diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 6725ebe75b..e9900373b4 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { IsNull } from 'typeorm'; import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; @@ -179,6 +180,13 @@ export class CacheService implements OnApplicationShutdown { return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); } + @bindThis + public async findLocalUserById(userId: MiUser['id']): Promise { + return await this.localUserByIdCache.fetchMaybe(userId, async () => { + return await this.usersRepository.findOneBy({ id: userId, host: IsNull() }) as MiLocalUser | null ?? undefined; + }) ?? null; + } + @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 14d814b0e6..d198707a42 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -29,7 +29,7 @@ export class CreateSystemUserService { } @bindThis - public async createSystemUser(username: string): Promise { + public async createSystemUser(username: string, data?: Partial): Promise { const password = randomUUID(); // Generate hash of password @@ -63,6 +63,7 @@ export class CreateSystemUserService { isExplorable: false, approved: true, isBot: true, + ...(data ?? {}), }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); await transactionalEntityManager.insert(MiUserKeypair, { diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 22c47297a3..6c0e360588 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -49,7 +49,15 @@ export class InstanceActorService { this.cache.set(user); return user; } else { - const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as MiLocalUser; + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME, { + /* we always allow requests about our instance actor, because when + a remote instance needs to check our signature on a request we + sent, it will need to fetch information about the user that + signed it (which is our instance actor), and if we try to check + their signature on *that* request, we'll fetch *their* instance + actor... leading to an infinite recursion */ + allowUnsignedFetch: 'always', + }) as MiLocalUser; this.cache.set(created); return created; } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index cb9b74f6d7..c7f8b97a5a 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -571,6 +571,38 @@ export class ApRendererService { return person; } + @bindThis + public async renderPersonRedacted(user: MiLocalUser) { + const id = this.userEntityService.genLocalUserUri(user.id); + const isSystem = user.username.includes('.'); + + const keypair = await this.userKeypairService.getUserKeypair(user.id); + + return { + // Basic federation metadata + type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', + id, + inbox: `${id}/inbox`, + outbox: `${id}/outbox`, + sharedInbox: `${this.config.url}/inbox`, + endpoints: { sharedInbox: `${this.config.url}/inbox` }, + url: `${this.config.url}/@${user.username}`, + preferredUsername: user.username, + publicKey: this.renderKey(user, keypair, '#main-key'), + + // Privacy settings + _misskey_requireSigninToViewContents: user.requireSigninToViewContents, + _misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore, + _misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore, + manuallyApprovesFollowers: user.isLocked, + discoverable: user.isExplorable, + hideOnlineStatus: user.hideOnlineStatus, + noindex: user.noindex, + indexable: !user.noindex, + enableRss: user.enableRss, + }; + } + @bindThis public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion { return { diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index a7679d06aa..3f3a1bad33 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -181,6 +181,7 @@ export class MetaEntityService { serviceWorker: instance.enableServiceWorker, miauth: true, }, + allowUnsignedFetch: instance.allowUnsignedFetch, }; return packDetailed; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 96fef863a0..f5452baaef 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -725,6 +725,7 @@ export class UserEntityService implements OnModuleInit { policies: this.roleService.getUserPolicies(user.id), defaultCW: profile!.defaultCW, defaultCWPriority: profile!.defaultCWPriority, + allowUnsignedFetch: user.allowUnsignedFetch, } : {}), ...(opts.includeSecrets ? { diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 0f1f4069ff..f4bc2a8db7 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -4,6 +4,7 @@ */ import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { type InstanceUnsignedFetchOption, instanceUnsignedFetchOptions } from '@/const.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; @@ -749,4 +750,14 @@ export class MiMeta { default: '{}', }) public federationHosts: string[]; + + /** + * In combination with user.allowUnsignedFetch, controls enforcement of HTTP signatures for inbound ActivityPub fetches (GET requests). + * TODO warning if config value is present + */ + @Column('enum', { + enum: instanceUnsignedFetchOptions, + default: 'always', + }) + public allowUnsignedFetch: InstanceUnsignedFetchOption; } diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 5d87c7fa12..3bc2494577 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -4,6 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { type UserUnsignedFetchOption, userUnsignedFetchOptions } from '@/const.js'; import { id } from './util/id.js'; import { MiDriveFile } from './DriveFile.js'; @@ -125,7 +126,7 @@ export class MiUser { }) public backgroundId: MiDriveFile['id'] | null; - @OneToOne(type => MiDriveFile, { + @OneToOne(() => MiDriveFile, { onDelete: 'SET NULL', }) @JoinColumn() @@ -357,6 +358,15 @@ export class MiUser { }) public rejectQuotes: boolean; + /** + * In combination with meta.allowUnsignedFetch, controls enforcement of HTTP signatures for inbound ActivityPub fetches (GET requests). + */ + @Column('enum', { + enum: userUnsignedFetchOptions, + default: 'staff', + }) + public allowUnsignedFetch: UserUnsignedFetchOption; + constructor(data: Partial) { if (data == null) return; @@ -394,5 +404,5 @@ export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as con export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; -export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const; +export const listenbrainzSchema = { type: 'string', minLength: 1, maxLength: 128 } as const; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index bf68208c37..fd735c1edd 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { instanceUnsignedFetchOptions } from '@/const.js'; + export const packedMetaLiteSchema = { type: 'object', optional: false, nullable: false, @@ -397,6 +399,11 @@ export const packedMetaDetailedOnlySchema = { type: 'boolean', optional: false, nullable: false, }, + allowUnsignedFetch: { + type: 'string', + enum: instanceUnsignedFetchOptions, + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 0f1601f138..83a456fc57 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { userUnsignedFetchOptions } from '@/const.js'; + export const notificationRecieveConfig = { type: 'object', oneOf: [ @@ -769,6 +771,11 @@ export const packedMeDetailedOnlySchema = { enum: ['default', 'parent', 'defaultParent', 'parentDefault'], nullable: false, optional: false, }, + allowUnsignedFetch: { + type: 'string', + enum: userUnsignedFetchOptions, + nullable: false, optional: false, + }, }, } as const; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 765d54bc71..ba112ca59a 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -14,7 +14,7 @@ import accepts from 'accepts'; import vary from 'vary'; import secureJson from 'secure-json-parse'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -22,7 +22,6 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { QueueService } from '@/core/QueueService.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; import type { MiFollowing } from '@/models/Following.js'; import { countIf } from '@/misc/prelude/array.js'; @@ -33,9 +32,10 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { IActivity } from '@/core/activitypub/type.js'; +import { IActivity, IAnnounce, ICreate } from '@/core/activitypub/type.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import * as Acct from '@/misc/acct.js'; +import { CacheService } from '@/core/CacheService.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; @@ -51,6 +51,9 @@ export class ActivityPubServerService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -77,13 +80,13 @@ export class ActivityPubServerService { private utilityService: UtilityService, private userEntityService: UserEntityService, - private instanceActorService: InstanceActorService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, private queueService: QueueService, private userKeypairService: UserKeypairService, private queryService: QueryService, private loggerService: LoggerService, + private readonly cacheService: CacheService, ) { //this.createServer = this.createServer.bind(this); this.logger = this.loggerService.getLogger('apserv', 'pink'); @@ -106,7 +109,7 @@ export class ActivityPubServerService { * @param author Author of the note */ @bindThis - private async packActivity(note: MiNote, author: MiUser): Promise { + private async packActivity(note: MiNote, author: MiUser): Promise { if (isRenote(note) && !isQuote(note)) { const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note); @@ -115,10 +118,55 @@ export class ActivityPubServerService { return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note); } - @bindThis - private async shouldRefuseGetRequest(request: FastifyRequest, reply: FastifyReply, userId: string | undefined = undefined): Promise { - if (!this.config.checkActivityPubGetSignature) return false; + /** + * Checks Authorized Fetch. + * Returns an object with two properties: + * * reject - true if the request should be ignored by the caller, false if it should be processed. + * * redact - true if the caller should redact response data, false if it should return full data. + * When "reject" is true, the HTTP status code will be automatically set to 401 unauthorized. + */ + private async checkAuthorizedFetch( + request: FastifyRequest, + reply: FastifyReply, + userId?: string, + essential?: boolean, + ): Promise<{ reject: boolean, redact: boolean }> { + // Federation disabled => reject + if (this.meta.federation === 'none') { + reply.code(401); + return { reject: true, redact: true }; + } + + // Auth fetch disabled => accept + const allowUnsignedFetch = await this.getUnsignedFetchAllowance(userId); + if (allowUnsignedFetch === 'always') { + return { reject: false, redact: false }; + } + + // Valid signature => accept + const error = await this.checkSignature(request); + if (!error) { + return { reject: false, redact: false }; + } + + // Unsigned, but essential => accept redacted + if (allowUnsignedFetch === 'essential' && essential) { + return { reject: false, redact: true }; + } + // Unsigned, not essential => reject + this.authlogger.warn(error); + reply.code(401); + return { reject: true, redact: true }; + } + + /** + * Verifies HTTP Signatures for a request. + * Returns null of success (valid signature). + * Returns a string error on validation failure. + */ + @bindThis + private async checkSignature(request: FastifyRequest): Promise { /* this code is inspired from the `inbox` function below, and `queue/processors/InboxProcessorService` @@ -129,59 +177,33 @@ export class ActivityPubServerService { this is also inspired by FireFish's `checkFetch` */ - /* tell any caching proxy that they should not cache these - responses: we wouldn't want the proxy to return a 403 to - someone presenting a valid signature, or return a cached - response body to someone we've blocked! - */ - reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); - - /* we always allow requests about our instance actor, because when - a remote instance needs to check our signature on a request we - sent, it will need to fetch information about the user that - signed it (which is our instance actor), and if we try to check - their signature on *that* request, we'll fetch *their* instance - actor... leading to an infinite recursion */ - if (userId) { - const instanceActor = await this.instanceActorService.getInstanceActor(); - - if (userId === instanceActor.id || userId === instanceActor.username) { - this.authlogger.debug(`${request.id} ${request.url} request to instance.actor, letting through`); - return false; - } - } - let signature; try { - signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' }); + signature = httpSignature.parseRequest(request.raw, { + headers: ['(request-target)', 'host', 'date'], + authorizationHeaderName: 'signature', + }); } catch (e) { // not signed, or malformed signature: refuse - this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`); - reply.code(401); - return true; + return `${request.id} ${request.url} not signed, or malformed signature: refuse`; } const keyId = new URL(signature.keyId); const keyHost = this.utilityService.toPuny(keyId.hostname); - const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) apparently from ${keyHost}:`; + const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) claims to be from ${keyHost}:`; - if (signature.params.headers.indexOf('host') === -1 - || request.headers.host !== this.config.host) { + if (signature.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) { // no destination host, or not us: refuse - this.authlogger.warn(`${logPrefix} no destination host, or not us: refuse`); - reply.code(401); - return true; + return `${logPrefix} no destination host, or not us: refuse`; } if (!this.utilityService.isFederationAllowedHost(keyHost)) { /* blocked instance: refuse (we don't care if the signature is good, if they even pretend to be from a blocked instance, they're out) */ - this.authlogger.warn(`${logPrefix} instance is blocked: refuse`); - reply.code(401); - return true; + return `${logPrefix} instance is blocked: refuse`; } // do we know the signer already? @@ -200,14 +222,18 @@ export class ActivityPubServerService { if (authUser?.key == null) { // we can't figure out who the signer is, or we can't get their key: refuse - this.authlogger.warn(`${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`); - reply.code(401); - return true; + return `${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`; + } + + if (authUser.user.isSuspended) { + // Signer is suspended locally + return `${logPrefix} signer is suspended: refuse`; } let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); // maybe they changed their key? refetch it + // TODO rate-limit this using lastFetchedAt if (!httpSignatureValidated) { authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); if (authUser.key != null) { @@ -217,13 +243,11 @@ export class ActivityPubServerService { if (!httpSignatureValidated) { // bad signature: refuse - this.authlogger.info(`${logPrefix} failed to validate signature: refuse`); - reply.code(401); - return true; + return `${logPrefix} failed to validate signature: refuse`; } // all good, don't refuse - return false; + return null; } @bindThis @@ -299,7 +323,8 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { - if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user); + if (reject) return; const userId = request.params.user; @@ -326,11 +351,9 @@ export class ActivityPubServerService { if (profile.followersVisibility === 'private') { reply.code(403); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } else if (profile.followersVisibility === 'followers') { reply.code(403); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } //#endregion @@ -382,7 +405,6 @@ export class ActivityPubServerService { user.followersCount, `${partOf}?page=true`, ); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -393,7 +415,8 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { - if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user); + if (reject) return; const userId = request.params.user; @@ -420,11 +443,9 @@ export class ActivityPubServerService { if (profile.followingVisibility === 'private') { reply.code(403); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } else if (profile.followingVisibility === 'followers') { reply.code(403); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } //#endregion @@ -476,7 +497,6 @@ export class ActivityPubServerService { user.followingCount, `${partOf}?page=true`, ); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -484,7 +504,8 @@ export class ActivityPubServerService { @bindThis private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) { - if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user); + if (reject) return; const userId = request.params.user; @@ -517,7 +538,6 @@ export class ActivityPubServerService { renderedNotes, ); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -530,7 +550,8 @@ export class ActivityPubServerService { }>, reply: FastifyReply, ) { - if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user); + if (reject) return; const userId = request.params.user; @@ -608,14 +629,13 @@ export class ActivityPubServerService { `${partOf}?page=true`, `${partOf}?page=true&since_id=000000000000000000000000`, ); - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } } @bindThis - private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) { + private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null, redact = false) { if (user == null) { reply.code(404); return; @@ -631,10 +651,12 @@ export class ActivityPubServerService { return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); - this.setResponseType(request, reply); - return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); + + const person = redact + ? await this.apRendererService.renderPersonRedacted(user as MiLocalUser) + : await this.apRendererService.renderPerson(user as MiLocalUser); + return this.apRendererService.addContext(person); } @bindThis @@ -687,6 +709,13 @@ export class ActivityPubServerService { reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); reply.header('Access-Control-Allow-Origin', '*'); reply.header('Access-Control-Expose-Headers', 'Vary'); + + /* tell any caching proxy that they should not cache these + responses: we wouldn't want the proxy to return a 403 to + someone presenting a valid signature, or return a cached + response body to someone we've blocked! + */ + reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); done(); }); @@ -697,8 +726,6 @@ export class ActivityPubServerService { // note fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply)) return; - vary(reply.raw, 'Accept'); const note = await this.notesRepository.findOneBy({ @@ -707,6 +734,9 @@ export class ActivityPubServerService { localOnly: false, }); + const { reject } = await this.checkAuthorizedFetch(request, reply, note?.userId); + if (reject) return; + if (note == null) { reply.code(404); return; @@ -722,7 +752,6 @@ export class ActivityPubServerService { return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); const author = await this.usersRepository.findOneByOrFail({ id: note.userId }); @@ -731,8 +760,6 @@ export class ActivityPubServerService { // note activity fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply)) return; - vary(reply.raw, 'Accept'); const note = await this.notesRepository.findOneBy({ @@ -742,12 +769,14 @@ export class ActivityPubServerService { localOnly: false, }); + const { reject } = await this.checkAuthorizedFetch(request, reply, note?.userId); + if (reject) return; + if (note == null) { reply.code(404); return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); const author = await this.usersRepository.findOneByOrFail({ id: note.userId }); @@ -777,7 +806,8 @@ export class ActivityPubServerService { // publickey fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.user, true); + if (reject) return; const userId = request.params.user; @@ -794,7 +824,6 @@ export class ActivityPubServerService { const keypair = await this.userKeypairService.getUserKeypair(user.id); if (this.userEntityService.isLocalUser(user)) { - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); } else { @@ -804,7 +833,8 @@ export class ActivityPubServerService { }); fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const { reject, redact } = await this.checkAuthorizedFetch(request, reply, request.params.user, true); + if (reject) return; vary(reply.raw, 'Accept'); @@ -815,12 +845,10 @@ export class ActivityPubServerService { isSuspended: false, }); - return await this.userInfo(request, reply, user); + return await this.userInfo(request, reply, user, redact); }); fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply, request.params.acct)) return; - vary(reply.raw, 'Accept'); const acct = Acct.parse(request.params.acct); @@ -831,13 +859,17 @@ export class ActivityPubServerService { isSuspended: false, }); - return await this.userInfo(request, reply, user); + const { reject, redact } = await this.checkAuthorizedFetch(request, reply, user?.id, true); + if (reject) return; + + return await this.userInfo(request, reply, user, redact); }); //#endregion // emoji fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply); + if (reject) return; const emoji = await this.emojisRepository.findOneBy({ host: IsNull(), @@ -849,17 +881,17 @@ export class ActivityPubServerService { return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji))); }); // like fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply)) return; - const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like }); + const { reject } = await this.checkAuthorizedFetch(request, reply, reaction?.userId); + if (reject) return; + if (reaction == null) { reply.code(404); return; @@ -872,14 +904,14 @@ export class ActivityPubServerService { return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note))); }); // follow fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply)) return; + const { reject } = await this.checkAuthorizedFetch(request, reply, request.params.follower); + if (reject) return; // This may be used before the follow is completed, so we do not // check if the following exists. @@ -900,15 +932,12 @@ export class ActivityPubServerService { return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); // follow fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => { - if (await this.shouldRefuseGetRequest(request, reply)) return; - // This may be used before the follow is completed, so we do not // check if the following exists and only check if the follow request exists. @@ -916,6 +945,9 @@ export class ActivityPubServerService { id: request.params.followRequestId, }); + const { reject } = await this.checkAuthorizedFetch(request, reply, followRequest?.followerId); + if (reject) return; + if (followRequest == null) { reply.code(404); return; @@ -937,11 +969,21 @@ export class ActivityPubServerService { return; } - if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); done(); } + + private async getUnsignedFetchAllowance(userId: string | undefined) { + const user = userId ? await this.cacheService.findLocalUserById(userId) : null; + + // User system value if there is no user, or if user has deferred the choice. + if (!user?.allowUnsignedFetch || user.allowUnsignedFetch === 'staff') { + return this.meta.allowUnsignedFetch; + } + + return user.allowUnsignedFetch; + } } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index d581c07e8c..d3f24e07bb 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -9,6 +9,7 @@ import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import { instanceUnsignedFetchOptions } from '@/const.js'; export const meta = { tags: ['meta'], @@ -589,6 +590,15 @@ export const meta = { optional: false, nullable: false, }, }, + hasLegacyAuthFetchSetting: { + type: 'boolean', + optional: false, nullable: false, + }, + allowUnsignedFetch: { + type: 'string', + enum: instanceUnsignedFetchOptions, + optional: false, nullable: false, + }, }, }, } as const; @@ -745,6 +755,8 @@ export default class extends Endpoint { // eslint- trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns, federation: instance.federation, federationHosts: instance.federationHosts, + hasLegacyAuthFetchSetting: config.checkActivityPubGetSignature != null, + allowUnsignedFetch: instance.allowUnsignedFetch, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index f6ce86790a..33d4bbd00f 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -8,6 +8,7 @@ import type { MiMeta } from '@/models/Meta.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; +import { instanceUnsignedFetchOptions } from '@/const.js'; export const meta = { tags: ['admin'], @@ -205,6 +206,11 @@ export const paramDef = { type: 'string', }, }, + allowUnsignedFetch: { + type: 'string', + enum: instanceUnsignedFetchOptions, + nullable: false, + }, }, required: [], } as const; @@ -753,6 +759,10 @@ export default class extends Endpoint { // eslint- set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); } + if (ps.allowUnsignedFetch !== undefined) { + set.allowUnsignedFetch = ps.allowUnsignedFetch; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index f74452e2af..f1d201d081 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -33,6 +33,7 @@ import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; +import { userUnsignedFetchOptions } from '@/const.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -255,6 +256,11 @@ export const paramDef = { enum: ['default', 'parent', 'defaultParent', 'parentDefault'], nullable: false, }, + allowUnsignedFetch: { + type: 'string', + enum: userUnsignedFetchOptions, + nullable: false, + }, }, } as const; @@ -519,6 +525,10 @@ export default class extends Endpoint { // eslint- profileUpdates.defaultCWPriority = ps.defaultCWPriority; } + if (ps.allowUnsignedFetch !== undefined) { + updates.allowUnsignedFetch = ps.allowUnsignedFetch; + } + //#region emojis/tags let emojis = [] as string[]; diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 553467499b..90649bfd8b 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -22,13 +22,16 @@ import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; -import { MiMeta, MiNote, MiUser, UserProfilesRepository, UserPublickeysRepository } from '@/models/_.js'; +import { MiMeta, MiNote, MiUser, MiUserKeypair, UserProfilesRepository, UserPublickeysRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; -import type { MiRemoteUser } from '@/models/User.js'; +import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { genAidx } from '@/misc/id/aidx.js'; import { MockResolver } from '../misc/mock-resolver.js'; +import { UserKeypairService } from '@/core/UserKeypairService.js'; +import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; +import { generateKeyPair } from 'crypto'; const host = 'https://host1.test'; @@ -97,6 +100,7 @@ describe('ActivityPub', () => { let resolver: MockResolver; let idService: IdService; let userPublickeysRepository: UserPublickeysRepository; + let userKeypairService: UserKeypairService; const metaInitial = { cacheRemoteFiles: true, @@ -146,6 +150,7 @@ describe('ActivityPub', () => { resolver = new MockResolver(await app.resolve(LoggerService)); idService = app.get(IdService); userPublickeysRepository = app.get(DI.userPublickeysRepository); + userKeypairService = app.get(UserKeypairService); // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error const federatedInstanceService = app.get(FederatedInstanceService); @@ -486,15 +491,57 @@ describe('ActivityPub', () => { describe(ApRendererService, () => { let note: MiNote; - let author: MiUser; + let author: MiLocalUser; + let keypair: MiUserKeypair; - beforeEach(() => { + beforeEach(async () => { author = new MiUser({ id: idService.gen(), + host: null, + uri: null, + username: 'testAuthor', + usernameLower: 'testauthor', + name: 'Test Author', + isCat: true, + requireSigninToViewContents: true, + makeNotesFollowersOnlyBefore: new Date(2025, 2, 20).valueOf(), + makeNotesHiddenBefore: new Date(2025, 2, 21).valueOf(), + isLocked: true, + isExplorable: true, + hideOnlineStatus: true, + noindex: true, + enableRss: true, + + }) as MiLocalUser; + + const [publicKey, privateKey] = await new Promise<[string, string]>((res, rej) => + generateKeyPair('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: undefined, + passphrase: undefined, + }, + }, (err, publicKey, privateKey) => + err ? rej(err) : res([publicKey, privateKey]), + )); + keypair = new MiUserKeypair({ + userId: author.id, + user: author, + publicKey, + privateKey, }); + ((userKeypairService as unknown as { cache: RedisKVCache }).cache as unknown as { memoryCache: MemoryKVCache }).memoryCache.set(author.id, keypair); + note = new MiNote({ id: idService.gen(), userId: author.id, + user: author, visibility: 'public', localOnly: false, text: 'Note text', @@ -621,6 +668,35 @@ describe('ActivityPub', () => { }); }); }); + + describe('renderPersonRedacted', () => { + it('should include minimal properties', async () => { + const result = await rendererService.renderPersonRedacted(author); + + expect(result.type).toBe('Person'); + expect(result.id).toBeTruthy(); + expect(result.inbox).toBeTruthy(); + expect(result.sharedInbox).toBeTruthy(); + expect(result.endpoints.sharedInbox).toBeTruthy(); + expect(result.url).toBeTruthy(); + expect(result.preferredUsername).toBe(author.username); + expect(result.publicKey.owner).toBe(result.id); + expect(result._misskey_requireSigninToViewContents).toBe(author.requireSigninToViewContents); + expect(result._misskey_makeNotesFollowersOnlyBefore).toBe(author.makeNotesFollowersOnlyBefore); + expect(result._misskey_makeNotesHiddenBefore).toBe(author.makeNotesHiddenBefore); + expect(result.discoverable).toBe(author.isExplorable); + expect(result.hideOnlineStatus).toBe(author.hideOnlineStatus); + expect(result.noindex).toBe(author.noindex); + expect(result.indexable).toBe(!author.noindex); + expect(result.enableRss).toBe(author.enableRss); + }); + + it('should not include sensitive properties', async () => { + const result = await rendererService.renderPersonRedacted(author) as IActor; + + expect(result.name).toBeUndefined(); + }); + }); }); describe(ApPersonService, () => { diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index cbd0d12dcc..3a95e0a5a6 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.noBotProtectionWarning }} {{ i18n.ts.configure }} {{ i18n.ts.noEmailServerWarning }} {{ i18n.ts.configure }} {{ i18n.ts.pendingUserApprovals }} {{ i18n.ts.check }} + {{ i18n.ts.authorizedFetchLegacyWarning }} @@ -69,6 +70,7 @@ const noEmailServer = computed(() => !instance.enableEmail); const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl)); const thereIsUnresolvedAbuseReport = ref(false); const pendingUserApprovals = ref(false); +const hasLegacyAuthFetchSetting = ref(false); const currentPage = computed(() => router.currentRef.value.child); misskeyApi('admin/abuse-user-reports', { @@ -86,6 +88,11 @@ misskeyApi('admin/show-users', { if (approvals.length > 0) pendingUserApprovals.value = true; }); +misskeyApi('admin/meta') + .then(meta => { + hasLegacyAuthFetchSetting.value = meta.hasLegacyAuthFetchSetting; + }); + const NARROW_THRESHOLD = 600; const ro = new ResizeObserver((entries, observer) => { if (entries.length === 0) return; diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 4358821092..38986dc977 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -8,6 +8,22 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + + + + + + + + + + + + @@ -96,6 +112,15 @@ import MkFormFooter from '@/components/MkFormFooter.vue'; const meta = await misskeyApi('admin/meta'); +const authFetchForm = useForm({ + allowUnsignedFetch: meta.allowUnsignedFetch, +}, async state => { + await os.apiWithDialog('admin/update-meta', { + allowUnsignedFetch: state.allowUnsignedFetch, + }); + fetchInstance(true); +}); + const ipLoggingForm = useForm({ enableIpLogging: meta.enableIpLogging, }, async (state) => { diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 0b8e89a6a5..bcedb8b139 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -132,6 +132,20 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}
+ + + + + + + + + + + + + +
@@ -192,6 +206,7 @@ import FormSlot from '@/components/form/slot.vue'; import { formatDateTimeString } from '@/scripts/format-time-string.js'; import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; +import MkRadios from '@/components/MkRadios.vue'; const $i = signinRequired(); @@ -210,6 +225,13 @@ const followingVisibility = ref($i.followingVisibility); const followersVisibility = ref($i.followersVisibility); const defaultCW = ref($i.defaultCW); const defaultCWPriority = ref($i.defaultCWPriority); +const allowUnsignedFetch = ref($i.allowUnsignedFetch); +const computedAllowUnsignedFetch = computed(() => { + if (allowUnsignedFetch.value !== 'staff') { + return allowUnsignedFetch.value; + } + return instance.allowUnsignedFetch; +}); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); @@ -270,6 +292,7 @@ function save() { followersVisibility: followersVisibility.value, defaultCWPriority: defaultCWPriority.value, defaultCW: defaultCW.value, + allowUnsignedFetch: allowUnsignedFetch.value, }); } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c1156a7ffa..8ddd20ab63 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4276,6 +4276,8 @@ export type components = { defaultCW: string | null; /** @enum {string} */ defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault'; + /** @enum {string} */ + allowUnsignedFetch: 'never' | 'always' | 'essential' | 'staff'; }; UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly']; MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly']; @@ -5385,6 +5387,8 @@ export type components = { requireSetup: boolean; cacheRemoteFiles: boolean; cacheRemoteSensitiveFiles: boolean; + /** @enum {string} */ + allowUnsignedFetch: 'never' | 'always' | 'essential'; }; MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly']; SystemWebhook: { @@ -8860,6 +8864,9 @@ export type operations = { trustedLinkUrlPatterns: string[]; federation: string; federationHosts: string[]; + hasLegacyAuthFetchSetting: boolean; + /** @enum {string} */ + allowUnsignedFetch: 'never' | 'always' | 'essential'; }; }; }; @@ -11476,6 +11483,8 @@ export type operations = { /** @enum {string} */ federation?: 'all' | 'none' | 'specified'; federationHosts?: string[]; + /** @enum {string} */ + allowUnsignedFetch?: 'never' | 'always' | 'essential'; }; }; }; @@ -22971,6 +22980,8 @@ export type operations = { defaultCW?: string | null; /** @enum {string} */ defaultCWPriority?: 'default' | 'parent' | 'defaultParent' | 'parentDefault'; + /** @enum {string} */ + allowUnsignedFetch?: 'never' | 'always' | 'essential' | 'staff'; }; }; }; diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index e25020de8e..2e00f15b6f 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -514,3 +514,18 @@ fetchLinkedNote: "Fetch linked note" _processErrors: quoteUnavailable: "Unable to process quote. This post may be missing context." + +authorizedFetchSection: "Authorized Fetch" +authorizedFetchLabel: "Allow unsigned ActivityPub requests:" +authorizedFetchDescription: "This setting controls the behavior when a remote instance or user attempts to access your content without verifying their identity. If disabled, any remote user can access your profile and posts - even one who has been blocked or defederated." +_authorizedFetchValue: + never: "Never" + always: "Always" + essential: "Only for essential metadata" + staff: "Use staff recommendation" +_authorizedFetchValueDescription: + never: "Block all unsigned requests. Improves privacy and makes blocks more effective, but is not compatible with some very old or uncommon instance software." + always: "Allow all unsigned requests. Provides the greatest compatibility with other instances, but reduces privacy and weakens blocks." + essential: "Allow some limited unsigned requests. Provides a hybrid between \"Never\" and \"Always\" by exposing only the minimum profile metadata that is required for federation with older software." + staff: "Use the default value of \"{value}\" recommended by the instance staff." +authorizedFetchLegacyWarning: "The configuration property 'checkActivityPubGetSignature' has been deprecated and replaced with the new Authorized Fetch setting. Please remove it from your configuration file." -- cgit v1.2.3-freya From 6c8f21b608eb6e9e7691983c7e57f1cbe0a28fc1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:21:09 +0900 Subject: fix(backend): 連合無しモードでも外部から照会可能だった問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../backend/src/server/ActivityPubServerService.ts | 80 +++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8027b050..cae884793b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### Server - Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 - Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 +- Fix: 連合無しモードでも外部から照会可能だった問題を修正 ## 2025.3.1 diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 20e985aaf2..48c80e5e61 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -13,7 +13,7 @@ import accepts from 'accepts'; import vary from 'vary'; import secureJson from 'secure-json-parse'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -42,6 +42,9 @@ export class ActivityPubServerService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -102,6 +105,11 @@ export class ActivityPubServerService { @bindThis private inbox(request: FastifyRequest, reply: FastifyReply) { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + let signature; try { @@ -173,6 +181,11 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const userId = request.params.user; const cursor = request.query.cursor; @@ -265,6 +278,11 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const userId = request.params.user; const cursor = request.query.cursor; @@ -354,6 +372,11 @@ export class ActivityPubServerService { @bindThis private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -398,6 +421,11 @@ export class ActivityPubServerService { }>, reply: FastifyReply, ) { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const userId = request.params.user; const sinceId = request.query.since_id; @@ -482,6 +510,11 @@ export class ActivityPubServerService { @bindThis private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + if (user == null) { reply.code(404); return; @@ -564,6 +597,11 @@ export class ActivityPubServerService { fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { vary(reply.raw, 'Accept'); + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const note = await this.notesRepository.findOneBy({ id: request.params.note, visibility: In(['public', 'home']), @@ -594,6 +632,11 @@ export class ActivityPubServerService { fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => { vary(reply.raw, 'Accept'); + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const note = await this.notesRepository.findOneBy({ id: request.params.note, userHost: IsNull(), @@ -634,6 +677,11 @@ export class ActivityPubServerService { // publickey fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -661,6 +709,11 @@ export class ActivityPubServerService { fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { vary(reply.raw, 'Accept'); + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -674,6 +727,11 @@ export class ActivityPubServerService { fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { vary(reply.raw, 'Accept'); + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const acct = Acct.parse(request.params.acct); const user = await this.usersRepository.findOneBy({ @@ -688,6 +746,11 @@ export class ActivityPubServerService { // emoji fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const emoji = await this.emojisRepository.findOneBy({ host: IsNull(), name: request.params.emoji, @@ -705,6 +768,11 @@ export class ActivityPubServerService { // like fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like }); if (reaction == null) { @@ -726,6 +794,11 @@ export class ActivityPubServerService { // follow fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + // This may be used before the follow is completed, so we do not // check if the following exists. @@ -752,6 +825,11 @@ export class ActivityPubServerService { // follow fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => { + if (this.meta.federation === 'none') { + reply.code(403); + return; + } + // This may be used before the follow is completed, so we do not // check if the following exists and only check if the follow request exists. -- cgit v1.2.3-freya From 5182f17d32f07d4ea42317a4258e76626eac1575 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 22 Feb 2025 20:33:45 -0500 Subject: implement replies collection for posts --- .../src/core/activitypub/ApRendererService.ts | 73 ++++++++++- packages/backend/src/core/activitypub/type.ts | 38 +++++- .../backend/src/server/ActivityPubServerService.ts | 46 +++++++ packages/backend/test/unit/activitypub.ts | 141 +++++++++++++++++++++ 4 files changed, 295 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index c7f8b97a5a..61878c60e8 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -29,10 +29,11 @@ import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { IdService } from '@/core/IdService.js'; import { appendContentWarning } from '@/misc/append-content-warning.js'; +import { QueryService } from '@/core/QueryService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; -import { getApId } from './type.js'; +import { getApId, IOrderedCollection, IOrderedCollectionPage } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() @@ -70,6 +71,7 @@ export class ApRendererService { private apMfmService: ApMfmService, private mfmService: MfmService, private idService: IdService, + private readonly queryService: QueryService, ) { } @@ -388,13 +390,16 @@ export class ApRendererService { let to: string[] = []; let cc: string[] = []; + let isPublic = false; if (note.visibility === 'public') { to = ['https://www.w3.org/ns/activitystreams#Public']; cc = [`${attributedTo}/followers`].concat(mentions); + isPublic = true; } else if (note.visibility === 'home') { to = [`${attributedTo}/followers`]; cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); + isPublic = true; } else if (note.visibility === 'followers') { to = [`${attributedTo}/followers`]; cc = mentions; @@ -455,6 +460,10 @@ export class ApRendererService { })), } as const : {}; + // Render the outer replies collection wrapper, which contains the count but not the actual URLs. + // This saves one hop (request) when de-referencing the replies. + const replies = isPublic ? await this.renderRepliesCollection(note.id) : undefined; + return { id: `${this.config.url}/notes/${note.id}`, type: 'Note', @@ -473,6 +482,7 @@ export class ApRendererService { to, cc, inReplyTo, + replies, attachment: files.map(x => this.renderDocument(x)), sensitive: note.cw != null || files.some(file => file.isSensitive), tag, @@ -909,6 +919,67 @@ export class ApRendererService { return page; } + /** + * Renders the reply collection wrapper object for a note + * @param noteId Note whose reply collection to render. + */ + @bindThis + public async renderRepliesCollection(noteId: string): Promise { + const replyCount = await this.notesRepository.countBy({ + replyId: noteId, + visibility: In(['public', 'home']), + localOnly: false, + }); + + return { + type: 'OrderedCollection', + id: `${this.config.url}/notes/${noteId}/replies`, + first: `${this.config.url}/notes/${noteId}/replies?page=true`, + totalItems: replyCount, + }; + } + + /** + * Renders a page of the replies collection for a note + * @param noteId Return notes that are inReplyTo this value. + * @param untilId If set, return only notes that are *older* than this value. + */ + @bindThis + public async renderRepliesCollectionPage(noteId: string, untilId: string | undefined): Promise { + const replyCount = await this.notesRepository.countBy({ + replyId: noteId, + visibility: In(['public', 'home']), + localOnly: false, + }); + + const limit = 50; + const results = await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), undefined, untilId) + .andWhere({ + replyId: noteId, + visibility: In(['public', 'home']), + localOnly: false, + }) + .select(['note.id', 'note.uri']) + .limit(limit) + .getRawMany<{ note_id: string, note_uri: string | null }>(); + + const hasNextPage = results.length >= limit; + const baseId = `${this.config.url}/notes/${noteId}/replies?page=true`; + + return { + type: 'OrderedCollectionPage', + id: untilId == null ? baseId : `${baseId}&until_id=${untilId}`, + partOf: `${this.config.url}/notes/${noteId}/replies`, + first: baseId, + next: hasNextPage ? `${baseId}&until_id=${results.at(-1)?.note_id}` : undefined, + totalItems: replyCount, + orderedItems: results.map(r => { + // Remote notes have a URI, local have just an ID. + return r.note_uri ?? `${this.config.url}/notes/${r.note_id}`; + }), + }; + } + @bindThis private async getEmojis(names: string[]): Promise { if (names.length === 0) return []; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index d8e7b3c9c3..5b93543f1e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -26,7 +26,7 @@ export interface IObject { attributedTo?: ApObject; attachment?: any[]; inReplyTo?: any; - replies?: ICollection; + replies?: ICollection | IOrderedCollection | string; content?: string | null; startTime?: Date; endTime?: Date; @@ -125,6 +125,8 @@ export interface ICollection extends IObject { type: 'Collection'; totalItems: number; first?: IObject | string; + last?: IObject | string; + current?: IObject | string; items?: ApObject; } @@ -132,6 +134,32 @@ export interface IOrderedCollection extends IObject { type: 'OrderedCollection'; totalItems: number; first?: IObject | string; + last?: IObject | string; + current?: IObject | string; + orderedItems?: ApObject; +} + +export interface ICollectionPage extends IObject { + type: 'CollectionPage'; + totalItems: number; + first?: IObject | string; + last?: IObject | string; + current?: IObject | string; + partOf?: IObject | string; + next?: IObject | string; + prev?: IObject | string; + items?: ApObject; +} + +export interface IOrderedCollectionPage extends IObject { + type: 'OrderedCollectionPage'; + totalItems: number; + first?: IObject | string; + last?: IObject | string; + current?: IObject | string; + partOf?: IObject | string; + next?: IObject | string; + prev?: IObject | string; orderedItems?: ApObject; } @@ -231,8 +259,14 @@ export const isCollection = (object: IObject): object is ICollection => export const isOrderedCollection = (object: IObject): object is IOrderedCollection => getApType(object) === 'OrderedCollection'; +export const isCollectionPage = (object: IObject): object is ICollectionPage => + getApType(object) === 'CollectionPage'; + +export const isOrderedCollectionPage = (object: IObject): object is IOrderedCollectionPage => + getApType(object) === 'OrderedCollectionPage'; + export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => - isCollection(object) || isOrderedCollection(object); + isCollection(object) || isOrderedCollection(object) || isCollectionPage(object) || isOrderedCollectionPage(object); export interface IApPropertyValue extends IObject { type: 'PropertyValue'; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index ba112ca59a..ea534af458 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -783,6 +783,52 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(await this.packActivity(note, author))); }); + // replies + fastify.get<{ + Params: { note: string; }; + Querystring: { page?: unknown; until_id?: unknown; }; + }>('/notes/:note/replies', async (request, reply) => { + vary(reply.raw, 'Accept'); + this.setResponseType(request, reply); + + // Raw query to avoid fetching the while entity just to check access and get the user ID + const note = await this.notesRepository + .createQueryBuilder('note') + .andWhere({ + id: request.params.note, + userHost: IsNull(), + visibility: In(['public', 'home']), + localOnly: false, + }) + .select(['note.id', 'note.userId']) + .getRawOne<{ note_id: string, note_userId: string }>(); + + const { reject } = await this.checkAuthorizedFetch(request, reply, note?.note_userId); + if (reject) return; + + if (note == null) { + reply.code(404); + return; + } + + const untilId = request.query.until_id; + if (untilId != null && typeof(untilId) !== 'string') { + reply.code(400); + return; + } + + // If page is unset, then we just provide the outer wrapper. + // This is because the spec doesn't allow the wrapper to contain both elements *and* pages. + // We could technically do it anyway, but that may break other instances. + if (request.query.page !== 'true') { + const collection = await this.apRendererService.renderRepliesCollection(note.note_id); + return this.apRendererService.addContext(collection); + } + + const page = await this.apRendererService.renderRepliesCollectionPage(note.note_id, untilId ?? undefined); + return this.apRendererService.addContext(page); + }); + // outbox fastify.get<{ Params: { user: string; }; diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index d3d27e182f..5767089109 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -9,6 +9,7 @@ import { generateKeyPair } from 'crypto'; import { Test } from '@nestjs/testing'; import { jest } from '@jest/globals'; +import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; @@ -99,6 +100,7 @@ describe('ActivityPub', () => { let idService: IdService; let userPublickeysRepository: UserPublickeysRepository; let userKeypairService: UserKeypairService; + let config: Config; const metaInitial = { cacheRemoteFiles: true, @@ -149,6 +151,7 @@ describe('ActivityPub', () => { idService = app.get(IdService); userPublickeysRepository = app.get(DI.userPublickeysRepository); userKeypairService = app.get(UserKeypairService); + config = app.get(DI.config); // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error const federatedInstanceService = app.get(FederatedInstanceService); @@ -612,6 +615,40 @@ describe('ActivityPub', () => { expect(result.summary).toBe('original and mandatory'); }); }); + + describe('replies', () => { + it('should be included when visibility=public', async () => { + note.visibility = 'public'; + + const rendered = await rendererService.renderNote(note, author, false); + + expect(rendered.replies).toBeDefined(); + }); + + it('should be included when visibility=home', async () => { + note.visibility = 'home'; + + const rendered = await rendererService.renderNote(note, author, false); + + expect(rendered.replies).toBeDefined(); + }); + + it('should be excluded when visibility=followers', async () => { + note.visibility = 'followers'; + + const rendered = await rendererService.renderNote(note, author, false); + + expect(rendered.replies).not.toBeDefined(); + }); + + it('should be excluded when visibility=specified', async () => { + note.visibility = 'specified'; + + const rendered = await rendererService.renderNote(note, author, false); + + expect(rendered.replies).not.toBeDefined(); + }); + }); }); describe('renderUpnote', () => { @@ -695,6 +732,110 @@ describe('ActivityPub', () => { expect(result.name).toBeUndefined(); }); }); + + describe('renderRepliesCollection', () => { + it('should include type', async () => { + const collection = await rendererService.renderRepliesCollection(note.id); + + expect(collection.type).toBe('OrderedCollection'); + }); + + it('should include id', async () => { + const collection = await rendererService.renderRepliesCollection(note.id); + + expect(collection.id).toBe(`${config.url}/notes/${note.id}/replies`); + }); + + it('should include first', async () => { + const collection = await rendererService.renderRepliesCollection(note.id); + + expect(collection.first).toBe(`${config.url}/notes/${note.id}/replies?page=true`); + }); + + it('should include totalItems', async () => { + const collection = await rendererService.renderRepliesCollection(note.id); + + expect(collection.totalItems).toBe(0); + }); + }); + + describe('renderRepliesCollectionPage', () => { + describe('with untilId', () => { + it('should include type', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, 'abc123'); + + expect(collection.type).toBe('OrderedCollectionPage'); + }); + + it('should include id', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, 'abc123'); + + expect(collection.id).toBe(`${config.url}/notes/${note.id}/replies?page=true&until_id=abc123`); + }); + + it('should include partOf', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, 'abc123'); + + expect(collection.partOf).toBe(`${config.url}/notes/${note.id}/replies`); + }); + + it('should include first', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, 'abc123'); + + expect(collection.first).toBe(`${config.url}/notes/${note.id}/replies?page=true`); + }); + + it('should include totalItems', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, 'abc123'); + + expect(collection.totalItems).toBe(0); + }); + + it('should include orderedItems', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, 'abc123'); + + expect(collection.orderedItems).toBeDefined(); + }); + }); + + describe('without untilId', () => { + it('should include type', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, undefined); + + expect(collection.type).toBe('OrderedCollectionPage'); + }); + + it('should include id', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, undefined); + + expect(collection.id).toBe(`${config.url}/notes/${note.id}/replies?page=true`); + }); + + it('should include partOf', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, undefined); + + expect(collection.partOf).toBe(`${config.url}/notes/${note.id}/replies`); + }); + + it('should include first', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, undefined); + + expect(collection.first).toBe(`${config.url}/notes/${note.id}/replies?page=true`); + }); + + it('should include totalItems', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, undefined); + + expect(collection.totalItems).toBe(0); + }); + + it('should include orderedItems', async () => { + const collection = await rendererService.renderRepliesCollectionPage(note.id, undefined); + + expect(collection.orderedItems).toBeDefined(); + }); + }); + }); }); describe(ApPersonService, () => { -- cgit v1.2.3-freya From 4c473eb76d77736ce4c46a7d0c967f3a872cd769 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 13 Apr 2025 18:34:33 +0900 Subject: fix: resolve with non-lowercased acct is broken (#15813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: resolve with non-lowercased acct is broken * docs(changelog): Fix: 大文字を含むユーザの URL で紹介された場合に 404 エラーを返す問題 --- CHANGELOG.md | 2 +- packages/backend/src/server/ActivityPubServerService.ts | 2 +- packages/backend/src/server/WellKnownServerService.ts | 2 +- packages/backend/test/unit/AnnouncementService.ts | 2 +- packages/backend/test/unit/SigninWithPasskeyApiService.ts | 6 +++--- packages/backend/test/unit/entities/UserEntityService.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c57d2ee56..3a4c155a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Server - Fix: システムアカウントの名前がサーバー名と同期されない問題を修正 - +- Fix: 大文字を含むユーザの URL で紹介された場合に 404 エラーを返す問題 #15813 ## 2025.4.0 diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 48c80e5e61..72d57a9b1b 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -735,7 +735,7 @@ export class ActivityPubServerService { const acct = Acct.parse(request.params.acct); const user = await this.usersRepository.findOneBy({ - usernameLower: acct.username, + usernameLower: acct.username.toLowerCase(), host: acct.host ?? IsNull(), isSuspended: false, }); diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index d106be5bc8..ebfd1a421d 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -138,7 +138,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => !acct.host || acct.host === this.config.host.toLowerCase() ? { - usernameLower: acct.username, + usernameLower: acct.username.toLowerCase(), host: IsNull(), isSuspended: false, } : 422; diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index 81da0fac31..a79655c9aa 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -44,7 +44,7 @@ describe('AnnouncementService', () => { return usersRepository.insert({ id: genAidx(Date.now()), username: un, - usernameLower: un, + usernameLower: un.toLowerCase(), ...data, }) .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts index bae2b88c60..0687ed8437 100644 --- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts +++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts @@ -89,8 +89,8 @@ describe('SigninWithPasskeyApiService', () => { app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], providers: [ - SigninWithPasskeyApiService, - { provide: RateLimiterService, useClass: FakeLimiter }, + SigninWithPasskeyApiService, + { provide: RateLimiterService, useClass: FakeLimiter }, { provide: SigninService, useClass: FakeSigninService }, ], }).useMocker((token) => { @@ -115,7 +115,7 @@ describe('SigninWithPasskeyApiService', () => { jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); const dummyUser = { - id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null, + id: uid, username: uid, usernameLower: uid.toLowerCase(), uri: null, host: null, }; const dummyProfile = { userId: uid, diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts index 6b7eedff55..ce3f931bb0 100644 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -74,7 +74,7 @@ describe('UserEntityService', () => { ...userData, id: genAidx(Date.now()), username: un, - usernameLower: un, + usernameLower: un.toLowerCase(), }) .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); -- cgit v1.2.3-freya From b2e3e658965b52873dd6771cf9771b3032a0ed15 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Tue, 15 Apr 2025 16:15:27 +0900 Subject: fix: use ftt for outbox (#15819) * fix: use ftt for outbox * chore: check for enableFanoutTimeline * lint: fix lint --- .../src/core/FanoutTimelineEndpointService.ts | 2 +- .../backend/src/server/ActivityPubServerService.ts | 48 +++++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index b05af99c5e..ce8cc83dfd 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -54,7 +54,7 @@ export class FanoutTimelineEndpointService { } @bindThis - private async getMiNotes(ps: TimelineOptions): Promise { + async getMiNotes(ps: TimelineOptions): Promise { // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 72d57a9b1b..f7b22c44c4 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -32,6 +32,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js'; import * as Acct from '@/misc/acct.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; @@ -75,6 +76,7 @@ export class ActivityPubServerService { private queueService: QueueService, private userKeypairService: UserKeypairService, private queryService: QueryService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { //this.createServer = this.createServer.bind(this); } @@ -461,16 +463,28 @@ export class ActivityPubServerService { const partOf = `${this.config.url}/users/${userId}/outbox`; if (page) { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) - .andWhere('note.userId = :userId', { userId: user.id }) - .andWhere(new Brackets(qb => { - qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) - .andWhere('note.localOnly = FALSE'); - - const notes = await query.limit(limit).getMany(); + const notes = this.meta.enableFanoutTimeline ? await this.fanoutTimelineEndpointService.getMiNotes({ + sinceId: sinceId ?? null, + untilId: untilId ?? null, + limit: limit, + allowPartial: false, // Possibly true? IDK it's OK for ordered collection. + me: null, + redisTimelines: [ + `userTimeline:${user.id}`, + `userTimelineWithReplies:${user.id}`, + ], + useDbFallback: true, + ignoreAuthorFromMute: true, + excludePureRenotes: false, + noteFilter: (note) => { + if (note.visibility !== 'home' && note.visibility !== 'public') return false; + if (note.localOnly) return false; + return true; + }, + dbFallback: async (untilId, sinceId, limit) => { + return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id); + }, + }) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id); if (sinceId) notes.reverse(); @@ -508,6 +522,20 @@ export class ActivityPubServerService { } } + @bindThis + private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) { + return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) + .andWhere('note.userId = :userId', { userId }) + .andWhere(new Brackets(qb => { + qb + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); + })) + .andWhere('note.localOnly = FALSE') + .limit(limit) + .getMany(); + } + @bindThis private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) { if (this.meta.federation === 'none') { -- cgit v1.2.3-freya From 58c0ac6c8986194d735071f17c008850c28b2064 Mon Sep 17 00:00:00 2001 From: dakkar Date: Mon, 21 Apr 2025 14:44:19 +0100 Subject: check signatures with and without query - fix #1036 @Oneric explained: > Spec says query params must be included in the signature; Mastodon > being Mastodon used to always exclude it though and for > compatibility everyone followed this. At some point GtS decided to > follow spec instead which caused interop issues, but succeeded in > getting Mastodon (and others like *oma) to accept incoming requests > with (and also still without) query params though outgoing requests > remaing query-param-free. Some still only accept query-param-less > requests though and GtS uses a retry mechanism to resend any request > failing with 401 with an query-parama-less signature once. (Also > see: > https://docs.gotosocial.org/en/latest/federation/http_signatures/ ) > > So for incoming requests both versions need to be checked. For > outgoing requests, unless you want to jump through retry hoops like > GtS, omitting query-params is the safer bet for now (presumably this > will only change if Mastodon ever decides to send out requests > signed with query params) --- .../backend/src/server/ActivityPubServerService.ts | 30 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index ea534af458..f40fbe8245 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -177,7 +177,7 @@ export class ActivityPubServerService { this is also inspired by FireFish's `checkFetch` */ - let signature; + let signature: httpSignature.IParsedSignature; try { signature = httpSignature.parseRequest(request.raw, { @@ -230,14 +230,38 @@ export class ActivityPubServerService { return `${logPrefix} signer is suspended: refuse`; } - let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + // some fedi implementations include the query (`?foo=bar`) in the + // signature, some don't, so we have to handle both cases + function verifyWithOrWithoutQuery() { + const httpSignatureValidated = httpSignature.verifySignature(signature, authUser!.key!.keyPem); + if (httpSignatureValidated) return true; + + const requestUrl = new URL(`http://whatever${request.raw.url}`); + if (! requestUrl.search) return false; + + // verification failed, the request URL contained a query, let's try without + const semiRawRequest = request.raw; + semiRawRequest.url = requestUrl.pathname; + + // no need for try/catch, if the original request parsed, this + // one will, too + const signatureWithoutQuery = httpSignature.parseRequest(semiRawRequest, { + headers: ['(request-target)', 'host', 'date'], + authorizationHeaderName: 'signature', + }); + + return httpSignature.verifySignature(signatureWithoutQuery, authUser!.key!.keyPem); + } + + console.warn('starting verification'); + let httpSignatureValidated = verifyWithOrWithoutQuery(); // maybe they changed their key? refetch it // TODO rate-limit this using lastFetchedAt if (!httpSignatureValidated) { authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); if (authUser.key != null) { - httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + httpSignatureValidated = verifyWithOrWithoutQuery(); } } -- cgit v1.2.3-freya From ec404fd3ce123b8c8dac84504009d8516b89599f Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 30 Apr 2025 20:30:52 +0100 Subject: remove leftover debug line --- packages/backend/src/server/ActivityPubServerService.ts | 1 - 1 file changed, 1 deletion(-) (limited to 'packages/backend/src/server/ActivityPubServerService.ts') diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index f40fbe8245..d27c4c9177 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -253,7 +253,6 @@ export class ActivityPubServerService { return httpSignature.verifySignature(signatureWithoutQuery, authUser!.key!.keyPem); } - console.warn('starting verification'); let httpSignatureValidated = verifyWithOrWithoutQuery(); // maybe they changed their key? refetch it -- cgit v1.2.3-freya