egirlskey/packages/backend/src/core/activitypub/ApRequestService.ts

198 lines
5.7 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
2022-09-17 18:27:08 +00:00
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
2022-09-20 20:33:11 +00:00
import type { Config } from '@/config.js';
2022-09-17 18:27:08 +00:00
import type { User } from '@/models/entities/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
2022-09-17 18:27:08 +00:00
type Request = {
url: string;
method: string;
2022-09-17 18:27:08 +00:00
headers: Record<string, string>;
};
type Signed = {
request: Request;
signingString: string;
signature: string;
signatureHeader: string;
};
type PrivateKey = {
privateKeyPem: string;
keyId: string;
};
export class ApRequestCreator {
static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
2022-09-17 18:27:08 +00:00
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: this.#objectAssignWithLcKey({
2022-09-17 18:27:08 +00:00
'Date': new Date().toUTCString(),
'Host': u.host,
2022-09-17 18:27:08 +00:00
'Content-Type': 'application/activity+json',
'Digest': digestHeader,
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
2022-09-17 18:27:08 +00:00
return {
request,
signingString: result.signingString,
signature: result.signature,
signatureHeader: result.signatureHeader,
};
}
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
2022-09-17 18:27:08 +00:00
const u = new URL(args.url);
const request: Request = {
url: u.href,
method: 'GET',
headers: this.#objectAssignWithLcKey({
2022-09-17 18:27:08 +00:00
'Accept': 'application/activity+json, application/ld+json',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
2022-09-17 18:27:08 +00:00
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
2022-09-17 18:27:08 +00:00
return {
request,
signingString: result.signingString,
signature: result.signature,
signatureHeader: result.signatureHeader,
};
}
static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
const signingString = this.#genSigningString(request, includeHeaders);
2022-09-17 18:27:08 +00:00
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 = this.#objectAssignWithLcKey(request.headers, {
2022-09-17 18:27:08 +00:00
Signature: signatureHeader,
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
delete request.headers['host'];
2022-09-17 18:27:08 +00:00
return {
request,
signingString,
signature,
signatureHeader,
};
}
static #genSigningString(request: Request, includeHeaders: string[]): string {
request.headers = this.#lcObjectKey(request.headers);
2022-09-17 18:27:08 +00:00
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');
}
static #lcObjectKey(src: Record<string, string>): Record<string, string> {
2022-09-17 18:27:08 +00:00
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;
}
static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
}
}
@Injectable()
export class ApRequestService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
private userKeypairService: UserKeypairService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
2022-09-17 18:27:08 +00:00
}
@bindThis
refactor(backend): `core/activitypub` (#11247) * eslint: `explicit-function-return-type` * eslint: `no-unnecessary-condition` * eslint: `eslint-disable-next-line` * eslint: `no-unused-vars` * eslint: `comma-dangle` * eslint: `import/order` * cleanup: unnecessary non-null assertion * cleanup: `IActivity`に`actor`は常に存在するようなので * cleanup: unnecessary `as` * cleanup: unnecessary `Promise.resolve` * cleanup * refactor: `String.prototype.match()`である必要がない部分をよりシンプルな書き方に変更 * refactor: よりよい型定義 * refactor: よりよい型定義 - `LdSignature`の`normalize`メソッドでの使われ方から、 - `data`引数の型定義を`any`から`JsonLdDocument`へ修正 - `getLoader`メソッドの返り値の型定義の一部を`any`から`RemoteDocument`へ修正 - `contextUrl`が不正な値(`null`)となっていたことが判明したため`undefined`へ修正 - `document`の型と合わせるために`CONTEXTS`の型定義の一部を`unknown`から`JsonLd`へ修正 - とりあえず`satisfies`を使用 - `document`の型と合わせるために`fetchDocument`メソッドの返り値の型定義の一部を`unknown`から`JsonLd`へ修正 - どうしようもなく`as`を使用 * refactor: 型ガードを使うことでnon-null assertionをやめた * refactor: non-null assertionをやめた `.filter()`で行っている型ガードなどの文脈から、より適しているだろうと思われる書き方に変更した。 * refactor: 型ガードを使うことで`as`をやめた * refactor: `as`をやめた * refactor: よりよい型定義 - `id`は`null`とのunionになっていたが、`null`を渡している場面はなかった - またおそらくこのメソッドは`IOrderedCollection`を返すため、そちらに合わせて`null`とのunionをやめた - `IOrderedCollection`とはまだ型に相違がある - `totalItems`をコメントや使われ方を元に`number`へ推論 * refactor: `for-of` -> `Array.prototype.map` * refactor: `delete`演算子を使わない形に
2023-07-13 03:48:34 +00:00
public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise<void> {
2022-09-17 18:27:08 +00:00
const body = JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
2022-09-17 18:27:08 +00:00
const req = ApRequestCreator.createSignedPost({
2022-09-17 18:27:08 +00:00
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
},
url,
body,
additionalHeaders: {
},
});
await this.httpRequestService.send(url, {
method: req.request.method,
headers: req.request.headers,
body,
});
2022-09-17 18:27:08 +00:00
}
/**
* Get AP object with http-signature
* @param user http-signature user
* @param url URL to fetch
*/
@bindThis
refactor(backend): `core/activitypub` (#11247) * eslint: `explicit-function-return-type` * eslint: `no-unnecessary-condition` * eslint: `eslint-disable-next-line` * eslint: `no-unused-vars` * eslint: `comma-dangle` * eslint: `import/order` * cleanup: unnecessary non-null assertion * cleanup: `IActivity`に`actor`は常に存在するようなので * cleanup: unnecessary `as` * cleanup: unnecessary `Promise.resolve` * cleanup * refactor: `String.prototype.match()`である必要がない部分をよりシンプルな書き方に変更 * refactor: よりよい型定義 * refactor: よりよい型定義 - `LdSignature`の`normalize`メソッドでの使われ方から、 - `data`引数の型定義を`any`から`JsonLdDocument`へ修正 - `getLoader`メソッドの返り値の型定義の一部を`any`から`RemoteDocument`へ修正 - `contextUrl`が不正な値(`null`)となっていたことが判明したため`undefined`へ修正 - `document`の型と合わせるために`CONTEXTS`の型定義の一部を`unknown`から`JsonLd`へ修正 - とりあえず`satisfies`を使用 - `document`の型と合わせるために`fetchDocument`メソッドの返り値の型定義の一部を`unknown`から`JsonLd`へ修正 - どうしようもなく`as`を使用 * refactor: 型ガードを使うことでnon-null assertionをやめた * refactor: non-null assertionをやめた `.filter()`で行っている型ガードなどの文脈から、より適しているだろうと思われる書き方に変更した。 * refactor: 型ガードを使うことで`as`をやめた * refactor: `as`をやめた * refactor: よりよい型定義 - `id`は`null`とのunionになっていたが、`null`を渡している場面はなかった - またおそらくこのメソッドは`IOrderedCollection`を返すため、そちらに合わせて`null`とのunionをやめた - `IOrderedCollection`とはまだ型に相違がある - `totalItems`をコメントや使われ方を元に`number`へ推論 * refactor: `for-of` -> `Array.prototype.map` * refactor: `delete`演算子を使わない形に
2023-07-13 03:48:34 +00:00
public async signedGet(url: string, user: { id: User['id'] }): Promise<unknown> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
2022-09-17 18:27:08 +00:00
const req = ApRequestCreator.createSignedGet({
2022-09-17 18:27:08 +00:00
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
},
url,
additionalHeaders: {
},
});
const res = await this.httpRequestService.send(url, {
method: req.request.method,
headers: req.request.headers,
});
2022-09-17 18:27:08 +00:00
return await res.json();
}
}