diff options
| author | MeiMei <30769358+mei23@users.noreply.github.com> | 2020-10-18 01:46:40 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-10-18 01:46:40 +0900 |
| commit | 85a0f696bcea779b02749dae596fff94a1df2467 (patch) | |
| tree | 747ee51f96c7ada22a835dfc7faee0b13cdfd0e4 /src | |
| parent | Fix lint (#6732) (diff) | |
| download | sharkey-85a0f696bcea779b02749dae596fff94a1df2467.tar.gz sharkey-85a0f696bcea779b02749dae596fff94a1df2467.tar.bz2 sharkey-85a0f696bcea779b02749dae596fff94a1df2467.zip | |
ActivityPubでリモートのオブジェクトをGETするときのリクエストをHTTP Signatureで署名するオプション (#6731)
* Sign ActivityPub GET
* Fix v12, v12.48.0 UI bug
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/pages/follow.vue | 8 | ||||
| -rw-r--r-- | src/client/scripts/search.ts | 6 | ||||
| -rw-r--r-- | src/config/types.ts | 2 | ||||
| -rw-r--r-- | src/misc/fetch.ts | 5 | ||||
| -rw-r--r-- | src/remote/activitypub/request.ts | 97 | ||||
| -rw-r--r-- | src/remote/activitypub/resolver.ts | 13 | ||||
| -rw-r--r-- | src/services/instance-actor.ts | 17 |
7 files changed, 141 insertions, 7 deletions
diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue index 35d5cc3b26..13e0a62a0d 100644 --- a/src/client/pages/follow.vue +++ b/src/client/pages/follow.vue @@ -12,6 +12,7 @@ export default defineComponent({ const acct = new URL(location.href).searchParams.get('acct'); if (acct == null) return; + /* const dialog = os.dialog({ type: 'waiting', text: this.$t('fetchingAsApObject') + '...', @@ -19,6 +20,7 @@ export default defineComponent({ showCancelButton: false, cancelableByBgClick: false }); + */ if (acct.startsWith('https://')) { os.api('ap/show', { @@ -26,6 +28,8 @@ export default defineComponent({ }).then(res => { if (res.type == 'User') { this.follow(res.object); + } else if (res.type === 'Note') { + this.$router.push(`/notes/${res.object.id}`); } else { os.dialog({ type: 'error', @@ -42,7 +46,7 @@ export default defineComponent({ window.close(); }); }).finally(() => { - dialog.close(); + //dialog.close(); }); } else { os.api('users/show', parseAcct(acct)).then(user => { @@ -55,7 +59,7 @@ export default defineComponent({ window.close(); }); }).finally(() => { - dialog.close(); + //dialog.close(); }); } }, diff --git a/src/client/scripts/search.ts b/src/client/scripts/search.ts index 45cc691fe4..fbdc32dfb1 100644 --- a/src/client/scripts/search.ts +++ b/src/client/scripts/search.ts @@ -48,6 +48,7 @@ export async function search(q?: string | null | undefined) { } if (q.startsWith('https://')) { + /* const dialog = os.dialog({ type: 'waiting', text: i18n.global.t('fetchingAsApObject') + '...', @@ -55,19 +56,20 @@ export async function search(q?: string | null | undefined) { showCancelButton: false, cancelableByBgClick: false }); + */ try { const res = await os.api('ap/show', { uri: q }); - dialog.cancel(); + //dialog.cancel(); if (res.type === 'User') { router.push(`/@${res.object.username}@${res.object.host}`); } else if (res.type === 'Note') { router.push(`/notes/${res.object.id}`); } } catch (e) { - dialog.cancel(); + //dialog.cancel(); // TODO: Show error } diff --git a/src/config/types.ts b/src/config/types.ts index 4f025750b0..8084be1864 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -58,6 +58,8 @@ export type Source = { }; mediaProxy?: string; + + signToActivityPubGet?: boolean; }; /** diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts index 7be0e53fd4..90d89a4392 100644 --- a/src/misc/fetch.ts +++ b/src/misc/fetch.ts @@ -5,6 +5,7 @@ import fetch, { HeadersInit } from 'node-fetch'; import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import config from '../config'; +import { URL } from 'url'; export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: HeadersInit) { const res = await fetch(url, { @@ -69,14 +70,14 @@ const _https = new https.Agent({ * Get http proxy or non-proxy agent */ export const httpAgent = config.proxy - ? new HttpProxyAgent(config.proxy) + ? new HttpProxyAgent(config.proxy) as unknown as http.Agent : _http; /** * Get https proxy or non-proxy agent */ export const httpsAgent = config.proxy - ? new HttpsProxyAgent(config.proxy) + ? new HttpsProxyAgent(config.proxy) as unknown as https.Agent : _https; /** diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index ab51fdd93c..0edfcee1e3 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -1,3 +1,4 @@ +import * as http from 'http'; import * as https from 'https'; import { sign } from 'http-signature'; import * as crypto from 'crypto'; @@ -7,6 +8,9 @@ import { ILocalUser } from '../../models/entities/user'; import { UserKeypairs } from '../../models'; import { ensure } from '../../prelude/ensure'; import { getAgentByUrl } from '../../misc/fetch'; +import { URL } from 'url'; +import got from 'got'; +import * as Got from 'got'; export default async (user: ILocalUser, url: string, object: any) => { const timeout = 10 * 1000; @@ -62,3 +66,96 @@ export default async (user: ILocalUser, url: string, object: any) => { req.end(data); }); }; + +/** + * Get AP object with http-signature + * @param user http-signature user + * @param url URL to fetch + */ +export async function signedGet(url: string, user: ILocalUser) { + const timeout = 10 * 1000; + + const keypair = await UserKeypairs.findOne({ + userId: user.id + }).then(ensure); + + const req = got.get<any>(url, { + headers: { + 'Accept': 'application/activity+json, application/ld+json', + 'User-Agent': config.userAgent, + }, + responseType: 'json', + timeout, + hooks: { + beforeRequest: [ + options => { + options.request = (url: URL, opt: http.RequestOptions, callback?: (response: any) => void) => { + // Select custom agent by URL + opt.agent = getAgentByUrl(url, false); + + // Wrap original https?.request + const requestFunc = url.protocol === 'http:' ? http.request : https.request; + const clientRequest = requestFunc(url, opt, callback) as http.ClientRequest; + + // HTTP-Signature + sign(clientRequest, { + authorizationHeaderName: 'Signature', + key: keypair.privateKey, + keyId: `${config.url}/users/${user.id}#main-key`, + headers: ['(request-target)', 'host', 'date', 'accept'] + }); + + return clientRequest; + }; + }, + ], + }, + retry: 0, + }); + + const res = await receiveResponce(req, 10 * 1024 * 1024); + + return res.body; +} + +/** + * Receive response (with size limit) + * @param req Request + * @param maxSize size limit + */ +export async function receiveResponce<T>(req: Got.CancelableRequest<Got.Response<T>>, maxSize: number) { + // 応答ヘッダでサイズチェック + req.on('response', (res: Got.Response) => { + const contentLength = res.headers['content-length']; + if (contentLength != null) { + const size = Number(contentLength); + if (size > maxSize) { + req.cancel(); + } + } + }); + + // 受信中のデータでサイズチェック + req.on('downloadProgress', (progress: Got.Progress) => { + if (progress.transferred > maxSize) { + req.cancel(); + } + }); + + // 応答取得 with ステータスコードエラーの整形 + const res = await req.catch(e => { + if (e.name === 'HTTPError') { + const statusCode = (e as Got.HTTPError).response.statusCode; + const statusMessage = (e as Got.HTTPError).response.statusMessage; + throw { + name: `StatusError`, + statusCode, + message: `${statusCode} ${statusMessage}`, + }; + } else { + throw e; + } + }); + + return res; +} diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index f4bf8f94f9..2871c1cb41 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,8 +1,13 @@ +import config from '../../config'; import { getJson } from '../../misc/fetch'; +import { ILocalUser } from '../../models/entities/user'; +import { getInstanceActor } from '../../services/instance-actor'; +import { signedGet } from './request'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type'; export default class Resolver { private history: Set<string>; + private user?: ILocalUser; constructor() { this.history = new Set(); @@ -39,7 +44,13 @@ export default class Resolver { this.history.add(value); - const object = await getJson(value, 'application/activity+json, application/ld+json'); + if (config.signToActivityPubGet && !this.user) { + this.user = await getInstanceActor(); + } + + const object = this.user + ? await signedGet(value, this.user) + : await getJson(value, 'application/activity+json, application/ld+json'); if (object == null || ( Array.isArray(object['@context']) ? diff --git a/src/services/instance-actor.ts b/src/services/instance-actor.ts new file mode 100644 index 0000000000..74591846fa --- /dev/null +++ b/src/services/instance-actor.ts @@ -0,0 +1,17 @@ +import { createSystemUser } from './create-system-user'; +import { ILocalUser } from '../models/entities/user'; +import { Users } from '../models'; + +const ACTOR_USERNAME = 'instance.actor' as const; + +export async function getInstanceActor(): Promise<ILocalUser> { + const user = await Users.findOne({ + host: null, + username: ACTOR_USERNAME + }); + + if (user) return user as ILocalUser; + + const created = await createSystemUser(ACTOR_USERNAME); + return created as ILocalUser; +} |