Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support receiving block-wise responses #22

Merged
merged 12 commits into from
Dec 24, 2017
24 changes: 21 additions & 3 deletions build/CoapClient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,12 +42,18 @@ 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;
/** 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,
Expand All @@ -60,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<CoapResponse>;
/**
* 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.
Expand All @@ -73,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://
Expand Down Expand Up @@ -102,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
Expand Down
226 changes: 160 additions & 66 deletions build/CoapClient.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions build/Message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export declare const MessageCodes: Readonly<{
valid: MessageCode;
changed: MessageCode;
content: MessageCode;
continue: MessageCode;
};
clientError: {
__major: number;
Expand All @@ -46,6 +47,7 @@ export declare const MessageCodes: Readonly<{
notFound: MessageCode;
methodNotAllowed: MessageCode;
notAcceptable: MessageCode;
requestEntityIncomplete: MessageCode;
preconditionFailed: MessageCode;
requestEntityTooLarge: MessageCode;
unsupportedContentFormat: MessageCode;
Expand Down Expand Up @@ -81,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;
}
15 changes: 15 additions & 0 deletions build/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -165,6 +167,19 @@ class Message {
}
return ret;
}
/**
* Checks if this message is part of a blockwise transfer
*/
isPartialMessage() {
// start with the response option, since that's more likely
const block2option = Option_1.findOption(this.options, "Block2");
if (this.code.isResponse() && block2option != null)
return true;
const block1option = Option_1.findOption(this.options, "Block1");
if (this.code.isRequest() && block1option != null)
return true;
return false;
}
}
exports.Message = Message;
/*
Expand Down
73 changes: 62 additions & 11 deletions build/Option.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/// <reference types="node" />
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;
Expand All @@ -30,36 +34,69 @@ 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: 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)
*/
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;
toString(): string;
}
/**
* 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<{
UriHost: (hostname: string) => Option;
Expand All @@ -68,4 +105,18 @@ 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;
}>;
/**
* 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: 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: OptionName): Option[];
112 changes: 111 additions & 1 deletion build/Option.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,78 @@ 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)
*/
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);
}
toString() {
return `${this.name} (${this.code}): ${this.blockNumber}/${this.isLastBlock ? 0 : 1}/${this.blockSize}`;
}
}
exports.BlockOption = BlockOption;
/**
* Specialized Message options for binary (and empty) content.
*/
Expand Down Expand Up @@ -206,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;
/**
Expand Down Expand Up @@ -237,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;
/**
Expand All @@ -254,6 +330,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(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);
defineOptionConstructor(BinaryOption, 4, "ETag", true, 1, 8);
Expand All @@ -265,13 +344,44 @@ 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)),
UriPort: (port) => optionConstructors["Uri-Port"](numberToBuffer(port)),
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
/**
* 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;
2 changes: 2 additions & 0 deletions build/lib/LogMessage.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { Message } from "../Message";
export declare function logMessage(msg: Message, includePayload?: boolean): void;
Loading