Compare commits
7 commits
d01374055a
...
5c017af2fe
Author | SHA1 | Date | |
---|---|---|---|
5c017af2fe | |||
a2c3a75f0f | |||
|
62d35a56c1 | ||
|
bb3d710927 | ||
|
97da78b46c | ||
|
e0f3780885 | ||
|
a35c2f214b |
29 changed files with 540 additions and 129 deletions
|
@ -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.
|
# 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.
|
# 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
|
attachLdSignatureForRelays: true
|
||||||
# check that inbound ActivityPub GET requests are signed ("authorized fetch")
|
|
||||||
checkActivityPubGetSignature: false
|
|
||||||
|
|
||||||
# For security reasons, uploading attachments from the intranet is prohibited,
|
# For security reasons, uploading attachments from the intranet is prohibited,
|
||||||
# but exceptions can be made from the following settings. Default value is "undefined".
|
# but exceptions can be made from the following settings. Default value is "undefined".
|
||||||
|
|
|
@ -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.
|
# 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.
|
# 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
|
attachLdSignatureForRelays: true
|
||||||
# check that inbound ActivityPub GET requests are signed ("authorized fetch")
|
|
||||||
checkActivityPubGetSignature: false
|
|
||||||
|
|
||||||
# For security reasons, uploading attachments from the intranet is prohibited,
|
# For security reasons, uploading attachments from the intranet is prohibited,
|
||||||
# but exceptions can be made from the following settings. Default value is "undefined".
|
# but exceptions can be made from the following settings. Default value is "undefined".
|
||||||
|
|
|
@ -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.
|
# 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.
|
# 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
|
attachLdSignatureForRelays: true
|
||||||
# check that inbound ActivityPub GET requests are signed ("authorized fetch")
|
|
||||||
checkActivityPubGetSignature: false
|
|
||||||
|
|
||||||
# For security reasons, uploading attachments from the intranet is prohibited,
|
# For security reasons, uploading attachments from the intranet is prohibited,
|
||||||
# but exceptions can be made from the following settings. Default value is "undefined".
|
# but exceptions can be made from the following settings. Default value is "undefined".
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
# Upgrade Notes
|
# 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
|
## 2024.10.0
|
||||||
|
|
||||||
### Hellspawns
|
### Hellspawns
|
||||||
|
|
52
locales/index.d.ts
vendored
52
locales/index.d.ts
vendored
|
@ -12231,6 +12231,58 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"quoteUnavailable": string;
|
"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: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "quollkey",
|
"name": "quollkey",
|
||||||
"version": "2025.3.2-dev",
|
"version": "2025.3.3-dev",
|
||||||
"codename": "quoll",
|
"codename": "quoll",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -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 system accounts
|
||||||
|
await queryRunner.query(`UPDATE "user" SET "allowUnsignedFetch" = 'always' WHERE "username" LIKE '%.%' 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -200,6 +200,7 @@ export type Config = {
|
||||||
customMOTD: string[] | undefined;
|
customMOTD: string[] | undefined;
|
||||||
signToActivityPubGet: boolean;
|
signToActivityPubGet: boolean;
|
||||||
attachLdSignatureForRelays: boolean;
|
attachLdSignatureForRelays: boolean;
|
||||||
|
/** @deprecated Use MiMeta.allowUnsignedFetch instead */
|
||||||
checkActivityPubGetSignature: boolean | undefined;
|
checkActivityPubGetSignature: boolean | undefined;
|
||||||
logging?: {
|
logging?: {
|
||||||
sql?: {
|
sql?: {
|
||||||
|
|
|
@ -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://github.com/sindresorhus/file-type/blob/main/core.js
|
||||||
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
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];
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.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 }));
|
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async findLocalUserById(userId: MiUser['id']): Promise<MiLocalUser | null> {
|
||||||
|
return await this.localUserByIdCache.fetchMaybe(userId, async () => {
|
||||||
|
return await this.usersRepository.findOneBy({ id: userId, host: IsNull() }) as MiLocalUser | null ?? undefined;
|
||||||
|
}) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
|
|
@ -63,6 +63,13 @@ export class CreateSystemUserService {
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
approved: true,
|
approved: true,
|
||||||
isBot: true,
|
isBot: true,
|
||||||
|
/* 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',
|
||||||
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
|
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
|
||||||
|
|
||||||
await transactionalEntityManager.insert(MiUserKeypair, {
|
await transactionalEntityManager.insert(MiUserKeypair, {
|
||||||
|
|
|
@ -101,6 +101,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||||
enableRss: true,
|
enableRss: true,
|
||||||
mandatoryCW: null,
|
mandatoryCW: null,
|
||||||
rejectQuotes: false,
|
rejectQuotes: false,
|
||||||
|
allowUnsignedFetch: 'staff',
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -571,6 +571,38 @@ export class ApRendererService {
|
||||||
return person;
|
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
|
@bindThis
|
||||||
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
|
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -191,6 +191,7 @@ export class MetaEntityService {
|
||||||
serviceWorker: instance.enableServiceWorker,
|
serviceWorker: instance.enableServiceWorker,
|
||||||
miauth: true,
|
miauth: true,
|
||||||
},
|
},
|
||||||
|
allowUnsignedFetch: instance.allowUnsignedFetch,
|
||||||
};
|
};
|
||||||
|
|
||||||
return packDetailed;
|
return packDetailed;
|
||||||
|
|
|
@ -725,6 +725,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
policies: this.roleService.getUserPolicies(user.id),
|
policies: this.roleService.getUserPolicies(user.id),
|
||||||
defaultCW: profile!.defaultCW,
|
defaultCW: profile!.defaultCW,
|
||||||
defaultCWPriority: profile!.defaultCWPriority,
|
defaultCWPriority: profile!.defaultCWPriority,
|
||||||
|
allowUnsignedFetch: user.allowUnsignedFetch,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(opts.includeSecrets ? {
|
...(opts.includeSecrets ? {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { type InstanceUnsignedFetchOption, instanceUnsignedFetchOptions } from '@/const.js';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
|
@ -809,4 +810,14 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public link5Label: string | null;
|
public link5Label: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||||
|
import { type UserUnsignedFetchOption, userUnsignedFetchOptions } from '@/const.js';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiDriveFile } from './DriveFile.js';
|
import { MiDriveFile } from './DriveFile.js';
|
||||||
|
|
||||||
|
@ -125,7 +126,7 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public backgroundId: MiDriveFile['id'] | null;
|
public backgroundId: MiDriveFile['id'] | null;
|
||||||
|
|
||||||
@OneToOne(type => MiDriveFile, {
|
@OneToOne(() => MiDriveFile, {
|
||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
|
@ -357,6 +358,15 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public rejectQuotes: boolean;
|
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<MiUser>) {
|
constructor(data: Partial<MiUser>) {
|
||||||
if (data == null) return;
|
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 descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||||
export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } 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 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;
|
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { instanceUnsignedFetchOptions } from '@/const.js';
|
||||||
|
|
||||||
export const packedMetaLiteSchema = {
|
export const packedMetaLiteSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -437,6 +439,11 @@ export const packedMetaDetailedOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
allowUnsignedFetch: {
|
||||||
|
type: 'string',
|
||||||
|
enum: instanceUnsignedFetchOptions,
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { userUnsignedFetchOptions } from '@/const.js';
|
||||||
|
|
||||||
export const notificationRecieveConfig = {
|
export const notificationRecieveConfig = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
oneOf: [
|
oneOf: [
|
||||||
|
@ -769,6 +771,11 @@ export const packedMeDetailedOnlySchema = {
|
||||||
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
|
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
allowUnsignedFetch: {
|
||||||
|
type: 'string',
|
||||||
|
enum: userUnsignedFetchOptions,
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import accepts from 'accepts';
|
||||||
import vary from 'vary';
|
import vary from 'vary';
|
||||||
import secureJson from 'secure-json-parse';
|
import secureJson from 'secure-json-parse';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 * as url from '@/misc/prelude/url.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.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 { QueueService } from '@/core/QueueService.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
|
||||||
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import type { MiFollowing } from '@/models/Following.js';
|
import type { MiFollowing } from '@/models/Following.js';
|
||||||
import { countIf } from '@/misc/prelude/array.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 type Logger from '@/logger.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.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 { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
import * as Acct from '@/misc/acct.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 { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ export class ActivityPubServerService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -77,13 +80,13 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private instanceActorService: InstanceActorService,
|
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairService: UserKeypairService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
|
private readonly cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
this.logger = this.loggerService.getLogger('apserv', 'pink');
|
this.logger = this.loggerService.getLogger('apserv', 'pink');
|
||||||
|
@ -106,7 +109,7 @@ export class ActivityPubServerService {
|
||||||
* @param author Author of the note
|
* @param author Author of the note
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async packActivity(note: MiNote, author: MiUser): Promise<any> {
|
private async packActivity(note: MiNote, author: MiUser): Promise<ICreate | IAnnounce> {
|
||||||
if (isRenote(note) && !isQuote(note)) {
|
if (isRenote(note) && !isQuote(note)) {
|
||||||
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
||||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
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);
|
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<boolean> {
|
* Checks Authorized Fetch.
|
||||||
if (!this.config.checkActivityPubGetSignature) return false;
|
* 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<string | null> {
|
||||||
/* this code is inspired from the `inbox` function below, and
|
/* this code is inspired from the `inbox` function below, and
|
||||||
`queue/processors/InboxProcessorService`
|
`queue/processors/InboxProcessorService`
|
||||||
|
|
||||||
|
@ -129,59 +177,33 @@ export class ActivityPubServerService {
|
||||||
this is also inspired by FireFish's `checkFetch`
|
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;
|
let signature;
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
// not signed, or malformed signature: refuse
|
// not signed, or malformed signature: refuse
|
||||||
this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`);
|
return `${request.id} ${request.url} not signed, or malformed signature: refuse`;
|
||||||
reply.code(401);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyId = new URL(signature.keyId);
|
const keyId = new URL(signature.keyId);
|
||||||
const keyHost = this.utilityService.toPuny(keyId.hostname);
|
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
|
if (signature.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) {
|
||||||
|| request.headers.host !== this.config.host) {
|
|
||||||
// no destination host, or not us: refuse
|
// no destination host, or not us: refuse
|
||||||
this.authlogger.warn(`${logPrefix} no destination host, or not us: refuse`);
|
return `${logPrefix} no destination host, or not us: refuse`;
|
||||||
reply.code(401);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedHost(keyHost)) {
|
if (!this.utilityService.isFederationAllowedHost(keyHost)) {
|
||||||
/* blocked instance: refuse (we don't care if the signature is
|
/* blocked instance: refuse (we don't care if the signature is
|
||||||
good, if they even pretend to be from a blocked instance,
|
good, if they even pretend to be from a blocked instance,
|
||||||
they're out) */
|
they're out) */
|
||||||
this.authlogger.warn(`${logPrefix} instance is blocked: refuse`);
|
return `${logPrefix} instance is blocked: refuse`;
|
||||||
reply.code(401);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do we know the signer already?
|
// do we know the signer already?
|
||||||
|
@ -200,14 +222,18 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
if (authUser?.key == null) {
|
if (authUser?.key == null) {
|
||||||
// we can't figure out who the signer is, or we can't get their key: refuse
|
// 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`);
|
return `${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`;
|
||||||
reply.code(401);
|
}
|
||||||
return true;
|
|
||||||
|
if (authUser.user.isSuspended) {
|
||||||
|
// Signer is suspended locally
|
||||||
|
return `${logPrefix} signer is suspended: refuse`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||||
|
|
||||||
// maybe they changed their key? refetch it
|
// maybe they changed their key? refetch it
|
||||||
|
// TODO rate-limit this using lastFetchedAt
|
||||||
if (!httpSignatureValidated) {
|
if (!httpSignatureValidated) {
|
||||||
authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
|
authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
|
||||||
if (authUser.key != null) {
|
if (authUser.key != null) {
|
||||||
|
@ -217,13 +243,11 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
if (!httpSignatureValidated) {
|
if (!httpSignatureValidated) {
|
||||||
// bad signature: refuse
|
// bad signature: refuse
|
||||||
this.authlogger.info(`${logPrefix} failed to validate signature: refuse`);
|
return `${logPrefix} failed to validate signature: refuse`;
|
||||||
reply.code(401);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// all good, don't refuse
|
// all good, don't refuse
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -299,7 +323,8 @@ export class ActivityPubServerService {
|
||||||
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
||||||
reply: FastifyReply,
|
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;
|
const userId = request.params.user;
|
||||||
|
|
||||||
|
@ -326,11 +351,9 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
if (profile.followersVisibility === 'private') {
|
if (profile.followersVisibility === 'private') {
|
||||||
reply.code(403);
|
reply.code(403);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
|
|
||||||
return;
|
return;
|
||||||
} else if (profile.followersVisibility === 'followers') {
|
} else if (profile.followersVisibility === 'followers') {
|
||||||
reply.code(403);
|
reply.code(403);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -382,7 +405,6 @@ export class ActivityPubServerService {
|
||||||
user.followersCount,
|
user.followersCount,
|
||||||
`${partOf}?page=true`,
|
`${partOf}?page=true`,
|
||||||
);
|
);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
}
|
}
|
||||||
|
@ -393,7 +415,8 @@ export class ActivityPubServerService {
|
||||||
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
||||||
reply: FastifyReply,
|
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;
|
const userId = request.params.user;
|
||||||
|
|
||||||
|
@ -420,11 +443,9 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
if (profile.followingVisibility === 'private') {
|
if (profile.followingVisibility === 'private') {
|
||||||
reply.code(403);
|
reply.code(403);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
|
|
||||||
return;
|
return;
|
||||||
} else if (profile.followingVisibility === 'followers') {
|
} else if (profile.followingVisibility === 'followers') {
|
||||||
reply.code(403);
|
reply.code(403);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -476,7 +497,6 @@ export class ActivityPubServerService {
|
||||||
user.followingCount,
|
user.followingCount,
|
||||||
`${partOf}?page=true`,
|
`${partOf}?page=true`,
|
||||||
);
|
);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
}
|
}
|
||||||
|
@ -484,7 +504,8 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
|
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;
|
const userId = request.params.user;
|
||||||
|
|
||||||
|
@ -517,7 +538,6 @@ export class ActivityPubServerService {
|
||||||
renderedNotes,
|
renderedNotes,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
}
|
}
|
||||||
|
@ -530,7 +550,8 @@ export class ActivityPubServerService {
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
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;
|
const userId = request.params.user;
|
||||||
|
|
||||||
|
@ -608,14 +629,13 @@ export class ActivityPubServerService {
|
||||||
`${partOf}?page=true`,
|
`${partOf}?page=true`,
|
||||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||||
);
|
);
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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) {
|
if (user == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
return;
|
return;
|
||||||
|
@ -631,10 +651,12 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
|
|
||||||
this.setResponseType(request, reply);
|
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
|
@bindThis
|
||||||
|
@ -687,6 +709,13 @@ export class ActivityPubServerService {
|
||||||
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
reply.header('Access-Control-Allow-Origin', '*');
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
reply.header('Access-Control-Expose-Headers', 'Vary');
|
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();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -697,8 +726,6 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
// note
|
// note
|
||||||
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||||
if (await this.shouldRefuseGetRequest(request, reply)) return;
|
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
const note = await this.notesRepository.findOneBy({
|
const note = await this.notesRepository.findOneBy({
|
||||||
|
@ -707,6 +734,9 @@ export class ActivityPubServerService {
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { reject } = await this.checkAuthorizedFetch(request, reply, note?.userId);
|
||||||
|
if (reject) return;
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
return;
|
return;
|
||||||
|
@ -722,7 +752,6 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
|
|
||||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||||
|
@ -731,8 +760,6 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
// note activity
|
// note activity
|
||||||
fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => {
|
fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => {
|
||||||
if (await this.shouldRefuseGetRequest(request, reply)) return;
|
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
const note = await this.notesRepository.findOneBy({
|
const note = await this.notesRepository.findOneBy({
|
||||||
|
@ -742,12 +769,14 @@ export class ActivityPubServerService {
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { reject } = await this.checkAuthorizedFetch(request, reply, note?.userId);
|
||||||
|
if (reject) return;
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
|
|
||||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||||
|
@ -777,7 +806,8 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
// publickey
|
// publickey
|
||||||
fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => {
|
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;
|
const userId = request.params.user;
|
||||||
|
|
||||||
|
@ -794,7 +824,6 @@ export class ActivityPubServerService {
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair)));
|
return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair)));
|
||||||
} else {
|
} else {
|
||||||
|
@ -804,7 +833,8 @@ export class ActivityPubServerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
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');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
|
@ -815,12 +845,10 @@ export class ActivityPubServerService {
|
||||||
isSuspended: false,
|
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) => {
|
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');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
const acct = Acct.parse(request.params.acct);
|
const acct = Acct.parse(request.params.acct);
|
||||||
|
@ -831,13 +859,17 @@ export class ActivityPubServerService {
|
||||||
isSuspended: false,
|
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
|
//#endregion
|
||||||
|
|
||||||
// emoji
|
// emoji
|
||||||
fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => {
|
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({
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
|
@ -849,17 +881,17 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
|
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// like
|
// like
|
||||||
fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => {
|
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 reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like });
|
||||||
|
|
||||||
|
const { reject } = await this.checkAuthorizedFetch(request, reply, reaction?.userId);
|
||||||
|
if (reject) return;
|
||||||
|
|
||||||
if (reaction == null) {
|
if (reaction == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
return;
|
return;
|
||||||
|
@ -872,14 +904,14 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note)));
|
return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// follow
|
// follow
|
||||||
fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => {
|
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
|
// This may be used before the follow is completed, so we do not
|
||||||
// check if the following exists.
|
// check if the following exists.
|
||||||
|
@ -900,15 +932,12 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
|
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// follow
|
// follow
|
||||||
fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => {
|
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
|
// 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.
|
// check if the following exists and only check if the follow request exists.
|
||||||
|
|
||||||
|
@ -916,6 +945,9 @@ export class ActivityPubServerService {
|
||||||
id: request.params.followRequestId,
|
id: request.params.followRequestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { reject } = await this.checkAuthorizedFetch(request, reply, followRequest?.followerId);
|
||||||
|
if (reject) return;
|
||||||
|
|
||||||
if (followRequest == null) {
|
if (followRequest == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
return;
|
return;
|
||||||
|
@ -937,11 +969,21 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
|
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
|
import { instanceUnsignedFetchOptions } from '@/const.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
@ -629,6 +630,15 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hasLegacyAuthFetchSetting: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
allowUnsignedFetch: {
|
||||||
|
type: 'string',
|
||||||
|
enum: instanceUnsignedFetchOptions,
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -795,6 +805,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
link4Label: instance.link4Label,
|
link4Label: instance.link4Label,
|
||||||
link5Url: instance.link5Url,
|
link5Url: instance.link5Url,
|
||||||
link5Label: instance.link5Label,
|
link5Label: instance.link5Label,
|
||||||
|
hasLegacyAuthFetchSetting: config.checkActivityPubGetSignature != null,
|
||||||
|
allowUnsignedFetch: instance.allowUnsignedFetch,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type { MiMeta } from '@/models/Meta.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { instanceUnsignedFetchOptions } from '@/const.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -215,6 +216,11 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowUnsignedFetch: {
|
||||||
|
type: 'string',
|
||||||
|
enum: instanceUnsignedFetchOptions,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -794,6 +800,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.link5Label = ps.link5Label;
|
set.link5Label = ps.link5Label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.allowUnsignedFetch !== undefined) {
|
||||||
|
set.allowUnsignedFetch = ps.allowUnsignedFetch;
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -33,6 +33,7 @@ import type { Config } from '@/config.js';
|
||||||
import { safeForSql } from '@/misc/safe-for-sql.js';
|
import { safeForSql } from '@/misc/safe-for-sql.js';
|
||||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||||
|
import { userUnsignedFetchOptions } from '@/const.js';
|
||||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
@ -255,6 +256,11 @@ export const paramDef = {
|
||||||
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
|
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
allowUnsignedFetch: {
|
||||||
|
type: 'string',
|
||||||
|
enum: userUnsignedFetchOptions,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -519,6 +525,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
profileUpdates.defaultCWPriority = ps.defaultCWPriority;
|
profileUpdates.defaultCWPriority = ps.defaultCWPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.allowUnsignedFetch !== undefined) {
|
||||||
|
updates.allowUnsignedFetch = ps.allowUnsignedFetch;
|
||||||
|
}
|
||||||
|
|
||||||
//#region emojis/tags
|
//#region emojis/tags
|
||||||
|
|
||||||
let emojis = [] as string[];
|
let emojis = [] as string[];
|
||||||
|
|
|
@ -2,15 +2,14 @@
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
|
import { generateKeyPair } from 'crypto';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
|
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
|
||||||
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
|
@ -22,13 +21,15 @@ import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.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 { DI } from '@/di-symbols.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
|
||||||
import { genAidx } from '@/misc/id/aidx.js';
|
import { genAidx } from '@/misc/id/aidx.js';
|
||||||
import { MockResolver } from '../misc/mock-resolver.js';
|
import { MockResolver } from '../misc/mock-resolver.js';
|
||||||
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
|
||||||
const host = 'https://host1.test';
|
const host = 'https://host1.test';
|
||||||
|
|
||||||
|
@ -97,6 +98,7 @@ describe('ActivityPub', () => {
|
||||||
let resolver: MockResolver;
|
let resolver: MockResolver;
|
||||||
let idService: IdService;
|
let idService: IdService;
|
||||||
let userPublickeysRepository: UserPublickeysRepository;
|
let userPublickeysRepository: UserPublickeysRepository;
|
||||||
|
let userKeypairService: UserKeypairService;
|
||||||
|
|
||||||
const metaInitial = {
|
const metaInitial = {
|
||||||
cacheRemoteFiles: true,
|
cacheRemoteFiles: true,
|
||||||
|
@ -146,6 +148,7 @@ describe('ActivityPub', () => {
|
||||||
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
|
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
|
||||||
idService = app.get<IdService>(IdService);
|
idService = app.get<IdService>(IdService);
|
||||||
userPublickeysRepository = app.get<UserPublickeysRepository>(DI.userPublickeysRepository);
|
userPublickeysRepository = app.get<UserPublickeysRepository>(DI.userPublickeysRepository);
|
||||||
|
userKeypairService = app.get<UserKeypairService>(UserKeypairService);
|
||||||
|
|
||||||
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
|
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
|
||||||
const federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService);
|
const federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService);
|
||||||
|
@ -486,15 +489,57 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
describe(ApRendererService, () => {
|
describe(ApRendererService, () => {
|
||||||
let note: MiNote;
|
let note: MiNote;
|
||||||
let author: MiUser;
|
let author: MiLocalUser;
|
||||||
|
let keypair: MiUserKeypair;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
author = new MiUser({
|
author = new MiUser({
|
||||||
id: idService.gen(),
|
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<MiUserKeypair> }).cache as unknown as { memoryCache: MemoryKVCache<MiUserKeypair> }).memoryCache.set(author.id, keypair);
|
||||||
|
|
||||||
note = new MiNote({
|
note = new MiNote({
|
||||||
id: idService.gen(),
|
id: idService.gen(),
|
||||||
userId: author.id,
|
userId: author.id,
|
||||||
|
user: author,
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
text: 'Note text',
|
text: 'Note text',
|
||||||
|
@ -621,6 +666,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, () => {
|
describe(ApPersonService, () => {
|
||||||
|
|
|
@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
<MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
<MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||||
|
<MkInfo v-if="hasLegacyAuthFetchSetting" warn class="info">{{ i18n.ts.authorizedFetchLegacyWarning }}</MkInfo>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||||
|
@ -69,6 +70,7 @@ const noEmailServer = computed(() => !instance.enableEmail);
|
||||||
const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
|
const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
|
||||||
const thereIsUnresolvedAbuseReport = ref(false);
|
const thereIsUnresolvedAbuseReport = ref(false);
|
||||||
const pendingUserApprovals = ref(false);
|
const pendingUserApprovals = ref(false);
|
||||||
|
const hasLegacyAuthFetchSetting = ref(false);
|
||||||
const currentPage = computed(() => router.currentRef.value.child);
|
const currentPage = computed(() => router.currentRef.value.child);
|
||||||
|
|
||||||
misskeyApi('admin/abuse-user-reports', {
|
misskeyApi('admin/abuse-user-reports', {
|
||||||
|
@ -86,6 +88,11 @@ misskeyApi('admin/show-users', {
|
||||||
if (approvals.length > 0) pendingUserApprovals.value = true;
|
if (approvals.length > 0) pendingUserApprovals.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
misskeyApi('admin/meta')
|
||||||
|
.then(meta => {
|
||||||
|
hasLegacyAuthFetchSetting.value = meta.hasLegacyAuthFetchSetting;
|
||||||
|
});
|
||||||
|
|
||||||
const NARROW_THRESHOLD = 600;
|
const NARROW_THRESHOLD = 600;
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
if (entries.length === 0) return;
|
if (entries.length === 0) return;
|
||||||
|
|
|
@ -8,6 +8,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
|
<MkFolder v-if="meta.federation !== 'none'">
|
||||||
|
<template #label>{{ i18n.ts.authorizedFetchSection }}</template>
|
||||||
|
<template #suffix>{{ meta.allowUnsignedFetch !== 'always' ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||||
|
<template v-if="authFetchForm.modified.value" #footer>
|
||||||
|
<MkFormFooter :form="authFetchForm"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<MkRadios v-model="authFetchForm.state.allowUnsignedFetch">
|
||||||
|
<template #label>{{ i18n.ts.authorizedFetchLabel }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.authorizedFetchDescription }}</template>
|
||||||
|
<option value="never">{{ i18n.ts._authorizedFetchValue.never }} - {{ i18n.ts._authorizedFetchValueDescription.never }}</option>
|
||||||
|
<option value="always">{{ i18n.ts._authorizedFetchValue.always }} - {{ i18n.ts._authorizedFetchValueDescription.always }}</option>
|
||||||
|
<option value="essential">{{ i18n.ts._authorizedFetchValue.essential }} - {{ i18n.ts._authorizedFetchValueDescription.essential }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<XBotProtection/>
|
<XBotProtection/>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
|
@ -96,6 +112,15 @@ import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||||
|
|
||||||
const meta = await misskeyApi('admin/meta');
|
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({
|
const ipLoggingForm = useForm({
|
||||||
enableIpLogging: meta.enableIpLogging,
|
enableIpLogging: meta.enableIpLogging,
|
||||||
}, async (state) => {
|
}, async (state) => {
|
||||||
|
|
|
@ -132,6 +132,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
|
<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
|
||||||
</template>
|
</template>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkFolder v-if="instance.federation !== 'none'">
|
||||||
|
<template #label>{{ i18n.ts.authorizedFetchSection }}</template>
|
||||||
|
<template #suffix>{{ computedAllowUnsignedFetch !== 'always' ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||||
|
|
||||||
|
<MkRadios v-model="allowUnsignedFetch" @update:modelValue="save()">
|
||||||
|
<template #label>{{ i18n.ts.authorizedFetchLabel }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.authorizedFetchDescription }}</template>
|
||||||
|
<option value="never">{{ i18n.ts._authorizedFetchValue.never }} - {{ i18n.ts._authorizedFetchValueDescription.never }}</option>
|
||||||
|
<option value="always">{{ i18n.ts._authorizedFetchValue.always }} - {{ i18n.ts._authorizedFetchValueDescription.always }}</option>
|
||||||
|
<option value="essential">{{ i18n.ts._authorizedFetchValue.essential }} - {{ i18n.ts._authorizedFetchValueDescription.essential }}</option>
|
||||||
|
<option value="staff">{{ i18n.ts._authorizedFetchValue.staff }} - {{ i18n.tsx._authorizedFetchValueDescription.staff({ value: i18n.ts._authorizedFetchValue[instance.allowUnsignedFetch] }) }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
@ -192,6 +206,7 @@ import FormSlot from '@/components/form/slot.vue';
|
||||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
@ -210,6 +225,13 @@ const followingVisibility = ref($i.followingVisibility);
|
||||||
const followersVisibility = ref($i.followersVisibility);
|
const followersVisibility = ref($i.followersVisibility);
|
||||||
const defaultCW = ref($i.defaultCW);
|
const defaultCW = ref($i.defaultCW);
|
||||||
const defaultCWPriority = ref($i.defaultCWPriority);
|
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 defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
|
||||||
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
|
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
|
||||||
|
@ -270,6 +292,7 @@ function save() {
|
||||||
followersVisibility: followersVisibility.value,
|
followersVisibility: followersVisibility.value,
|
||||||
defaultCWPriority: defaultCWPriority.value,
|
defaultCWPriority: defaultCWPriority.value,
|
||||||
defaultCW: defaultCW.value,
|
defaultCW: defaultCW.value,
|
||||||
|
allowUnsignedFetch: allowUnsignedFetch.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4276,6 +4276,8 @@ export type components = {
|
||||||
defaultCW: string | null;
|
defaultCW: string | null;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
||||||
|
/** @enum {string} */
|
||||||
|
allowUnsignedFetch: 'never' | 'always' | 'essential' | 'staff';
|
||||||
};
|
};
|
||||||
UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'];
|
UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'];
|
||||||
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
||||||
|
@ -5395,6 +5397,8 @@ export type components = {
|
||||||
requireSetup: boolean;
|
requireSetup: boolean;
|
||||||
cacheRemoteFiles: boolean;
|
cacheRemoteFiles: boolean;
|
||||||
cacheRemoteSensitiveFiles: boolean;
|
cacheRemoteSensitiveFiles: boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
allowUnsignedFetch: 'never' | 'always' | 'essential';
|
||||||
};
|
};
|
||||||
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
|
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
|
||||||
SystemWebhook: {
|
SystemWebhook: {
|
||||||
|
@ -8870,6 +8874,9 @@ export type operations = {
|
||||||
trustedLinkUrlPatterns: string[];
|
trustedLinkUrlPatterns: string[];
|
||||||
federation: string;
|
federation: string;
|
||||||
federationHosts: string[];
|
federationHosts: string[];
|
||||||
|
hasLegacyAuthFetchSetting: boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
allowUnsignedFetch: 'never' | 'always' | 'essential';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -11486,6 +11493,8 @@ export type operations = {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
federation?: 'all' | 'none' | 'specified';
|
federation?: 'all' | 'none' | 'specified';
|
||||||
federationHosts?: string[];
|
federationHosts?: string[];
|
||||||
|
/** @enum {string} */
|
||||||
|
allowUnsignedFetch?: 'never' | 'always' | 'essential';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -22981,6 +22990,8 @@ export type operations = {
|
||||||
defaultCW?: string | null;
|
defaultCW?: string | null;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
defaultCWPriority?: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
defaultCWPriority?: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
||||||
|
/** @enum {string} */
|
||||||
|
allowUnsignedFetch?: 'never' | 'always' | 'essential' | 'staff';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -514,3 +514,18 @@ fetchLinkedNote: "Fetch linked note"
|
||||||
|
|
||||||
_processErrors:
|
_processErrors:
|
||||||
quoteUnavailable: "Unable to process quote. This post may be missing context."
|
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."
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue