signedPost, signedGet
This commit is contained in:
parent
fc20ef0181
commit
735714d61c
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class HttpSignImplLv1709242519122 {
|
||||||
|
name = 'HttpSignImplLv1709242519122'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" ADD "httpMessageSignaturesImplementationLevel" character varying(16) NOT NULL DEFAULT '00'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "httpMessageSignaturesImplementationLevel"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ type NodeInfo = {
|
||||||
version?: unknown;
|
version?: unknown;
|
||||||
};
|
};
|
||||||
metadata?: {
|
metadata?: {
|
||||||
|
httpMessageSignaturesImplementationLevel?: unknown,
|
||||||
name?: unknown;
|
name?: unknown;
|
||||||
nodeName?: unknown;
|
nodeName?: unknown;
|
||||||
nodeDescription?: unknown;
|
nodeDescription?: unknown;
|
||||||
|
@ -70,7 +71,7 @@ export class FetchInstanceMetadataService {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const _instance = await this.federatedInstanceService.fetch(host);
|
const _instance = await this.federatedInstanceService.fetch(host);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) {
|
||||||
// unlock at the finally caluse
|
// unlock at the finally caluse
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -104,6 +105,9 @@ export class FetchInstanceMetadataService {
|
||||||
updates.openRegistrations = info.openRegistrations;
|
updates.openRegistrations = info.openRegistrations;
|
||||||
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
|
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
|
||||||
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
|
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
|
||||||
|
if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel) {
|
||||||
|
updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel.toString() ?? '00';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name) updates.name = name;
|
if (name) updates.name = name;
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as crypto from 'node:crypto';
|
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { genRFC3230DigestHeader, RequestLike, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
@ -16,12 +16,6 @@ import { bindThis } from '@/decorators.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||||
|
|
||||||
type Request = {
|
|
||||||
url: string;
|
|
||||||
method: string;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Signed = {
|
type Signed = {
|
||||||
request: Request;
|
request: Request;
|
||||||
signingString: string;
|
signingString: string;
|
||||||
|
@ -34,103 +28,51 @@ type PrivateKey = {
|
||||||
keyId: string;
|
keyId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ApRequestCreator {
|
export function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string; additionalHeaders: Record<string, string> }) {
|
||||||
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
|
const u = new URL(args.url);
|
||||||
const u = new URL(args.url);
|
const request: RequestLike = {
|
||||||
const digestHeader = args.digest ?? this.createDigest(args.body);
|
url: u.href,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Date': new Date().toUTCString(),
|
||||||
|
'Host': u.host,
|
||||||
|
'Content-Type': 'application/activity+json',
|
||||||
|
...args.additionalHeaders,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const request: Request = {
|
// TODO: levelによって処理を分ける
|
||||||
url: u.href,
|
const digestHeader = args.digest ?? genRFC3230DigestHeader(args.body);
|
||||||
method: 'POST',
|
request.headers['Digest'] = digestHeader;
|
||||||
headers: this.#objectAssignWithLcKey({
|
|
||||||
'Date': new Date().toUTCString(),
|
|
||||||
'Host': u.host,
|
|
||||||
'Content-Type': 'application/activity+json',
|
|
||||||
'Digest': digestHeader,
|
|
||||||
}, args.additionalHeaders),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
|
const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
signingString: result.signingString,
|
...result,
|
||||||
signature: result.signature,
|
};
|
||||||
signatureHeader: result.signatureHeader,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static createDigest(body: string) {
|
export function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record<string, string> }) {
|
||||||
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
|
const u = new URL(args.url);
|
||||||
}
|
const request: RequestLike = {
|
||||||
|
url: u.href,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
'Date': new Date().toUTCString(),
|
||||||
|
'Host': new URL(args.url).host,
|
||||||
|
...args.additionalHeaders,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
|
// TODO: levelによって処理を分ける
|
||||||
const u = new URL(args.url);
|
const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
|
||||||
|
|
||||||
const request: Request = {
|
return {
|
||||||
url: u.href,
|
request,
|
||||||
method: 'GET',
|
...result,
|
||||||
headers: this.#objectAssignWithLcKey({
|
};
|
||||||
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
||||||
'Date': new Date().toUTCString(),
|
|
||||||
'Host': new URL(args.url).host,
|
|
||||||
}, args.additionalHeaders),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
|
|
||||||
|
|
||||||
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);
|
|
||||||
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, {
|
|
||||||
Signature: signatureHeader,
|
|
||||||
});
|
|
||||||
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
|
|
||||||
delete request.headers['host'];
|
|
||||||
|
|
||||||
return {
|
|
||||||
request,
|
|
||||||
signingString,
|
|
||||||
signature,
|
|
||||||
signatureHeader,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static #genSigningString(request: Request, includeHeaders: string[]): string {
|
|
||||||
request.headers = this.#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');
|
|
||||||
}
|
|
||||||
|
|
||||||
static #lcObjectKey(src: Record<string, string>): 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
|
|
||||||
return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -150,16 +92,25 @@ export class ApRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
|
private async getPrivateKey(userId: MiUser['id'], level: string): Promise<PrivateKey> {
|
||||||
|
const keypair = await this.userKeypairService.getUserKeypair(userId);
|
||||||
|
|
||||||
|
return (level !== '00' && keypair.ed25519PrivateKey) ? {
|
||||||
|
privateKeyPem: keypair.ed25519PrivateKey,
|
||||||
|
keyId: `${this.config.url}/users/${userId}#ed25519-key`,
|
||||||
|
} : {
|
||||||
|
privateKeyPem: keypair.privateKey,
|
||||||
|
keyId: `${this.config.url}/users/${userId}#main-key`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise<void> {
|
||||||
const body = typeof object === 'string' ? object : JSON.stringify(object);
|
const body = typeof object === 'string' ? object : JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const req = createSignedPost({
|
||||||
|
level,
|
||||||
const req = ApRequestCreator.createSignedPost({
|
key: await this.getPrivateKey(user.id, level),
|
||||||
key: {
|
|
||||||
privateKeyPem: keypair.privateKey,
|
|
||||||
keyId: `${this.config.url}/users/${user.id}#main-key`,
|
|
||||||
},
|
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
digest,
|
digest,
|
||||||
|
@ -180,14 +131,10 @@ export class ApRequestService {
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
|
public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> {
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const req = createSignedGet({
|
||||||
|
level,
|
||||||
const req = ApRequestCreator.createSignedGet({
|
key: await this.getPrivateKey(user.id, level),
|
||||||
key: {
|
|
||||||
privateKeyPem: keypair.privateKey,
|
|
||||||
keyId: `${this.config.url}/users/${user.id}#main-key`,
|
|
||||||
},
|
|
||||||
url,
|
url,
|
||||||
additionalHeaders: {
|
additionalHeaders: {
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { isCollectionOrOrderedCollection } from './type.js';
|
import { isCollectionOrOrderedCollection } from './type.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
|
@ -41,6 +42,7 @@ export class Resolver {
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private recursionLimit = 100,
|
private recursionLimit = 100,
|
||||||
) {
|
) {
|
||||||
|
@ -103,8 +105,10 @@ export class Resolver {
|
||||||
this.user = await this.instanceActorService.getInstanceActor();
|
this.user = await this.instanceActorService.getInstanceActor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const server = await this.federatedInstanceService.fetch(host);
|
||||||
|
|
||||||
const object = (this.user
|
const object = (this.user
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? await this.apRequestService.signedGet(value, this.user, server.httpMessageSignaturesImplementationLevel) as IObject
|
||||||
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -200,6 +204,7 @@ export class ApResolverService {
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -220,6 +225,7 @@ export class ApResolverService {
|
||||||
this.httpRequestService,
|
this.httpRequestService,
|
||||||
this.apRendererService,
|
this.apRendererService,
|
||||||
this.apDbResolverService,
|
this.apDbResolverService,
|
||||||
|
this.federatedInstanceService,
|
||||||
this.loggerService,
|
this.loggerService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,4 +149,9 @@ export class MiInstance {
|
||||||
length: 16384, default: '',
|
length: 16384, default: '',
|
||||||
})
|
})
|
||||||
public moderationNote: string;
|
public moderationNote: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 16, default: '00', nullable: false,
|
||||||
|
})
|
||||||
|
public httpMessageSignaturesImplementationLevel: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,24 +72,25 @@ export class DeliverProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
|
const _server = await this.federatedInstanceService.fetch(host);
|
||||||
|
await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {});
|
||||||
|
const server = await this.federatedInstanceService.fetch(host);
|
||||||
|
|
||||||
|
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel, job.data.digest);
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.fetch(host).then(i => {
|
if (server.isNotResponding) {
|
||||||
if (i.isNotResponding) {
|
this.federatedInstanceService.update(server.id, {
|
||||||
this.federatedInstanceService.update(i.id, {
|
isNotResponding: false,
|
||||||
isNotResponding: false,
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.apRequestChart.deliverSucc();
|
||||||
this.apRequestChart.deliverSucc();
|
this.federationChart.deliverd(server.host, true);
|
||||||
this.federationChart.deliverd(i.host, true);
|
|
||||||
|
|
||||||
if (meta.enableChartsForFederatedInstances) {
|
if (meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.requestSent(i.host, true);
|
this.instanceChart.requestSent(server.host, true);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return 'Success';
|
return 'Success';
|
||||||
} catch (res) {
|
} catch (res) {
|
||||||
|
|
Loading…
Reference in New Issue