summaryrefslogtreecommitdiff
path: root/src/remote
diff options
context:
space:
mode:
authorMeiMei <30769358+mei23@users.noreply.github.com>2021-10-16 17:16:24 +0900
committerGitHub <noreply@github.com>2021-10-16 17:16:24 +0900
commit482081c41b45ab3798e73c4d11e8a7c1c1f5e8c9 (patch)
treefed02348c2fce534f8101370853eb83fbf3aa899 /src/remote
parentテスト用コンテナの調整 (#7838) (diff)
downloadsharkey-482081c41b45ab3798e73c4d11e8a7c1c1f5e8c9.tar.gz
sharkey-482081c41b45ab3798e73c4d11e8a7c1c1f5e8c9.tar.bz2
sharkey-482081c41b45ab3798e73c4d11e8a7c1c1f5e8c9.zip
Refactor request (#7814)
* status code * Test ap-request.ts https://github.com/mei23/crytest/blob/4397fc5e70536e4175fe56e974ca83b8047bef3a/test/ap-request.ts * tune
Diffstat (limited to 'src/remote')
-rw-r--r--src/remote/activitypub/ap-request.ts104
-rw-r--r--src/remote/activitypub/kernel/announce/note.ts3
-rw-r--r--src/remote/activitypub/kernel/create/note.ts3
-rw-r--r--src/remote/activitypub/models/note.ts3
-rw-r--r--src/remote/activitypub/request.ts162
5 files changed, 142 insertions, 133 deletions
diff --git a/src/remote/activitypub/ap-request.ts b/src/remote/activitypub/ap-request.ts
new file mode 100644
index 0000000000..76a3857140
--- /dev/null
+++ b/src/remote/activitypub/ap-request.ts
@@ -0,0 +1,104 @@
+import * as crypto from 'crypto';
+import { URL } from 'url';
+
+type Request = {
+ url: string;
+ method: string;
+ headers: Record<string, string>;
+};
+
+type PrivateKey = {
+ privateKeyPem: string;
+ keyId: string;
+};
+
+export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }) {
+ const u = new URL(args.url);
+ const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
+
+ const request: Request = {
+ url: u.href,
+ method: 'POST',
+ headers: objectAssignWithLcKey({
+ 'Date': new Date().toUTCString(),
+ 'Host': u.hostname,
+ 'Content-Type': 'application/activity+json',
+ 'Digest': digestHeader,
+ }, args.additionalHeaders),
+ };
+
+ const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
+
+ return {
+ request,
+ signingString: result.signingString,
+ signature: result.signature,
+ signatureHeader: result.signatureHeader,
+ };
+}
+
+export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }) {
+ const u = new URL(args.url);
+
+ const request: Request = {
+ url: u.href,
+ method: 'GET',
+ headers: objectAssignWithLcKey({
+ 'Accept': 'application/activity+json, application/ld+json',
+ 'Date': new Date().toUTCString(),
+ 'Host': new URL(args.url).hostname,
+ }, args.additionalHeaders),
+ };
+
+ const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
+
+ return {
+ request,
+ signingString: result.signingString,
+ signature: result.signature,
+ signatureHeader: result.signatureHeader,
+ };
+}
+
+function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) {
+ const signingString = genSigningString(request, includeHeaders);
+ const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
+ const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
+
+ request.headers = objectAssignWithLcKey(request.headers, {
+ Signature: signatureHeader
+ });
+
+ return {
+ request,
+ signingString,
+ signature,
+ signatureHeader,
+ };
+}
+
+function genSigningString(request: Request, includeHeaders: string[]) {
+ request.headers = lcObjectKey(request.headers);
+
+ const results: string[] = [];
+
+ for (const key of includeHeaders.map(x => x.toLowerCase())) {
+ if (key === '(request-target)') {
+ results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
+ } else {
+ results.push(`${key}: ${request.headers[key]}`);
+ }
+ }
+
+ return results.join('\n');
+}
+
+function lcObjectKey(src: Record<string, string>) {
+ const dst: Record<string, string> = {};
+ for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
+ return dst;
+}
+
+function objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>) {
+ return Object.assign(lcObjectKey(a), lcObjectKey(b));
+}
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index b6ec090b99..5230867f24 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -8,6 +8,7 @@ import { extractDbHost } from '@/misc/convert-host';
import { fetchMeta } from '@/misc/fetch-meta';
import { getApLock } from '@/misc/app-lock';
import { parseAudience } from '../../audience';
+import { StatusError } from '@/misc/fetch';
const logger = apLogger;
@@ -41,7 +42,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
renote = await resolveNote(targetUri);
} catch (e) {
// 対象が4xxならスキップ
- if (e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
return;
}
diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
index 5dda85d0f5..14e311e4cd 100644
--- a/src/remote/activitypub/kernel/create/note.ts
+++ b/src/remote/activitypub/kernel/create/note.ts
@@ -4,6 +4,7 @@ import { createNote, fetchNote } from '../../models/note';
import { getApId, IObject, ICreate } from '../../type';
import { getApLock } from '@/misc/app-lock';
import { extractDbHost } from '@/misc/convert-host';
+import { StatusError } from '@/misc/fetch';
/**
* 投稿作成アクティビティを捌きます
@@ -32,7 +33,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
await createNote(note, resolver, silent);
return 'ok';
} catch (e) {
- if (e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
return `skip ${e.statusCode}`;
} else {
throw e;
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 25004cb4d2..cf68f3005d 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -26,6 +26,7 @@ import { createMessage } from '@/services/messages/create';
import { parseAudience } from '../audience';
import { extractApMentions } from './mention';
import DbResolver from '../db-resolver';
+import { StatusError } from '@/misc/fetch';
const logger = apLogger;
@@ -177,7 +178,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
}
} catch (e) {
return {
- status: e.statusCode >= 400 && e.statusCode < 500 ? 'permerror' : 'temperror'
+ status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror'
};
}
};
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index fe1009243c..d6ced630c1 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -1,66 +1,31 @@
-import * as http from 'http';
-import * as https from 'https';
-import { sign } from 'http-signature';
-import * as crypto from 'crypto';
-
import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { getAgentByUrl } from '@/misc/fetch';
-import { URL } from 'url';
-import got from 'got';
-import * as Got from 'got';
import { getUserKeypair } from '@/misc/keypair-store';
+import { User } from '@/models/entities/user';
+import { getResponse } from '../../misc/fetch';
+import { createSignedPost, createSignedGet } from './ap-request';
export default async (user: { id: User['id'] }, url: string, object: any) => {
- const timeout = 10 * 1000;
-
- const { protocol, hostname, port, pathname, search } = new URL(url);
-
- const data = JSON.stringify(object);
-
- const sha256 = crypto.createHash('sha256');
- sha256.update(data);
- const hash = sha256.digest('base64');
+ const body = JSON.stringify(object);
const keypair = await getUserKeypair(user.id);
- await new Promise<void>((resolve, reject) => {
- const req = https.request({
- agent: getAgentByUrl(new URL(`https://example.net`)),
- protocol,
- hostname,
- port,
- method: 'POST',
- path: pathname + search,
- timeout,
- headers: {
- 'User-Agent': config.userAgent,
- 'Content-Type': 'application/activity+json',
- 'Digest': `SHA-256=${hash}`
- }
- }, res => {
- if (res.statusCode! >= 400) {
- reject(res);
- } else {
- resolve();
- }
- });
-
- sign(req, {
- authorizationHeaderName: 'Signature',
- key: keypair.privateKey,
- keyId: `${config.url}/users/${user.id}#main-key`,
- headers: ['(request-target)', 'date', 'host', 'digest']
- });
-
- req.on('timeout', () => req.abort());
-
- req.on('error', e => {
- if (req.aborted) reject('timeout');
- reject(e);
- });
+ const req = createSignedPost({
+ key: {
+ privateKeyPem: keypair.privateKey,
+ keyId: `${config.url}/users/${user.id}#main-key`
+ },
+ url,
+ body,
+ additionalHeaders: {
+ 'User-Agent': config.userAgent,
+ }
+ });
- req.end(data);
+ await getResponse({
+ url,
+ method: req.request.method,
+ headers: req.request.headers,
+ body,
});
};
@@ -70,87 +35,24 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
* @param url URL to fetch
*/
export async function signedGet(url: string, user: { id: User['id'] }) {
- const timeout = 10 * 1000;
-
const keypair = await getUserKeypair(user.id);
- const req = got.get<any>(url, {
- headers: {
- 'Accept': 'application/activity+json, application/ld+json',
- 'User-Agent': config.userAgent,
+ const req = createSignedGet({
+ key: {
+ privateKeyPem: keypair.privateKey,
+ keyId: `${config.url}/users/${user.id}#main-key`
},
- 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();
+ url,
+ additionalHeaders: {
+ 'User-Agent': config.userAgent,
}
});
- // 応答取得 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;
- }
+ const res = await getResponse({
+ url,
+ method: req.request.method,
+ headers: req.request.headers
});
- return res;
+ return await res.json();
}