|  |  |  | @@ -3,10 +3,11 @@ | 
		
	
		
			
				|  |  |  |  |  * SPDX-License-Identifier: AGPL-3.0-only | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | import * as crypto from 'node:crypto'; | 
		
	
		
			
				|  |  |  |  | import { IncomingMessage } from 'node:http'; | 
		
	
		
			
				|  |  |  |  | import { Inject, Injectable } from '@nestjs/common'; | 
		
	
		
			
				|  |  |  |  | import fastifyAccepts from '@fastify/accepts'; | 
		
	
		
			
				|  |  |  |  | import { verifyDigestHeader, parseRequestSignature } from '@misskey-dev/node-http-message-signatures'; | 
		
	
		
			
				|  |  |  |  | import httpSignature from '@peertube/http-signature'; | 
		
	
		
			
				|  |  |  |  | import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; | 
		
	
		
			
				|  |  |  |  | import accepts from 'accepts'; | 
		
	
		
			
				|  |  |  |  | import vary from 'vary'; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -30,17 +31,12 @@ import { IActivity } from '@/core/activitypub/type.js'; | 
		
	
		
			
				|  |  |  |  | import { isQuote, isRenote } from '@/misc/is-renote.js'; | 
		
	
		
			
				|  |  |  |  | import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; | 
		
	
		
			
				|  |  |  |  | import type { FindOptionsWhere } from 'typeorm'; | 
		
	
		
			
				|  |  |  |  | import { LoggerService } from '@/core/LoggerService.js'; | 
		
	
		
			
				|  |  |  |  | import Logger from '@/logger.js'; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; | 
		
	
		
			
				|  |  |  |  | const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | @Injectable() | 
		
	
		
			
				|  |  |  |  | export class ActivityPubServerService { | 
		
	
		
			
				|  |  |  |  | 	private logger: Logger; | 
		
	
		
			
				|  |  |  |  | 	private inboxLogger: Logger; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 	constructor( | 
		
	
		
			
				|  |  |  |  | 		@Inject(DI.config) | 
		
	
		
			
				|  |  |  |  | 		private config: Config, | 
		
	
	
		
			
				
					
					|  |  |  | @@ -75,11 +71,8 @@ export class ActivityPubServerService { | 
		
	
		
			
				|  |  |  |  | 		private queueService: QueueService, | 
		
	
		
			
				|  |  |  |  | 		private userKeypairService: UserKeypairService, | 
		
	
		
			
				|  |  |  |  | 		private queryService: QueryService, | 
		
	
		
			
				|  |  |  |  | 		private loggerService: LoggerService, | 
		
	
		
			
				|  |  |  |  | 	) { | 
		
	
		
			
				|  |  |  |  | 		//this.createServer = this.createServer.bind(this); | 
		
	
		
			
				|  |  |  |  | 		this.logger = this.loggerService.getLogger('server-ap', 'gray'); | 
		
	
		
			
				|  |  |  |  | 		this.inboxLogger = this.logger.createSubLogger('inbox', 'gray'); | 
		
	
		
			
				|  |  |  |  | 	} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 	@bindThis | 
		
	
	
		
			
				
					
					|  |  |  | @@ -107,44 +100,70 @@ export class ActivityPubServerService { | 
		
	
		
			
				|  |  |  |  | 	} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 	@bindThis | 
		
	
		
			
				|  |  |  |  | 	private async inbox(request: FastifyRequest, reply: FastifyReply) { | 
		
	
		
			
				|  |  |  |  | 		if (request.body == null) { | 
		
	
		
			
				|  |  |  |  | 			this.inboxLogger.warn('request body is empty'); | 
		
	
		
			
				|  |  |  |  | 			reply.code(400); | 
		
	
		
			
				|  |  |  |  | 			return; | 
		
	
		
			
				|  |  |  |  | 		} | 
		
	
		
			
				|  |  |  |  | 	private inbox(request: FastifyRequest, reply: FastifyReply) { | 
		
	
		
			
				|  |  |  |  | 		let signature; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 		let signature: ReturnType<typeof parseRequestSignature>; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 		const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true); | 
		
	
		
			
				|  |  |  |  | 		if (verifyDigest !== true) { | 
		
	
		
			
				|  |  |  |  | 			this.inboxLogger.warn('digest verification failed'); | 
		
	
		
			
				|  |  |  |  | 		try { | 
		
	
		
			
				|  |  |  |  | 			signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); | 
		
	
		
			
				|  |  |  |  | 		} catch (e) { | 
		
	
		
			
				|  |  |  |  | 			reply.code(401); | 
		
	
		
			
				|  |  |  |  | 			return; | 
		
	
		
			
				|  |  |  |  | 		} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 		try { | 
		
	
		
			
				|  |  |  |  | 			signature = parseRequestSignature(request.raw, { | 
		
	
		
			
				|  |  |  |  | 				requiredInputs: { | 
		
	
		
			
				|  |  |  |  | 					draft: ['(request-target)', 'digest', 'host', 'date'], | 
		
	
		
			
				|  |  |  |  | 				}, | 
		
	
		
			
				|  |  |  |  | 			}); | 
		
	
		
			
				|  |  |  |  | 		} catch (err) { | 
		
	
		
			
				|  |  |  |  | 			this.inboxLogger.warn('signature header parsing failed', { err }); | 
		
	
		
			
				|  |  |  |  | 		if (signature.params.headers.indexOf('host') === -1 | 
		
	
		
			
				|  |  |  |  | 			|| request.headers.host !== this.config.host) { | 
		
	
		
			
				|  |  |  |  | 			// Host not specified or not match. | 
		
	
		
			
				|  |  |  |  | 			reply.code(401); | 
		
	
		
			
				|  |  |  |  | 			return; | 
		
	
		
			
				|  |  |  |  | 		} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			if (typeof request.body === 'object' && 'signature' in request.body) { | 
		
	
		
			
				|  |  |  |  | 				// LD SignatureがあればOK | 
		
	
		
			
				|  |  |  |  | 				this.queueService.inbox(request.body as IActivity, null); | 
		
	
		
			
				|  |  |  |  | 				reply.code(202); | 
		
	
		
			
				|  |  |  |  | 		if (signature.params.headers.indexOf('digest') === -1) { | 
		
	
		
			
				|  |  |  |  | 			// Digest not found. | 
		
	
		
			
				|  |  |  |  | 			reply.code(401); | 
		
	
		
			
				|  |  |  |  | 		} else { | 
		
	
		
			
				|  |  |  |  | 			const digest = request.headers.digest; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			if (typeof digest !== 'string') { | 
		
	
		
			
				|  |  |  |  | 				// Huh? | 
		
	
		
			
				|  |  |  |  | 				reply.code(401); | 
		
	
		
			
				|  |  |  |  | 				return; | 
		
	
		
			
				|  |  |  |  | 			} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			this.inboxLogger.warn('signature header parsing failed and LD signature not found'); | 
		
	
		
			
				|  |  |  |  | 			reply.code(401); | 
		
	
		
			
				|  |  |  |  | 			return; | 
		
	
		
			
				|  |  |  |  | 			const re = /^([a-zA-Z0-9\-]+)=(.+)$/; | 
		
	
		
			
				|  |  |  |  | 			const match = digest.match(re); | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			if (match == null) { | 
		
	
		
			
				|  |  |  |  | 				// Invalid digest | 
		
	
		
			
				|  |  |  |  | 				reply.code(401); | 
		
	
		
			
				|  |  |  |  | 				return; | 
		
	
		
			
				|  |  |  |  | 			} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			const algo = match[1].toUpperCase(); | 
		
	
		
			
				|  |  |  |  | 			const digestValue = match[2]; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			if (algo !== 'SHA-256') { | 
		
	
		
			
				|  |  |  |  | 				// Unsupported digest algorithm | 
		
	
		
			
				|  |  |  |  | 				reply.code(401); | 
		
	
		
			
				|  |  |  |  | 				return; | 
		
	
		
			
				|  |  |  |  | 			} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			if (request.rawBody == null) { | 
		
	
		
			
				|  |  |  |  | 				// Bad request | 
		
	
		
			
				|  |  |  |  | 				reply.code(400); | 
		
	
		
			
				|  |  |  |  | 				return; | 
		
	
		
			
				|  |  |  |  | 			} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			const hash = crypto.createHash('sha256').update(request.rawBody).digest('base64'); | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 			if (hash !== digestValue) { | 
		
	
		
			
				|  |  |  |  | 				// Invalid digest | 
		
	
		
			
				|  |  |  |  | 				reply.code(401); | 
		
	
		
			
				|  |  |  |  | 				return; | 
		
	
		
			
				|  |  |  |  | 			} | 
		
	
		
			
				|  |  |  |  | 		} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 		this.queueService.inbox(request.body as IActivity, signature); | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 		reply.code(202); | 
		
	
		
			
				|  |  |  |  | 	} | 
		
	
		
			
				|  |  |  |  |  | 
		
	
	
		
			
				
					
					|  |  |  | @@ -621,7 +640,7 @@ export class ActivityPubServerService { | 
		
	
		
			
				|  |  |  |  | 			if (this.userEntityService.isLocalUser(user)) { | 
		
	
		
			
				|  |  |  |  | 				reply.header('Cache-Control', 'public, max-age=180'); | 
		
	
		
			
				|  |  |  |  | 				this.setResponseType(request, reply); | 
		
	
		
			
				|  |  |  |  | 				return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair.publicKey))); | 
		
	
		
			
				|  |  |  |  | 				return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); | 
		
	
		
			
				|  |  |  |  | 			} else { | 
		
	
		
			
				|  |  |  |  | 				reply.code(400); | 
		
	
		
			
				|  |  |  |  | 				return; | 
		
	
	
		
			
				
					
					|  |  |  |   |