Implement instance blocking (#4182)

* Implement instance blocking

* Add missing text

* Delete unnecessary file

* Covert Punycode to Unicode
This commit is contained in:
syuilo 2019-02-08 04:26:43 +09:00 committed by GitHub
parent 5a28632af7
commit e6612f610c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 105 additions and 4 deletions

View file

@ -1383,6 +1383,7 @@ admin/views/federation.vue:
latest-request-received-at: "直近のリクエスト受信" latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除" remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会" lookup: "照会"
instances: "インスタンス" instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません" instance-not-registered: "そのインスタンスは登録されていません"
@ -1398,6 +1399,11 @@ admin/views/federation.vue:
followingDesc: "フォローが多い順" followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順" followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順" followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue: desktop/views/pages/welcome.vue:
about: "詳しく..." about: "詳しく..."

View file

@ -39,6 +39,7 @@
<ui-input :value="instance.latestRequestReceivedAt" type="text" readonly> <ui-input :value="instance.latestRequestReceivedAt" type="text" readonly>
<span>{{ $t('latest-request-received-at') }}</span> <span>{{ $t('latest-request-received-at') }}</span>
</ui-input> </ui-input>
<ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch>
<section> <section>
<ui-button @click="removeAllFollowing()"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button> <ui-button @click="removeAllFollowing()"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button>
<ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info> <ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info>
@ -64,6 +65,11 @@
<option value="-followers">{{ $t('sorts.followersAsc') }}</option> <option value="-followers">{{ $t('sorts.followersAsc') }}</option>
<option value="+followers">{{ $t('sorts.followersDesc') }}</option> <option value="+followers">{{ $t('sorts.followersDesc') }}</option>
</ui-select> </ui-select>
<ui-select v-model="state">
<span slot="label">{{ $t('state') }}</span>
<option value="all">{{ $t('states.all') }}</option>
<option value="blocked">{{ $t('states.blocked') }}</option>
</ui-select>
</ui-horizon-group> </ui-horizon-group>
<div class="instances"> <div class="instances">
@ -84,6 +90,8 @@
<span>{{ instance.latestStatus }}</span> <span>{{ instance.latestStatus }}</span>
</div> </div>
</div> </div>
<ui-info v-if="instances.length == limit">{{ $t('result-is-truncated', { n: limit }) }}</ui-info>
</section> </section>
</ui-card> </ui-card>
</div> </div>
@ -102,6 +110,7 @@ export default Vue.extend({
instance: null, instance: null,
target: null, target: null,
sort: '+caughtAt', sort: '+caughtAt',
state: 'all',
limit: 50, limit: 50,
instances: [], instances: [],
faGlobe, faTerminal, faSearch, faMinusCircle faGlobe, faTerminal, faSearch, faMinusCircle
@ -110,7 +119,10 @@ export default Vue.extend({
watch: { watch: {
sort() { sort() {
this.instances = []; this.fetchInstances();
},
state() {
this.fetchInstances(); this.fetchInstances();
}, },
}, },
@ -137,9 +149,11 @@ export default Vue.extend({
}, },
fetchInstances() { fetchInstances() {
this.instances = [];
this.$root.api('federation/instances', { this.$root.api('federation/instances', {
state: this.state,
sort: this.sort, sort: this.sort,
limit: 50 limit: this.limit
}).then(instances => { }).then(instances => {
this.instances = instances; this.instances = instances;
}); });
@ -154,7 +168,14 @@ export default Vue.extend({
splash: true splash: true
}); });
}); });
} },
updateInstance() {
this.$root.api('admin/federation/update-instance', {
host: this.instance.host,
isBlocked: this.instance.isBlocked,
});
},
} }
}); });
</script> </script>

View file

@ -57,4 +57,9 @@ export interface IInstance {
* *
*/ */
latestRequestReceivedAt?: Date; latestRequestReceivedAt?: Date;
/**
*
*/
isBlocked: boolean;
} }

View file

@ -45,6 +45,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
return; return;
} }
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`);
done();
return;
}
user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser; user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
} else { } else {
// アクティビティ内のホストの検証 // アクティビティ内のホストの検証
@ -57,6 +66,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
return; return;
} }
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`);
done();
return;
}
user = await User.findOne({ user = await User.findOne({
host: { $ne: null }, host: { $ne: null },
'publicKey.id': signature.keyId 'publicKey.id': signature.keyId

View file

@ -4,11 +4,13 @@ import { URL } from 'url';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { lookup, IRunOptions } from 'lookup-dns-cache'; import { lookup, IRunOptions } from 'lookup-dns-cache';
import * as promiseAny from 'promise-any'; import * as promiseAny from 'promise-any';
import { toUnicode } from 'punycode';
import config from '../../config'; import config from '../../config';
import { ILocalUser } from '../../models/user'; import { ILocalUser } from '../../models/user';
import { publishApLogStream } from '../../services/stream'; import { publishApLogStream } from '../../services/stream';
import { apLogger } from './logger'; import { apLogger } from './logger';
import Instance from '../../models/instance';
export const logger = apLogger.createSubLogger('deliver'); export const logger = apLogger.createSubLogger('deliver');
@ -19,6 +21,11 @@ export default (user: ILocalUser, url: string, object: any) => new Promise(async
const { protocol, host, hostname, port, pathname, search } = new URL(url); const { protocol, host, hostname, port, pathname, search } = new URL(url);
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: toUnicode(host) });
if (instance && instance.isBlocked) return;
const data = JSON.stringify(object); const data = JSON.stringify(object);
const sha256 = crypto.createHash('sha256'); const sha256 = crypto.createHash('sha256');

View file

@ -0,0 +1,34 @@
import $ from 'cafy';
import define from '../../../define';
import Instance from '../../../../../models/instance';
export const meta = {
requireCredential: true,
requireModerator: true,
params: {
host: {
validator: $.str
},
isBlocked: {
validator: $.bool
},
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const instance = await Instance.findOne({ host: ps.host });
if (instance == null) {
return rej('instance not found');
}
Instance.update({ host: ps.host }, {
$set: {
isBlocked: ps.isBlocked
}
});
res();
}));

View file

@ -6,6 +6,10 @@ export const meta = {
requireCredential: false, requireCredential: false,
params: { params: {
state: {
validator: $.str.optional,
},
limit: { limit: {
validator: $.num.optional.range(1, 100), validator: $.num.optional.range(1, 100),
default: 30 default: 30
@ -73,8 +77,14 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
}; };
} }
const q = {} as any;
if (ps.state === 'blocked') {
q.isBlocked = true;
}
const instances = await Instance const instances = await Instance
.find({}, { .find(q, {
limit: ps.limit, limit: ps.limit,
sort: sort, sort: sort,
skip: ps.offset skip: ps.offset