From d4fd2e590fb7c439b0983743d220d81ae812e7ae Mon Sep 17 00:00:00 2001 From: Pranav Ghorpade Date: Thu, 12 Feb 2026 17:49:19 +0000 Subject: [PATCH 1/6] docs(binding-coap): document PSK usage with CoAPs Add example and explanation for configuring PSK security scheme in CoapsClient. Closes #954 Signed-off-by: Pranav Ghorpade --- packages/binding-coap/README.md | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/binding-coap/README.md b/packages/binding-coap/README.md index 1562f44ec..0feb619b6 100644 --- a/packages/binding-coap/README.md +++ b/packages/binding-coap/README.md @@ -94,6 +94,65 @@ servient.start().then((WoT) => { }); ``` +## Using CoAPs with PSK + +The CoAP binding also supports secure CoAP (`coaps://`) using DTLS with the +`psk` (Pre-Shared Key) security scheme. + +Currently, PSK support is implemented in the `CoapsClient` and can be +configured via the Thing Description and client credentials. + +### Thing Description Example + +To use PSK, the Thing Description must define a `psk` security scheme: + +```json +{ + "securityDefinitions": { + "psk_sc": { + "scheme": "psk" + } + }, + "security": ["psk_sc"] +} +``` + +### Client Configuration Example + +On the client side, credentials must be provided via the Servient +using `addCredentials()`. The credentials are associated with the +Thing's `id` and are automatically applied based on the TD security +configuration. + +```js +const { Servient } = require("@node-wot/core"); +const { CoapsClientFactory } = require("@node-wot/binding-coap"); + +const servient = new Servient(); +servient.addClientFactory(new CoapsClientFactory()); + +servient.start().then(async (WoT) => { + const td = await WoT.requestThingDescription("coaps://example.com/secure-thing"); + + // Configure PSK credentials for this Thing + servient.addCredentials({ + [td.id]: { + identity: "Client_identity", + psk: "secretPSK", + }, + }); + + const thing = await WoT.consume(td); + + await thing.invokeAction("someAction"); +}); +``` + +The `identity` and `psk` values must match the configuration of the +CoAPs server. + +> **Note:** Only the psk security scheme is currently supported for CoAPs. + ### More Details See From 477dc140f333566b5d4112d408c2a41c8178475d Mon Sep 17 00:00:00 2001 From: Pranav Ghorpade Date: Fri, 13 Feb 2026 17:13:09 +0000 Subject: [PATCH 2/6] docs(binding-coap): document PSK usage with CoAPs Add example and explanation for configuring PSK security scheme in CoapsClient. Closes #954 Signed-off-by: Pranav Ghorpade --- packages/binding-coap/README.md | 131 ++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/packages/binding-coap/README.md b/packages/binding-coap/README.md index 0feb619b6..087f7b103 100644 --- a/packages/binding-coap/README.md +++ b/packages/binding-coap/README.md @@ -52,6 +52,78 @@ servient }); ``` +## Using PSK with CoAPs (DTLS) + +The CoAP binding also supports secure communication over `coaps://` using DTLS with Pre-Shared Keys (PSK). + +To use PSK security, define a `psk` security scheme in the Thing Description and provide the credentials when consuming the Thing. + +### Thing Description Example (PSK) + +```json +{ + "title": "SecureThing", + "securityDefinitions": { + "psk_sc": { + "scheme": "psk" + } + }, + "security": ["psk_sc"], + "properties": { + "count": { + "type": "integer", + "forms": [ + { + "href": "coaps://localhost:5684/count" + } + ] + } + } +} +``` + +### Client Example with PSK + +```js +const { Servient } = require("@node-wot/core"); +const { CoapClientFactory } = require("@node-wot/binding-coap"); + +const servient = new Servient(); +servient.addClientFactory(new CoapClientFactory()); + +servient + .start() + .then(async (WoT) => { + try { + const td = await WoT.requestThingDescription("coaps://localhost:5684/secureThing"); + const thing = await WoT.consume(td); + + // configure PSK security + thing.setSecurity( + td.securityDefinitions, + { + identity: "Client_identity", + psk: "secretPSK" + } + ); + + const value = await thing.readProperty("count"); + console.log("count value is:", await value.value()); + } catch (err) { + console.error("Script error:", err); + } + }) + .catch((err) => { + console.error("Start error:", err); + }); +``` + +### Notes + +- The `identity` must match the server configuration. +- The `psk` must match the server's configured secret. +- Currently, only the `psk` security scheme is supported for `coaps://` in this binding. + ### Server Example The server example produces a thing that allows for setting a property `count`. The thing is reachable through CoAP. @@ -94,65 +166,6 @@ servient.start().then((WoT) => { }); ``` -## Using CoAPs with PSK - -The CoAP binding also supports secure CoAP (`coaps://`) using DTLS with the -`psk` (Pre-Shared Key) security scheme. - -Currently, PSK support is implemented in the `CoapsClient` and can be -configured via the Thing Description and client credentials. - -### Thing Description Example - -To use PSK, the Thing Description must define a `psk` security scheme: - -```json -{ - "securityDefinitions": { - "psk_sc": { - "scheme": "psk" - } - }, - "security": ["psk_sc"] -} -``` - -### Client Configuration Example - -On the client side, credentials must be provided via the Servient -using `addCredentials()`. The credentials are associated with the -Thing's `id` and are automatically applied based on the TD security -configuration. - -```js -const { Servient } = require("@node-wot/core"); -const { CoapsClientFactory } = require("@node-wot/binding-coap"); - -const servient = new Servient(); -servient.addClientFactory(new CoapsClientFactory()); - -servient.start().then(async (WoT) => { - const td = await WoT.requestThingDescription("coaps://example.com/secure-thing"); - - // Configure PSK credentials for this Thing - servient.addCredentials({ - [td.id]: { - identity: "Client_identity", - psk: "secretPSK", - }, - }); - - const thing = await WoT.consume(td); - - await thing.invokeAction("someAction"); -}); -``` - -The `identity` and `psk` values must match the configuration of the -CoAPs server. - -> **Note:** Only the psk security scheme is currently supported for CoAPs. - ### More Details See From bf19d2f557a877710c8b158b38b2f9ca8e6c1bc2 Mon Sep 17 00:00:00 2001 From: Pranav Ghorpade Date: Sun, 15 Feb 2026 17:20:24 +0000 Subject: [PATCH 3/6] fix(binding-coap): align affordance forms typing and enable no-unnecessary-condition rule Signed-off-by: Pranav Ghorpade --- packages/binding-coap/README.md | 47 +++++----- packages/binding-coap/eslint.config.mjs | 10 ++ packages/binding-coap/src/coap-client.ts | 21 +++-- packages/binding-coap/src/coap-server.ts | 96 +++++++++----------- packages/binding-coap/src/coaps-client.ts | 10 +- packages/binding-coap/src/mdns-introducer.ts | 2 +- 6 files changed, 96 insertions(+), 90 deletions(-) create mode 100644 packages/binding-coap/eslint.config.mjs diff --git a/packages/binding-coap/README.md b/packages/binding-coap/README.md index 087f7b103..5b5d9406c 100644 --- a/packages/binding-coap/README.md +++ b/packages/binding-coap/README.md @@ -62,23 +62,23 @@ To use PSK security, define a `psk` security scheme in the Thing Description and ```json { - "title": "SecureThing", - "securityDefinitions": { - "psk_sc": { - "scheme": "psk" - } - }, - "security": ["psk_sc"], - "properties": { - "count": { - "type": "integer", - "forms": [ - { - "href": "coaps://localhost:5684/count" + "title": "SecureThing", + "securityDefinitions": { + "psk_sc": { + "scheme": "psk" + } + }, + "security": ["psk_sc"], + "properties": { + "count": { + "type": "integer", + "forms": [ + { + "href": "coaps://localhost:5684/count" + } + ] } - ] } - } } ``` @@ -99,13 +99,10 @@ servient const thing = await WoT.consume(td); // configure PSK security - thing.setSecurity( - td.securityDefinitions, - { - identity: "Client_identity", - psk: "secretPSK" - } - ); + thing.setSecurity(td.securityDefinitions, { + identity: "Client_identity", + psk: "secretPSK", + }); const value = await thing.readProperty("count"); console.log("count value is:", await value.value()); @@ -120,9 +117,9 @@ servient ### Notes -- The `identity` must match the server configuration. -- The `psk` must match the server's configured secret. -- Currently, only the `psk` security scheme is supported for `coaps://` in this binding. +- The `identity` must match the server configuration. +- The `psk` must match the server's configured secret. +- Currently, only the `psk` security scheme is supported for `coaps://` in this binding. ### Server Example diff --git a/packages/binding-coap/eslint.config.mjs b/packages/binding-coap/eslint.config.mjs new file mode 100644 index 000000000..04032d1ae --- /dev/null +++ b/packages/binding-coap/eslint.config.mjs @@ -0,0 +1,10 @@ +import rootConfig from "../../eslint.config.mjs"; + +export default [ + ...rootConfig, + { + rules: { + "@typescript-eslint/no-unnecessary-condition": "warn", + }, + }, +]; diff --git a/packages/binding-coap/src/coap-client.ts b/packages/binding-coap/src/coap-client.ts index d264c7cfe..1cb095f7d 100644 --- a/packages/binding-coap/src/coap-client.ts +++ b/packages/binding-coap/src/coap-client.ts @@ -65,7 +65,9 @@ export default class CoapClient implements ProtocolClient { debug(`CoapClient received Content-Format: ${res.headers["Content-Format"]}`); // FIXME does not work with blockwise because of node-coap - const contentType = (res.headers["Content-Format"] as string) ?? form.contentType; + const rawContentType = res.headers["Content-Format"]; + const contentType = + typeof rawContentType === "string" ? rawContentType : (form.contentType ?? ContentSerdes.DEFAULT); resolve(new Content(contentType, Readable.from(res.payload))); }); @@ -109,8 +111,11 @@ export default class CoapClient implements ProtocolClient { debug(`CoapClient received ${res.code} from ${form.href}`); debug(`CoapClient received Content-Format: ${res.headers["Content-Format"]}`); debug(`CoapClient received headers: ${JSON.stringify(res.headers)}`); - const contentType = res.headers["Content-Format"] as string; - resolve(new Content(contentType ?? "", Readable.from(res.payload))); + + const rawContentType = res.headers["Content-Format"]; + const contentType = typeof rawContentType === "string" ? rawContentType : ContentSerdes.DEFAULT; + + resolve(new Content(contentType, Readable.from(res.payload))); }); req.on("error", (err: Error) => reject(err)); (async () => { @@ -156,10 +161,12 @@ export default class CoapClient implements ProtocolClient { debug(`CoapClient received Content-Format: ${res.headers["Content-Format"]}`); // FIXME does not work with blockwise because of node-coap - const contentType = res.headers["Content-Format"] ?? form.contentType ?? ContentSerdes.DEFAULT; + const rawContentType = res.headers["Content-Format"]; + const contentType = + typeof rawContentType === "string" ? rawContentType : (form.contentType ?? ContentSerdes.DEFAULT); res.on("data", (data: Buffer) => { - next(new Content(`${contentType}`, Readable.from(res.payload))); + next(new Content(contentType, Readable.from(res.payload))); }); resolve( @@ -190,7 +197,9 @@ export default class CoapClient implements ProtocolClient { req.setOption("Accept", "application/td+json"); return new Promise((resolve, reject) => { req.on("response", (res: IncomingMessage) => { - const contentType = (res.headers["Content-Format"] as string) ?? "application/td+json"; + const rawContentType = res.headers["Content-Format"]; + const contentType = typeof rawContentType === "string" ? rawContentType : "application/td+json"; + resolve(new Content(contentType, Readable.from(res.payload))); }); req.on("error", (err: Error) => reject(err)); diff --git a/packages/binding-coap/src/coap-server.ts b/packages/binding-coap/src/coap-server.ts index d31acfa72..ac12aa9c3 100644 --- a/packages/binding-coap/src/coap-server.ts +++ b/packages/binding-coap/src/coap-server.ts @@ -41,7 +41,10 @@ const { debug, warn, info, error } = createLoggers("binding-coap", "coap-server" type CoreLinkFormatParameters = Map; -type AffordanceElement = PropertyElement | ActionElement | EventElement; +type AffordanceElement = + Omit | + Omit | + Omit; // TODO: Move to core? type AugmentedInteractionOptions = WoT.InteractionOptions & { formIndex: number }; @@ -145,11 +148,6 @@ export default class CoapServer implements ProtocolServer { const port = this.getPort(); const urlPath = this.createThingUrlPath(thing); - if (port === -1) { - warn("CoapServer is assigned an invalid port, aborting expose process."); - return; - } - this.fillInBindingData(thing, port, urlPath); debug(`CoapServer on port ${port} exposes '${thing.title}' as unique '/${urlPath}'`); @@ -236,12 +234,8 @@ export default class CoapServer implements ProtocolServer { } private addFormToAffordance(form: Form, affordance: AffordanceElement): void { - const affordanceForms = affordance.forms; - if (affordanceForms == null) { - affordance.forms = [form]; - } else { - affordanceForms.push(form); - } + const withForms = affordance as AffordanceElement & { forms?: Form[] }; + (withForms.forms ??= []).push(form); } private fillInPropertyBindingData(thing: ExposedThing, base: string, offeredMediaType: string) { @@ -354,9 +348,8 @@ export default class CoapServer implements ProtocolServer { public async destroy(thingId: string): Promise { debug(`CoapServer on port ${this.getPort()} destroying thingId '${thingId}'`); - for (const name of this.things.keys()) { - const exposedThing = this.things.get(name); - if (exposedThing?.id === thingId) { + for (const [name, exposedThing] of this.things.entries()) { + if (exposedThing.id === thingId) { this.things.delete(name); this.coreResources.delete(name); this.mdnsIntroducer?.delete(name); @@ -374,7 +367,7 @@ export default class CoapServer implements ProtocolServer { return Array.from(this.coreResources.values()) .map((resource) => { const formattedPath = ``; - const parameters = Array.from(resource.parameters?.entries() ?? []); + const parameters = resource.parameters ? Array.from(resource.parameters.entries()) : []; const parameterValues = parameters.map((parameter) => { const key = parameter[0]; @@ -499,20 +492,24 @@ export default class CoapServer implements ProtocolServer { const { thingKey, affordanceType, affordanceKey } = this.parseUriSegments(requestUri); const thing = this.things.get(thingKey); - if (thing == null) { + if (thing === undefined) { this.sendNotFoundResponse(res); return; } // TODO: Remove support for trailing slashes (or rather: trailing empty URI path segments) - if (affordanceType == null || affordanceType === "") { + if (!affordanceType) { await this.handleTdRequest(req, res, thing); return; } switch (affordanceType) { case this.PROPERTY_DIR: - this.handlePropertyRequest(thing, affordanceKey, req, res, contentType); + if (!affordanceKey) { + this.handlePropertiesRequest(req, contentType, thing, res); + } else { + this.handlePropertyRequest(thing, affordanceKey, req, res, contentType); + } break; case this.ACTION_DIR: this.handleActionRequest(thing, affordanceKey, req, res, contentType); @@ -554,11 +551,6 @@ export default class CoapServer implements ProtocolServer { ) { const property = thing.properties[affordanceKey]; - if (property == null) { - this.handlePropertiesRequest(req, contentType, thing, res); - return; - } - switch (req.method) { case "GET": if (req.headers.Observe == null) { @@ -588,7 +580,7 @@ export default class CoapServer implements ProtocolServer { ) { const forms = thing.forms; - if (forms == null) { + if (!forms || forms.length === 0) { this.sendNotFoundResponse(res); return; } @@ -618,26 +610,33 @@ export default class CoapServer implements ProtocolServer { contentType, thing.uriVariables ); - const readablePropertyKeys = this.getReadableProperties(thing).map(([key, _]) => key); + + const readablePropertyKeys = this.getReadableProperties(thing).map(([key]) => key); + const contentMap = await thing.handleReadMultipleProperties(readablePropertyKeys, interactionOptions); const recordResponse: Record = {}; for (const [key, content] of contentMap.entries()) { - const value = ContentSerdes.get().contentToValue( - { type: ContentSerdes.DEFAULT, body: await content.toBuffer() }, - {} - ); - - if (value == null) { - // TODO: How should this case be handled? + try { + if ( + content.type !== ContentSerdes.DEFAULT && + content.type !== "application/json" + ) { + continue; + } + const buffer = await content.toBuffer(); + const parsed = JSON.parse(buffer.toString()); + + recordResponse[key] = parsed; + } catch { + // Ignore non-JSON properties continue; } - - recordResponse[key] = value; } - const content = ContentSerdes.get().valueToContent(recordResponse, undefined, contentType); - this.streamContentResponse(res, content); + const responseContent = ContentSerdes.get().valueToContent(recordResponse,undefined,contentType); + + this.streamContentResponse(res, responseContent); } catch (err) { const errorMessage = `${err}`; error(`CoapServer on port ${this.getPort()} got internal error on read '${req.url}': ${errorMessage}`); @@ -775,11 +774,6 @@ export default class CoapServer implements ProtocolServer { ) { const action = thing.actions[affordanceKey]; - if (action == null) { - this.sendNotFoundResponse(res); - return; - } - if (req.method !== "POST") { this.sendMethodNotAllowedResponse(res); return; @@ -837,19 +831,14 @@ export default class CoapServer implements ProtocolServer { ) { const event = thing.events[affordanceKey]; - if (event == null) { - this.sendNotFoundResponse(res); - return; - } - if (req.method !== "GET") { this.sendMethodNotAllowedResponse(res); return; } - const observe = req.headers.Observe as number; + const observe = req.headers.Observe as number | undefined; - if (observe == null) { + if (observe === undefined) { debug( `CoapServer on port ${this.getPort()} rejects '${affordanceKey}' event subscription from ${Helpers.toUriLiteral( req.rsinfo.address @@ -923,17 +912,18 @@ export default class CoapServer implements ProtocolServer { } private getContentTypeFromRequest(req: IncomingMessage): string { - const contentType = req.headers["Content-Format"] as string; + const contentType = req.headers["Content-Format"] as string | undefined; - if (contentType == null) { + if (contentType === undefined) { warn( `CoapServer on port ${this.getPort()} received no Content-Format from ${Helpers.toUriLiteral( req.rsinfo.address )}:${req.rsinfo.port}` ); + return ContentSerdes.DEFAULT; } - return contentType ?? ContentSerdes.DEFAULT; + return contentType; } private checkContentTypeSupportForInput(method: string, contentType: string): boolean { @@ -974,7 +964,7 @@ export default class CoapServer implements ProtocolServer { } // TODO: The name of this method might not be ideal yet. - private streamContentResponse(res: OutgoingMessage, content: Content, options?: { end?: boolean | undefined }) { + private streamContentResponse(res: OutgoingMessage, content: Content, options?: { end?: boolean }) { res.setOption("Content-Format", content.type); res.code = "2.05"; content.body.pipe(res, options); diff --git a/packages/binding-coap/src/coaps-client.ts b/packages/binding-coap/src/coaps-client.ts index 0120e831f..8e885e4bb 100644 --- a/packages/binding-coap/src/coaps-client.ts +++ b/packages/binding-coap/src/coaps-client.ts @@ -110,13 +110,13 @@ export default class CoapsClient implements ProtocolClient { ): Promise { return new Promise((resolve, reject) => { const requestUri = new URL(form.href.replace(/$coaps/, "https")); - if (this.authorization != null) { + if (this.authorization !== undefined) { coaps.setSecurityParams(requestUri.hostname, this.authorization); } const callback = (resp: CoapResponse) => { if (resp.payload != null) { - next(new Content(form?.contentType ?? ContentSerdes.DEFAULT, Readable.from(resp.payload))); + next(new Content(form.contentType ?? ContentSerdes.DEFAULT, Readable.from(resp.payload))); } }; @@ -163,14 +163,14 @@ export default class CoapsClient implements ProtocolClient { } public setSecurity(metadata: Array, credentials?: pskSecurityParameters): boolean { - if (metadata === undefined || !Array.isArray(metadata) || metadata.length === 0) { + if (!Array.isArray(metadata) || metadata.length === 0) { warn(`CoapsClient received empty security metadata`); return false; } const security: SecurityScheme = metadata[0]; - if (security.scheme === "psk" && credentials != null) { + if (security.scheme === "psk" && credentials !== undefined) { this.authorization = { psk: {} }; this.authorization.psk[credentials.identity] = credentials.psk; } else if (security.scheme === "apikey") { @@ -224,7 +224,7 @@ export default class CoapsClient implements ProtocolClient { ): Promise { // url only works with http* const requestUri = new URL(form.href.replace(/$coaps/, "https")); - if (this.authorization != null) { + if (this.authorization !== undefined) { coaps.setSecurityParams(requestUri.hostname, this.authorization); } diff --git a/packages/binding-coap/src/mdns-introducer.ts b/packages/binding-coap/src/mdns-introducer.ts index e2979881c..6cfee92b8 100644 --- a/packages/binding-coap/src/mdns-introducer.ts +++ b/packages/binding-coap/src/mdns-introducer.ts @@ -73,7 +73,7 @@ export class MdnsIntroducer { private determineTarget(): string { const interfaces = networkInterfaces(); - for (const iface of Object.values(interfaces ?? {})) { + for (const iface of Object.values(interfaces)) { for (const entry of iface ?? []) { if (entry.internal === false) { if (entry.family === this.ipAddressFamily) { From 10a117af928ca6cfc71083db190bb2c1af2c074b Mon Sep 17 00:00:00 2001 From: Pranav Ghorpade Date: Sun, 15 Feb 2026 18:39:44 +0000 Subject: [PATCH 4/6] feat(core): allow registering multiple ContentCodecs per media type with optional scheme support Signed-off-by: Pranav Ghorpade --- packages/core/src/content-serdes.ts | 55 +++++++++++++++++++---------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index d6cac1189..4f413f467 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -53,7 +53,7 @@ export class ContentSerdes { public static readonly TD: string = "application/td+json"; public static readonly JSON_LD: string = "application/ld+json"; - private codecs: Map = new Map(); + private codecs: Map> = new Map(); private offered: Set = new Set(); public static get(): ContentSerdes { @@ -107,9 +107,31 @@ export class ContentSerdes { return params; } - public addCodec(codec: ContentCodec, offered = false): void { - ContentSerdes.get().codecs.set(codec.getMediaType(), codec); - if (offered) ContentSerdes.get().offered.add(codec.getMediaType()); + public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void { + const mediaType = codec.getMediaType(); + + if (!this.codecs.has(mediaType)) { + this.codecs.set(mediaType, new Map()); + } + + const schemeMap = this.codecs.get(mediaType)!; + + // store codec under specific scheme (or undefined for generic) + schemeMap.set(scheme, codec); + + if (offered) { + this.offered.add(mediaType); + } + } + private getCodec(mediaType: string, scheme?: string): ContentCodec | undefined { + const schemeMap = this.codecs.get(mediaType); + if (!schemeMap) return undefined; + + if (scheme !== undefined && schemeMap.has(scheme)) { + return schemeMap.get(scheme); + } + + return schemeMap.get(undefined); } public getSupportedMediaTypes(): Array { @@ -122,7 +144,7 @@ export class ContentSerdes { public isSupported(contentType: string): boolean { const mt = ContentSerdes.getMediaType(contentType); - return this.codecs.has(mt); + return this.getCodec(mt) !== undefined; } public contentToValue(content: ReadContent, schema: DataSchema): DataSchemaValue | undefined { @@ -141,20 +163,15 @@ export class ContentSerdes { const par = ContentSerdes.getMediaTypeParameters(content.type); // choose codec based on mediaType - if (this.codecs.has(mt)) { - debug(`ContentSerdes deserializing from ${content.type}`); - - const codec = this.codecs.get(mt); - - // use codec to deserialize - // this.codecs.has(mt) is true - const res = codec!.bytesToValue(content.body, schema, par); + const codec = this.getCodec(mt); - return res; - } else { - warn(`ContentSerdes passthrough due to unsupported media type '${mt}'`); - return content.body.toString(); - } + if (codec) { + debug(`ContentSerdes deserializing from ${content.type}`); + return codec.bytesToValue(content.body, schema, par); + } else { + warn(`ContentSerdes passthrough due to unsupported media type '${mt}'`); + return content.body.toString(); + } } public valueToContent( @@ -175,7 +192,7 @@ export class ContentSerdes { const par = ContentSerdes.getMediaTypeParameters(contentType); // choose codec based on mediaType - const codec = this.codecs.get(mt); + const codec = this.getCodec(mt); if (codec) { debug(`ContentSerdes serializing to ${contentType}`); bytes = codec.valueToBytes(value, schema, par); From 074faa42b71974db2f704006b0d4735ba0ee803e Mon Sep 17 00:00:00 2001 From: Pranav Ghorpade Date: Sun, 15 Feb 2026 18:39:44 +0000 Subject: [PATCH 5/6] feat(core): allow registering multiple ContentCodecs per media type with optional scheme support Signed-off-by: Pranav Ghorpade --- packages/binding-coap/src/coaps-client.ts | 448 +++++++++++----------- 1 file changed, 224 insertions(+), 224 deletions(-) diff --git a/packages/binding-coap/src/coaps-client.ts b/packages/binding-coap/src/coaps-client.ts index 8e885e4bb..4900f6b6c 100644 --- a/packages/binding-coap/src/coaps-client.ts +++ b/packages/binding-coap/src/coaps-client.ts @@ -1,253 +1,253 @@ -/******************************************************************************** - * Copyright (c) 2018 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and - * Document License (2015-05-13) which is available at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. - * - * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 - ********************************************************************************/ - -/** - * CoAPS client based on node-coap-client by AlCalzone - */ - -import { Subscription } from "rxjs/Subscription"; - -import { ProtocolClient, Content, createLoggers, ContentSerdes, SecurityScheme } from "@node-wot/core"; -import { CoapForm, CoapMethodName, isValidCoapMethod, isSupportedCoapMethod } from "./coap"; -import { CoapClient as coaps, CoapResponse, RequestMethod, SecurityParameters } from "node-coap-client"; -import { Readable } from "stream"; - -const { debug, warn, error } = createLoggers("binding-coap", "coaps-client"); - -declare interface pskSecurityParameters { - [identity: string]: string; -} - -export default class CoapsClient implements ProtocolClient { - // FIXME coap Agent closes socket when no messages in flight -> new socket with every request - private authorization?: SecurityParameters; - - public toString(): string { - return "[CoapsClient]"; - } + /******************************************************************************** + * Copyright (c) 2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ - public readResource(form: CoapForm): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "GET") - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - - // FIXME: Add toString conversion for response Content-Format - const contentType = form.contentType ?? ContentSerdes.DEFAULT; - const body = Readable.from(res.payload ?? Buffer.alloc(0)); - - resolve(new Content(contentType, body)); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + /** + * CoAPS client based on node-coap-client by AlCalzone + */ - public writeResource(form: CoapForm, content: Content): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "PUT", content) - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - - resolve(); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + import { Subscription } from "rxjs/Subscription"; - public invokeResource(form: CoapForm, content?: Content): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "POST", content) - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - - // FIXME: Add toString conversion for response Content-Format - const contentType = form.contentType ?? ContentSerdes.DEFAULT; - const body = Readable.from(res.payload ?? Buffer.alloc(0)); - - resolve(new Content(contentType, body)); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + import { ProtocolClient, Content, createLoggers, ContentSerdes, SecurityScheme } from "@node-wot/core"; + import { CoapForm, CoapMethodName, isValidCoapMethod, isSupportedCoapMethod } from "./coap"; + import { CoapClient as coaps, CoapResponse, RequestMethod, SecurityParameters } from "node-coap-client"; + import { Readable } from "stream"; - public unlinkResource(form: CoapForm): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "DELETE") - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - debug(`CoapsClient received headers: ${JSON.stringify(res.format)}`); - resolve(); - }) - .catch((err: Error) => { - reject(err); - }); - }); + const { debug, warn, error } = createLoggers("binding-coap", "coaps-client"); + + declare interface pskSecurityParameters { + [identity: string]: string; } - public subscribeResource( - form: CoapForm, - next: (value: Content) => void, - error?: (error: Error) => void, - complete?: () => void - ): Promise { - return new Promise((resolve, reject) => { - const requestUri = new URL(form.href.replace(/$coaps/, "https")); - if (this.authorization !== undefined) { - coaps.setSecurityParams(requestUri.hostname, this.authorization); - } + export default class CoapsClient implements ProtocolClient { + // FIXME coap Agent closes socket when no messages in flight -> new socket with every request + private authorization?: SecurityParameters; - const callback = (resp: CoapResponse) => { - if (resp.payload != null) { - next(new Content(form.contentType ?? ContentSerdes.DEFAULT, Readable.from(resp.payload))); - } - }; - - coaps - .observe(form.href, "get", callback) - .then(() => { - resolve( - new Subscription(() => { - coaps.stopObserving(form.href); - complete?.(); - }) - ); - }) - .catch((err) => { - error?.(err); - reject(err); - }); - }); - } + public toString(): string { + return "[CoapsClient]"; + } - /** - * @inheritdoc - */ - public async requestThingDescription(uri: string): Promise { - const response = await coaps.request(uri, "get", undefined, { - // FIXME: Add accept option - // Currently not supported by node-coap-client - }); - - // TODO: Respect Content-Format in response. - // Currently not really well supported by node-coap-client - const contentType = "application/td+json"; - const payload = response.payload ?? Buffer.alloc(0); - - return new Content(contentType, Readable.from(payload)); - } + public readResource(form: CoapForm): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "GET") + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + + // FIXME: Add toString conversion for response Content-Format + const contentType = form.contentType ?? ContentSerdes.DEFAULT; + const body = Readable.from(res.payload ?? Buffer.alloc(0)); + + resolve(new Content(contentType, body)); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public async start(): Promise { - // do nothing - } + public writeResource(form: CoapForm, content: Content): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "PUT", content) + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + + resolve(); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public async stop(): Promise { - // FIXME coap does not provide proper API to close Agent - } + public invokeResource(form: CoapForm, content?: Content): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "POST", content) + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + + // FIXME: Add toString conversion for response Content-Format + const contentType = form.contentType ?? ContentSerdes.DEFAULT; + const body = Readable.from(res.payload ?? Buffer.alloc(0)); + + resolve(new Content(contentType, body)); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } + + public unlinkResource(form: CoapForm): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "DELETE") + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + debug(`CoapsClient received headers: ${JSON.stringify(res.format)}`); + resolve(); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public setSecurity(metadata: Array, credentials?: pskSecurityParameters): boolean { - if (!Array.isArray(metadata) || metadata.length === 0) { - warn(`CoapsClient received empty security metadata`); - return false; + public subscribeResource( + form: CoapForm, + next: (value: Content) => void, + error?: (error: Error) => void, + complete?: () => void + ): Promise { + return new Promise((resolve, reject) => { + const requestUri = new URL(form.href.replace(/$coaps/, "https")); + if (this.authorization !== undefined) { + coaps.setSecurityParams(requestUri.hostname, this.authorization); + } + + const callback = (resp: CoapResponse) => { + if (resp.payload != null) { + next(new Content(form.contentType ?? ContentSerdes.DEFAULT, Readable.from(resp.payload))); + } + }; + + coaps + .observe(form.href, "get", callback) + .then(() => { + resolve( + new Subscription(() => { + coaps.stopObserving(form.href); + complete?.(); + }) + ); + }) + .catch((err) => { + error?.(err); + reject(err); + }); + }); } - const security: SecurityScheme = metadata[0]; - - if (security.scheme === "psk" && credentials !== undefined) { - this.authorization = { psk: {} }; - this.authorization.psk[credentials.identity] = credentials.psk; - } else if (security.scheme === "apikey") { - error(`CoapsClient cannot use Apikey: Not implemented`); - return false; - } else { - error(`CoapsClient cannot set security scheme '${security.scheme}'`); - error(`${metadata}`); - return false; + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + const response = await coaps.request(uri, "get", undefined, { + // FIXME: Add accept option + // Currently not supported by node-coap-client + }); + + // TODO: Respect Content-Format in response. + // Currently not really well supported by node-coap-client + const contentType = "application/td+json"; + const payload = response.payload ?? Buffer.alloc(0); + + return new Content(contentType, Readable.from(payload)); } - // TODO: node-coap-client does not support proxy / options in general :o - /* - if (security.proxyURI) { - if (this.proxyOptions !== null) { - info(`HttpClient overriding client-side proxy with security proxyURI '${security.proxyURI}`); - } - - this.proxyOptions = this.uriToOptions(security.proxyURI); - - if (metadata.proxyauthorization == "Basic") { - this.proxyOptions.headers = {}; - this.proxyOptions.headers['Proxy-Authorization'] = "Basic " + Buffer.from(credentials.username + ":" + credentials.password).toString('base64'); - } else if (metadata.proxyauthorization == "Bearer") { - this.proxyOptions.headers = {}; - this.proxyOptions.headers['Proxy-Authorization'] = "Bearer " + credentials.token; - } - } - */ + public async start(): Promise { + // do nothing + } - debug(`CoapsClient using security scheme '${security.scheme}'`); - return true; - } + public async stop(): Promise { + // FIXME coap does not provide proper API to close Agent + } - private determineRequestMethod(formMethod: CoapMethodName, defaultMethod: string) { - if (isSupportedCoapMethod(formMethod)) { - return formMethod; - } else if (isValidCoapMethod(formMethod)) { - debug(`Method ${formMethod} is not supported yet.`, `Using default method ${defaultMethod} instead.`); - } else { - debug(`Unknown method ${formMethod} found.`, `Using default method ${defaultMethod} instead.`); + public setSecurity(metadata: Array, credentials?: pskSecurityParameters): boolean { + if (!Array.isArray(metadata) || metadata.length === 0) { + warn(`CoapsClient received empty security metadata`); + return false; + } + + const security: SecurityScheme = metadata[0]; + + if (security.scheme === "psk" && credentials !== undefined) { + this.authorization = { psk: {} }; + this.authorization.psk[credentials.identity] = credentials.psk; + } else if (security.scheme === "apikey") { + error(`CoapsClient cannot use Apikey: Not implemented`); + return false; + } else { + error(`CoapsClient cannot set security scheme '${security.scheme}'`); + error(`${metadata}`); + return false; + } + + // TODO: node-coap-client does not support proxy / options in general :o + /* + if (security.proxyURI) { + if (this.proxyOptions !== null) { + info(`HttpClient overriding client-side proxy with security proxyURI '${security.proxyURI}`); } - return defaultMethod; - } + this.proxyOptions = this.uriToOptions(security.proxyURI); + + if (metadata.proxyauthorization == "Basic") { + this.proxyOptions.headers = {}; + this.proxyOptions.headers['Proxy-Authorization'] = "Basic " + Buffer.from(credentials.username + ":" + credentials.password).toString('base64'); + } else if (metadata.proxyauthorization == "Bearer") { + this.proxyOptions.headers = {}; + this.proxyOptions.headers['Proxy-Authorization'] = "Bearer " + credentials.token; + } + } + */ - private async generateRequest( - form: CoapForm, - defaultMethod: CoapMethodName, - content?: Content - ): Promise { - // url only works with http* - const requestUri = new URL(form.href.replace(/$coaps/, "https")); - if (this.authorization !== undefined) { - coaps.setSecurityParams(requestUri.hostname, this.authorization); + debug(`CoapsClient using security scheme '${security.scheme}'`); + return true; } - let method; + private determineRequestMethod(formMethod: CoapMethodName, defaultMethod: string) { + if (isSupportedCoapMethod(formMethod)) { + return formMethod; + } else if (isValidCoapMethod(formMethod)) { + debug(`Method ${formMethod} is not supported yet.`, `Using default method ${defaultMethod} instead.`); + } else { + debug(`Unknown method ${formMethod} found.`, `Using default method ${defaultMethod} instead.`); + } - if (form["cov:method"] != null) { - const formMethodName = form["cov:method"]; - debug(`CoapClient got Form "methodName" ${formMethodName}`); - method = this.determineRequestMethod(formMethodName, defaultMethod); - } else { - method = defaultMethod; + return defaultMethod; } - debug(`CoapsClient sending ${method} to ${form.href}`); + private async generateRequest( + form: CoapForm, + defaultMethod: CoapMethodName, + content?: Content + ): Promise { + // url only works with http* + const requestUri = new URL(form.href.replace(/$coaps/, "https")); + if (this.authorization !== undefined) { + coaps.setSecurityParams(requestUri.hostname, this.authorization); + } + + let method; + + if (form["cov:method"] != null) { + const formMethodName = form["cov:method"]; + debug(`CoapClient got Form "methodName" ${formMethodName}`); + method = this.determineRequestMethod(formMethodName, defaultMethod); + } else { + method = defaultMethod; + } + + debug(`CoapsClient sending ${method} to ${form.href}`); - const body = await content?.toBuffer(); + const body = await content?.toBuffer(); - const req = coaps.request( - form.href /* string */, - method.toLowerCase() as RequestMethod /* "get" | "post" | "put" | "delete" */, - body - ); + const req = coaps.request( + form.href /* string */, + method.toLowerCase() as RequestMethod /* "get" | "post" | "put" | "delete" */, + body + ); - return req; + return req; + } } -} From 5b89b6dfb6b62f71f4b85782bd1ed22c14dc1a2c Mon Sep 17 00:00:00 2001 From: Pranav Ghorpade Date: Mon, 16 Feb 2026 12:43:47 +0000 Subject: [PATCH 6/6] style: apply prettier formatting Signed-off-by: Pranav Ghorpade --- packages/binding-coap/src/coaps-client.ts | 420 +++++++++++----------- 1 file changed, 210 insertions(+), 210 deletions(-) diff --git a/packages/binding-coap/src/coaps-client.ts b/packages/binding-coap/src/coaps-client.ts index 4900f6b6c..6bd8aba85 100644 --- a/packages/binding-coap/src/coaps-client.ts +++ b/packages/binding-coap/src/coaps-client.ts @@ -1,189 +1,189 @@ - /******************************************************************************** - * Copyright (c) 2018 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and - * Document License (2015-05-13) which is available at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. - * - * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 - ********************************************************************************/ - - /** - * CoAPS client based on node-coap-client by AlCalzone - */ - - import { Subscription } from "rxjs/Subscription"; - - import { ProtocolClient, Content, createLoggers, ContentSerdes, SecurityScheme } from "@node-wot/core"; - import { CoapForm, CoapMethodName, isValidCoapMethod, isSupportedCoapMethod } from "./coap"; - import { CoapClient as coaps, CoapResponse, RequestMethod, SecurityParameters } from "node-coap-client"; - import { Readable } from "stream"; - - const { debug, warn, error } = createLoggers("binding-coap", "coaps-client"); - - declare interface pskSecurityParameters { - [identity: string]: string; +/******************************************************************************** + * Copyright (c) 2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +/** + * CoAPS client based on node-coap-client by AlCalzone + */ + +import { Subscription } from "rxjs/Subscription"; + +import { ProtocolClient, Content, createLoggers, ContentSerdes, SecurityScheme } from "@node-wot/core"; +import { CoapForm, CoapMethodName, isValidCoapMethod, isSupportedCoapMethod } from "./coap"; +import { CoapClient as coaps, CoapResponse, RequestMethod, SecurityParameters } from "node-coap-client"; +import { Readable } from "stream"; + +const { debug, warn, error } = createLoggers("binding-coap", "coaps-client"); + +declare interface pskSecurityParameters { + [identity: string]: string; +} + +export default class CoapsClient implements ProtocolClient { + // FIXME coap Agent closes socket when no messages in flight -> new socket with every request + private authorization?: SecurityParameters; + + public toString(): string { + return "[CoapsClient]"; } - export default class CoapsClient implements ProtocolClient { - // FIXME coap Agent closes socket when no messages in flight -> new socket with every request - private authorization?: SecurityParameters; - - public toString(): string { - return "[CoapsClient]"; - } + public readResource(form: CoapForm): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "GET") + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + + // FIXME: Add toString conversion for response Content-Format + const contentType = form.contentType ?? ContentSerdes.DEFAULT; + const body = Readable.from(res.payload ?? Buffer.alloc(0)); + + resolve(new Content(contentType, body)); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public readResource(form: CoapForm): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "GET") - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - - // FIXME: Add toString conversion for response Content-Format - const contentType = form.contentType ?? ContentSerdes.DEFAULT; - const body = Readable.from(res.payload ?? Buffer.alloc(0)); - - resolve(new Content(contentType, body)); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + public writeResource(form: CoapForm, content: Content): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "PUT", content) + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + + resolve(); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public writeResource(form: CoapForm, content: Content): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "PUT", content) - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - - resolve(); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + public invokeResource(form: CoapForm, content?: Content): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "POST", content) + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + + // FIXME: Add toString conversion for response Content-Format + const contentType = form.contentType ?? ContentSerdes.DEFAULT; + const body = Readable.from(res.payload ?? Buffer.alloc(0)); + + resolve(new Content(contentType, body)); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public invokeResource(form: CoapForm, content?: Content): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "POST", content) - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - - // FIXME: Add toString conversion for response Content-Format - const contentType = form.contentType ?? ContentSerdes.DEFAULT; - const body = Readable.from(res.payload ?? Buffer.alloc(0)); - - resolve(new Content(contentType, body)); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + public unlinkResource(form: CoapForm): Promise { + return new Promise((resolve, reject) => { + this.generateRequest(form, "DELETE") + .then((res: CoapResponse) => { + debug(`CoapsClient received ${res.code} from ${form.href}`); + debug(`CoapsClient received headers: ${JSON.stringify(res.format)}`); + resolve(); + }) + .catch((err: Error) => { + reject(err); + }); + }); + } - public unlinkResource(form: CoapForm): Promise { - return new Promise((resolve, reject) => { - this.generateRequest(form, "DELETE") - .then((res: CoapResponse) => { - debug(`CoapsClient received ${res.code} from ${form.href}`); - debug(`CoapsClient received headers: ${JSON.stringify(res.format)}`); - resolve(); - }) - .catch((err: Error) => { - reject(err); - }); - }); - } + public subscribeResource( + form: CoapForm, + next: (value: Content) => void, + error?: (error: Error) => void, + complete?: () => void + ): Promise { + return new Promise((resolve, reject) => { + const requestUri = new URL(form.href.replace(/$coaps/, "https")); + if (this.authorization !== undefined) { + coaps.setSecurityParams(requestUri.hostname, this.authorization); + } - public subscribeResource( - form: CoapForm, - next: (value: Content) => void, - error?: (error: Error) => void, - complete?: () => void - ): Promise { - return new Promise((resolve, reject) => { - const requestUri = new URL(form.href.replace(/$coaps/, "https")); - if (this.authorization !== undefined) { - coaps.setSecurityParams(requestUri.hostname, this.authorization); + const callback = (resp: CoapResponse) => { + if (resp.payload != null) { + next(new Content(form.contentType ?? ContentSerdes.DEFAULT, Readable.from(resp.payload))); } + }; + + coaps + .observe(form.href, "get", callback) + .then(() => { + resolve( + new Subscription(() => { + coaps.stopObserving(form.href); + complete?.(); + }) + ); + }) + .catch((err) => { + error?.(err); + reject(err); + }); + }); + } - const callback = (resp: CoapResponse) => { - if (resp.payload != null) { - next(new Content(form.contentType ?? ContentSerdes.DEFAULT, Readable.from(resp.payload))); - } - }; - - coaps - .observe(form.href, "get", callback) - .then(() => { - resolve( - new Subscription(() => { - coaps.stopObserving(form.href); - complete?.(); - }) - ); - }) - .catch((err) => { - error?.(err); - reject(err); - }); - }); - } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + const response = await coaps.request(uri, "get", undefined, { + // FIXME: Add accept option + // Currently not supported by node-coap-client + }); + + // TODO: Respect Content-Format in response. + // Currently not really well supported by node-coap-client + const contentType = "application/td+json"; + const payload = response.payload ?? Buffer.alloc(0); + + return new Content(contentType, Readable.from(payload)); + } - /** - * @inheritdoc - */ - public async requestThingDescription(uri: string): Promise { - const response = await coaps.request(uri, "get", undefined, { - // FIXME: Add accept option - // Currently not supported by node-coap-client - }); - - // TODO: Respect Content-Format in response. - // Currently not really well supported by node-coap-client - const contentType = "application/td+json"; - const payload = response.payload ?? Buffer.alloc(0); - - return new Content(contentType, Readable.from(payload)); - } + public async start(): Promise { + // do nothing + } - public async start(): Promise { - // do nothing - } + public async stop(): Promise { + // FIXME coap does not provide proper API to close Agent + } - public async stop(): Promise { - // FIXME coap does not provide proper API to close Agent + public setSecurity(metadata: Array, credentials?: pskSecurityParameters): boolean { + if (!Array.isArray(metadata) || metadata.length === 0) { + warn(`CoapsClient received empty security metadata`); + return false; } - public setSecurity(metadata: Array, credentials?: pskSecurityParameters): boolean { - if (!Array.isArray(metadata) || metadata.length === 0) { - warn(`CoapsClient received empty security metadata`); - return false; - } - - const security: SecurityScheme = metadata[0]; - - if (security.scheme === "psk" && credentials !== undefined) { - this.authorization = { psk: {} }; - this.authorization.psk[credentials.identity] = credentials.psk; - } else if (security.scheme === "apikey") { - error(`CoapsClient cannot use Apikey: Not implemented`); - return false; - } else { - error(`CoapsClient cannot set security scheme '${security.scheme}'`); - error(`${metadata}`); - return false; - } + const security: SecurityScheme = metadata[0]; + + if (security.scheme === "psk" && credentials !== undefined) { + this.authorization = { psk: {} }; + this.authorization.psk[credentials.identity] = credentials.psk; + } else if (security.scheme === "apikey") { + error(`CoapsClient cannot use Apikey: Not implemented`); + return false; + } else { + error(`CoapsClient cannot set security scheme '${security.scheme}'`); + error(`${metadata}`); + return false; + } - // TODO: node-coap-client does not support proxy / options in general :o - /* + // TODO: node-coap-client does not support proxy / options in general :o + /* if (security.proxyURI) { if (this.proxyOptions !== null) { info(`HttpClient overriding client-side proxy with security proxyURI '${security.proxyURI}`); @@ -201,53 +201,53 @@ } */ - debug(`CoapsClient using security scheme '${security.scheme}'`); - return true; + debug(`CoapsClient using security scheme '${security.scheme}'`); + return true; + } + + private determineRequestMethod(formMethod: CoapMethodName, defaultMethod: string) { + if (isSupportedCoapMethod(formMethod)) { + return formMethod; + } else if (isValidCoapMethod(formMethod)) { + debug(`Method ${formMethod} is not supported yet.`, `Using default method ${defaultMethod} instead.`); + } else { + debug(`Unknown method ${formMethod} found.`, `Using default method ${defaultMethod} instead.`); } - private determineRequestMethod(formMethod: CoapMethodName, defaultMethod: string) { - if (isSupportedCoapMethod(formMethod)) { - return formMethod; - } else if (isValidCoapMethod(formMethod)) { - debug(`Method ${formMethod} is not supported yet.`, `Using default method ${defaultMethod} instead.`); - } else { - debug(`Unknown method ${formMethod} found.`, `Using default method ${defaultMethod} instead.`); - } + return defaultMethod; + } - return defaultMethod; + private async generateRequest( + form: CoapForm, + defaultMethod: CoapMethodName, + content?: Content + ): Promise { + // url only works with http* + const requestUri = new URL(form.href.replace(/$coaps/, "https")); + if (this.authorization !== undefined) { + coaps.setSecurityParams(requestUri.hostname, this.authorization); } - private async generateRequest( - form: CoapForm, - defaultMethod: CoapMethodName, - content?: Content - ): Promise { - // url only works with http* - const requestUri = new URL(form.href.replace(/$coaps/, "https")); - if (this.authorization !== undefined) { - coaps.setSecurityParams(requestUri.hostname, this.authorization); - } - - let method; + let method; - if (form["cov:method"] != null) { - const formMethodName = form["cov:method"]; - debug(`CoapClient got Form "methodName" ${formMethodName}`); - method = this.determineRequestMethod(formMethodName, defaultMethod); - } else { - method = defaultMethod; - } + if (form["cov:method"] != null) { + const formMethodName = form["cov:method"]; + debug(`CoapClient got Form "methodName" ${formMethodName}`); + method = this.determineRequestMethod(formMethodName, defaultMethod); + } else { + method = defaultMethod; + } - debug(`CoapsClient sending ${method} to ${form.href}`); + debug(`CoapsClient sending ${method} to ${form.href}`); - const body = await content?.toBuffer(); + const body = await content?.toBuffer(); - const req = coaps.request( - form.href /* string */, - method.toLowerCase() as RequestMethod /* "get" | "post" | "put" | "delete" */, - body - ); + const req = coaps.request( + form.href /* string */, + method.toLowerCase() as RequestMethod /* "get" | "post" | "put" | "delete" */, + body + ); - return req; - } + return req; } +}