upd: add FriendlyCaptcha as a captcha solution

FriendlyCaptcha is a german captcha solution which is GDPR compliant and has a non-commerical free license
This commit is contained in:
Marie 2024-11-02 02:20:35 +01:00
parent 8824422cb5
commit d786e96c2b
No known key found for this signature in database
GPG key ID: 7ADF6C9CD9A28555
18 changed files with 175 additions and 7 deletions

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: marie and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class friendlyCaptcha1730505338000 {
name = 'friendlyCaptcha1730505338000';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFC" boolean NOT NULL DEFAULT false`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ADD "fcSiteKey" character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ADD "fcSecretKey" character varying(1024)`, undefined);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "fcSecretKey"`, undefined);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "fcSiteKey"`, undefined);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFC"`, undefined);
}
}

View file

@ -10,6 +10,7 @@ import { bindThis } from '@/decorators.js';
type CaptchaResponse = {
success: boolean;
'error-codes'?: string[];
'errors'?: string[];
};
@Injectable()
@ -73,6 +74,35 @@ export class CaptchaService {
}
}
@bindThis
public async verifyFriendlyCaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
throw new Error('recaptcha-failed: no response provided');
}
const result = await this.httpRequestService.send('https://api.friendlycaptcha.com/api/v1/siteverify', {
method: 'POST',
body: JSON.stringify({
secret: secret,
solution: response,
}),
headers: {
'Content-Type': 'application/json',
},
});
if (result.status !== 200) {
throw new Error('frc-failed: frc didn\'t return 200 OK');
}
const resp = await result.json() as CaptchaResponse;
if (resp.success !== true) {
const errorCodes = resp['errors'] ? resp['errors'].join(', ') : '';
throw new Error(`frc-failed: ${errorCodes}`);
}
}
// https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go
@bindThis
public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {

View file

@ -98,6 +98,8 @@ export class MetaEntityService {
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
enableFC: instance.enableFC,
fcSiteKey: instance.fcSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',

View file

@ -269,6 +269,23 @@ export class MiMeta {
})
public turnstileSecretKey: string | null;
@Column('boolean', {
default: false,
})
public enableFC: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public fcSiteKey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public fcSecretKey: string | null;
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
@Column('enum', {

View file

@ -127,6 +127,14 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
enableFC: {
type: 'boolean',
optional: false, nullable: false,
},
fcSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableAchievements: {
type: 'boolean',
optional: false, nullable: true,

View file

@ -121,6 +121,7 @@ export class NodeinfoServerService {
enableRecaptcha: meta.enableRecaptcha,
enableMcaptcha: meta.enableMcaptcha,
enableTurnstile: meta.enableTurnstile,
enableFC: meta.enableFC,
maxNoteTextLength: this.config.maxNoteLength,
maxRemoteNoteTextLength: this.config.maxRemoteNoteLength,
maxCwLength: this.config.maxCwLength,

View file

@ -118,6 +118,7 @@ export class ApiServerService {
'hcaptcha-response'?: string;
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'frc-captcha-solution'?: string;
}
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));

View file

@ -72,6 +72,7 @@ export class SignupApiService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
'frc-captcha-solution'?: string;
}
}>,
reply: FastifyReply,
@ -104,6 +105,12 @@ export class SignupApiService {
throw new FastifyReplyError(400, err);
});
}
if (this.meta.enableFC && this.meta.fcSecretKey) {
await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
}
const username = body['username'];

View file

@ -73,6 +73,14 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
enableFC: {
type: 'boolean',
optional: false, nullable: false,
},
fcSiteKey: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
@ -219,6 +227,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
fcSecretKey: {
type: 'string',
optional: false, nullable: true,
},
sensitiveMediaDetection: {
type: 'string',
optional: false, nullable: false,
@ -600,6 +612,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
enableFC: instance.enableFC,
fcSiteKey: instance.fcSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@ -634,6 +648,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mcaptchaSecretKey: instance.mcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
turnstileSecretKey: instance.turnstileSecretKey,
fcSecretKey: instance.fcSecretKey,
sensitiveMediaDetection: instance.sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,

View file

@ -81,6 +81,9 @@ export const paramDef = {
enableTurnstile: { type: 'boolean' },
turnstileSiteKey: { type: 'string', nullable: true },
turnstileSecretKey: { type: 'string', nullable: true },
enableFC: { type: 'boolean' },
fcSiteKey: { type: 'string', nullable: true },
fcSecretKey: { type: 'string', nullable: true },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@ -383,6 +386,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.turnstileSecretKey = ps.turnstileSecretKey;
}
if (ps.enableFC !== undefined) {
set.enableFC = ps.enableFC;
}
if (ps.fcSiteKey !== undefined) {
set.fcSiteKey = ps.fcSiteKey;
}
if (ps.fcSecretKey !== undefined) {
set.fcSecretKey = ps.fcSecretKey;
}
if (ps.enableBotTrending !== undefined) {
set.enableBotTrending = ps.enableBotTrending;
}

View file

@ -123,12 +123,14 @@ describe('2要素認証', () => {
password: string,
'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null,
'frc-captcha-solution'?: string | null,
} => {
return {
username,
password,
'g-recaptcha-response': null,
'hcaptcha-response': null,
'frc-captcha-solution': null,
};
};