From 826039d9edb6b337eec875e535fff5e6e3518b2a Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Fri, 8 Dec 2017 16:52:10 +0100 Subject: [PATCH 01/12] added a basic test for the block-wise transfer option --- src/Message.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/Message.test.ts b/src/Message.test.ts index 7ce53e8c..5c112bd6 100644 --- a/src/Message.test.ts +++ b/src/Message.test.ts @@ -32,3 +32,45 @@ describe("Message Tests =>", () => { }); }); + +describe.only("blockwise tests =>", () => { + // This buffer from https://github.com/AlCalzone/node-coap-client/issues/21 + // is a raw message contains the Block option + const buf = Buffer.from( + "64450025018fccf460613223093a80910eff7b2239303031223a224556455259444159" + + "222c2239303032223a313530383235303135392c2239303638223a312c223930303322" + + "3a3230303436382c2239303537223a302c223135303133223a5b7b2235383530223a31" + + "2c2235383531223a3230332c2235373037223a353432372c2235373038223a34323539" + + "362c2235373039223a33303031352c2235373130223a32363837302c2235373131223a" + + "302c2235373036223a22663165306235222c2239303033223a36353533397d2c7b2235" + + "383530223a312c2235383531223a3230332c2235373037223a353432372c2235373038" + + "223a34323539362c2235373039223a33303031352c2235373130223a32363837302c22" + + "35373131223a302c2235373036223a22663165306235222c2239303033223a36353534" + + "307d2c7b2235383530223a312c2235383531223a3230332c2235373037223a35343237" + + "2c2235373038223a34323539362c2235373039223a33303031352c2235373130223a32" + + "363837302c2235373131223a302c2235373036223a22663165306235222c2239303033" + + "223a36353534317d2c7b2235383530223a312c2235383531223a3230332c2235373037" + + "223a353432372c2235373038223a34323539362c2235373039223a33303031352c2235" + + "373130223a32363837302c2235373131223a302c2235373036223a2266316530623522" + + "2c2239303033223a36353534327d2c7b2235383530223a312c2235383531223a323033" + + "2c2235373037223a353432372c2235373038223a34323539362c2235373039223a3330" + + "3031352c2235373130223a32363837302c2235373131223a302c2235373036223a2266" + + "3165306235222c2239303033223a36353534337d2c7b2235383530223a312c22353835" + + "31223a3230332c2235373037223a353432372c2235373038223a34323539362c223537" + + "3039223a33303031352c2235373130223a32363837302c2235373131223a302c223537" + + "3036223a22663165306235222c2239303033223a36353534347d2c7b2235383530223a" + + "312c2235383531223a3230332c2235373037223a353432372c2235373038223a343235" + + "39362c2235373039223a33303031352c2235373130223a32363837302c223537313122" + + "3a302c2235373036223a22663165306235222c2239303033223a36353534357d2c7b22" + + "35383530223a312c2235383531223a3230332c2235373037223a353432372c22353730" + + "38223a34323539362c2235373039223a33303031352c2235373130223a32363837302c" + + "2235373131223a302c2235373036223a22663165306235222c2239303033223a363535" + + "34367d2c7b2235383530223a312c2235383531223a3230332c2235373037223a353432" + + "372c2235373038223a34323539362c2235373039223a3330303135", + "hex", + ); + + it("should parse without crashing", () => { + const msg = Message.parse(buf); + }); +}); From b2d53544148dd57fd05188aeb62f2daa519527e9 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Fri, 8 Dec 2017 17:00:47 +0100 Subject: [PATCH 02/12] defined Block1, Block2 and Size2 options --- build/Option.js | 3 +++ src/Option.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/build/Option.js b/build/Option.js index 24d51e38..bdecefe0 100644 --- a/build/Option.js +++ b/build/Option.js @@ -254,6 +254,9 @@ defineOptionConstructor(NumericOption, 7, "Uri-Port", false, 2); defineOptionConstructor(NumericOption, 12, "Content-Format", false, 2); defineOptionConstructor(NumericOption, 14, "Max-Age", false, 4); defineOptionConstructor(NumericOption, 17, "Accept", false, 2); +defineOptionConstructor(NumericOption, 23, "Block2", false, 3); +defineOptionConstructor(NumericOption, 27, "Block1", false, 3); +defineOptionConstructor(NumericOption, 28, "Size2", false, 4); defineOptionConstructor(NumericOption, 60, "Size1", false, 4); defineOptionConstructor(BinaryOption, 1, "If-Match", true, 0, 8); defineOptionConstructor(BinaryOption, 4, "ETag", true, 1, 8); diff --git a/src/Option.ts b/src/Option.ts index 501dd042..0f7754bf 100644 --- a/src/Option.ts +++ b/src/Option.ts @@ -312,6 +312,9 @@ defineOptionConstructor(NumericOption, 7, "Uri-Port", false, 2); defineOptionConstructor(NumericOption, 12, "Content-Format", false, 2); defineOptionConstructor(NumericOption, 14, "Max-Age", false, 4); defineOptionConstructor(NumericOption, 17, "Accept", false, 2); +defineOptionConstructor(NumericOption, 23, "Block2", false, 3); +defineOptionConstructor(NumericOption, 27, "Block1", false, 3); +defineOptionConstructor(NumericOption, 28, "Size2", false, 4); defineOptionConstructor(NumericOption, 60, "Size1", false, 4); defineOptionConstructor(BinaryOption, 1, "If-Match", true, 0, 8); defineOptionConstructor(BinaryOption, 4, "ETag", true, 1, 8); From 9e0d08497dd6bf764b5cb7cc257bcc997a9325d4 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Fri, 8 Dec 2017 17:03:31 +0100 Subject: [PATCH 03/12] added new message codes --- build/Message.d.ts | 2 ++ build/Message.js | 2 ++ src/Message.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/build/Message.d.ts b/build/Message.d.ts index 501fe610..0c542443 100644 --- a/build/Message.d.ts +++ b/build/Message.d.ts @@ -36,6 +36,7 @@ export declare const MessageCodes: Readonly<{ valid: MessageCode; changed: MessageCode; content: MessageCode; + continue: MessageCode; }; clientError: { __major: number; @@ -46,6 +47,7 @@ export declare const MessageCodes: Readonly<{ notFound: MessageCode; methodNotAllowed: MessageCode; notAcceptable: MessageCode; + requestEntityIncomplete: MessageCode; preconditionFailed: MessageCode; requestEntityTooLarge: MessageCode; unsupportedContentFormat: MessageCode; diff --git a/build/Message.js b/build/Message.js index aff20d4d..d7bf2221 100644 --- a/build/Message.js +++ b/build/Message.js @@ -49,6 +49,7 @@ exports.MessageCodes = Object.freeze({ valid: new MessageCode(2, 3), changed: new MessageCode(2, 4), content: new MessageCode(2, 5), + continue: new MessageCode(2, 31), }, clientError: { __major: 4, @@ -59,6 +60,7 @@ exports.MessageCodes = Object.freeze({ notFound: new MessageCode(4, 4), methodNotAllowed: new MessageCode(4, 5), notAcceptable: new MessageCode(4, 6), + requestEntityIncomplete: new MessageCode(4, 8), preconditionFailed: new MessageCode(4, 12), requestEntityTooLarge: new MessageCode(4, 13), unsupportedContentFormat: new MessageCode(4, 15), diff --git a/src/Message.ts b/src/Message.ts index 0d68df4f..6454f109 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -58,6 +58,7 @@ export const MessageCodes = Object.freeze({ valid: new MessageCode(2, 3), changed: new MessageCode(2, 4), content: new MessageCode(2, 5), + continue: new MessageCode(2, 31), }, clientError: { @@ -69,6 +70,7 @@ export const MessageCodes = Object.freeze({ notFound: new MessageCode(4, 4), methodNotAllowed: new MessageCode(4, 5), notAcceptable: new MessageCode(4, 6), + requestEntityIncomplete: new MessageCode(4, 8), preconditionFailed: new MessageCode(4, 12), requestEntityTooLarge: new MessageCode(4, 13), unsupportedContentFormat: new MessageCode(4, 15), From 662005b57c54dbb8907115df726bd7e40196c5b5 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Fri, 8 Dec 2017 17:33:21 +0100 Subject: [PATCH 04/12] added check for partial messages --- build/Message.d.ts | 4 ++++ build/Message.js | 13 +++++++++++++ src/Message.ts | 13 +++++++++++++ 3 files changed, 30 insertions(+) diff --git a/build/Message.d.ts b/build/Message.d.ts index 0c542443..068257a0 100644 --- a/build/Message.d.ts +++ b/build/Message.d.ts @@ -83,4 +83,8 @@ export declare class Message { * serializes this message into a buffer */ serialize(): Buffer; + /** + * Checks if this message is part of a blockwise transfer + */ + isPartialMessage(): boolean; } diff --git a/build/Message.js b/build/Message.js index d7bf2221..d9a03dcc 100644 --- a/build/Message.js +++ b/build/Message.js @@ -167,6 +167,19 @@ class Message { } return ret; } + /** + * Checks if this message is part of a blockwise transfer + */ + isPartialMessage() { + // TODO: can we put the codes in an enum? + const block1option = this.options.find(o => o.code === 27 /* Block1 */); + const block2option = this.options.find(o => o.code === 23 /* Block2 */); + if (this.code.isRequest() && block1option != null) + return true; + if (this.code.isResponse() && block2option != null) + return true; + return false; + } } exports.Message = Message; /* diff --git a/src/Message.ts b/src/Message.ts index 6454f109..53eb75a0 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -1,4 +1,5 @@ import { Option } from "./Option"; +import { fail } from "assert"; export enum MessageType { CON = 0, // Confirmable @@ -202,6 +203,18 @@ export class Message { return ret; } + /** + * Checks if this message is part of a blockwise transfer + */ + public isPartialMessage(): boolean { + // TODO: can we put the codes in an enum? + const block1option = this.options.find(o => o.code === 27 /* Block1 */); + const block2option = this.options.find(o => o.code === 23 /* Block2 */); + if (this.code.isRequest() && block1option != null) return true; + if (this.code.isResponse() && block2option != null) return true; + return false; + } + } /* From 7a45de24bdac0d8b524da14cc2346dc02e65017f Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Mon, 18 Dec 2017 12:02:18 +0100 Subject: [PATCH 05/12] removed a bunch of unused code --- build/CoapClient.d.ts | 2 -- build/CoapClient.js | 11 ++--------- src/CoapClient.ts | 11 ++--------- src/Message.ts | 1 - 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/build/CoapClient.d.ts b/build/CoapClient.d.ts index 8d5bab5d..4b1c3d03 100644 --- a/build/CoapClient.d.ts +++ b/build/CoapClient.d.ts @@ -40,8 +40,6 @@ export declare class CoapClient { private static pendingRequestsByUrl; /** Queue of the messages waiting to be sent */ private static sendQueue; - /** Number of message we expect an answer for */ - private static concurrency; /** * Sets the security params to be used for the given hostname */ diff --git a/build/CoapClient.js b/build/CoapClient.js index ce4a97e7..a03e0325 100644 --- a/build/CoapClient.js +++ b/build/CoapClient.js @@ -184,14 +184,12 @@ class CoapClient { options.retransmit = true; // retrieve or create the connection we're going to use const origin = Origin_1.Origin.fromUrl(url); - const originString = origin.toString(); const connection = yield CoapClient.getConnection(origin); // find all the message parameters const type = options.confirmable ? Message_1.MessageType.CON : Message_1.MessageType.NON; const code = Message_1.MessageCodes.request[method]; const messageId = connection.lastMsgId = incrementMessageID(connection.lastMsgId); const token = connection.lastToken = incrementToken(connection.lastToken); - const tokenString = token.toString("hex"); payload = payload || Buffer.from([]); // create message options, be careful to order them by code, no sorting is implemented yet const msgOptions = []; @@ -368,14 +366,12 @@ class CoapClient { options.retransmit = true; // retrieve or create the connection we're going to use const origin = Origin_1.Origin.fromUrl(url); - const originString = origin.toString(); const connection = yield CoapClient.getConnection(origin); // find all the message parameters const type = options.confirmable ? Message_1.MessageType.CON : Message_1.MessageType.NON; const code = Message_1.MessageCodes.request[method]; const messageId = connection.lastMsgId = incrementMessageID(connection.lastMsgId); const token = connection.lastToken = incrementToken(connection.lastToken); - const tokenString = token.toString("hex"); payload = payload || Buffer.from([]); // create message options, be careful to order them by code, no sorting is implemented yet const msgOptions = []; @@ -393,8 +389,8 @@ class CoapClient { msgOptions.push(...pathParts.map(part => Option_1.Options.UriPath(part))); // [12] content format msgOptions.push(Option_1.Options.ContentFormat(ContentFormats_1.ContentFormats.application_json)); - // create the promise we're going to return - const response = DeferredPromise_1.createDeferredPromise(); + // In contrast to requests, we don't work with a deferred promise when observing + // Instead, we invoke a callback for *every* response. // create the message we're going to send const message = CoapClient.createMessage(type, code, messageId, token, msgOptions, payload); // create the retransmission info @@ -733,7 +729,6 @@ class CoapClient { target = Origin_1.Origin.fromUrl(target); } // retrieve or create the connection we're going to use - const originString = target.toString(); try { yield CoapClient.getConnection(target); return true; @@ -876,6 +871,4 @@ CoapClient.pendingRequestsByMsgID = {}; CoapClient.pendingRequestsByUrl = {}; /** Queue of the messages waiting to be sent */ CoapClient.sendQueue = []; -/** Number of message we expect an answer for */ -CoapClient.concurrency = 0; exports.CoapClient = CoapClient; diff --git a/src/CoapClient.ts b/src/CoapClient.ts index 7b9f5735..3e5d4695 100644 --- a/src/CoapClient.ts +++ b/src/CoapClient.ts @@ -179,8 +179,6 @@ export class CoapClient { private static pendingRequestsByUrl: { [url: string]: PendingRequest } = {}; /** Queue of the messages waiting to be sent */ private static sendQueue: QueuedMessage[] = []; - /** Number of message we expect an answer for */ - private static concurrency: number = 0; /** * Sets the security params to be used for the given hostname @@ -273,7 +271,6 @@ export class CoapClient { // retrieve or create the connection we're going to use const origin = Origin.fromUrl(url); - const originString = origin.toString(); const connection = await CoapClient.getConnection(origin); // find all the message parameters @@ -281,7 +278,6 @@ export class CoapClient { const code = MessageCodes.request[method]; const messageId = connection.lastMsgId = incrementMessageID(connection.lastMsgId); const token = connection.lastToken = incrementToken(connection.lastToken); - const tokenString = token.toString("hex"); payload = payload || Buffer.from([]); // create message options, be careful to order them by code, no sorting is implemented yet @@ -482,7 +478,6 @@ export class CoapClient { // retrieve or create the connection we're going to use const origin = Origin.fromUrl(url); - const originString = origin.toString(); const connection = await CoapClient.getConnection(origin); // find all the message parameters @@ -490,7 +485,6 @@ export class CoapClient { const code = MessageCodes.request[method]; const messageId = connection.lastMsgId = incrementMessageID(connection.lastMsgId); const token = connection.lastToken = incrementToken(connection.lastToken); - const tokenString = token.toString("hex"); payload = payload || Buffer.from([]); // create message options, be careful to order them by code, no sorting is implemented yet @@ -508,8 +502,8 @@ export class CoapClient { // [12] content format msgOptions.push(Options.ContentFormat(ContentFormats.application_json)); - // create the promise we're going to return - const response = createDeferredPromise(); + // In contrast to requests, we don't work with a deferred promise when observing + // Instead, we invoke a callback for *every* response. // create the message we're going to send const message = CoapClient.createMessage(type, code, messageId, token, msgOptions, payload); @@ -929,7 +923,6 @@ export class CoapClient { } // retrieve or create the connection we're going to use - const originString = target.toString(); try { await CoapClient.getConnection(target); return true; diff --git a/src/Message.ts b/src/Message.ts index 53eb75a0..88c9e592 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -1,5 +1,4 @@ import { Option } from "./Option"; -import { fail } from "assert"; export enum MessageType { CON = 0, // Confirmable From beb128a8e6bcb80a01fe3c0e2a9a6e16fabf3179 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Mon, 18 Dec 2017 12:57:09 +0100 Subject: [PATCH 06/12] defined the blockwise option with properties --- build/BlockOption.d.ts | 3 ++ build/BlockOption.js | 6 +++ build/BlockwiseOptions.d.ts | 3 ++ build/BlockwiseOptions.js | 6 +++ build/Option.d.ts | 28 ++++++++++++ build/Option.js | 65 ++++++++++++++++++++++++++- build/options/BlockwiseOptions.d.ts | 0 build/options/BlockwiseOptions.js | 0 src/Option.ts | 70 ++++++++++++++++++++++++++++- 9 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 build/BlockOption.d.ts create mode 100644 build/BlockOption.js create mode 100644 build/BlockwiseOptions.d.ts create mode 100644 build/BlockwiseOptions.js create mode 100644 build/options/BlockwiseOptions.d.ts create mode 100644 build/options/BlockwiseOptions.js diff --git a/build/BlockOption.d.ts b/build/BlockOption.d.ts new file mode 100644 index 00000000..dfb8f6c7 --- /dev/null +++ b/build/BlockOption.d.ts @@ -0,0 +1,3 @@ +import { NumericOption } from "./Option"; +export declare class Block1Option extends NumericOption { +} diff --git a/build/BlockOption.js b/build/BlockOption.js new file mode 100644 index 00000000..f5ed6144 --- /dev/null +++ b/build/BlockOption.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const Option_1 = require("./Option"); +class Block1Option extends Option_1.NumericOption { +} +exports.Block1Option = Block1Option; diff --git a/build/BlockwiseOptions.d.ts b/build/BlockwiseOptions.d.ts new file mode 100644 index 00000000..dfb8f6c7 --- /dev/null +++ b/build/BlockwiseOptions.d.ts @@ -0,0 +1,3 @@ +import { NumericOption } from "./Option"; +export declare class Block1Option extends NumericOption { +} diff --git a/build/BlockwiseOptions.js b/build/BlockwiseOptions.js new file mode 100644 index 00000000..f5ed6144 --- /dev/null +++ b/build/BlockwiseOptions.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const Option_1 = require("./Option"); +class Block1Option extends Option_1.NumericOption { +} +exports.Block1Option = Block1Option; diff --git a/build/Option.d.ts b/build/Option.d.ts index 4757f213..1ecfb969 100644 --- a/build/Option.d.ts +++ b/build/Option.d.ts @@ -37,6 +37,34 @@ export declare class NumericOption extends Option { value: number; static create(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer): NumericOption; } +/** + * Specialized Message optionis for blockwise transfer + */ +export declare class BlockOption extends NumericOption { + /** + * The size exponent of this block in the range 0..6 + * The actual block size is calculated by 2**(4 + exp) + */ + sizeExponent: number; + /** + * The size of this block in bytes + */ + readonly blockSize: number; + /** + * Indicates if there are more blocks following after this one. + */ + isLastBlock: boolean; + /** + * The sequence number of this block. + * When present in a request message, this determines the number of the block being requested + * When present in a response message, this indicates the number of the provided block + */ + blockNumber: number; + /** + * Returns the position of the first byte of this block in the complete message + */ + readonly byteOffset: number; +} /** * Specialized Message options for binary (and empty) content. */ diff --git a/build/Option.js b/build/Option.js index bdecefe0..26f7dc58 100644 --- a/build/Option.js +++ b/build/Option.js @@ -177,6 +177,67 @@ class NumericOption extends Option { } } exports.NumericOption = NumericOption; +/** + * Specialized Message optionis for blockwise transfer + */ +class BlockOption extends NumericOption { + /** + * The size exponent of this block in the range 0..6 + * The actual block size is calculated by 2**(4 + exp) + */ + get sizeExponent() { + return this.value & 0b111; + } + set sizeExponent(value) { + if (value < 0 || value > 6) { + throw new Error("the size exponent must be in the range of 0..6"); + } + // overwrite the last 3 bits + this.value = (this.value & ~0b111) | value; + } + /** + * The size of this block in bytes + */ + get blockSize() { + return 1 << (this.sizeExponent + 4); + } + /** + * Indicates if there are more blocks following after this one. + */ + get isLastBlock() { + const moreBlocks = (this.value & 0b1000) === 0b1000; + return !moreBlocks; + } + set isLastBlock(value) { + const moreBlocks = !value; + // overwrite the 4th bit + this.value = (this.value & ~0b1000) | (moreBlocks ? 0b1000 : 0); + } + /** + * The sequence number of this block. + * When present in a request message, this determines the number of the block being requested + * When present in a response message, this indicates the number of the provided block + */ + get blockNumber() { + return this.value >>> 4; + } + set blockNumber(value) { + // TODO: check if we need to update the value length + this.value = (value << 4) | (this.value & 0b1111); + } + /** + * Returns the position of the first byte of this block in the complete message + */ + get byteOffset() { + // from the spec: + // Implementation note: As an implementation convenience, "(val & ~0xF) + // << (val & 7)", i.e., the option value with the last 4 bits masked + // out, shifted to the left by the value of SZX, gives the byte + // position of the first byte of the block being transferred. + return (this.value & ~0b1111) << (this.value & 0b111); + } +} +exports.BlockOption = BlockOption; /** * Specialized Message options for binary (and empty) content. */ @@ -254,8 +315,8 @@ defineOptionConstructor(NumericOption, 7, "Uri-Port", false, 2); defineOptionConstructor(NumericOption, 12, "Content-Format", false, 2); defineOptionConstructor(NumericOption, 14, "Max-Age", false, 4); defineOptionConstructor(NumericOption, 17, "Accept", false, 2); -defineOptionConstructor(NumericOption, 23, "Block2", false, 3); -defineOptionConstructor(NumericOption, 27, "Block1", false, 3); +defineOptionConstructor(BlockOption, 23, "Block2", false, 3); +defineOptionConstructor(BlockOption, 27, "Block1", false, 3); defineOptionConstructor(NumericOption, 28, "Size2", false, 4); defineOptionConstructor(NumericOption, 60, "Size1", false, 4); defineOptionConstructor(BinaryOption, 1, "If-Match", true, 0, 8); diff --git a/build/options/BlockwiseOptions.d.ts b/build/options/BlockwiseOptions.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/build/options/BlockwiseOptions.js b/build/options/BlockwiseOptions.js new file mode 100644 index 00000000..e69de29b diff --git a/src/Option.ts b/src/Option.ts index 0f7754bf..f545ad6b 100644 --- a/src/Option.ts +++ b/src/Option.ts @@ -208,6 +208,72 @@ export class NumericOption extends Option { } +/** + * Specialized Message optionis for blockwise transfer + */ +export class BlockOption extends NumericOption { + + /** + * The size exponent of this block in the range 0..6 + * The actual block size is calculated by 2**(4 + exp) + */ + public get sizeExponent(): number { + return this.value & 0b111; + } + public set sizeExponent(value: number) { + if (value < 0 || value > 6) { + throw new Error("the size exponent must be in the range of 0..6"); + } + // overwrite the last 3 bits + this.value = (this.value & ~0b111) | value; + } + /** + * The size of this block in bytes + */ + public get blockSize(): number { + return 1 << (this.sizeExponent + 4); + } + + /** + * Indicates if there are more blocks following after this one. + */ + public get isLastBlock(): boolean { + const moreBlocks = (this.value & 0b1000) === 0b1000; + return !moreBlocks; + } + public set isLastBlock(value: boolean) { + const moreBlocks = !value; + // overwrite the 4th bit + this.value = (this.value & ~0b1000) | (moreBlocks ? 0b1000 : 0); + } + + /** + * The sequence number of this block. + * When present in a request message, this determines the number of the block being requested + * When present in a response message, this indicates the number of the provided block + */ + public get blockNumber(): number { + return this.value >>> 4; + } + public set blockNumber(value: number) { + // TODO: check if we need to update the value length + this.value = (value << 4) | (this.value & 0b1111); + } + + /** + * Returns the position of the first byte of this block in the complete message + */ + public get byteOffset(): number { + // from the spec: + // Implementation note: As an implementation convenience, "(val & ~0xF) + // << (val & 7)", i.e., the option value with the last 4 bits masked + // out, shifted to the left by the value of SZX, gives the byte + // position of the first byte of the block being transferred. + return (this.value & ~0b1111) << (this.value & 0b111); + } + +} + /** * Specialized Message options for binary (and empty) content. */ @@ -312,8 +378,8 @@ defineOptionConstructor(NumericOption, 7, "Uri-Port", false, 2); defineOptionConstructor(NumericOption, 12, "Content-Format", false, 2); defineOptionConstructor(NumericOption, 14, "Max-Age", false, 4); defineOptionConstructor(NumericOption, 17, "Accept", false, 2); -defineOptionConstructor(NumericOption, 23, "Block2", false, 3); -defineOptionConstructor(NumericOption, 27, "Block1", false, 3); +defineOptionConstructor(BlockOption, 23, "Block2", false, 3); +defineOptionConstructor(BlockOption, 27, "Block1", false, 3); defineOptionConstructor(NumericOption, 28, "Size2", false, 4); defineOptionConstructor(NumericOption, 60, "Size1", false, 4); defineOptionConstructor(BinaryOption, 1, "If-Match", true, 0, 8); From 968570036e8cf463f68f23ef344ab936d57f38b6 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Mon, 18 Dec 2017 15:59:53 +0100 Subject: [PATCH 07/12] improved logging of options --- build/BlockOption.d.ts | 3 --- build/BlockOption.js | 6 ------ build/BlockwiseOptions.d.ts | 3 --- build/BlockwiseOptions.js | 6 ------ build/Option.d.ts | 5 +++++ build/Option.js | 15 +++++++++++++++ build/options/BlockwiseOptions.d.ts | 0 build/options/BlockwiseOptions.js | 0 src/Message.test.ts | 13 +++++++++++++ src/Option.ts | 26 ++++++++++++++++++++++++++ 10 files changed, 59 insertions(+), 18 deletions(-) delete mode 100644 build/BlockOption.d.ts delete mode 100644 build/BlockOption.js delete mode 100644 build/BlockwiseOptions.d.ts delete mode 100644 build/BlockwiseOptions.js delete mode 100644 build/options/BlockwiseOptions.d.ts delete mode 100644 build/options/BlockwiseOptions.js diff --git a/build/BlockOption.d.ts b/build/BlockOption.d.ts deleted file mode 100644 index dfb8f6c7..00000000 --- a/build/BlockOption.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { NumericOption } from "./Option"; -export declare class Block1Option extends NumericOption { -} diff --git a/build/BlockOption.js b/build/BlockOption.js deleted file mode 100644 index f5ed6144..00000000 --- a/build/BlockOption.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const Option_1 = require("./Option"); -class Block1Option extends Option_1.NumericOption { -} -exports.Block1Option = Block1Option; diff --git a/build/BlockwiseOptions.d.ts b/build/BlockwiseOptions.d.ts deleted file mode 100644 index dfb8f6c7..00000000 --- a/build/BlockwiseOptions.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { NumericOption } from "./Option"; -export declare class Block1Option extends NumericOption { -} diff --git a/build/BlockwiseOptions.js b/build/BlockwiseOptions.js deleted file mode 100644 index f5ed6144..00000000 --- a/build/BlockwiseOptions.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const Option_1 = require("./Option"); -class Block1Option extends Option_1.NumericOption { -} -exports.Block1Option = Block1Option; diff --git a/build/Option.d.ts b/build/Option.d.ts index 1ecfb969..2937f3d4 100644 --- a/build/Option.d.ts +++ b/build/Option.d.ts @@ -36,11 +36,13 @@ export declare class NumericOption extends Option { constructor(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer); value: number; static create(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer): NumericOption; + toString(): string; } /** * Specialized Message optionis for blockwise transfer */ export declare class BlockOption extends NumericOption { + static create(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer): BlockOption; /** * The size exponent of this block in the range 0..6 * The actual block size is calculated by 2**(4 + exp) @@ -64,6 +66,7 @@ export declare class BlockOption extends NumericOption { * Returns the position of the first byte of this block in the complete message */ readonly byteOffset: number; + toString(): string; } /** * Specialized Message options for binary (and empty) content. @@ -76,6 +79,7 @@ export declare class BinaryOption extends Option { constructor(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer); value: Buffer; static create(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer): BinaryOption; + toString(): string; } /** * Specialized Message options for string content. @@ -88,6 +92,7 @@ export declare class StringOption extends Option { constructor(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer); value: string; static create(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer): StringOption; + toString(): string; } export declare const Options: Readonly<{ UriHost: (hostname: string) => Option; diff --git a/build/Option.js b/build/Option.js index 26f7dc58..fa50e811 100644 --- a/build/Option.js +++ b/build/Option.js @@ -175,12 +175,18 @@ class NumericOption extends Option { static create(code, name, repeatable, maxLength, rawValue) { return new NumericOption(code, name, repeatable, maxLength, rawValue); } + toString() { + return `${this.name} (${this.code}): ${this.value}`; + } } exports.NumericOption = NumericOption; /** * Specialized Message optionis for blockwise transfer */ class BlockOption extends NumericOption { + static create(code, name, repeatable, maxLength, rawValue) { + return new BlockOption(code, name, repeatable, maxLength, rawValue); + } /** * The size exponent of this block in the range 0..6 * The actual block size is calculated by 2**(4 + exp) @@ -236,6 +242,9 @@ class BlockOption extends NumericOption { // position of the first byte of the block being transferred. return (this.value & ~0b1111) << (this.value & 0b111); } + toString() { + return `${this.name} (${this.code}): ${this.blockNumber}/${this.isLastBlock ? 0 : 1}/${this.blockSize}`; + } } exports.BlockOption = BlockOption; /** @@ -267,6 +276,9 @@ class BinaryOption extends Option { static create(code, name, repeatable, minLength, maxLength, rawValue) { return new BinaryOption(code, name, repeatable, minLength, maxLength, rawValue); } + toString() { + return `${this.name} (${this.code}): 0x${this.rawValue.toString("hex")}`; + } } exports.BinaryOption = BinaryOption; /** @@ -298,6 +310,9 @@ class StringOption extends Option { static create(code, name, repeatable, minLength, maxLength, rawValue) { return new StringOption(code, name, repeatable, minLength, maxLength, rawValue); } + toString() { + return `${this.name} (${this.code}): "${this.value}"`; + } } exports.StringOption = StringOption; /** diff --git a/build/options/BlockwiseOptions.d.ts b/build/options/BlockwiseOptions.d.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/build/options/BlockwiseOptions.js b/build/options/BlockwiseOptions.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Message.test.ts b/src/Message.test.ts index 5c112bd6..bad23b41 100644 --- a/src/Message.test.ts +++ b/src/Message.test.ts @@ -72,5 +72,18 @@ describe.only("blockwise tests =>", () => { it("should parse without crashing", () => { const msg = Message.parse(buf); + console.log(`code: ${msg.code}`); + console.log(`messageId: ${msg.messageId}`); + if (msg.token != null) { + console.log(`token: ${msg.token.toString("hex")}`); + } + console.log(`type: ${msg.type}`); + console.log(`version: ${msg.version}`); + console.log("options:"); + for (const opt of msg.options) { + console.log(` [${opt.constructor.name}] ${opt.toString()}`); + } + console.log("payload:"); + console.log(msg.payload.toString("utf-8")); }); }); diff --git a/src/Option.ts b/src/Option.ts index f545ad6b..93f5b31d 100644 --- a/src/Option.ts +++ b/src/Option.ts @@ -206,6 +206,10 @@ export class NumericOption extends Option { return new NumericOption(code, name, repeatable, maxLength, rawValue); } + public toString(): string { + return `${this.name} (${this.code}): ${this.value}`; + } + } /** @@ -213,6 +217,16 @@ export class NumericOption extends Option { */ export class BlockOption extends NumericOption { + public static create( + code: number, + name: string, + repeatable: boolean, + maxLength: number, + rawValue: Buffer, + ): BlockOption { + return new BlockOption(code, name, repeatable, maxLength, rawValue); + } + /** * The size exponent of this block in the range 0..6 * The actual block size is calculated by 2**(4 + exp) @@ -272,6 +286,10 @@ export class BlockOption extends NumericOption { return (this.value & ~0b1111) << (this.value & 0b111); } + public toString(): string { + return `${this.name} (${this.code}): ${this.blockNumber}/${this.isLastBlock ? 0 : 1}/${this.blockSize}`; + } + } /** @@ -315,6 +333,10 @@ export class BinaryOption extends Option { return new BinaryOption(code, name, repeatable, minLength, maxLength, rawValue); } + public toString(): string { + return `${this.name} (${this.code}): 0x${this.rawValue.toString("hex")}`; + } + } /** @@ -358,6 +380,10 @@ export class StringOption extends Option { return new StringOption(code, name, repeatable, minLength, maxLength, rawValue); } + public toString(): string { + return `${this.name} (${this.code}): "${this.value}"`; + } + } /** From 2cf786db7c1c9a5c579dfedc2b491e3c55126751 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 19 Dec 2017 15:43:36 +0100 Subject: [PATCH 08/12] provide a preferred response block size (1st part of #24) --- build/CoapClient.d.ts | 10 +++++ build/CoapClient.js | 81 ++++++++++++++++++++++++++++++--------- build/Option.d.ts | 2 + build/Option.js | 15 +++++++- build/lib/LogMessage.d.ts | 2 + build/lib/LogMessage.js | 25 ++++++++++++ src/CoapClient.ts | 71 ++++++++++++++++++++++++++++------ src/Option.ts | 16 +++++++- src/lib/LogMessage.ts | 25 ++++++++++++ 9 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 build/lib/LogMessage.d.ts create mode 100644 build/lib/LogMessage.js create mode 100644 src/lib/LogMessage.ts diff --git a/build/CoapClient.d.ts b/build/CoapClient.d.ts index 4b1c3d03..be82b302 100644 --- a/build/CoapClient.d.ts +++ b/build/CoapClient.d.ts @@ -12,6 +12,8 @@ export interface RequestOptions { confirmable?: boolean; /** Whether this message will be retransmitted on loss */ retransmit?: boolean; + /** The preferred block size of partial responses */ + preferredBlockSize?: number; } export interface CoapResponse { code: MessageCode; @@ -40,10 +42,18 @@ export declare class CoapClient { private static pendingRequestsByUrl; /** Queue of the messages waiting to be sent */ private static sendQueue; + /** Default values for request options */ + private static defaultRequestOptions; /** * Sets the security params to be used for the given hostname */ static setSecurityParams(hostname: string, params: SecurityParameters): void; + /** + * Sets the default options for requests + * @param defaults The default options to use for requests when no options are given + */ + static setDefaultRequestOptions(defaults: RequestOptions): void; + private static getRequestOptions(options?); /** * Closes and forgets about connections, useful if DTLS session is reset on remote end * @param originOrHostname - Origin (protocol://hostname:port) or Hostname to reset, diff --git a/build/CoapClient.js b/build/CoapClient.js index a03e0325..9c2bfee2 100644 --- a/build/CoapClient.js +++ b/build/CoapClient.js @@ -21,6 +21,7 @@ const Message_1 = require("./Message"); const Option_1 = require("./Option"); // initialize debugging const debugPackage = require("debug"); +const LogMessage_1 = require("./lib/LogMessage"); const debug = debugPackage("node-coap-client"); // print version info // tslint:disable-next-line:no-var-requires @@ -95,6 +96,17 @@ function findOption(opts, name) { function findOptions(opts, name) { return opts.filter(opt => opt.name === name); } +function validateBlockSize(size) { + // block size is represented as 2**(4 + X) where X is an integer from 0..6 + const exp = Math.log2(size) - 4; + // is the exponent an integer? + if (exp % 1 !== 0) + return false; + // is the exponent in the range of 0..6? + if (exp < 0 || exp > 6) + return false; + return true; +} /** * provides methods to access CoAP server resources */ @@ -105,6 +117,43 @@ class CoapClient { static setSecurityParams(hostname, params) { CoapClient.dtlsParams[hostname] = params; } + /** + * Sets the default options for requests + * @param defaults The default options to use for requests when no options are given + */ + static setDefaultRequestOptions(defaults) { + if (defaults.confirmable != null) + this.defaultRequestOptions.confirmable = defaults.confirmable; + if (defaults.keepAlive != null) + this.defaultRequestOptions.keepAlive = defaults.keepAlive; + if (defaults.retransmit != null) + this.defaultRequestOptions.retransmit = defaults.retransmit; + if (defaults.preferredBlockSize != null) { + if (!validateBlockSize(defaults.preferredBlockSize)) { + throw new Error(`${defaults.preferredBlockSize} is not a valid block size. The value must be a power of 2 between 16 and 1024`); + } + this.defaultRequestOptions.preferredBlockSize = defaults.preferredBlockSize; + } + } + static getRequestOptions(options) { + // ensure we have options and set the default params + options = options || {}; + if (options.confirmable == null) + options.confirmable = this.defaultRequestOptions.confirmable; + if (options.keepAlive == null) + options.keepAlive = this.defaultRequestOptions.keepAlive; + if (options.retransmit == null) + options.retransmit = this.defaultRequestOptions.retransmit; + if (options.preferredBlockSize == null) { + options.preferredBlockSize = this.defaultRequestOptions.preferredBlockSize; + } + else { + if (!validateBlockSize(options.preferredBlockSize)) { + throw new Error(`${options.preferredBlockSize} is not a valid block size. The value must be a power of 2 between 16 and 1024`); + } + } + return options; + } /** * Closes and forgets about connections, useful if DTLS session is reset on remote end * @param originOrHostname - Origin (protocol://hostname:port) or Hostname to reset, @@ -175,13 +224,7 @@ class CoapClient { url = nodeUrl.parse(url); } // ensure we have options and set the default params - options = options || {}; - if (options.confirmable == null) - options.confirmable = true; - if (options.keepAlive == null) - options.keepAlive = true; - if (options.retransmit == null) - options.retransmit = true; + options = this.getRequestOptions(options); // retrieve or create the connection we're going to use const origin = Origin_1.Origin.fromUrl(url); const connection = yield CoapClient.getConnection(origin); @@ -193,8 +236,6 @@ class CoapClient { payload = payload || Buffer.from([]); // create message options, be careful to order them by code, no sorting is implemented yet const msgOptions = []; - //// [6] observe or not? - // msgOptions.push(Options.Observe(options.observe)) // [11] path of the request let pathname = url.pathname || ""; while (pathname.startsWith("/")) { @@ -207,6 +248,10 @@ class CoapClient { msgOptions.push(...pathParts.map(part => Option_1.Options.UriPath(part))); // [12] content format msgOptions.push(Option_1.Options.ContentFormat(ContentFormats_1.ContentFormats.application_json)); + // [23] Block2 (preferred response block size) + if (options.preferredBlockSize != null) { + msgOptions.push(Option_1.Options.Block2(0, true, options.preferredBlockSize)); + } // create the promise we're going to return const response = DeferredPromise_1.createDeferredPromise(); // create the message we're going to send @@ -357,13 +402,7 @@ class CoapClient { url = nodeUrl.parse(url); } // ensure we have options and set the default params - options = options || {}; - if (options.confirmable == null) - options.confirmable = true; - if (options.keepAlive == null) - options.keepAlive = true; - if (options.retransmit == null) - options.retransmit = true; + options = this.getRequestOptions(options); // retrieve or create the connection we're going to use const origin = Origin_1.Origin.fromUrl(url); const connection = yield CoapClient.getConnection(origin); @@ -438,7 +477,7 @@ class CoapClient { static onMessage(origin, message, rinfo) { // parse the CoAP message const coapMsg = Message_1.Message.parse(message); - debug(`received message: ID=0x${coapMsg.messageId.toString(16)}${(coapMsg.token && coapMsg.token.length) ? (", token=" + coapMsg.token.toString("hex")) : ""}`); + LogMessage_1.logMessage(coapMsg); if (coapMsg.code.isEmpty()) { // ACK or RST // see if we have a request for this message id @@ -474,7 +513,6 @@ class CoapClient { // ignore them } else if (coapMsg.code.isResponse()) { - debug(`response with payload: ${coapMsg.payload.toString("utf8")}`); // this is a response, find out what to do with it if (coapMsg.token && coapMsg.token.length) { // this message has a token, check which request it belongs to @@ -871,4 +909,11 @@ CoapClient.pendingRequestsByMsgID = {}; CoapClient.pendingRequestsByUrl = {}; /** Queue of the messages waiting to be sent */ CoapClient.sendQueue = []; +/** Default values for request options */ +CoapClient.defaultRequestOptions = { + confirmable: true, + keepAlive: true, + retransmit: true, + preferredBlockSize: null, +}; exports.CoapClient = CoapClient; diff --git a/build/Option.d.ts b/build/Option.d.ts index 2937f3d4..c23a6ab8 100644 --- a/build/Option.d.ts +++ b/build/Option.d.ts @@ -101,4 +101,6 @@ export declare const Options: Readonly<{ LocationPath: (pathname: string) => Option; ContentFormat: (format: ContentFormats) => Option; Observe: (observe: boolean) => Option; + Block1: (num: number, isLast: boolean, size: number) => Option; + Block2: (num: number, isLast: boolean, size: number) => Option; }>; diff --git a/build/Option.js b/build/Option.js index fa50e811..0a71d365 100644 --- a/build/Option.js +++ b/build/Option.js @@ -344,6 +344,7 @@ defineOptionConstructor(StringOption, 15, "Uri-Query", true, 0, 255); defineOptionConstructor(StringOption, 20, "Location-Query", true, 0, 255); defineOptionConstructor(StringOption, 35, "Proxy-Uri", true, 1, 1034); defineOptionConstructor(StringOption, 39, "Proxy-Scheme", true, 1, 255); +// tslint:disable:no-string-literal // tslint:disable-next-line:variable-name exports.Options = Object.freeze({ UriHost: (hostname) => optionConstructors["Uri-Host"](Buffer.from(hostname)), @@ -351,6 +352,18 @@ exports.Options = Object.freeze({ UriPath: (pathname) => optionConstructors["Uri-Path"](Buffer.from(pathname)), LocationPath: (pathname) => optionConstructors["Location-Path"](Buffer.from(pathname)), ContentFormat: (format) => optionConstructors["Content-Format"](numberToBuffer(format)), - // tslint:disable-next-line:no-string-literal Observe: (observe) => optionConstructors["Observe"](Buffer.from([observe ? 0 : 1])), + Block1: (num, isLast, size) => { + // Warning: we're not checking for a valid size here, do that in advance! + const sizeExp = Math.log2(size) - 4; + const value = (num << 4) | (isLast ? 0 : 0b1000) | (sizeExp & 0b111); + return optionConstructors["Block1"](numberToBuffer(value)); + }, + Block2: (num, isLast, size) => { + // Warning: we're not checking for a valid size here, do that in advance! + const sizeExp = Math.log2(size) - 4; + const value = (num << 4) | (isLast ? 0 : 0b1000) | (sizeExp & 0b111); + return optionConstructors["Block2"](numberToBuffer(value)); + }, }); +// tslint:enable:no-string-literal diff --git a/build/lib/LogMessage.d.ts b/build/lib/LogMessage.d.ts new file mode 100644 index 00000000..fc0569da --- /dev/null +++ b/build/lib/LogMessage.d.ts @@ -0,0 +1,2 @@ +import { Message } from "../Message"; +export declare function logMessage(msg: Message, includePayload?: boolean): void; diff --git a/build/lib/LogMessage.js b/build/lib/LogMessage.js new file mode 100644 index 00000000..e805fc4d --- /dev/null +++ b/build/lib/LogMessage.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// initialize debugging +const debugPackage = require("debug"); +const debug = debugPackage("node-coap-client:message"); +function logMessage(msg, includePayload = false) { + debug("============================="); + debug(`received message`); + debug(`messageId: ${msg.messageId}`); + if (msg.token != null) { + debug(`token: ${msg.token.toString("hex")}`); + } + debug(`code: ${msg.code}`); + debug(`type: ${msg.type}`); + debug(`version: ${msg.version}`); + debug("options:"); + for (const opt of msg.options) { + debug(` [${opt.constructor.name}] ${opt.toString()}`); + } + debug("payload:"); + debug(msg.payload.toString("utf-8")); + debug("============================="); + debug(""); +} +exports.logMessage = logMessage; diff --git a/src/CoapClient.ts b/src/CoapClient.ts index 3e5d4695..19fe3915 100644 --- a/src/CoapClient.ts +++ b/src/CoapClient.ts @@ -12,6 +12,7 @@ import { BinaryOption, NumericOption, Option, Options, StringOption } from "./Op // initialize debugging import * as debugPackage from "debug"; +import { logMessage } from "./lib/LogMessage"; const debug = debugPackage("node-coap-client"); // print version info @@ -29,6 +30,8 @@ export interface RequestOptions { confirmable?: boolean; /** Whether this message will be retransmitted on loss */ retransmit?: boolean; + /** The preferred block size of partial responses */ + preferredBlockSize?: number; } export interface CoapResponse { @@ -161,6 +164,16 @@ function findOptions(opts: Option[], name: string): Option[] { return opts.filter(opt => opt.name === name); } +function validateBlockSize(size: number): boolean { + // block size is represented as 2**(4 + X) where X is an integer from 0..6 + const exp = Math.log2(size) - 4; + // is the exponent an integer? + if (exp % 1 !== 0) return false; + // is the exponent in the range of 0..6? + if (exp < 0 || exp > 6) return false; + return true; +} + /** * provides methods to access CoAP server resources */ @@ -179,6 +192,13 @@ export class CoapClient { private static pendingRequestsByUrl: { [url: string]: PendingRequest } = {}; /** Queue of the messages waiting to be sent */ private static sendQueue: QueuedMessage[] = []; + /** Default values for request options */ + private static defaultRequestOptions: RequestOptions = { + confirmable: true, + keepAlive: true, + retransmit: true, + preferredBlockSize: null, + }; /** * Sets the security params to be used for the given hostname @@ -187,6 +207,38 @@ export class CoapClient { CoapClient.dtlsParams[hostname] = params; } + /** + * Sets the default options for requests + * @param defaults The default options to use for requests when no options are given + */ + public static setDefaultRequestOptions(defaults: RequestOptions): void { + if (defaults.confirmable != null) this.defaultRequestOptions.confirmable = defaults.confirmable; + if (defaults.keepAlive != null) this.defaultRequestOptions.keepAlive = defaults.keepAlive; + if (defaults.retransmit != null) this.defaultRequestOptions.retransmit = defaults.retransmit; + if (defaults.preferredBlockSize != null) { + if (!validateBlockSize(defaults.preferredBlockSize)) { + throw new Error(`${defaults.preferredBlockSize} is not a valid block size. The value must be a power of 2 between 16 and 1024`); + } + this.defaultRequestOptions.preferredBlockSize = defaults.preferredBlockSize; + } + } + + private static getRequestOptions(options?: RequestOptions): RequestOptions { + // ensure we have options and set the default params + options = options || {}; + if (options.confirmable == null) options.confirmable = this.defaultRequestOptions.confirmable; + if (options.keepAlive == null) options.keepAlive = this.defaultRequestOptions.keepAlive; + if (options.retransmit == null) options.retransmit = this.defaultRequestOptions.retransmit; + if (options.preferredBlockSize == null) { + options.preferredBlockSize = this.defaultRequestOptions.preferredBlockSize; + } else { + if (!validateBlockSize(options.preferredBlockSize)) { + throw new Error(`${options.preferredBlockSize} is not a valid block size. The value must be a power of 2 between 16 and 1024`); + } + } + return options; + } + /** * Closes and forgets about connections, useful if DTLS session is reset on remote end * @param originOrHostname - Origin (protocol://hostname:port) or Hostname to reset, @@ -264,10 +316,7 @@ export class CoapClient { } // ensure we have options and set the default params - options = options || {}; - if (options.confirmable == null) options.confirmable = true; - if (options.keepAlive == null) options.keepAlive = true; - if (options.retransmit == null) options.retransmit = true; + options = this.getRequestOptions(options); // retrieve or create the connection we're going to use const origin = Origin.fromUrl(url); @@ -282,8 +331,6 @@ export class CoapClient { // create message options, be careful to order them by code, no sorting is implemented yet const msgOptions: Option[] = []; - //// [6] observe or not? - // msgOptions.push(Options.Observe(options.observe)) // [11] path of the request let pathname = url.pathname || ""; while (pathname.startsWith("/")) { pathname = pathname.slice(1); } @@ -294,6 +341,10 @@ export class CoapClient { ); // [12] content format msgOptions.push(Options.ContentFormat(ContentFormats.application_json)); + // [23] Block2 (preferred response block size) + if (options.preferredBlockSize != null) { + msgOptions.push(Options.Block2(0, true, options.preferredBlockSize)); + } // create the promise we're going to return const response = createDeferredPromise(); @@ -471,10 +522,7 @@ export class CoapClient { } // ensure we have options and set the default params - options = options || {}; - if (options.confirmable == null) options.confirmable = true; - if (options.keepAlive == null) options.keepAlive = true; - if (options.retransmit == null) options.retransmit = true; + options = this.getRequestOptions(options); // retrieve or create the connection we're going to use const origin = Origin.fromUrl(url); @@ -559,7 +607,7 @@ export class CoapClient { private static onMessage(origin: string, message: Buffer, rinfo: dgram.RemoteInfo) { // parse the CoAP message const coapMsg = Message.parse(message); - debug(`received message: ID=0x${coapMsg.messageId.toString(16)}${(coapMsg.token && coapMsg.token.length) ? (", token=" + coapMsg.token.toString("hex")) : ""}`); + logMessage(coapMsg); if (coapMsg.code.isEmpty()) { // ACK or RST @@ -596,7 +644,6 @@ export class CoapClient { // we are a client implementation, we should not get requests // ignore them } else if (coapMsg.code.isResponse()) { - debug(`response with payload: ${coapMsg.payload.toString("utf8")}`); // this is a response, find out what to do with it if (coapMsg.token && coapMsg.token.length) { // this message has a token, check which request it belongs to diff --git a/src/Option.ts b/src/Option.ts index 93f5b31d..26bc3a7f 100644 --- a/src/Option.ts +++ b/src/Option.ts @@ -419,6 +419,7 @@ defineOptionConstructor(StringOption, 20, "Location-Query", true, 0, 255); defineOptionConstructor(StringOption, 35, "Proxy-Uri", true, 1, 1034); defineOptionConstructor(StringOption, 39, "Proxy-Scheme", true, 1, 255); +// tslint:disable:no-string-literal // tslint:disable-next-line:variable-name export const Options = Object.freeze({ UriHost: (hostname: string) => optionConstructors["Uri-Host"](Buffer.from(hostname)), @@ -428,6 +429,19 @@ export const Options = Object.freeze({ LocationPath: (pathname: string) => optionConstructors["Location-Path"](Buffer.from(pathname)), ContentFormat: (format: ContentFormats) => optionConstructors["Content-Format"](numberToBuffer(format)), - // tslint:disable-next-line:no-string-literal Observe: (observe: boolean) => optionConstructors["Observe"](Buffer.from([observe ? 0 : 1])), + + Block1: (num: number, isLast: boolean, size: number) => { + // Warning: we're not checking for a valid size here, do that in advance! + const sizeExp = Math.log2(size) - 4; + const value = (num << 4) | (isLast ? 0 : 0b1000) | (sizeExp & 0b111); + return optionConstructors["Block1"](numberToBuffer(value)); + }, + Block2: (num: number, isLast: boolean, size: number) => { + // Warning: we're not checking for a valid size here, do that in advance! + const sizeExp = Math.log2(size) - 4; + const value = (num << 4) | (isLast ? 0 : 0b1000) | (sizeExp & 0b111); + return optionConstructors["Block2"](numberToBuffer(value)); + }, }); +// tslint:enable:no-string-literal diff --git a/src/lib/LogMessage.ts b/src/lib/LogMessage.ts new file mode 100644 index 00000000..8aaa156b --- /dev/null +++ b/src/lib/LogMessage.ts @@ -0,0 +1,25 @@ +// initialize debugging +import * as debugPackage from "debug"; +const debug = debugPackage("node-coap-client:message"); + +import { Message } from "../Message"; + +export function logMessage(msg: Message, includePayload: boolean = false): void { + debug("============================="); + debug(`received message`); + debug(`messageId: ${msg.messageId}`); + if (msg.token != null) { + debug(`token: ${msg.token.toString("hex")}`); + } + debug(`code: ${msg.code}`); + debug(`type: ${msg.type}`); + debug(`version: ${msg.version}`); + debug("options:"); + for (const opt of msg.options) { + debug(` [${opt.constructor.name}] ${opt.toString()}`); + } + debug("payload:"); + debug(msg.payload.toString("utf-8")); + debug("============================="); + debug(""); +} From e8e2372f04c625338203abf76b782da8699771d5 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 20 Dec 2017 08:05:14 +0100 Subject: [PATCH 09/12] working on receiving partial messages --- build/CoapClient.js | 35 +++++++++++++++++++++++------------ build/Message.js | 7 ++++--- build/Option.d.ts | 12 ++++++++++++ build/Option.js | 18 ++++++++++++++++++ src/CoapClient.ts | 39 ++++++++++++++++++++++++++------------- src/Message.test.ts | 28 ++++++++++++++++++++++++++++ src/Message.ts | 6 ++++-- src/Option.ts | 18 ++++++++++++++++++ 8 files changed, 133 insertions(+), 30 deletions(-) diff --git a/build/CoapClient.js b/build/CoapClient.js index 9c2bfee2..bb920c1e 100644 --- a/build/CoapClient.js +++ b/build/CoapClient.js @@ -87,15 +87,6 @@ function incrementToken(token) { function incrementMessageID(msgId) { return (++msgId > 0xffff) ? 1 : msgId; } -function findOption(opts, name) { - for (const opt of opts) { - if (opt.name === name) - return opt; - } -} -function findOptions(opts, name) { - return opts.filter(opt => opt.name === name); -} function validateBlockSize(size) { // block size is represented as 2**(4 + X) where X is an integer from 0..6 const exp = Math.log2(size) - 4; @@ -523,17 +514,37 @@ class CoapClient { if (coapMsg.type === Message_1.MessageType.ACK) { debug(`received ACK for message 0x${coapMsg.messageId.toString(16)}, stopping retransmission...`); CoapClient.stopRetransmission(request); - // reduce the request's concurrency, since it was handled on the server - request.concurrency = 0; } // parse options let contentFormat = null; if (coapMsg.options && coapMsg.options.length) { // see if the response contains information about the content format - const optCntFmt = findOption(coapMsg.options, "Content-Format"); + const optCntFmt = Option_1.findOption(coapMsg.options, "Content-Format"); if (optCntFmt) contentFormat = optCntFmt.value; } + if (coapMsg.isPartialMessage()) { + // Check if we expect more blocks + const blockOption = Option_1.findOption(coapMsg.options, "Block2"); // we know this is != null + // TODO: check for outdated partial responses + // assemble the partial blocks + if (request.partialResponse == null) { + request.partialResponse = coapMsg; + } + else { + // extend the stored buffer + // TODO: we might have to check if we got the correct fragment + request.partialResponse.payload = Buffer.concat([request.partialResponse.payload, coapMsg.payload]); + } + if (!blockOption.isLastBlock) { + // TODO: request the next block + return; + } + } + // Now that the response is complete, also reduce the request's concurrency, + // so other requests can be fired off + if (coapMsg.type === Message_1.MessageType.ACK) + request.concurrency = 0; // prepare the response const response = { code: coapMsg.code, diff --git a/build/Message.js b/build/Message.js index d9a03dcc..c5154a51 100644 --- a/build/Message.js +++ b/build/Message.js @@ -172,12 +172,13 @@ class Message { */ isPartialMessage() { // TODO: can we put the codes in an enum? - const block1option = this.options.find(o => o.code === 27 /* Block1 */); + // start with the response option, since that's more likely const block2option = this.options.find(o => o.code === 23 /* Block2 */); - if (this.code.isRequest() && block1option != null) - return true; if (this.code.isResponse() && block2option != null) return true; + const block1option = this.options.find(o => o.code === 27 /* Block1 */); + if (this.code.isRequest() && block1option != null) + return true; return false; } } diff --git a/build/Option.d.ts b/build/Option.d.ts index c23a6ab8..10e67533 100644 --- a/build/Option.d.ts +++ b/build/Option.d.ts @@ -104,3 +104,15 @@ export declare const Options: Readonly<{ Block1: (num: number, isLast: boolean, size: number) => Option; Block2: (num: number, isLast: boolean, size: number) => Option; }>; +/** + * Searches for a single option in an array of options + * @param opts The options array to search for the option + * @param name The name of the option to search for + */ +export declare function findOption(opts: Option[], name: string): Option; +/** + * Searches for a repeatable option in an array of options + * @param opts The options array to search for the option + * @param name The name of the option to search for + */ +export declare function findOptions(opts: Option[], name: string): Option[]; diff --git a/build/Option.js b/build/Option.js index 0a71d365..923eb9f4 100644 --- a/build/Option.js +++ b/build/Option.js @@ -367,3 +367,21 @@ exports.Options = Object.freeze({ }, }); // tslint:enable:no-string-literal +/** + * Searches for a single option in an array of options + * @param opts The options array to search for the option + * @param name The name of the option to search for + */ +function findOption(opts, name) { + return opts.find(o => o.name === name); +} +exports.findOption = findOption; +/** + * Searches for a repeatable option in an array of options + * @param opts The options array to search for the option + * @param name The name of the option to search for + */ +function findOptions(opts, name) { + return opts.filter(o => o.name === name); +} +exports.findOptions = findOptions; diff --git a/src/CoapClient.ts b/src/CoapClient.ts index 19fe3915..1231e3d5 100644 --- a/src/CoapClient.ts +++ b/src/CoapClient.ts @@ -8,7 +8,7 @@ import { createDeferredPromise, DeferredPromise } from "./lib/DeferredPromise"; import { Origin } from "./lib/Origin"; import { SocketWrapper } from "./lib/SocketWrapper"; import { Message, MessageCode, MessageCodes, MessageType } from "./Message"; -import { BinaryOption, NumericOption, Option, Options, StringOption } from "./Option"; +import { BinaryOption, BlockOption, findOption, NumericOption, Option, Options, StringOption } from "./Option"; // initialize debugging import * as debugPackage from "debug"; @@ -56,6 +56,7 @@ interface IPendingRequest { url: string; originalMessage: Message; // allows resending the message, includes token and message id retransmit: RetransmissionInfo; + partialResponse?: Message; // either (request): promise: Promise; // or (observe) @@ -84,6 +85,7 @@ class PendingRequest extends EventEmitter implements IPendingRequest { public connection: ConnectionInfo; public url: string; public originalMessage: Message; // allows resending the message, includes token and message id + public partialResponse: Message; // allows buffering for block-wise message receipt public retransmit: RetransmissionInfo; // either (request): public promise: Promise; @@ -154,16 +156,6 @@ function incrementMessageID(msgId: number): number { return (++msgId > 0xffff) ? 1 : msgId; } -function findOption(opts: Option[], name: string): Option { - for (const opt of opts) { - if (opt.name === name) return opt; - } -} - -function findOptions(opts: Option[], name: string): Option[] { - return opts.filter(opt => opt.name === name); -} - function validateBlockSize(size: number): boolean { // block size is represented as 2**(4 + X) where X is an integer from 0..6 const exp = Math.log2(size) - 4; @@ -655,8 +647,6 @@ export class CoapClient { if (coapMsg.type === MessageType.ACK) { debug(`received ACK for message 0x${coapMsg.messageId.toString(16)}, stopping retransmission...`); CoapClient.stopRetransmission(request); - // reduce the request's concurrency, since it was handled on the server - request.concurrency = 0; } // parse options @@ -667,6 +657,29 @@ export class CoapClient { if (optCntFmt) contentFormat = (optCntFmt as NumericOption).value; } + if (coapMsg.isPartialMessage()) { + // Check if we expect more blocks + const blockOption = findOption(coapMsg.options, "Block2") as BlockOption; // we know this is != null + // TODO: check for outdated partial responses + + // assemble the partial blocks + if (request.partialResponse == null) { + request.partialResponse = coapMsg; + } else { + // extend the stored buffer + // TODO: we might have to check if we got the correct fragment + request.partialResponse.payload = Buffer.concat([request.partialResponse.payload, coapMsg.payload]); + } + if (!blockOption.isLastBlock) { + // TODO: request the next block + return; + } + } + + // Now that the response is complete, also reduce the request's concurrency, + // so other requests can be fired off + if (coapMsg.type === MessageType.ACK) request.concurrency = 0; + // prepare the response const response: CoapResponse = { code: coapMsg.code, diff --git a/src/Message.test.ts b/src/Message.test.ts index bad23b41..2e059ed9 100644 --- a/src/Message.test.ts +++ b/src/Message.test.ts @@ -1,5 +1,7 @@ +// tslint:disable:no-unused-expression import { expect } from "chai"; +import { CoapClient as coap } from "./CoapClient"; import { Message, MessageCode, MessageCodes, MessageType } from "./Message"; describe("Message Tests =>", () => { @@ -70,6 +72,14 @@ describe.only("blockwise tests =>", () => { "hex", ); + const settings = { + host: "gw-b072bf257a41", + securityCode: "", + identity: "tradfri_1509642359115", + psk: "gzqZY5HUlFOOVu9f", + }; + const requestBase = `coaps://${settings.host}:5684/`; + it("should parse without crashing", () => { const msg = Message.parse(buf); console.log(`code: ${msg.code}`); @@ -86,4 +96,22 @@ describe.only("blockwise tests =>", () => { console.log("payload:"); console.log(msg.payload.toString("utf-8")); }); + + it.only("custom tests", async () => { + coap.setSecurityParams(settings.host, { + psk: { [settings.identity]: settings.psk }, + }); + + // connect + expect(await coap.tryToConnect(requestBase)).to.be.true; + + // limit response size + coap.setDefaultRequestOptions({ + preferredBlockSize: 16, + }); + + const resp = await coap.request(`${requestBase}15011/15012`, "get"); + + coap.reset(); + }); }); diff --git a/src/Message.ts b/src/Message.ts index 88c9e592..a3861623 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -207,10 +207,12 @@ export class Message { */ public isPartialMessage(): boolean { // TODO: can we put the codes in an enum? - const block1option = this.options.find(o => o.code === 27 /* Block1 */); + + // start with the response option, since that's more likely const block2option = this.options.find(o => o.code === 23 /* Block2 */); - if (this.code.isRequest() && block1option != null) return true; if (this.code.isResponse() && block2option != null) return true; + const block1option = this.options.find(o => o.code === 27 /* Block1 */); + if (this.code.isRequest() && block1option != null) return true; return false; } diff --git a/src/Option.ts b/src/Option.ts index 26bc3a7f..7b621d8c 100644 --- a/src/Option.ts +++ b/src/Option.ts @@ -445,3 +445,21 @@ export const Options = Object.freeze({ }, }); // tslint:enable:no-string-literal + +/** + * Searches for a single option in an array of options + * @param opts The options array to search for the option + * @param name The name of the option to search for + */ +export function findOption(opts: Option[], name: string): Option { + return opts.find(o => o.name === name); +} + +/** + * Searches for a repeatable option in an array of options + * @param opts The options array to search for the option + * @param name The name of the option to search for + */ +export function findOptions(opts: Option[], name: string): Option[] { + return opts.filter(o => o.name === name); +} From 5e4ebf00421659798a9400d8bd17a6d2bb5087c1 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Wed, 20 Dec 2017 10:50:52 +0100 Subject: [PATCH 10/12] access options by predefined names --- build/Message.js | 5 ++--- build/Option.d.ts | 32 ++++++++++++++++++-------------- src/Message.ts | 8 +++----- src/Option.ts | 47 ++++++++++++++++++++++++++++++++++++----------- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/build/Message.js b/build/Message.js index c5154a51..fde6c1ce 100644 --- a/build/Message.js +++ b/build/Message.js @@ -171,12 +171,11 @@ class Message { * Checks if this message is part of a blockwise transfer */ isPartialMessage() { - // TODO: can we put the codes in an enum? // start with the response option, since that's more likely - const block2option = this.options.find(o => o.code === 23 /* Block2 */); + const block2option = Option_1.findOption(this.options, "Block2"); if (this.code.isResponse() && block2option != null) return true; - const block1option = this.options.find(o => o.code === 27 /* Block1 */); + const block1option = Option_1.findOption(this.options, "Block1"); if (this.code.isRequest() && block1option != null) return true; return false; diff --git a/build/Option.d.ts b/build/Option.d.ts index 10e67533..532d6783 100644 --- a/build/Option.d.ts +++ b/build/Option.d.ts @@ -1,13 +1,17 @@ /// import { ContentFormats } from "./ContentFormats"; +/** + * All defined option names + */ +export declare type OptionName = "Observe" | "Uri-Port" | "Content-Format" | "Max-Age" | "Accept" | "Block2" | "Block1" | "Size2" | "Size1" | "If-Match" | "ETag" | "If-None-Match" | "Uri-Host" | "Location-Path" | "Uri-Path" | "Uri-Query" | "Location-Query" | "Proxy-Uri" | "Proxy-Scheme"; /** * Abstract base class for all message options. Provides methods to parse and serialize. */ export declare abstract class Option { readonly code: number; - readonly name: string; + readonly name: OptionName; rawValue: Buffer; - constructor(code: number, name: string, rawValue: Buffer); + constructor(code: number, name: OptionName, rawValue: Buffer); readonly noCacheKey: boolean; readonly unsafe: boolean; readonly critical: boolean; @@ -30,19 +34,19 @@ export declare abstract class Option { * Specialized Message option for numeric contents */ export declare class NumericOption extends Option { - readonly name: string; + readonly name: OptionName; readonly repeatable: boolean; readonly maxLength: number; - constructor(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer); + constructor(code: number, name: OptionName, repeatable: boolean, maxLength: number, rawValue: Buffer); value: number; - static create(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer): NumericOption; + static create(code: number, name: OptionName, repeatable: boolean, maxLength: number, rawValue: Buffer): NumericOption; toString(): string; } /** * Specialized Message optionis for blockwise transfer */ export declare class BlockOption extends NumericOption { - static create(code: number, name: string, repeatable: boolean, maxLength: number, rawValue: Buffer): BlockOption; + static create(code: number, name: OptionName, repeatable: boolean, maxLength: number, rawValue: Buffer): BlockOption; /** * The size exponent of this block in the range 0..6 * The actual block size is calculated by 2**(4 + exp) @@ -72,26 +76,26 @@ export declare class BlockOption extends NumericOption { * Specialized Message options for binary (and empty) content. */ export declare class BinaryOption extends Option { - readonly name: string; + readonly name: OptionName; readonly repeatable: boolean; readonly minLength: number; readonly maxLength: number; - constructor(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer); + constructor(code: number, name: OptionName, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer); value: Buffer; - static create(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer): BinaryOption; + static create(code: number, name: OptionName, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer): BinaryOption; toString(): string; } /** * Specialized Message options for string content. */ export declare class StringOption extends Option { - readonly name: string; + readonly name: OptionName; readonly repeatable: boolean; readonly minLength: number; readonly maxLength: number; - constructor(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer); + constructor(code: number, name: OptionName, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer); value: string; - static create(code: number, name: string, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer): StringOption; + static create(code: number, name: OptionName, repeatable: boolean, minLength: number, maxLength: number, rawValue: Buffer): StringOption; toString(): string; } export declare const Options: Readonly<{ @@ -109,10 +113,10 @@ export declare const Options: Readonly<{ * @param opts The options array to search for the option * @param name The name of the option to search for */ -export declare function findOption(opts: Option[], name: string): Option; +export declare function findOption(opts: Option[], name: OptionName): Option; /** * Searches for a repeatable option in an array of options * @param opts The options array to search for the option * @param name The name of the option to search for */ -export declare function findOptions(opts: Option[], name: string): Option[]; +export declare function findOptions(opts: Option[], name: OptionName): Option[]; diff --git a/src/Message.ts b/src/Message.ts index a3861623..9438bd6c 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -1,4 +1,4 @@ -import { Option } from "./Option"; +import { findOption, Option } from "./Option"; export enum MessageType { CON = 0, // Confirmable @@ -206,12 +206,10 @@ export class Message { * Checks if this message is part of a blockwise transfer */ public isPartialMessage(): boolean { - // TODO: can we put the codes in an enum? - // start with the response option, since that's more likely - const block2option = this.options.find(o => o.code === 23 /* Block2 */); + const block2option = findOption(this.options, "Block2"); if (this.code.isResponse() && block2option != null) return true; - const block1option = this.options.find(o => o.code === 27 /* Block1 */); + const block1option = findOption(this.options, "Block1"); if (this.code.isRequest() && block1option != null) return true; return false; } diff --git a/src/Option.ts b/src/Option.ts index 7b621d8c..6c109ca8 100644 --- a/src/Option.ts +++ b/src/Option.ts @@ -9,6 +9,31 @@ function numberToBuffer(value: number): Buffer { return Buffer.from(ret); } +/** + * All defined option names + */ +export type OptionName = + "Observe" | + "Uri-Port" | + "Content-Format" | + "Max-Age" | + "Accept" | + "Block2" | + "Block1" | + "Size2" | + "Size1" | + "If-Match" | + "ETag" | + "If-None-Match" | + "Uri-Host" | + "Location-Path" | + "Uri-Path" | + "Uri-Query" | + "Location-Query" | + "Proxy-Uri" | + "Proxy-Scheme" +; + /** * Abstract base class for all message options. Provides methods to parse and serialize. */ @@ -16,7 +41,7 @@ export abstract class Option { constructor( public readonly code: number, - public readonly name: string, + public readonly name: OptionName, public rawValue: Buffer, ) { @@ -173,7 +198,7 @@ export class NumericOption extends Option { constructor( code: number, - public readonly name: string, + public readonly name: OptionName, public readonly repeatable: boolean, public readonly maxLength: number, rawValue: Buffer, @@ -198,7 +223,7 @@ export class NumericOption extends Option { public static create( code: number, - name: string, + name: OptionName, repeatable: boolean, maxLength: number, rawValue: Buffer, @@ -219,7 +244,7 @@ export class BlockOption extends NumericOption { public static create( code: number, - name: string, + name: OptionName, repeatable: boolean, maxLength: number, rawValue: Buffer, @@ -299,7 +324,7 @@ export class BinaryOption extends Option { constructor( code: number, - public readonly name: string, + public readonly name: OptionName, public readonly repeatable: boolean, public readonly minLength: number, public readonly maxLength: number, @@ -324,7 +349,7 @@ export class BinaryOption extends Option { public static create( code: number, - name: string, + name: OptionName, repeatable: boolean, minLength: number, maxLength: number, @@ -346,7 +371,7 @@ export class StringOption extends Option { constructor( code: number, - public readonly name: string, + public readonly name: OptionName, public readonly repeatable: boolean, public readonly minLength: number, public readonly maxLength: number, @@ -371,7 +396,7 @@ export class StringOption extends Option { public static create( code: number, - name: string, + name: OptionName, repeatable: boolean, minLength: number, maxLength: number, @@ -393,7 +418,7 @@ const optionConstructors: {[code: string]: (raw: Buffer) => Option} = {}; function defineOptionConstructor( // tslint:disable-next-line:ban-types constructor: Function, - code: number, name: string, repeatable: boolean, + code: number, name: OptionName, repeatable: boolean, ...args: any[], ): void { optionConstructors[code] = optionConstructors[name] = @@ -451,7 +476,7 @@ export const Options = Object.freeze({ * @param opts The options array to search for the option * @param name The name of the option to search for */ -export function findOption(opts: Option[], name: string): Option { +export function findOption(opts: Option[], name: OptionName): Option { return opts.find(o => o.name === name); } @@ -460,6 +485,6 @@ export function findOption(opts: Option[], name: string): Option { * @param opts The options array to search for the option * @param name The name of the option to search for */ -export function findOptions(opts: Option[], name: string): Option[] { +export function findOptions(opts: Option[], name: OptionName): Option[] { return opts.filter(o => o.name === name); } From b5ff66f0680f568145c7ec06f3ab960a462c2f11 Mon Sep 17 00:00:00 2001 From: "dominic.griesel" Date: Wed, 20 Dec 2017 10:51:40 +0100 Subject: [PATCH 11/12] implemented logic for blockwise requests untested! --- build/CoapClient.d.ts | 12 ++++- build/CoapClient.js | 100 ++++++++++++++++++++++++++++------------ src/CoapClient.ts | 105 ++++++++++++++++++++++++++++++------------ src/Message.test.ts | 1 + 4 files changed, 158 insertions(+), 60 deletions(-) diff --git a/build/CoapClient.d.ts b/build/CoapClient.d.ts index be82b302..4cb19268 100644 --- a/build/CoapClient.d.ts +++ b/build/CoapClient.d.ts @@ -68,6 +68,11 @@ export declare class CoapClient { * @param options - Various options to control the request. */ static request(url: string | nodeUrl.Url, method: RequestMethod, payload?: Buffer, options?: RequestOptions): Promise; + /** + * Creates a RetransmissionInfo to use for retransmission of lost packets + * @param messageId The message id of the corresponding request + */ + private static createRetransmissionInfo(messageId); /** * Pings a CoAP endpoint to check if it is alive * @param target - The target to be pinged. Must be a string, NodeJS.Url or Origin and has to contain the protocol, host and port. @@ -81,6 +86,11 @@ export declare class CoapClient { private static retransmit(msgID); private static getRetransmissionInterval(); private static stopRetransmission(request); + /** + * When the server responds with block-wise responses, this requests the next block. + * @param request The original request which resulted in a block-wise response + */ + private static requestNextBlock(request); /** * Observes a CoAP resource * @param url - The URL to be requested. Must start with coap:// or coaps:// @@ -110,7 +120,7 @@ export declare class CoapClient { * @param message The message to send * @param highPriority Whether the message should be prioritized */ - private static send(connection, message, highPriority?); + private static send(connection, message, priority?); private static workOffSendQueue(); /** * Does the actual sending of a message and starts concurrency/retransmission handling diff --git a/build/CoapClient.js b/build/CoapClient.js index bb920c1e..d9ab72ce 100644 --- a/build/CoapClient.js +++ b/build/CoapClient.js @@ -250,13 +250,7 @@ class CoapClient { // create the retransmission info let retransmit; if (options.retransmit && type === Message_1.MessageType.CON) { - const timeout = CoapClient.getRetransmissionInterval(); - retransmit = { - timeout, - action: () => CoapClient.retransmit(messageId), - jsTimeout: null, - counter: 0, - }; + retransmit = CoapClient.createRetransmissionInfo(messageId); } // remember the request const req = new PendingRequest({ @@ -277,6 +271,18 @@ class CoapClient { return response; }); } + /** + * Creates a RetransmissionInfo to use for retransmission of lost packets + * @param messageId The message id of the corresponding request + */ + static createRetransmissionInfo(messageId) { + return { + timeout: CoapClient.getRetransmissionInterval(), + action: () => CoapClient.retransmit(messageId), + jsTimeout: null, + counter: 0, + }; + } /** * Pings a CoAP endpoint to check if it is alive * @param target - The target to be pinged. Must be a string, NodeJS.Url or Origin and has to contain the protocol, host and port. @@ -363,7 +369,7 @@ class CoapClient { } debug(`retransmitting message ${msgID.toString(16)}, try #${request.retransmit.counter + 1}`); // resend the message - CoapClient.send(request.connection, request.originalMessage, true); + CoapClient.send(request.connection, request.originalMessage, "immediate"); // and increase the params request.retransmit.counter++; request.retransmit.timeout *= 2; @@ -379,6 +385,30 @@ class CoapClient { clearTimeout(request.retransmit.jsTimeout); request.retransmit = null; } + /** + * When the server responds with block-wise responses, this requests the next block. + * @param request The original request which resulted in a block-wise response + */ + static requestNextBlock(request) { + const message = request.originalMessage; + const connection = request.connection; + // requests for the next block are a new message with a new message id + const oldMsgID = message.messageId; + message.messageId = connection.lastMsgId = incrementMessageID(connection.lastMsgId); + // this means we have to update the dictionaries aswell, so the request is still found + CoapClient.pendingRequestsByMsgID[message.messageId] = request; + delete CoapClient.pendingRequestsByMsgID[oldMsgID]; + // even if the original request was an observe, the partial requests are not + message.options = message.options.filter(o => o.name !== "Observe"); + // Change the Block2 option, so the server knows which block to send + const block2Opt = Option_1.findOption(message.options, "Block2"); + block2Opt.isLastBlock = true; // not sure if that's necessary, but better be safe + block2Opt.blockNumber++; + // enable retransmission for this updated request + request.retransmit = CoapClient.createRetransmissionInfo(message.messageId); + // and enqueue it for sending + CoapClient.send(connection, message, "high"); + } /** * Observes a CoAP resource * @param url - The URL to be requested. Must start with coap:// or coaps:// @@ -426,13 +456,7 @@ class CoapClient { // create the retransmission info let retransmit; if (options.retransmit && type === Message_1.MessageType.CON) { - const timeout = CoapClient.getRetransmissionInterval(); - retransmit = { - timeout, - action: () => CoapClient.retransmit(messageId), - jsTimeout: null, - counter: 0, - }; + retransmit = CoapClient.createRetransmissionInfo(messageId); } // remember the request const req = new PendingRequest({ @@ -523,6 +547,7 @@ class CoapClient { if (optCntFmt) contentFormat = optCntFmt.value; } + let responseIsComplete = true; if (coapMsg.isPartialMessage()) { // Check if we expect more blocks const blockOption = Option_1.findOption(coapMsg.options, "Block2"); // we know this is != null @@ -537,14 +562,17 @@ class CoapClient { request.partialResponse.payload = Buffer.concat([request.partialResponse.payload, coapMsg.payload]); } if (!blockOption.isLastBlock) { - // TODO: request the next block - return; + CoapClient.requestNextBlock(request); + responseIsComplete = false; } } - // Now that the response is complete, also reduce the request's concurrency, + // Now that we have a response, also reduce the request's concurrency, // so other requests can be fired off if (coapMsg.type === Message_1.MessageType.ACK) request.concurrency = 0; + // while we only have a partial response, we cannot return it to the caller yet + if (!responseIsComplete) + return; // prepare the response const response = { code: coapMsg.code, @@ -565,7 +593,7 @@ class CoapClient { if (coapMsg.type === Message_1.MessageType.CON) { debug(`sending ACK for message 0x${coapMsg.messageId.toString(16)}`); const ACK = CoapClient.createMessage(Message_1.MessageType.ACK, Message_1.MessageCodes.empty, coapMsg.messageId); - CoapClient.send(request.connection, ACK, true); + CoapClient.send(request.connection, ACK, "immediate"); } } else { @@ -577,7 +605,7 @@ class CoapClient { // and send the reset debug(`sending RST for message 0x${coapMsg.messageId.toString(16)}`); const RST = CoapClient.createMessage(Message_1.MessageType.RST, Message_1.MessageCodes.empty, coapMsg.messageId); - CoapClient.send(connection, RST, true); + CoapClient.send(connection, RST, "immediate"); } } // request != null? } // (coapMsg.token && coapMsg.token.length) @@ -602,17 +630,29 @@ class CoapClient { * @param message The message to send * @param highPriority Whether the message should be prioritized */ - static send(connection, message, highPriority = false) { + static send(connection, message, priority = "normal") { const request = CoapClient.findRequest({ msgID: message.messageId }); - if (highPriority) { - // Send high-prio messages immediately - debug(`sending high priority message 0x${message.messageId.toString(16)}`); - CoapClient.doSend(connection, request, message); - } - else { - // Put the message in the queue - CoapClient.sendQueue.push({ connection, message }); - debug(`added message to send queue, new length = ${CoapClient.sendQueue.length}`); + switch (priority) { + case "immediate": { + // Send high-prio messages immediately + // This is for ACKs, RSTs and retransmissions + debug(`sending high priority message 0x${message.messageId.toString(16)}`); + CoapClient.doSend(connection, request, message); + break; + } + case "normal": { + // Put the message in the queue + CoapClient.sendQueue.push({ connection, message }); + debug(`added message to the send queue with normal priority, new length = ${CoapClient.sendQueue.length}`); + break; + } + case "high": { + // Put the message in the queue (in first position) + // This is for subsequent requests to blockwise resources + CoapClient.sendQueue.unshift({ connection, message }); + debug(`added message to the send queue with high priority, new length = ${CoapClient.sendQueue.length}`); + break; + } } // if there's a request for this message, listen for concurrency changes if (request != null) { diff --git a/src/CoapClient.ts b/src/CoapClient.ts index 1231e3d5..ffb46c58 100644 --- a/src/CoapClient.ts +++ b/src/CoapClient.ts @@ -347,13 +347,7 @@ export class CoapClient { // create the retransmission info let retransmit: RetransmissionInfo; if (options.retransmit && type === MessageType.CON) { - const timeout = CoapClient.getRetransmissionInterval(); - retransmit = { - timeout, - action: () => CoapClient.retransmit(messageId), - jsTimeout: null, - counter: 0, - }; + retransmit = CoapClient.createRetransmissionInfo(messageId); } // remember the request @@ -378,6 +372,19 @@ export class CoapClient { } + /** + * Creates a RetransmissionInfo to use for retransmission of lost packets + * @param messageId The message id of the corresponding request + */ + private static createRetransmissionInfo(messageId: number): RetransmissionInfo { + return { + timeout: CoapClient.getRetransmissionInterval(), + action: () => CoapClient.retransmit(messageId), + jsTimeout: null, + counter: 0, + }; + } + /** * Pings a CoAP endpoint to check if it is alive * @param target - The target to be pinged. Must be a string, NodeJS.Url or Origin and has to contain the protocol, host and port. @@ -476,7 +483,7 @@ export class CoapClient { debug(`retransmitting message ${msgID.toString(16)}, try #${request.retransmit.counter + 1}`); // resend the message - CoapClient.send(request.connection, request.originalMessage, true); + CoapClient.send(request.connection, request.originalMessage, "immediate"); // and increase the params request.retransmit.counter++; request.retransmit.timeout *= 2; @@ -493,6 +500,35 @@ export class CoapClient { request.retransmit = null; } + /** + * When the server responds with block-wise responses, this requests the next block. + * @param request The original request which resulted in a block-wise response + */ + private static requestNextBlock(request: PendingRequest) { + const message = request.originalMessage; + const connection = request.connection; + + // requests for the next block are a new message with a new message id + const oldMsgID = message.messageId; + message.messageId = connection.lastMsgId = incrementMessageID(connection.lastMsgId); + // this means we have to update the dictionaries aswell, so the request is still found + CoapClient.pendingRequestsByMsgID[message.messageId] = request; + delete CoapClient.pendingRequestsByMsgID[oldMsgID]; + + // even if the original request was an observe, the partial requests are not + message.options = message.options.filter(o => o.name !== "Observe"); + + // Change the Block2 option, so the server knows which block to send + const block2Opt = findOption(message.options, "Block2") as BlockOption; + block2Opt.isLastBlock = true; // not sure if that's necessary, but better be safe + block2Opt.blockNumber++; + + // enable retransmission for this updated request + request.retransmit = CoapClient.createRetransmissionInfo(message.messageId); + // and enqueue it for sending + CoapClient.send(connection, message, "high"); + } + /** * Observes a CoAP resource * @param url - The URL to be requested. Must start with coap:// or coaps:// @@ -551,13 +587,7 @@ export class CoapClient { // create the retransmission info let retransmit: RetransmissionInfo; if (options.retransmit && type === MessageType.CON) { - const timeout = CoapClient.getRetransmissionInterval(); - retransmit = { - timeout, - action: () => CoapClient.retransmit(messageId), - jsTimeout: null, - counter: 0, - }; + retransmit = CoapClient.createRetransmissionInfo(messageId); } // remember the request @@ -657,6 +687,7 @@ export class CoapClient { if (optCntFmt) contentFormat = (optCntFmt as NumericOption).value; } + let responseIsComplete: boolean = true; if (coapMsg.isPartialMessage()) { // Check if we expect more blocks const blockOption = findOption(coapMsg.options, "Block2") as BlockOption; // we know this is != null @@ -671,15 +702,18 @@ export class CoapClient { request.partialResponse.payload = Buffer.concat([request.partialResponse.payload, coapMsg.payload]); } if (!blockOption.isLastBlock) { - // TODO: request the next block - return; + CoapClient.requestNextBlock(request); + responseIsComplete = false; } } - // Now that the response is complete, also reduce the request's concurrency, + // Now that we have a response, also reduce the request's concurrency, // so other requests can be fired off if (coapMsg.type === MessageType.ACK) request.concurrency = 0; + // while we only have a partial response, we cannot return it to the caller yet + if (!responseIsComplete) return; + // prepare the response const response: CoapResponse = { code: coapMsg.code, @@ -705,7 +739,7 @@ export class CoapClient { MessageCodes.empty, coapMsg.messageId, ); - CoapClient.send(request.connection, ACK, true); + CoapClient.send(request.connection, ACK, "immediate"); } } else { // request == null @@ -723,7 +757,7 @@ export class CoapClient { MessageCodes.empty, coapMsg.messageId, ); - CoapClient.send(connection, RST, true); + CoapClient.send(connection, RST, "immediate"); } } // request != null? } // (coapMsg.token && coapMsg.token.length) @@ -763,19 +797,32 @@ export class CoapClient { private static send( connection: ConnectionInfo, message: Message, - highPriority: boolean = false, + priority: "normal" | "high" | "immediate" = "normal", ): void { const request = CoapClient.findRequest({msgID: message.messageId}); - if (highPriority) { - // Send high-prio messages immediately - debug(`sending high priority message 0x${message.messageId.toString(16)}`); - CoapClient.doSend(connection, request, message); - } else { - // Put the message in the queue - CoapClient.sendQueue.push({connection, message}); - debug(`added message to send queue, new length = ${CoapClient.sendQueue.length}`); + switch (priority) { + case "immediate": { + // Send high-prio messages immediately + // This is for ACKs, RSTs and retransmissions + debug(`sending high priority message 0x${message.messageId.toString(16)}`); + CoapClient.doSend(connection, request, message); + break; + } + case "normal": { + // Put the message in the queue + CoapClient.sendQueue.push({connection, message}); + debug(`added message to the send queue with normal priority, new length = ${CoapClient.sendQueue.length}`); + break; + } + case "high": { + // Put the message in the queue (in first position) + // This is for subsequent requests to blockwise resources + CoapClient.sendQueue.unshift({connection, message}); + debug(`added message to the send queue with high priority, new length = ${CoapClient.sendQueue.length}`); + break; + } } // if there's a request for this message, listen for concurrency changes diff --git a/src/Message.test.ts b/src/Message.test.ts index 2e059ed9..a1a3e3b9 100644 --- a/src/Message.test.ts +++ b/src/Message.test.ts @@ -1,3 +1,4 @@ +// tslint:disable:no-console // tslint:disable:no-unused-expression import { expect } from "chai"; From b041be3c1092ac9b52e2ce167278b3bf3f7c293b Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 21 Dec 2017 18:33:17 +0100 Subject: [PATCH 12/12] return the complete payload to listeners instead of the last fragment --- build/CoapClient.js | 9 +++++++-- src/CoapClient.ts | 8 ++++++-- src/Message.test.ts | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/build/CoapClient.js b/build/CoapClient.js index d9ab72ce..7588ef28 100644 --- a/build/CoapClient.js +++ b/build/CoapClient.js @@ -561,7 +561,12 @@ class CoapClient { // TODO: we might have to check if we got the correct fragment request.partialResponse.payload = Buffer.concat([request.partialResponse.payload, coapMsg.payload]); } - if (!blockOption.isLastBlock) { + if (blockOption.isLastBlock) { + // override the message payload with the assembled partial payload + // so the full payload gets returned to the listeners + coapMsg.payload = request.partialResponse.payload; + } + else { CoapClient.requestNextBlock(request); responseIsComplete = false; } @@ -659,7 +664,7 @@ class CoapClient { // and continue working off the queue when it drops request.on("concurrencyChanged", (req) => { debug(`request 0x${message.messageId.toString(16)}: concurrency changed => ${req.concurrency}`); - if (request.concurrency === 0) + if (req.concurrency === 0) CoapClient.workOffSendQueue(); }); } diff --git a/src/CoapClient.ts b/src/CoapClient.ts index ffb46c58..afbb8256 100644 --- a/src/CoapClient.ts +++ b/src/CoapClient.ts @@ -701,7 +701,11 @@ export class CoapClient { // TODO: we might have to check if we got the correct fragment request.partialResponse.payload = Buffer.concat([request.partialResponse.payload, coapMsg.payload]); } - if (!blockOption.isLastBlock) { + if (blockOption.isLastBlock) { + // override the message payload with the assembled partial payload + // so the full payload gets returned to the listeners + coapMsg.payload = request.partialResponse.payload; + } else { CoapClient.requestNextBlock(request); responseIsComplete = false; } @@ -830,7 +834,7 @@ export class CoapClient { // and continue working off the queue when it drops request.on("concurrencyChanged", (req: PendingRequest) => { debug(`request 0x${message.messageId.toString(16)}: concurrency changed => ${req.concurrency}`); - if (request.concurrency === 0) CoapClient.workOffSendQueue(); + if (req.concurrency === 0) CoapClient.workOffSendQueue(); }); } diff --git a/src/Message.test.ts b/src/Message.test.ts index a1a3e3b9..0f157c94 100644 --- a/src/Message.test.ts +++ b/src/Message.test.ts @@ -108,10 +108,12 @@ describe.only("blockwise tests =>", () => { // limit response size coap.setDefaultRequestOptions({ - preferredBlockSize: 16, + preferredBlockSize: 64, }); const resp = await coap.request(`${requestBase}15011/15012`, "get"); + console.log("got complete payload:"); + console.log(resp.payload.toString("utf8")); coap.reset(); });