diff --git a/src/namespace/expressing-point.ts b/src/namespace/expressing-point.ts index d6e8939..45aeea7 100644 --- a/src/namespace/expressing-point.ts +++ b/src/namespace/expressing-point.ts @@ -19,6 +19,7 @@ export interface ExpressingPointEvents extends BaseNodeEvents { target: schemaTree.StrictMatch; deadline: number; pkt: Verifier.Verifiable; + prevResult: VerifyResult; }, ): Promise; @@ -41,7 +42,7 @@ export type ExpressingPointOpts = { }; export class ExpressingPoint extends BaseNode { - /** Called when Interest received */ + /** Called when Interest received. Note: calling `need` on this node will not trigger this callback. */ public readonly onInterest = new EventChain(); /** Verify Interest event. Also verifies Data if this is a LeafNode */ @@ -71,8 +72,14 @@ export class ExpressingPoint extends BaseNode { ) { const verifyResult = await this.onVerify.chain( VerifyResult.Unknown, - (ret, args) => Promise.resolve((ret < VerifyResult.Unknown || ret >= VerifyResult.Bypass) ? Stop : [args]), - { target: matched, pkt, deadline }, + (ret, args) => + Promise.resolve( + (ret < VerifyResult.Unknown || ret >= VerifyResult.Bypass) ? Stop : [{ + ...args, + prevResult: ret, + }], + ), + { target: matched, pkt, deadline, prevResult: VerifyResult.Unknown }, ); return verifyResult >= VerifyResult.Pass; } diff --git a/src/namespace/leaf-node.ts b/src/namespace/leaf-node.ts index d5529ed..f7ba415 100644 --- a/src/namespace/leaf-node.ts +++ b/src/namespace/leaf-node.ts @@ -13,7 +13,7 @@ export interface LeafNodeEvents extends ExpressingPointEvents { wire: Uint8Array; validUntil: number; }, - ): Promise; + ): Promise; } export type LeafNodeOpts = ExpressingPointOpts & { diff --git a/src/namespace/nt-schema.test.ts b/src/namespace/nt-schema.test.ts index 2285c07..da9f50c 100644 --- a/src/namespace/nt-schema.test.ts +++ b/src/namespace/nt-schema.test.ts @@ -1 +1,301 @@ -// TODO: To be added +import { assert } from '../dep.ts'; +import { AsyncDisposableStack, name, Responder } from '../utils/mod.ts'; +import { Endpoint } from '@ndn/endpoint'; +import { Data, digestSigning, SigType } from '@ndn/packet'; +import { Decoder, Encoder } from '@ndn/tlv'; +import { Bridge } from '@ndn/l3face'; +import { Ed25519, generateSigningKey } from '@ndn/keychain'; +import { NtSchema, VerifyResult } from './nt-schema.ts'; +import { InMemoryStorage } from '../storage/mod.ts'; +import { LeafNode } from './leaf-node.ts'; +import * as namePattern from './name-pattern.ts'; +import * as Tree from './schema-tree.ts'; +import { BaseNode } from './base-node.ts'; + +const { pattern } = namePattern; +export const b = ([value]: TemplateStringsArray) => new TextEncoder().encode(value); + +Deno.test('NtSchema.1 Basic Interest and Data', async () => { + using bridge = Bridge.create({}); + const { fwA, fwB } = bridge; + const epA = new Endpoint({ fw: fwA }); + const epB = new Endpoint({ fw: fwB }); + await using closers = new AsyncDisposableStack(); + const appPrefix = name`/prefix`; + + // NTSchema side + const schema = new NtSchema(); + const leaf = new LeafNode({ + lifetimeMs: 100, + freshnessMs: 60000, + signer: digestSigning, + }); + const leafNode = Tree.touch( + schema.tree, + pattern`/records/<8=recordId:string>`, + leaf, + ); + leaf.onVerify.addListener(async ({ pkt }) => { + try { + await digestSigning.verify(pkt); + return VerifyResult.Pass; + } catch { + return VerifyResult.Fail; + } + }); + leaf.onInterest.addListener(async () => { + const data3 = new Data( + name`${appPrefix}/records/8=rec3`, + Data.FreshnessPeriod(60000), + b`Hello, World.`, + ); + await digestSigning.sign(data3); + return data3; + }); + await schema.attach(appPrefix, epA); + closers.defer(async () => await schema.detach()); + + // Responder side + const storage = new InMemoryStorage(); + closers.use(storage); + const responder = new Responder(appPrefix, epB, storage); + closers.use(responder); + const data1 = new Data( + name`${appPrefix}/records/8=rec1`, + Data.FreshnessPeriod(1000), + b`Hello,`, + ); + await digestSigning.sign(data1); + await storage.set(data1.name.toString(), Encoder.encode(data1)); + const data2 = new Data( + name`${appPrefix}/records/8=rec2`, + Data.FreshnessPeriod(1000), + b`World.`, + ); + await digestSigning.sign(data2); + await storage.set(data2.name.toString(), Encoder.encode(data2)); + + // Express Interest + const recved1 = await Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec1' }), + 'need', + ); + assert.assertExists(recved1); + assert.assert(recved1.name.equals(name`${appPrefix}/records/8=rec1`)); + assert.assertEquals(recved1.content, b`Hello,`); + + const recved2 = await Tree.call( + Tree.cast(schema.match(name`${appPrefix}/records/8=rec2`), LeafNode)!, + 'need', + ); + assert.assertExists(recved2); + assert.assert(recved2.name.equals(name`${appPrefix}/records/8=rec2`)); + assert.assertEquals(recved2.content, b`World.`); + + // Test NTSchema's producing data (on request, without storage) + const recved3 = await epB.consume(name`${appPrefix}/records/8=rec3`, { + verifier: digestSigning, + }); + assert.assertExists(recved3); + assert.assert(recved3.name.equals(name`${appPrefix}/records/8=rec3`)); + assert.assertEquals(recved3.freshnessPeriod, 60000); + assert.assertEquals(recved3.content, b`Hello, World.`); +}); + +Deno.test('NtSchema.2 Data Storage', async () => { + using bridge = Bridge.create({}); + const { fwA, fwB } = bridge; + const epA = new Endpoint({ fw: fwA }); + const epB = new Endpoint({ fw: fwB }); + await using closers = new AsyncDisposableStack(); + const appPrefix = name`/prefix`; + + // NTSchema side + const schema = new NtSchema(); + const storageA = new InMemoryStorage(); + const leaf = new LeafNode({ + lifetimeMs: 100, + freshnessMs: 60000, + signer: digestSigning, + }); + const leafNode = Tree.touch( + schema.tree, + pattern`/records/<8=recordId:string>`, + leaf, + ); + leaf.onVerify.addListener(async ({ pkt }) => { + try { + await digestSigning.verify(pkt); + return VerifyResult.Pass; + } catch { + return VerifyResult.Fail; + } + }); + leaf.onSaveStorage.addListener(({ data, wire }) => storageA.set(data.name.toString(), wire)); + leaf.onSearchStorage.addListener(async ({ interest }) => { + const wire = await storageA.get(interest.name.toString()); + if (wire) { + return Decoder.decode(wire, Data); + } else { + return undefined; + } + }); + await schema.attach(appPrefix, epA); + closers.defer(async () => await schema.detach()); + + // Responder side + const storageB = new InMemoryStorage(); + closers.use(storageB); + const responder = new Responder(appPrefix, epB, storageB); + closers.use(responder); + const data1 = new Data( + name`${appPrefix}/records/8=rec1`, + Data.FreshnessPeriod(1000), + b`Hello,`, + ); + await digestSigning.sign(data1); + await storageB.set(data1.name.toString(), Encoder.encode(data1)); + + // Test NTSchema's producing data (with storage) + await Tree.call( + Tree.cast(schema.match(name`${appPrefix}/records/8=rec2`), LeafNode)!, + 'provide', + b`World.`, + ); + const received = await epB.consume(name`${appPrefix}/records/8=rec2`, { + verifier: digestSigning, + }); + assert.assertExists(received); + assert.assert(received.name.equals(name`${appPrefix}/records/8=rec2`)); + assert.assertEquals(received.freshnessPeriod, 60000); + assert.assertEquals(received.contentType, 0); + assert.assertEquals(received.content, b`World.`); + + // Test NTSchema can cache received Data + const recved1 = await Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec1' }), + 'need', + ); + assert.assertExists(recved1); + // Remove the responder and test again + await storageB.delete(data1.name.toString()); + const recved2 = await Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec1' }), + 'need', + ); + assert.assertExists(recved2); + assert.assert(recved2.name.equals(name`${appPrefix}/records/8=rec1`)); + assert.assertEquals(recved2.content, b`Hello,`); +}); + +Deno.test('NtSchema.3 Verification', async () => { + using bridge = Bridge.create({}); + const { fwA, fwB } = bridge; + const epA = new Endpoint({ fw: fwA }); + const epB = new Endpoint({ fw: fwB }); + await using closers = new AsyncDisposableStack(); + const appPrefix = name`/prefix`; + const [prvKey, pubKey] = await generateSigningKey(/*identity*/ appPrefix, Ed25519); + const [wrongKey, _wrongPubKey] = await generateSigningKey(/*identity*/ appPrefix, Ed25519); + + // NTSchema side + const schema = new NtSchema(); + const leaf = new LeafNode({ + lifetimeMs: 100, + freshnessMs: 60000, + signer: digestSigning, + }); + const leafNode = Tree.touch( + schema.tree, + pattern`/records/<8=recordId:string>`, + leaf, + ); + leaf.onVerify.addListener(async ({ pkt, prevResult }) => { + if (pkt.sigInfo?.type === SigType.Sha256) { + try { + await digestSigning.verify(pkt); + return VerifyResult.Pass; + } catch { + return VerifyResult.Fail; + } + } else { + return prevResult; + } + }); + leaf.onVerify.addListener(async ({ pkt, prevResult }) => { + if (pkt.sigInfo?.type === SigType.Ed25519) { + try { + await pubKey.verify(pkt); + return VerifyResult.Pass; + } catch { + return VerifyResult.Fail; + } + } else { + return prevResult; + } + }); + await schema.attach(appPrefix, epA); + closers.defer(async () => await schema.detach()); + + // Responder side + const storage = new InMemoryStorage(); + closers.use(storage); + const responder = new Responder(appPrefix, epB, storage); + closers.use(responder); + const data1 = new Data( + name`${appPrefix}/records/8=rec1`, + Data.FreshnessPeriod(1000), + b`Hello,`, + ); + await digestSigning.sign(data1); + await storage.set(data1.name.toString(), Encoder.encode(data1)); + const data2 = new Data( + name`${appPrefix}/records/8=rec2`, + Data.FreshnessPeriod(1000), + b`World.`, + ); + await prvKey.sign(data2); + await storage.set(data2.name.toString(), Encoder.encode(data2)); + const data3 = new Data( + name`${appPrefix}/records/8=rec3`, + Data.FreshnessPeriod(1000), + b`World.`, + ); + // data3 is unsigned + await storage.set(data3.name.toString(), Encoder.encode(data3)); + const data4 = new Data( + name`${appPrefix}/records/8=rec4`, + Data.FreshnessPeriod(1000), + b`World.`, + ); + await wrongKey.sign(data4); + await storage.set(data4.name.toString(), Encoder.encode(data4)); + + // Express Interest + const recved1 = await Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec1' }), + 'need', + ); + assert.assertExists(recved1); + const recved2 = await Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec2' }), + 'need', + ); + assert.assertExists(recved2); + assert.assertRejects(() => + Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec3' }), + 'need', + ) + ); + assert.assertRejects(() => + Tree.call( + Tree.apply(leafNode, { 'recordId': 'rec4' }), + 'need', + ) + ); +}); + +Deno.test('NtSchema.4 Signed Interest', async () => { + // TODO: +}); diff --git a/src/namespace/nt-schema.ts b/src/namespace/nt-schema.ts index fc18fe0..04dfefd 100644 --- a/src/namespace/nt-schema.ts +++ b/src/namespace/nt-schema.ts @@ -75,16 +75,20 @@ export class NtSchema implements NamespaceHandler, AsyncDisposable { this._attachedPrefix = prefix; this._endpoint = endpoint; await schemaTree.traverse(this.tree, { - post: async (node, path) => await node.resource?.onAttach?.emit(path, endpoint), + post: async (node, path) => await node.resource?.processAttach(path, this), }); - this._producer = endpoint.produce(prefix, this.onInterest.bind(this)); + this._producer = endpoint.produce(prefix, this.onInterest.bind(this), { + describe: `NtSchema[${prefix.toString()}]`, + routeCapture: false, + announcement: prefix, + }); } public async detach() { this._producer!.close(); await schemaTree.traverse(this.tree, { - pre: async (node) => await node.resource?.onDetach?.emit(this.endpoint!), + pre: async (node) => await node.resource?.processDetach(), }); this._endpoint = undefined; this._attachedPrefix = undefined; diff --git a/src/namespace/schema-tree.ts b/src/namespace/schema-tree.ts index c37b6f7..e4240e6 100644 --- a/src/namespace/schema-tree.ts +++ b/src/namespace/schema-tree.ts @@ -109,11 +109,11 @@ export const get = ( return cur; }; -export const touch = ( +export const touch = ( root: Node, path: namePattern.Pattern, resource?: R, -): Node => { +): Node => { let cur: Node = root; for (const pat of path) { if (pat instanceof Component) { @@ -164,7 +164,7 @@ export const touch = ( if (resource) { cur.resource = resource; } - return cur; + return cur as Node; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -219,8 +219,20 @@ export const traverse = async ( for (const child of root.fixedChildren) { await traverse(child.dest, action, [...path, child.edge]); } - for (const child of root.fixedChildren) { - await traverse(child.dest, action, [...path, child.edge]); + for (const child of root.children.values()) { + await traverse(child, action, [...path, child.upEdge!]); } await action.post?.(root, path); }; + +export const cast = ( + object: MatchedObject | undefined, + // deno-lint-ignore no-explicit-any + cls: { new (...args: any[]): T }, +): StrictMatch | undefined => { + if (object?.resource instanceof cls) { + return object as StrictMatch; + } else { + return undefined; + } +};