Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pub-relay #6341

Merged
merged 7 commits into from May 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions migration/1589023282116-pubRelay.ts
@@ -0,0 +1,18 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class pubRelay1589023282116 implements MigrationInterface {
name = 'pubRelay1589023282116'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "relay_status_enum" AS ENUM('requesting', 'accepted', 'rejected')`, undefined);
await queryRunner.query(`CREATE TABLE "relay" ("id" character varying(32) NOT NULL, "inbox" character varying(512) NOT NULL, "status" "relay_status_enum" NOT NULL, CONSTRAINT "PK_78ebc9cfddf4292633b7ba57aee" PRIMARY KEY ("id"))`, undefined);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab" ON "relay" ("inbox") `, undefined);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab"`, undefined);
await queryRunner.query(`DROP TABLE "relay"`, undefined);
await queryRunner.query(`DROP TYPE "relay_status_enum"`, undefined);
}

}
5 changes: 5 additions & 0 deletions src/client/app.vue
Expand Up @@ -413,6 +413,11 @@ export default Vue.extend({
text: this.$t('federation'),
to: '/instance/federation',
icon: faGlobe,
}, {
type: 'link',
text: this.$t('relays'),
to: '/instance/relays',
icon: faLaugh,
}, {
type: 'link',
text: this.$t('announcements'),
Expand Down
97 changes: 97 additions & 0 deletions src/client/pages/instance/relays.vue
@@ -0,0 +1,97 @@
<template>
<div class="relaycxt">
<portal to="icon"><fa :icon="faBroadcastTower"/></portal>
<portal to="title">{{ $t('relays') }}</portal>


<section class="_card lookup">
<mk-input v-model="inbox">
<span>{{ $t('inbox') }}</span>
</mk-input>
<mk-button @click="add(inbox)" primary style="margin: 0 auto 16px auto;"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
</section>

<section class="_card relays">
<div class="_content relay" v-for="relay in relays" :key="relay.inbox">
<div>{{ relay.inbox }}</div>
<div>{{ relay.status }}</div>
<div class="buttons">
<mk-button class="button" inline @click="remove(relay.inbox)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
</div>
</div>
</section>
</div>
</template>

<script lang="ts">
import Vue from 'vue';
import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../i18n';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';

export default Vue.extend({
i18n,

metaInfo() {
return {
title: this.$t('relays') as string
};
},

components: {
MkButton,
MkInput,
MkTextarea,
},

data() {
return {
relays: [],
inbox: '',
faBroadcastTower, faSave, faTrashAlt, faPlus
}
},

created() {
this.$root.api('admin/relays/list').then(relays => {
this.relays = relays;
});
},

methods: {
add(inbox: string) {
this.$root.api('admin/relays/add', {
inbox
}).then(relay => {

});
},

remove(inbox: string) {
this.$root.api('admin/relays/remove', {
inbox
}).then(() => {

});
},

}
});
</script>

<style lang="scss" scoped>
.ztgjmzrw {
> .relays {
> .relay {
> .buttons {
> .button:first-child {
margin-right: 8px;
}
}
}
}
}
</style>
1 change: 1 addition & 0 deletions src/client/router.ts
Expand Up @@ -58,6 +58,7 @@ export const router = new VueRouter({
{ path: '/instance/queue', component: page('instance/queue') },
{ path: '/instance/settings', component: page('instance/settings') },
{ path: '/instance/federation', component: page('instance/federation') },
{ path: '/instance/relays', component: page('instance/relays') },
{ path: '/instance/announcements', component: page('instance/announcements') },
{ path: '/notes/:note', name: 'note', component: page('note') },
{ path: '/tags/:tag', component: page('tag') },
Expand Down
2 changes: 2 additions & 0 deletions src/db/postgre.ts
Expand Up @@ -58,6 +58,7 @@ import { AntennaNote } from '../models/entities/antenna-note';
import { PromoNote } from '../models/entities/promo-note';
import { PromoRead } from '../models/entities/promo-read';
import { program } from '../argv';
import { Relay } from '../models/entities/relay';

const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);

Expand Down Expand Up @@ -149,6 +150,7 @@ export const entities = [
PromoRead,
ReversiGame,
ReversiMatching,
Relay,
...charts as any
];

Expand Down
36 changes: 36 additions & 0 deletions src/misc/gen-key-pair.ts
@@ -0,0 +1,36 @@
import * as crypto from 'crypto';
import * as util from 'util';

const generateKeyPair = util.promisify(crypto.generateKeyPair);

export async function genRsaKeyPair(modulusLength = 2048) {
return await generateKeyPair('rsa', {
modulusLength,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: undefined,
passphrase: undefined
}
});
}

export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') {
return await generateKeyPair('ec', {
namedCurve,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: undefined,
passphrase: undefined
}
});
}
19 changes: 19 additions & 0 deletions src/models/entities/relay.ts
@@ -0,0 +1,19 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';

@Entity()
export class Relay {
@PrimaryColumn(id())
public id: string;

@Index({ unique: true })
@Column('varchar', {
length: 512, nullable: false,
})
public inbox: string;

@Column('enum', {
enum: ['requesting', 'accepted', 'rejected'],
})
public status: 'requesting' | 'accepted' | 'rejected';
}
2 changes: 2 additions & 0 deletions src/models/index.ts
Expand Up @@ -52,6 +52,7 @@ import { AntennaNote } from './entities/antenna-note';
import { PromoNote } from './entities/promo-note';
import { PromoRead } from './entities/promo-read';
import { EmojiRepository } from './repositories/emoji';
import { RelayRepository } from './repositories/relay';

export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead);
Expand Down Expand Up @@ -106,3 +107,4 @@ export const Antennas = getCustomRepository(AntennaRepository);
export const AntennaNotes = getRepository(AntennaNote);
export const PromoNotes = getRepository(PromoNote);
export const PromoReads = getRepository(PromoRead);
export const Relays = getCustomRepository(RelayRepository);
6 changes: 6 additions & 0 deletions src/models/repositories/relay.ts
@@ -0,0 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { Relay } from '../entities/relay';

@EntityRepository(Relay)
export class RelayRepository extends Repository<Relay> {
}
10 changes: 4 additions & 6 deletions src/queue/processors/inbox.ts
Expand Up @@ -56,12 +56,10 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
}

// HTTP-Signatureの検証
if (!httpSignature.verifySignature(signature, authUser.key.keyPem)) {
return 'signature verification failed';
}
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);

// signatureのsignerは、activity.actorと一致する必要がある
if (authUser.user.uri !== activity.actor) {
// また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
if (activity.signature) {
if (activity.signature.type !== 'RsaSignature2017') {
Expand Down Expand Up @@ -93,7 +91,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
}
} else {
return 'signature verification failed';
throw `skip: http-signature verification failed.`;
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/remote/activitypub/kernel/accept/follow.ts
Expand Up @@ -2,6 +2,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import accept from '../../../../services/following/requests/accept';
import { IFollow } from '../../type';
import DbResolver from '../../db-resolver';
import { relayAccepted } from '../../../../services/relay';

export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
Expand All @@ -17,6 +18,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower is not a local user`;
}

// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await relayAccepted(match[1]);
}

await accept(actor, follower);
return `ok`;
};
7 changes: 7 additions & 0 deletions src/remote/activitypub/kernel/reject/follow.ts
Expand Up @@ -2,6 +2,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import reject from '../../../../services/following/requests/reject';
import { IFollow } from '../../type';
import DbResolver from '../../db-resolver';
import { relayRejected } from '../../../../services/relay';

export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
Expand All @@ -17,6 +18,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower is not a local user`;
}

// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await relayRejected(match[1]);
}

await reject(actor, follower);
return `ok`;
};
1 change: 1 addition & 0 deletions src/remote/activitypub/misc/ld-signature.ts
Expand Up @@ -70,6 +70,7 @@ export class LdSignature {
const transformedData = { ...data };
delete transformedData['signature'];
const cannonidedData = await this.normalize(transformedData);
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
const documentHash = this.sha256(cannonidedData);
const verifyData = `${optionsHash}${documentHash}`;
return verifyData;
Expand Down
14 changes: 14 additions & 0 deletions src/remote/activitypub/renderer/follow-relay.ts
@@ -0,0 +1,14 @@
import config from '../../../config';
import { Relay } from '../../../models/entities/relay';
import { ILocalUser } from '../../../models/entities/user';

export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
const follow = {
id: `${config.url}/activities/follow-relay/${relay.id}`,
type: 'Follow',
actor: `${config.url}/users/${relayActor.id}`,
object: 'https://www.w3.org/ns/activitystreams#Public'
};

return follow;
}
49 changes: 46 additions & 3 deletions src/remote/activitypub/renderer/index.ts
@@ -1,7 +1,12 @@
import config from '../../../config';
import { v4 as uuid } from 'uuid';
import { IActivity } from '../type';
import { LdSignature } from '../misc/ld-signature';
import { ILocalUser } from '../../../models/entities/user';
import { UserKeypairs } from '../../../models';
import { ensure } from '../../../prelude/ensure';

export const renderActivity = (x: any) => {
export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null;

if (x !== null && typeof x === 'object' && x.id == null) {
Expand All @@ -11,8 +16,46 @@ export const renderActivity = (x: any) => {
return Object.assign({
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{ Hashtag: 'as:Hashtag' }
'https://w3id.org/security/v1'
]
}, x);
};

export const attachLdSignature = async (activity: any, user: ILocalUser): Promise<IActivity | null> => {
if (activity == null) return null;

const keypair = await UserKeypairs.findOne({
userId: user.id
}).then(ensure);

const obj = {
// as non-standards
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
sensitive: 'as:sensitive',
Hashtag: 'as:Hashtag',
quoteUrl: 'as:quoteUrl',
// Mastodon
toot: 'http://joinmastodon.org/ns#',
Emoji: 'toot:Emoji',
featured: 'toot:featured',
// schema
schema: 'http://schema.org#',
PropertyValue: 'schema:PropertyValue',
value: 'schema:value',
// Misskey
misskey: `${config.url}/ns#`,
'_misskey_content': 'misskey:_misskey_content',
'_misskey_quote': 'misskey:_misskey_quote',
'_misskey_reaction': 'misskey:_misskey_reaction',
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_talk': 'misskey:_misskey_talk',
};

activity['@context'].push(obj);

const ldSignature = new LdSignature();
ldSignature.debug = true;
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);

return activity;
};