From f6eb49e7e05508cf58d9bf1b2b997e41c8ff4516 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:39:10 +0900 Subject: [PATCH] wip --- locales/index.d.ts | 4 +++ locales/ja-JP.yml | 1 + .../migration/1728550878802-testcaptcha.js | 16 ++++++++++ packages/backend/src/core/CaptchaService.ts | 13 ++++++++ .../src/core/entities/MetaEntityService.ts | 1 + packages/backend/src/models/Meta.ts | 5 ++++ .../backend/src/models/json-schema/meta.ts | 4 +++ .../src/server/api/ApiServerService.ts | 2 ++ .../src/server/api/SigninApiService.ts | 7 +++++ .../src/server/api/SignupApiService.ts | 7 +++++ .../src/server/api/endpoints/admin/meta.ts | 5 ++++ .../server/api/endpoints/admin/update-meta.ts | 5 ++++ packages/frontend/assets/testcaptcha.png | Bin 0 -> 2634 bytes .../frontend/src/components/MkCaptcha.vue | 28 ++++++++++++++++-- .../src/components/MkSignin.password.vue | 9 +++++- packages/frontend/src/components/MkSignin.vue | 6 ++-- .../src/components/MkSignupDialog.form.vue | 6 ++++ .../src/pages/admin/bot-protection.vue | 15 +++++++++- packages/misskey-js/src/autogen/types.ts | 3 ++ 19 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 packages/backend/migration/1728550878802-testcaptcha.js create mode 100644 packages/frontend/assets/testcaptcha.png diff --git a/locales/index.d.ts b/locales/index.d.ts index f0dead1245..dab8eb0361 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5166,6 +5166,10 @@ export interface Locale extends ILocale { * 対象 */ "target": string; + /** + * CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。 + */ + "testCaptchaWarning": string; "_abuseUserReport": { /** * 転送 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0076c467ec..812cd1854f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1287,6 +1287,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" messageToFollower: "フォロワーへのメッセージ" target: "対象" +testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。" _abuseUserReport: forward: "転送" diff --git a/packages/backend/migration/1728550878802-testcaptcha.js b/packages/backend/migration/1728550878802-testcaptcha.js new file mode 100644 index 0000000000..d8d987c0c1 --- /dev/null +++ b/packages/backend/migration/1728550878802-testcaptcha.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Testcaptcha1728550878802 { + name = 'Testcaptcha1728550878802' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`); + } +} diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f6b7955cd2..206d0dbe0a 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -119,5 +119,18 @@ export class CaptchaService { throw new Error(`turnstile-failed: ${errorCodes}`); } } + + @bindThis + public async verifyTestcaptcha(response: string | null | undefined): Promise { + if (response == null) { + throw new Error('testcaptcha-failed: no response provided'); + } + + const success = response === 'testcaptcha-passed'; + + if (!success) { + throw new Error('testcaptcha-failed'); + } + } } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index fbd982eb34..409dca3426 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -96,6 +96,7 @@ export class MetaEntityService { recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + enableTestcaptcha: instance.enableTestcaptcha, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index d29689f907..fd007de6c6 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -258,6 +258,11 @@ export class MiMeta { }) public turnstileSecretKey: string | null; + @Column('boolean', { + default: false, + }) + public enableTestcaptcha: boolean; + // chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること @Column('enum', { diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 99feeaa7d7..e3fd63464a 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -115,6 +115,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + enableTestcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, swPublickey: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index be63635efe..3a8cb19f01 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -119,6 +119,7 @@ export class ApiServerService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; } }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); @@ -132,6 +133,7 @@ export class ApiServerService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; }; }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply)); diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 0d24ffa56a..1d983ca4bc 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -71,6 +71,7 @@ export class SigninApiService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; }; }>, reply: FastifyReply, @@ -194,6 +195,12 @@ export class SigninApiService { throw new FastifyReplyError(400, err); }); } + + if (this.meta.enableTestcaptcha) { + await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } } if (same) { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index c499638018..3ec5e5d3e6 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -67,6 +67,7 @@ export class SignupApiService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; } }>, reply: FastifyReply, @@ -99,6 +100,12 @@ export class SignupApiService { throw new FastifyReplyError(400, err); }); } + + if (this.meta.enableTestcaptcha) { + await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } } const username = body['username']; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index b76ed5c524..abb3c17be3 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -69,6 +69,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableTestcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -555,6 +559,7 @@ export default class extends Endpoint { // eslint- recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + enableTestcaptcha: instance.enableTestcaptcha, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, 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 9ffae840b6..e97ac4e2b9 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -78,6 +78,7 @@ export const paramDef = { enableTurnstile: { type: 'boolean' }, turnstileSiteKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true }, + enableTestcaptcha: { type: 'boolean' }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, @@ -357,6 +358,10 @@ export default class extends Endpoint { // eslint- set.turnstileSecretKey = ps.turnstileSecretKey; } + if (ps.enableTestcaptcha !== undefined) { + set.enableTestcaptcha = ps.enableTestcaptcha; + } + if (ps.sensitiveMediaDetection !== undefined) { set.sensitiveMediaDetection = ps.sensitiveMediaDetection; } diff --git a/packages/frontend/assets/testcaptcha.png b/packages/frontend/assets/testcaptcha.png new file mode 100644 index 0000000000000000000000000000000000000000..9bfd252b51c6057f99ff1012897c4d9e6971f897 GIT binary patch literal 2634 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index c5b6e0caed..82fc89e51c 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + Test captcha passed! + + + Type "ai-chan-kawaii" to pass captcha + + Submit + + @@ -29,7 +40,7 @@ export type Captcha = { getResponse(id: string): string; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -54,12 +65,16 @@ const available = ref(false); const captchaEl = shallowRef(); +const testcaptchaInput = ref(''); +const testcaptchaPassed = ref(false); + const variable = computed(() => { switch (props.provider) { case 'hcaptcha': return 'hcaptcha'; case 'recaptcha': return 'grecaptcha'; case 'turnstile': return 'turnstile'; case 'mcaptcha': return 'mcaptcha'; + case 'testcaptcha': return 'testcaptcha'; } }); @@ -71,6 +86,7 @@ const src = computed(() => { case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; case 'mcaptcha': return null; + case 'testcaptcha': return null; } }); @@ -78,7 +94,7 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed(() => window[variable.value] || {} as unknown as Captcha); -if (loaded || props.provider === 'mcaptcha') { +if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') { available.value = true; } else if (src.value !== null) { (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), { @@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') { function reset() { if (captcha.value.reset) captcha.value.reset(); + testcaptchaPassed.value = false; + testcaptchaInput.value = ''; } async function requestRender() { @@ -127,6 +145,12 @@ function onReceivedMessage(message: MessageEvent) { } } +function testcaptchaSubmit() { + testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii'; + callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined); + if (!testcaptchaPassed.value) testcaptchaInput.value = ''; +} + onMounted(() => { if (available.value) { window.addEventListener('message', onReceivedMessage); diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue index f30bf5f861..5608122a39 100644 --- a/packages/frontend/src/components/MkSignin.password.vue +++ b/packages/frontend/src/components/MkSignin.password.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.ts.continue }} @@ -44,6 +45,7 @@ export type PwResponse = { mCaptchaResponse: string | null; reCaptchaResponse: string | null; turnstileResponse: string | null; + testcaptchaResponse: string | null; }; }; @@ -75,18 +77,21 @@ const hCaptcha = useTemplateRef('hcaptcha'); const mCaptcha = useTemplateRef('mcaptcha'); const reCaptcha = useTemplateRef('recaptcha'); const turnstile = useTemplateRef('turnstile'); +const testcaptcha = useTemplateRef('testcaptcha'); const hCaptchaResponse = ref(null); const mCaptchaResponse = ref(null); const reCaptchaResponse = ref(null); const turnstileResponse = ref(null); +const testcaptchaResponse = ref(null); const captchaFailed = computed((): boolean => { return ( (instance.enableHcaptcha && !hCaptchaResponse.value) || (instance.enableMcaptcha && !mCaptchaResponse.value) || (instance.enableRecaptcha && !reCaptchaResponse.value) || - (instance.enableTurnstile && !turnstileResponse.value) + (instance.enableTurnstile && !turnstileResponse.value) || + (instance.enableTestcaptcha && !testcaptchaResponse.value) ); }); @@ -104,6 +109,7 @@ function onSubmit() { mCaptchaResponse: mCaptchaResponse.value, reCaptchaResponse: reCaptchaResponse.value, turnstileResponse: turnstileResponse.value, + testcaptchaResponse: testcaptchaResponse.value, }, }); } @@ -113,6 +119,7 @@ function resetCaptcha() { mCaptcha.value?.reset(); reCaptcha.value?.reset(); turnstile.value?.reset(); + testcaptcha.value?.reset(); } defineExpose({ diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index a773cefdab..776ee20e36 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -68,6 +68,8 @@ import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue' import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; +import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { login } from '@/account.js'; @@ -79,9 +81,6 @@ import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue'; import XTotp from '@/components/MkSignin.totp.vue'; import XPasskey from '@/components/MkSignin.passkey.vue'; -import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; - const emit = defineEmits<{ (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; }>(); @@ -188,6 +187,7 @@ async function onPasswordSubmitted(pw: PwResponse) { 'm-captcha-response': pw.captcha.mCaptchaResponse, 'g-recaptcha-response': pw.captcha.reCaptchaResponse, 'turnstile-response': pw.captcha.turnstileResponse, + 'testcaptcha-response': pw.captcha.testcaptchaResponse, }); } } diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index ffb5551ff3..3d1c44fc90 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -108,6 +109,7 @@ const hcaptcha = ref(); const mcaptcha = ref(); const recaptcha = ref(); const turnstile = ref(); +const testcaptcha = ref(); const username = ref(''); const password = ref(''); @@ -123,6 +125,7 @@ const hCaptchaResponse = ref(null); const mCaptchaResponse = ref(null); const reCaptchaResponse = ref(null); const turnstileResponse = ref(null); +const testcaptchaResponse = ref(null); const usernameAbortController = ref(null); const emailAbortController = ref(null); @@ -132,6 +135,7 @@ const shouldDisableSubmitting = computed((): boolean => { instance.enableMcaptcha && !mCaptchaResponse.value || instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || + instance.enableTestcaptcha && !testcaptchaResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || usernameState.value !== 'ok' || passwordRetypeState.value !== 'match'; @@ -259,6 +263,7 @@ async function onSubmit(): Promise { 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, 'turnstile-response': turnstileResponse.value, + 'testcaptcha-response': testcaptchaResponse.value, }; const res = await fetch(`${config.apiUrl}/signup`, { @@ -301,6 +306,7 @@ function onSignupApiError() { mcaptcha.value?.reset?.(); recaptcha.value?.reset?.(); turnstile.value?.reset?.(); + testcaptcha.value?.reset?.(); os.alert({ type: 'error', diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index b34592cd6a..d07add4408 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only mCaptcha reCAPTCHA Turnstile + testCaptcha {{ i18n.ts.none }} ({{ i18n.ts.notRecommended }}) @@ -23,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only mCaptcha reCAPTCHA Turnstile + testCaptcha @@ -85,6 +87,13 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + {{ i18n.ts.preview }} + + + @@ -101,6 +110,7 @@ import { i18n } from '@/i18n.js'; import { useForm } from '@/scripts/use-form.js'; import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFolder from '@/components/MkFolder.vue'; +import MkInfo from '@/components/MkInfo.vue'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); @@ -115,7 +125,9 @@ const botProtectionForm = useForm({ ? 'turnstile' : meta.enableMcaptcha ? 'mcaptcha' - : null, + : meta.enableTestcaptcha + ? 'testcaptcha' + : null, hcaptchaSiteKey: meta.hcaptchaSiteKey, hcaptchaSecretKey: meta.hcaptchaSecretKey, mcaptchaSiteKey: meta.mcaptchaSiteKey, @@ -140,6 +152,7 @@ const botProtectionForm = useForm({ enableTurnstile: state.provider === 'turnstile', turnstileSiteKey: state.turnstileSiteKey, turnstileSecretKey: state.turnstileSecretKey, + enableTestcaptcha: state.provider === 'testcaptcha', }); fetchInstance(true); }); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 76ef7ea1fb..e40cb050fd 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4972,6 +4972,7 @@ export type components = { recaptchaSiteKey: string | null; enableTurnstile: boolean; turnstileSiteKey: string | null; + enableTestcaptcha: boolean; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string; @@ -5102,6 +5103,7 @@ export type operations = { recaptchaSiteKey: string | null; enableTurnstile: boolean; turnstileSiteKey: string | null; + enableTestcaptcha: boolean; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string | null; @@ -9491,6 +9493,7 @@ export type operations = { enableTurnstile?: boolean; turnstileSiteKey?: string | null; turnstileSecretKey?: string | null; + enableTestcaptcha?: boolean; /** @enum {string} */ sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote'; /** @enum {string} */