From 345a9d3525adcc36f828874a32cc54532f06454c Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Sat, 16 Oct 2021 10:10:19 +0200
Subject: [PATCH 1/3] remove unnecessary imports (#7871)

---
 src/client/pages/user/clips.vue       | 7 -------
 src/client/pages/user/follow-list.vue | 7 -------
 src/client/pages/user/gallery.vue     | 7 -------
 src/client/pages/user/pages.vue       | 7 -------
 4 files changed, 28 deletions(-)

diff --git a/src/client/pages/user/clips.vue b/src/client/pages/user/clips.vue
index fc40d583c6..53ee554383 100644
--- a/src/client/pages/user/clips.vue
+++ b/src/client/pages/user/clips.vue
@@ -12,7 +12,6 @@
 <script lang="ts">
 import { defineComponent } from 'vue';
 import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
 
 export default defineComponent({
 	components: {
@@ -43,12 +42,6 @@ export default defineComponent({
 			this.$refs.list.reload();
 		}
 	},
-
-	methods: {
-		userPage,
-		
-		acct
-	}
 });
 </script>
 
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
index f6df28309f..1f5ab5993c 100644
--- a/src/client/pages/user/follow-list.vue
+++ b/src/client/pages/user/follow-list.vue
@@ -12,7 +12,6 @@
 import { defineComponent } from 'vue';
 import MkUserInfo from '@client/components/user-info.vue';
 import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
 
 export default defineComponent({
 	components: {
@@ -51,12 +50,6 @@ export default defineComponent({
 		user() {
 			this.$refs.list.reload();
 		}
-	},
-
-	methods: {
-		userPage,
-		
-		acct
 	}
 });
 </script>
diff --git a/src/client/pages/user/gallery.vue b/src/client/pages/user/gallery.vue
index 67a5fac109..c21b3e6428 100644
--- a/src/client/pages/user/gallery.vue
+++ b/src/client/pages/user/gallery.vue
@@ -12,7 +12,6 @@
 import { defineComponent } from 'vue';
 import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
 import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
 
 export default defineComponent({
 	components: {
@@ -43,12 +42,6 @@ export default defineComponent({
 		user() {
 			this.$refs.list.reload();
 		}
-	},
-
-	methods: {
-		userPage,
-		
-		acct
 	}
 });
 </script>
diff --git a/src/client/pages/user/pages.vue b/src/client/pages/user/pages.vue
index 819bd9f2ef..ece418cf62 100644
--- a/src/client/pages/user/pages.vue
+++ b/src/client/pages/user/pages.vue
@@ -10,7 +10,6 @@
 import { defineComponent } from 'vue';
 import MkPagePreview from '@client/components/page-preview.vue';
 import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
 
 export default defineComponent({
 	components: {
@@ -41,12 +40,6 @@ export default defineComponent({
 		user() {
 			this.$refs.list.reload();
 		}
-	},
-
-	methods: {
-		userPage,
-		
-		acct
 	}
 });
 </script>

From 03b04acb1628e710dec1d768d74aedfc0a4f1ccd Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sat, 16 Oct 2021 17:12:20 +0900
Subject: [PATCH 2/3] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E7=94=A8?=
 =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=86=E3=83=8A=E3=81=AE=E8=AA=BF=E6=95=B4?=
 =?UTF-8?q?=20(#7838)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Tune test container

* docs

* fix cp config

* doc

* a
---
 .github/workflows/nodejs.yml | 10 +++++-----
 CONTRIBUTING.md              | 11 +++++++++++
 test/docker-compose.yml      | 15 +++++++++++++++
 test/test.yml                | 12 ++++++++++++
 4 files changed, 43 insertions(+), 5 deletions(-)
 create mode 100644 test/docker-compose.yml
 create mode 100644 test/test.yml

diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 9a32dac94e..a91572ad78 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -16,16 +16,16 @@ jobs:
 
     services:
       postgres:
-        image: postgres:10-alpine
+        image: postgres:12.2-alpine
         ports:
-          - 5432:5432
+          - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
       redis:
-        image: redis:alpine
+        image: redis:4.0-alpine
         ports:
-          - 6379:6379
+          - 56312:6379
 
     steps:
     - uses: actions/checkout@v2
@@ -40,7 +40,7 @@ jobs:
     - name: Check yarn.lock
       run: git diff --exit-code yarn.lock
     - name: Copy Configure
-      run: cp .circleci/misskey/*.yml .config
+      run: cp test/test.yml .config
     - name: Build
       run: yarn build
     - name: Test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 06154f1f44..f5e0eece1f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -57,6 +57,17 @@ If your language is not listed in Crowdin, please open an issue.
 - Test codes are located in [`/test`](/test).
 
 ### Run test
+Create a config file.
+```
+cp test/test.yml .config/
+```
+Prepare DB/Redis for testing.
+```
+docker-compose -f test/docker-compose.yml up
+```
+Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. 
+
+Run all test.
 ```
 npm run test
 ```
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
new file mode 100644
index 0000000000..c045e7c6c4
--- /dev/null
+++ b/test/docker-compose.yml
@@ -0,0 +1,15 @@
+version: "3"
+
+services:
+  redistest:
+    image: redis:4.0-alpine
+    ports:
+      - "127.0.0.1:56312:6379"
+
+  dbtest:
+    image: postgres:12.2-alpine
+    ports:
+      - "127.0.0.1:54312:5432"
+    environment:
+      POSTGRES_DB: "test-misskey"
+      POSTGRES_HOST_AUTH_METHOD: trust
diff --git a/test/test.yml b/test/test.yml
new file mode 100644
index 0000000000..2d3094653e
--- /dev/null
+++ b/test/test.yml
@@ -0,0 +1,12 @@
+url: 'http://misskey.local'
+port: 61812
+db:
+  host: localhost
+  port: 54312
+  db: test-misskey
+  user: postgres
+  pass: ''
+redis:
+  host: localhost
+  port: 56312
+id: aid

From 482081c41b45ab3798e73c4d11e8a7c1c1f5e8c9 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sat, 16 Oct 2021 17:16:24 +0900
Subject: [PATCH 3/3] Refactor request (#7814)

* status code

* Test ap-request.ts

https://github.com/mei23/crytest/blob/4397fc5e70536e4175fe56e974ca83b8047bef3a/test/ap-request.ts

* tune
---
 src/misc/download-url.ts                      |  21 +--
 src/misc/fetch.ts                             |  67 ++++---
 src/queue/processors/deliver.ts               |   7 +-
 src/queue/processors/inbox.ts                 |   3 +-
 src/remote/activitypub/ap-request.ts          | 104 +++++++++++
 .../activitypub/kernel/announce/note.ts       |   3 +-
 src/remote/activitypub/kernel/create/note.ts  |   3 +-
 src/remote/activitypub/models/note.ts         |   3 +-
 src/remote/activitypub/request.ts             | 166 ++++--------------
 src/server/file/send-drive-file.ts            |   5 +-
 src/server/proxy/proxy-media.ts               |   5 +-
 test/ap-request.ts                            |  55 ++++++
 12 files changed, 268 insertions(+), 174 deletions(-)
 create mode 100644 src/remote/activitypub/ap-request.ts
 create mode 100644 test/ap-request.ts

diff --git a/src/misc/download-url.ts b/src/misc/download-url.ts
index 8a8640a8cd..c96b4fd1d6 100644
--- a/src/misc/download-url.ts
+++ b/src/misc/download-url.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
 import * as stream from 'stream';
 import * as util from 'util';
 import got, * as Got from 'got';
-import { httpAgent, httpsAgent } from './fetch';
+import { httpAgent, httpsAgent, StatusError } from './fetch';
 import config from '@/config/index';
 import * as chalk from 'chalk';
 import Logger from '@/services/logger';
@@ -37,6 +37,7 @@ export async function downloadUrl(url: string, path: string) {
 			http: httpAgent,
 			https: httpsAgent,
 		},
+		http2: false,	// default
 		retry: 0,
 	}).on('response', (res: Got.Response) => {
 		if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
@@ -59,17 +60,17 @@ export async function downloadUrl(url: string, path: string) {
 			logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
 			req.destroy();
 		}
-	}).on('error', (e: any) => {
-		if (e.name === 'HTTPError') {
-			const statusCode = e.response?.statusCode;
-			const statusMessage = e.response?.statusMessage;
-			e.name = `StatusError`;
-			e.statusCode = statusCode;
-			e.message = `${statusCode} ${statusMessage}`;
-		}
 	});
 
-	await pipeline(req, fs.createWriteStream(path));
+	try {
+		await pipeline(req, fs.createWriteStream(path));
+	} catch (e) {
+		if (e instanceof Got.HTTPError) {
+			throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
+		} else {
+			throw e;
+		}
+	}
 
 	logger.succ(`Download finished: ${chalk.cyan(url)}`);
 }
diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts
index 82db2f2f8c..f4f16a27e2 100644
--- a/src/misc/fetch.ts
+++ b/src/misc/fetch.ts
@@ -1,51 +1,62 @@
 import * as http from 'http';
 import * as https from 'https';
 import CacheableLookup from 'cacheable-lookup';
-import fetch, { HeadersInit } from 'node-fetch';
+import fetch from 'node-fetch';
 import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
 import config from '@/config/index';
 import { URL } from 'url';
 
-export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: HeadersInit) {
-	const res = await fetch(url, {
+export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
+	const res = await getResponse({
+		url,
+		method: 'GET',
 		headers: Object.assign({
 			'User-Agent': config.userAgent,
 			Accept: accept
 		}, headers || {}),
-		timeout,
-		agent: getAgentByUrl,
+		timeout
 	});
 
-	if (!res.ok) {
-		throw {
-			name: `StatusError`,
-			statusCode: res.status,
-			message: `${res.status} ${res.statusText}`,
-		};
-	}
-
 	return await res.json();
 }
 
-export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: HeadersInit) {
-	const res = await fetch(url, {
+export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>) {
+	const res = await getResponse({
+		url,
+		method: 'GET',
 		headers: Object.assign({
 			'User-Agent': config.userAgent,
 			Accept: accept
 		}, headers || {}),
+		timeout
+	});
+
+	return await res.text();
+}
+
+export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
+	const timeout = args?.timeout || 10 * 1000;
+
+	const controller = new AbortController();
+	setTimeout(() => {
+		controller.abort();
+	}, timeout * 6);
+
+	const res = await fetch(args.url, {
+		method: args.method,
+		headers: args.headers,
+		body: args.body,
 		timeout,
+		size: args?.size || 10 * 1024 * 1024,
 		agent: getAgentByUrl,
+		signal: controller.signal,
 	});
 
 	if (!res.ok) {
-		throw {
-			name: `StatusError`,
-			statusCode: res.status,
-			message: `${res.status} ${res.statusText}`,
-		};
+		throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
 	}
 
-	return await res.text();
+	return res;
 }
 
 const cache = new CacheableLookup({
@@ -114,3 +125,17 @@ export function getAgentByUrl(url: URL, bypassProxy = false) {
 		return url.protocol == 'http:' ? httpAgent : httpsAgent;
 	}
 }
+
+export class StatusError extends Error {
+	public statusCode: number;
+	public statusMessage?: string;
+	public isClientError: boolean;
+
+	constructor(message: string, statusCode: number, statusMessage?: string) {
+		super(message);
+		this.name = 'StatusError';
+		this.statusCode = statusCode;
+		this.statusMessage = statusMessage;
+		this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
+	}
+}
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index 373e57cbd5..3c61896a2f 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -11,6 +11,7 @@ import { toPuny } from '@/misc/convert-host';
 import { Cache } from '@/misc/cache';
 import { Instance } from '@/models/entities/instance';
 import { DeliverJobData } from '../types';
+import { StatusError } from '@/misc/fetch';
 
 const logger = new Logger('deliver');
 
@@ -68,16 +69,16 @@ export default async (job: Bull.Job<DeliverJobData>) => {
 		registerOrFetchInstanceDoc(host).then(i => {
 			Instances.update(i.id, {
 				latestRequestSentAt: new Date(),
-				latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
+				latestStatus: res instanceof StatusError ? res.statusCode : null,
 				isNotResponding: true
 			});
 
 			instanceChart.requestSent(i.host, false);
 		});
 
-		if (res != null && res.hasOwnProperty('statusCode')) {
+		if (res instanceof StatusError) {
 			// 4xx
-			if (res.statusCode >= 400 && res.statusCode < 500) {
+			if (res.isClientError) {
 				// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
 				// 何回再送しても成功することはないということなのでエラーにはしないでおく
 				return `${res.statusCode} ${res.statusMessage}`;
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index e2c271cdf8..4032ce8653 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -14,6 +14,7 @@ import { InboxJobData } from '../types';
 import DbResolver from '@/remote/activitypub/db-resolver';
 import { resolvePerson } from '@/remote/activitypub/models/person';
 import { LdSignature } from '@/remote/activitypub/misc/ld-signature';
+import { StatusError } from '@/misc/fetch';
 
 const logger = new Logger('inbox');
 
@@ -53,7 +54,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
 			authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
 		} catch (e) {
 			// 対象が4xxならスキップ
-			if (e.statusCode >= 400 && e.statusCode < 500) {
+			if (e instanceof StatusError && e.isClientError) {
 				return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
 			}
 			throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
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();
-			}
-		});
+	const req = createSignedPost({
+		key: {
+			privateKeyPem: keypair.privateKey,
+			keyId: `${config.url}/users/${user.id}#main-key`
+		},
+		url,
+		body,
+		additionalHeaders: {
+			'User-Agent': config.userAgent,
+		}
+	});
 
-		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);
-		});
-
-		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',
+	const req = createSignedGet({
+		key: {
+			privateKeyPem: keypair.privateKey,
+			keyId: `${config.url}/users/${user.id}#main-key`
+		},
+		url,
+		additionalHeaders: {
 			'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);
+	const res = await getResponse({
+		url,
+		method: req.request.method,
+		headers: req.request.headers
+	});
 
-	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;
+	return await res.json();
 }
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index a73164ed21..1908c969a5 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -13,6 +13,7 @@ import { downloadUrl } from '@/misc/download-url';
 import { detectType } from '@/misc/get-file-info';
 import { convertToJpeg, convertToPngOrJpeg } from '@/services/drive/image-processor';
 import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail';
+import { StatusError } from '@/misc/fetch';
 
 //const _filename = fileURLToPath(import.meta.url);
 const _filename = __filename;
@@ -83,9 +84,9 @@ export default async function(ctx: Koa.Context) {
 				ctx.set('Content-Type', image.type);
 				ctx.set('Cache-Control', 'max-age=31536000, immutable');
 			} catch (e) {
-				serverLogger.error(e.statusCode);
+				serverLogger.error(`${e}`);
 
-				if (typeof e.statusCode === 'number' && e.statusCode >= 400 && e.statusCode < 500) {
+				if (e instanceof StatusError && e.isClientError) {
 					ctx.status = e.statusCode;
 					ctx.set('Cache-Control', 'max-age=86400');
 				} else {
diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts
index 3bd65dfe67..9e13c0877f 100644
--- a/src/server/proxy/proxy-media.ts
+++ b/src/server/proxy/proxy-media.ts
@@ -5,6 +5,7 @@ import { IImage, convertToPng, convertToJpeg } from '@/services/drive/image-proc
 import { createTemp } from '@/misc/create-temp';
 import { downloadUrl } from '@/misc/download-url';
 import { detectType } from '@/misc/get-file-info';
+import { StatusError } from '@/misc/fetch';
 
 export async function proxyMedia(ctx: Koa.Context) {
 	const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
@@ -37,9 +38,9 @@ export async function proxyMedia(ctx: Koa.Context) {
 		ctx.set('Cache-Control', 'max-age=31536000, immutable');
 		ctx.body = image.data;
 	} catch (e) {
-		serverLogger.error(e);
+		serverLogger.error(`${e}`);
 
-		if (typeof e.statusCode === 'number' && e.statusCode >= 400 && e.statusCode < 500) {
+		if (e instanceof StatusError && e.isClientError) {
 			ctx.status = e.statusCode;
 		} else {
 			ctx.status = 500;
diff --git a/test/ap-request.ts b/test/ap-request.ts
new file mode 100644
index 0000000000..4a9799eb99
--- /dev/null
+++ b/test/ap-request.ts
@@ -0,0 +1,55 @@
+import * as assert from 'assert';
+import { genRsaKeyPair } from '../src/misc/gen-key-pair';
+import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request';
+const httpSignature = require('http-signature');
+
+export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
+	return {
+		scheme: 'Signature',
+		params: {
+			keyId: 'KeyID',	// dummy, not used for verify
+			algorithm: algorithm,
+			headers: [ '(request-target)', 'date', 'host', 'digest' ],	// dummy, not used for verify
+			signature: signature,
+		},
+		signingString: signingString,
+		algorithm: algorithm?.toUpperCase(),
+		keyId: 'KeyID',	// dummy, not used for verify
+	};
+};
+
+describe('ap-request', () => {
+	it('createSignedPost with verify', async () => {
+		const keypair = await genRsaKeyPair();
+		const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
+		const url = 'https://example.com/inbox';
+		const activity = { a: 1 };
+		const body = JSON.stringify(activity);
+		const headers = {
+			'User-Agent': 'UA'
+		};
+
+		const req = createSignedPost({ key, url, body, additionalHeaders: headers });
+
+		const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
+
+		const result = httpSignature.verifySignature(parsed, keypair.publicKey);
+		assert.deepStrictEqual(result, true);
+	});
+
+	it('createSignedGet with verify', async () => {
+		const keypair = await genRsaKeyPair();
+		const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
+		const url = 'https://example.com/outbox';
+		const headers = {
+			'User-Agent': 'UA'
+		};
+
+		const req = createSignedGet({ key, url, additionalHeaders: headers });
+
+		const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
+
+		const result = httpSignature.verifySignature(parsed, keypair.publicKey);
+		assert.deepStrictEqual(result, true);
+	});
+});