@@ -9,14 +9,48 @@ import { Injectable } from '@nestjs/common';
99import { RsaKeyPair } from 'slacc' ;
1010import { HttpRequestService } from '@/core/HttpRequestService.js' ;
1111import { bindThis } from '@/decorators.js' ;
12+ import { IdentifiableError } from '@/misc/identifiable-error.js' ;
1213import { CONTEXT , PRELOADED_CONTEXTS } from './misc/contexts.js' ;
1314import { validateContentTypeSetAsJsonLD } from './misc/validator.js' ;
1415import type { JsonLdDocument } from 'jsonld' ;
1516import type { JsonLd as JsonLdObject , RemoteDocument } from 'jsonld/jsonld-spec.js' ;
1617
1718// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
1819
19- class JsonLd {
20+ export class JsonLdError extends IdentifiableError {
21+ constructor ( id : string , message ?: string ) {
22+ super ( id , message ) ;
23+ }
24+ }
25+
26+ export class JsonLdCacheOverflowError extends JsonLdError {
27+ constructor ( ) {
28+ super ( '42fb039c-69fb-4f75-8187-d3aee412423e' , 'context cache overflow' ) ;
29+ }
30+ }
31+
32+ export class JsonLdCacheFrozenError extends JsonLdError {
33+ constructor ( ) {
34+ super ( '202c41fa-72d5-4e22-95af-94a8ac83346f' , 'attempt to insert into frozen context cache' ) ;
35+ }
36+ }
37+
38+ export class JsonLdForbiddenDriectiveError extends JsonLdError {
39+ constructor ( public directive : string ) {
40+ super ( '0297f79b-0ed9-4b6c-875f-b0a82ff96781' , `${ directive } is forbidden by Misskey in ActivityPub documents` ) ;
41+ }
42+ }
43+
44+ export class JsonLd {
45+ private static forbiddenDirectives = new Set ( [
46+ '@included' ,
47+ '@graph' ,
48+ '@reverse' ,
49+ ] ) ;
50+
51+ private frozen = false ;
52+ private cache : Map < string , RemoteDocument > = new Map ( ) ;
53+
2054 public debug = false ;
2155 public preLoad = true ;
2256 public loderTimeout = 5000 ;
@@ -81,9 +115,9 @@ class JsonLd {
81115 const optionsHash = this . sha256 ( canonizedOptions . toString ( ) ) ;
82116 const transformedData = { ...data } ;
83117 delete transformedData [ 'signature' ] ;
84- const cannonidedData = await this . normalize ( transformedData ) ;
85- if ( this . debug ) console . debug ( `cannonidedData : ${ cannonidedData } ` ) ;
86- const documentHash = this . sha256 ( cannonidedData . toString ( ) ) ;
118+ const cannonizedData = await this . normalize ( transformedData ) ;
119+ if ( this . debug ) console . debug ( `cannonizedData : ${ cannonizedData } ` ) ;
120+ const documentHash = this . sha256 ( cannonizedData . toString ( ) ) ;
87121 const verifyData = `${ optionsHash } ${ documentHash } ` ;
88122 return verifyData ;
89123 }
@@ -106,6 +140,34 @@ class JsonLd {
106140 } ) ;
107141 }
108142
143+ /**
144+ * Prevent any further HTTP requests from being made for the sake of
145+ * validating JSON-LD signatures.
146+ */
147+ @bindThis
148+ public freeze ( ) : void { this . frozen = true ; }
149+
150+ @bindThis
151+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152+ public checkForForbiddenDirectives ( value : any ) : void {
153+ if ( typeof value === 'object' && value !== null ) {
154+ if ( Array . isArray ( value ) ) {
155+ for ( const item of value ) this . checkForForbiddenDirectives ( item ) ;
156+ } else {
157+ const object = value ;
158+ for ( const [ key , value ] of Object . entries ( object ) ) {
159+ if ( JsonLd . forbiddenDirectives . has ( key ) ) {
160+ throw new JsonLdForbiddenDriectiveError ( key ) ;
161+ }
162+
163+ if ( typeof value === 'object' && value !== null ) {
164+ this . checkForForbiddenDirectives ( value ) ;
165+ }
166+ }
167+ }
168+ }
169+ }
170+
109171 @bindThis
110172 private getLoader ( ) {
111173 return async ( url : string ) : Promise < RemoteDocument > => {
@@ -122,13 +184,27 @@ class JsonLd {
122184 }
123185 }
124186
187+ const cached = this . cache . get ( url ) ;
188+ if ( cached ) {
189+ if ( this . debug ) console . debug ( `HIT: ${ url } ` ) ;
190+ return cached ;
191+ }
192+
125193 if ( this . debug ) console . debug ( `MISS: ${ url } ` ) ;
194+
195+ if ( this . frozen ) throw new JsonLdCacheFrozenError ( ) ;
196+
126197 const document = await this . fetchDocument ( url ) ;
127- return {
198+ this . checkForForbiddenDirectives ( document ) ;
199+
200+ const remoteDocument = {
128201 contextUrl : undefined ,
129202 document : document ,
130203 documentUrl : url ,
131204 } ;
205+ this . cache . set ( url , remoteDocument ) ;
206+ if ( this . cache . size > 256 ) throw new JsonLdCacheOverflowError ( ) ;
207+ return remoteDocument ;
132208 } ;
133209 }
134210
0 commit comments