Add Secure Mode and Private Mode

- Add instance actor
- Add private mode, which uses an allowlist
- Add Secure Mode, restricts access to blocked instances
This commit is contained in:
nullobsi 2021-07-20 09:45:41 -07:00 committed by Kayden Tebau
parent 7887082622
commit a03f13fb3f
No known key found for this signature in database
GPG key ID: 933A1F44222C2634
11 changed files with 289 additions and 13 deletions

View file

@ -77,6 +77,21 @@ export class Meta {
}) })
public blockedHosts: string[]; public blockedHosts: string[];
@Column('boolean', {
default: false
})
public secureMode: boolean;
@Column('boolean', {
default: false
})
public privateMode: boolean;
@Column('varchar', {
length: 256, array: true, default: '{}'
})
public allowedHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}',
}) })

View file

@ -28,6 +28,10 @@ export default async (job: Bull.Job<DeliverJobData>) => {
return 'skip (blocked)'; return 'skip (blocked)';
} }
if (meta.privateMode && !meta.allowedHosts.includes(toPuny(host))) {
return 'skip (not allowed)';
}
// isSuspendedなら中断 // isSuspendedなら中断
let suspendedHosts = suspendedHostsCache.get(null); let suspendedHosts = suspendedHostsCache.get(null);
if (suspendedHosts == null) { if (suspendedHosts == null) {

View file

@ -39,6 +39,11 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `Blocked request: ${host}`; return `Blocked request: ${host}`;
} }
// 非公開モードなら許可なインスタンスのみ
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
return `Blocked request: ${host}`;
}
const keyIdLower = signature.keyId.toLowerCase(); const keyIdLower = signature.keyId.toLowerCase();
if (keyIdLower.startsWith('acct:')) { if (keyIdLower.startsWith('acct:')) {
return `Old keyId is no longer supported. ${keyIdLower}`; return `Old keyId is no longer supported. ${keyIdLower}`;

View file

@ -0,0 +1,73 @@
import config from '@/config/index.js';
import { IncomingMessage } from 'http';
import { fetchMeta } from '@/misc/fetch-meta.js';
import httpSignature from '@peertube/http-signature';
import { URL } from 'url';
import { toPuny } from '@/misc/convert-host.js';
import DbResolver from '@/remote/activitypub/db-resolver.js';
import { getApId } from '@/remote/activitypub/type.js';
export default async function checkFetch(req: IncomingMessage): Promise<number> {
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
let signature;
try {
signature = httpSignature.parseRequest(req, { 'headers': [] });
} catch (e) {
return 401;
}
const keyId = new URL(signature.keyId);
const host = toPuny(keyId.hostname);
if (meta.blockedHosts.includes(host)) {
return 403;
}
if (meta.privateMode && host !== config.host && !meta.allowedHosts.includes(host)) {
return 403;
}
const keyIdLower = signature.keyId.toLowerCase();
if (keyIdLower.startsWith('acct:')) {
// Old keyId is no longer supported.
return 401;
}
const dbResolver = new DbResolver();
// HTTP-Signature keyIdを元にDBから取得
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、resolveしてみる
if (authUser == null) {
try {
keyId.hash = '';
authUser = await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
} catch (e) {
// できなければ駄目
return 403;
}
}
// publicKey がなくても終了
if (authUser?.key == null) {
return 403;
}
// もう一回チェック
if (authUser.user.host !== host) {
return 403;
}
// HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
if (!httpSignatureValidated) {
return 403;
}
}
return 200;
}

View file

@ -72,6 +72,10 @@ export default class Resolver {
throw new Error('Instance is blocked'); throw new Error('Instance is blocked');
} }
if (meta.privateMode && config.host !== host && !meta.allowedHosts.includes(host)) {
throw new Error('Instance is not allowed');
}
if (config.signToActivityPubGet && !this.user) { if (config.signToActivityPubGet && !this.user) {
this.user = await getInstanceActor(); this.user = await getInstanceActor();
} }

View file

@ -12,12 +12,15 @@ import Followers from './activitypub/followers.js';
import Following from './activitypub/following.js'; import Following from './activitypub/following.js';
import Featured from './activitypub/featured.js'; import Featured from './activitypub/featured.js';
import { inbox as processInbox } from '@/queue/index.js'; import { inbox as processInbox } from '@/queue/index.js';
import { isSelfHost } from '@/misc/convert-host.js'; import { isSelfHost, toPuny } from '@/misc/convert-host.js';
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
import { ILocalUser, User } from '@/models/entities/user.js'; import { ILocalUser, User } from '@/models/entities/user.js';
import { In, IsNull, Not } from 'typeorm'; import { In, IsNull, Not } from 'typeorm';
import { renderLike } from '@/remote/activitypub/renderer/like.js'; import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { getUserKeypair } from '@/misc/keypair-store.js'; import { getUserKeypair } from '@/misc/keypair-store.js';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { getInstanceActor } from '@/services/instance-actor.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js';
// Init router // Init router
@ -66,6 +69,12 @@ router.post('/users/:user/inbox', json(), inbox);
router.get('/notes/:note', async (ctx, next) => { router.get('/notes/:note', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next(); if (!isActivityPubReq(ctx)) return await next();
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const note = await Notes.findOneBy({ const note = await Notes.findOneBy({
id: ctx.params.note, id: ctx.params.note,
visibility: In(['public' as const, 'home' as const]), visibility: In(['public' as const, 'home' as const]),
@ -88,12 +97,24 @@ router.get('/notes/:note', async (ctx, next) => {
} }
ctx.body = renderActivity(await renderNote(note, false)); ctx.body = renderActivity(await renderNote(note, false));
ctx.set('Cache-Control', 'public, max-age=180');
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
}); });
// note activity // note activity
router.get('/notes/:note/activity', async ctx => { router.get('/notes/:note/activity', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const note = await Notes.findOneBy({ const note = await Notes.findOneBy({
id: ctx.params.note, id: ctx.params.note,
userHost: IsNull(), userHost: IsNull(),
@ -107,7 +128,12 @@ router.get('/notes/:note/activity', async ctx => {
} }
ctx.body = renderActivity(await packActivity(note)); ctx.body = renderActivity(await packActivity(note));
ctx.set('Cache-Control', 'public, max-age=180'); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
}); });
@ -125,6 +151,20 @@ router.get('/users/:user/collections/featured', Featured);
// publickey // publickey
router.get('/users/:user/publickey', async ctx => { router.get('/users/:user/publickey', async ctx => {
const instanceActor = await getInstanceActor();
if (ctx.params.user === instanceActor.id) {
ctx.body = renderActivity(renderKey(instanceActor, await getUserKeypair(instanceActor.id)));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
return;
}
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const userId = ctx.params.user; const userId = ctx.params.user;
const user = await Users.findOneBy({ const user = await Users.findOneBy({
@ -141,7 +181,12 @@ router.get('/users/:user/publickey', async ctx => {
if (Users.isLocalUser(user)) { if (Users.isLocalUser(user)) {
ctx.body = renderActivity(renderKey(user, keypair)); ctx.body = renderActivity(renderKey(user, keypair));
ctx.set('Cache-Control', 'public, max-age=180'); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
} else { } else {
ctx.status = 400; ctx.status = 400;
@ -156,13 +201,30 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
} }
ctx.body = renderActivity(await renderPerson(user as ILocalUser)); ctx.body = renderActivity(await renderPerson(user as ILocalUser));
ctx.set('Cache-Control', 'public, max-age=180'); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
} }
router.get('/users/:user', async (ctx, next) => { router.get('/users/:user', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next(); if (!isActivityPubReq(ctx)) return await next();
const instanceActor = await getInstanceActor();
if (ctx.params.user === instanceActor.id) {
await userInfo(ctx, instanceActor);
return;
}
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const userId = ctx.params.user; const userId = ctx.params.user;
const user = await Users.findOneBy({ const user = await Users.findOneBy({
@ -177,6 +239,18 @@ router.get('/users/:user', async (ctx, next) => {
router.get('/@:user', async (ctx, next) => { router.get('/@:user', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next(); if (!isActivityPubReq(ctx)) return await next();
if (ctx.params.user === 'instance.actor') {
const instanceActor = await getInstanceActor();
await userInfo(ctx, instanceActor);
return;
}
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const user = await Users.findOneBy({ const user = await Users.findOneBy({
usernameLower: ctx.params.user.toLowerCase(), usernameLower: ctx.params.user.toLowerCase(),
host: IsNull(), host: IsNull(),
@ -185,10 +259,21 @@ router.get('/@:user', async (ctx, next) => {
await userInfo(ctx, user); await userInfo(ctx, user);
}); });
router.get('/actor', async (ctx, next) => {
const instanceActor = await getInstanceActor();
await userInfo(ctx, instanceActor);
});
//#endregion //#endregion
// emoji // emoji
router.get('/emojis/:emoji', async ctx => { router.get('/emojis/:emoji', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const emoji = await Emojis.findOneBy({ const emoji = await Emojis.findOneBy({
host: IsNull(), host: IsNull(),
name: ctx.params.emoji, name: ctx.params.emoji,
@ -200,12 +285,23 @@ router.get('/emojis/:emoji', async ctx => {
} }
ctx.body = renderActivity(await renderEmoji(emoji)); ctx.body = renderActivity(await renderEmoji(emoji));
ctx.set('Cache-Control', 'public, max-age=180'); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
}); });
// like // like
router.get('/likes/:like', async ctx => { router.get('/likes/:like', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); const reaction = await NoteReactions.findOneBy({ id: ctx.params.like });
if (reaction == null) { if (reaction == null) {
@ -221,12 +317,22 @@ router.get('/likes/:like', async ctx => {
} }
ctx.body = renderActivity(await renderLike(reaction, note)); ctx.body = renderActivity(await renderLike(reaction, note));
ctx.set('Cache-Control', 'public, max-age=180'); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
}); });
// follow // follow
router.get('/follows/:follower/:followee', async ctx => { router.get('/follows/:follower/:followee', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
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.
@ -247,7 +353,12 @@ router.get('/follows/:follower/:followee', async ctx => {
} }
ctx.body = renderActivity(renderFollow(follower, followee)); ctx.body = renderActivity(renderFollow(follower, followee));
ctx.set('Cache-Control', 'public, max-age=180'); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
}); });

View file

@ -6,8 +6,16 @@ import { setResponseType } from '../activitypub.js';
import renderNote from '@/remote/activitypub/renderer/note.js'; import renderNote from '@/remote/activitypub/renderer/note.js';
import { Users, Notes, UserNotePinings } from '@/models/index.js'; import { Users, Notes, UserNotePinings } from '@/models/index.js';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const userId = ctx.params.user; const userId = ctx.params.user;
const user = await Users.findOneBy({ const user = await Users.findOneBy({
@ -36,6 +44,12 @@ export default async (ctx: Router.RouterContext) => {
); );
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180');
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
setResponseType(ctx); setResponseType(ctx);
}; };

View file

@ -9,8 +9,16 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { Users, Followings, UserProfiles } from '@/models/index.js'; import { Users, Followings, UserProfiles } from '@/models/index.js';
import { Following } from '@/models/entities/following.js'; import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js'; import { setResponseType } from '../activitypub.js';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const userId = ctx.params.user; const userId = ctx.params.user;
const cursor = ctx.request.query.cursor; const cursor = ctx.request.query.cursor;
@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
// index page // index page
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
} }
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
}; };

View file

@ -9,8 +9,16 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { Users, Followings, UserProfiles } from '@/models/index.js'; import { Users, Followings, UserProfiles } from '@/models/index.js';
import { Following } from '@/models/entities/following.js'; import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js'; import { setResponseType } from '../activitypub.js';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const userId = ctx.params.user; const userId = ctx.params.user;
const cursor = ctx.request.query.cursor; const cursor = ctx.request.query.cursor;
@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
// index page // index page
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
} }
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
}; };

View file

@ -13,8 +13,16 @@ import { Users, Notes } from '@/models/index.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { makePaginationQuery } from '../api/common/make-pagination-query.js'; import { makePaginationQuery } from '../api/common/make-pagination-query.js';
import { setResponseType } from '../activitypub.js'; import { setResponseType } from '../activitypub.js';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
ctx.status = verify;
return;
}
const userId = ctx.params.user; const userId = ctx.params.user;
const sinceId = ctx.request.query.since_id; const sinceId = ctx.request.query.since_id;
@ -89,9 +97,15 @@ export default async (ctx: Router.RouterContext) => {
`${partOf}?page=true&since_id=000000000000000000000000`, `${partOf}?page=true&since_id=000000000000000000000000`,
); );
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
} }
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
}; };
/** /**

View file

@ -291,6 +291,16 @@ export const meta = {
}, },
}, },
}, },
secureMode: {
type: 'boolean',
optional: true, nullable: false,
default: false,
},
privateMode: {
type: 'boolean',
optional: true, nullable: false,
default: false,
},
}, },
}, },
} as const; } as const;
@ -326,7 +336,7 @@ export default define(meta, paramDef, async (ps, me) => {
expiresAt: MoreThan(new Date()), expiresAt: MoreThan(new Date()),
}, },
}); });
// TODO: add secure mode, etc
const response: any = { const response: any = {
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail, maintainerEmail: instance.maintainerEmail,