From 8d586014b6a9d4eafe4b8f0d13a4a70f66e2366c Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 22 Sep 2023 16:46:49 +0200 Subject: [PATCH 01/19] chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/.eslintrc.json | 5 +- .../binding-http/src/codecs/tuya-codec.ts | 21 ++- packages/binding-http/src/credential.ts | 56 +++++-- packages/binding-http/src/http-client-impl.ts | 10 +- packages/binding-http/src/http-server.ts | 151 +++++++++--------- .../src/oauth-token-validation.ts | 11 +- packages/binding-http/src/routes/action.ts | 8 +- packages/binding-http/src/routes/common.ts | 13 +- packages/binding-http/src/routes/event.ts | 6 +- .../binding-http/src/routes/properties.ts | 4 +- .../src/routes/property-observe.ts | 6 +- packages/binding-http/src/routes/property.ts | 8 +- .../src/routes/thing-description.ts | 35 ++-- .../test/http-client-basic-test.ts | 2 +- .../binding-http/test/http-client-test.ts | 22 ++- .../binding-http/test/http-server-test.ts | 2 +- packages/binding-http/test/memory-model.ts | 4 +- .../test/oauth-token-validation-tests.ts | 4 +- 18 files changed, 212 insertions(+), 156 deletions(-) diff --git a/packages/binding-http/.eslintrc.json b/packages/binding-http/.eslintrc.json index c4237119e..70b9f134b 100644 --- a/packages/binding-http/.eslintrc.json +++ b/packages/binding-http/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "../../.eslintrc.js" + "extends": "../../.eslintrc.js", + "rules": { + "@typescript-eslint/strict-boolean-expressions": ["error"] + } } diff --git a/packages/binding-http/src/codecs/tuya-codec.ts b/packages/binding-http/src/codecs/tuya-codec.ts index ff769988f..3b0f54e52 100644 --- a/packages/binding-http/src/codecs/tuya-codec.ts +++ b/packages/binding-http/src/codecs/tuya-codec.ts @@ -17,17 +17,28 @@ import { ContentCodec } from "@node-wot/core"; import * as TD from "@node-wot/td-tools"; import { DataSchemaValue } from "wot-typescript-definitions"; +interface TuyaOutput { + success?: boolean; + msg?: string; + result?: { + code?: string; + }[]; +} + export default class HttpTuyaCodec implements ContentCodec { getMediaType(): string { return "application/json+tuya"; } bytesToValue(bytes: Buffer, schema: TD.DataSchema, parameters: { [key: string]: string }): DataSchemaValue { - const parsedBody = JSON.parse(bytes.toString()); - if (!parsedBody.success) throw new Error(parsedBody.msg ? parsedBody.msg : JSON.stringify(parsedBody)); - for (const key in parsedBody.result) { - if (parsedBody.result[key].code === schema["tuya:PropertyName"]) { - return parsedBody.result[key].value; + const parsedBody: TuyaOutput = JSON.parse(bytes.toString()); + if (parsedBody.success !== true) { + throw new Error(parsedBody.msg != null ? parsedBody.msg : JSON.stringify(parsedBody)); + } + + for (const value of Object.values(parsedBody.result ?? {})) { + if (value.code === schema["tuya:PropertyName"]) { + return value; } } throw new Error("Property not found"); diff --git a/packages/binding-http/src/credential.ts b/packages/binding-http/src/credential.ts index 1a0ddb853..a40ddad76 100644 --- a/packages/binding-http/src/credential.ts +++ b/packages/binding-http/src/credential.ts @@ -163,6 +163,18 @@ export interface TuyaCustomBearerCredentialConfiguration { secret: string; } +interface TokenResponse { + success?: boolean; + result?: { + // eslint-disable-next-line camelcase + access_token?: string; + // eslint-disable-next-line camelcase + refresh_token?: string; + // eslint-disable-next-line camelcase + expire_time?: number; + }; +} + export class TuyaCustomBearer extends Credential { protected key: string; protected secret: string; @@ -184,14 +196,15 @@ export class TuyaCustomBearer extends Credential { await this.requestAndRefreshToken(isTokenExpired); const url: string = request.url; - const body = request.body ? request.body.read().toString() : ""; - const headers = this.getHeaders(true, request.headers.raw(), body, url, request.method); + const body = request.body?.read().toString(); + const method = request.method; + const headers = this.getHeaders(true, request.headers.raw(), body, url, method); Object.assign(headers, request.headers.raw()); - return new Request(url, { method: request.method, body: body !== "" ? body : undefined, headers }); + return new Request(url, { method, body: body !== "" ? body : undefined, headers }); } protected async requestAndRefreshToken(refresh: boolean): Promise { - const headers = this.getHeaders(false, {}, ""); + const headers = this.getHeaders(false, {}); const request = { headers, method: "GET", @@ -200,36 +213,49 @@ export class TuyaCustomBearer extends Credential { if (refresh) { url = `${this.baseUri}/token/${this.refreshToken}`; } - const data = await (await fetch(url, request)).json(); - if (data.success) { - this.token = data.result.access_token; - this.refreshToken = data.result.refresh_token; - this.expireTime = new Date(Date.now() + data.result.expire_time * 1000); + const data: TokenResponse = await (await fetch(url, request)).json(); + if (data.success === true) { + this.token = data.result?.access_token; + this.refreshToken = data.result?.refresh_token; + + const expireTime = data.result?.expire_time; + if (expireTime != null) { + this.expireTime = new Date(Date.now() + expireTime * 1000); + } } else { throw new Error("token fetch failed"); } } - private getHeaders(NormalRequest: boolean, headers: unknown, body: string, url?: string, method?: string) { + private getHeaders(NormalRequest: boolean, headers: unknown, body?: string, url?: string, method?: string) { const requestTime = Date.now().toString(); const replaceUri = this.baseUri.replace("/v1.0", ""); - const _url = url ? url.replace(`${replaceUri}`, "") : undefined; + const _url = url?.replace(replaceUri, ""); const sign = this.requestSign(NormalRequest, requestTime, body, _url, method); return { t: requestTime, client_id: this.key, sign_method: "HMAC-SHA256", sign, - access_token: this.token || "", + access_token: this.token ?? "", }; } - private requestSign(NormalRequest: boolean, requestTime: string, body: string, path = "", method?: string): string { - const bodyHash = crypto.createHash("sha256").update(body).digest("hex"); + private requestSign( + NormalRequest: boolean, + requestTime: string, + body?: string, + path = "", + method?: string + ): string { + const bodyHash = crypto + .createHash("sha256") + .update(body ?? "") + .digest("hex"); let signUrl = "/v1.0/token?grant_type=1"; const headerString = ""; let useToken = ""; - const _method = method || "GET"; + const _method = method ?? "GET"; if (NormalRequest) { useToken = this.token ?? ""; const pathQuery = queryString.parse(path.split("?")[1]); diff --git a/packages/binding-http/src/http-client-impl.ts b/packages/binding-http/src/http-client-impl.ts index 8223277de..de5c93182 100644 --- a/packages/binding-http/src/http-client-impl.ts +++ b/packages/binding-http/src/http-client-impl.ts @@ -175,7 +175,7 @@ export default class HttpClient implements ProtocolClient { } public async invokeResource(form: HttpForm, content?: Content): Promise { - const headers = content ? [["content-type", content.type]] : []; + const headers = content != null ? [["content-type", content.type]] : []; const request = await this.generateFetchRequest(form, "POST", { headers, @@ -184,8 +184,8 @@ export default class HttpClient implements ProtocolClient { debug( `HttpClient (invokeResource) sending ${request.method} ${ - content ? "with '" + request.headers.get("Content-Type") + "' " : " " - }to ${request.url}` + content != null ? `with '"${request.headers.get("Content-Type")}"` : "" + } to ${request.url}` ); const result = await this.fetch(request); @@ -218,7 +218,7 @@ export default class HttpClient implements ProtocolClient { public async stop(): Promise { // When running in browser mode, Agent.destroy() might not exist. - if (this.agent && this.agent.destroy) this.agent.destroy(); + this.agent?.destroy?.(); } public setSecurity(metadata: Array, credentials?: unknown): boolean { @@ -280,7 +280,7 @@ export default class HttpClient implements ProtocolClient { return false; } - if (security.proxy) { + if (security.proxy != null) { if (this.proxyRequest !== null) { debug(`HttpClient overriding client-side proxy with security proxy '${security.proxy}`); } diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index ebb969dd9..e42b8dc96 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -22,8 +22,6 @@ import * as http from "http"; import * as https from "https"; import bauth from "basic-auth"; -import { AddressInfo } from "net"; - import * as TD from "@node-wot/td-tools"; import Servient, { ProtocolServer, @@ -47,6 +45,7 @@ import actionRoute from "./routes/action"; import eventRoute from "./routes/event"; import propertiesRoute from "./routes/properties"; import propertyObserveRoute from "./routes/property-observe"; +import { AddressInfo } from "net"; const { debug, info, warn, error } = createLoggers("binding-http", "http-server"); @@ -62,14 +61,14 @@ export default class HttpServer implements ProtocolServer { // private readonly OPTIONS_URI_VARIABLES ='uriVariables'; // private readonly OPTIONS_BODY_VARIABLES ='body'; - private readonly port: number = 8080; - private readonly address?: string = undefined; - private readonly baseUri?: string = undefined; - private readonly urlRewrite?: Record = undefined; + private readonly port: number; + private readonly address?: string; + private readonly baseUri?: string; + private readonly urlRewrite?: Record; private readonly supportedSecuritySchemes: string[] = ["nosec"]; private readonly validOAuthClients: RegExp = /.*/g; private readonly server: http.Server | https.Server; - private readonly middleware: MiddlewareRequestHandler | null = null; + private readonly middleware?: MiddlewareRequestHandler; private readonly things: Map = new Map(); private servient: Servient | null = null; private oAuthValidator?: Validator = undefined; @@ -80,33 +79,11 @@ export default class HttpServer implements ProtocolServer { throw new Error(`HttpServer requires config object (got ${typeof config})`); } - if (config.port !== undefined) { - this.port = config.port; - } - - const environmentObj = ["WOT_PORT", "PORT"] - .map((envVar) => { - return { key: envVar, value: process.env[envVar] }; - }) - .find((envObj) => envObj.value != null && envObj.value !== undefined) as { key: string; value: string }; - - if (environmentObj) { - info(`HttpServer Port Overridden to ${environmentObj.value} by Environment Variable ${environmentObj.key}`); - this.port = parseInt(environmentObj.value); - } - - if (config.address !== undefined) { - this.address = config.address; - } - if (config.baseUri !== undefined) { - this.baseUri = config.baseUri; - } - if (config.urlRewrite !== undefined) { - this.urlRewrite = config.urlRewrite; - } - if (config.middleware !== undefined) { - this.middleware = config.middleware; - } + this.port = this.obtainEnvironmentPortNumber() ?? config.port ?? 8080; + this.address = config.address; + this.baseUri = config.baseUri; + this.urlRewrite = config.urlRewrite; + this.middleware = config.middleware; const router = Router({ ignoreTrailingSlash: true, @@ -145,7 +122,7 @@ export default class HttpServer implements ProtocolServer { this.router.on(["GET", "HEAD", "OPTIONS"], "/:thing/" + this.EVENT_DIR + "/:event", eventRoute); // TLS - if (config.serverKey && config.serverCert) { + if (config.serverKey != null && config.serverCert != null) { const options: https.ServerOptions = {}; options.key = fs.readFileSync(config.serverKey); options.cert = fs.readFileSync(config.serverCert); @@ -200,6 +177,28 @@ export default class HttpServer implements ProtocolServer { } } + private obtainEnvironmentPortNumber(): number | undefined { + for (const portVariable of ["WOT_PORT", "PORT"]) { + const environmentValue = process.env[portVariable]; + + if (environmentValue == null) { + continue; + } + + const parsedPort = parseInt(environmentValue); + + if (isNaN(parsedPort)) { + debug(`Ignoring environment variable ${portVariable} because it is not an integer.`); + continue; + } + + info(`HttpServer Port Overridden to ${parsedPort} by Environment Variable ${portVariable}`); + return parsedPort; + } + + return undefined; + } + public start(servient: Servient): Promise { info(`HttpServer starting on ${this.address !== undefined ? this.address + " " : ""}port ${this.port}`); return new Promise((resolve, reject) => { @@ -253,8 +252,10 @@ export default class HttpServer implements ProtocolServer { /** returns server port number and indicates that server is running when larger than -1 */ public getPort(): number { - if (this.server.address() && typeof this.server.address() === "object") { - return (this.server.address()).port; + const address: AddressInfo | string | null = this.server?.address?.(); + + if (typeof address === "object") { + return address?.port ?? -1; } else { // includes address() typeof "string" case, which is only for unix sockets return -1; @@ -312,7 +313,7 @@ export default class HttpServer implements ProtocolServer { } private addUrlRewriteEndpoints(form: TD.Form, forms: Array): void { - if (this.urlRewrite) { + if (this.urlRewrite != null) { for (const inUri in this.urlRewrite) { const toUri = this.urlRewrite[inUri]; if (form.href.endsWith(toUri)) { @@ -332,10 +333,10 @@ export default class HttpServer implements ProtocolServer { let anyProperties = false; for (const propertyName in thing.properties) { anyProperties = true; - if (!thing.properties[propertyName].readOnly) { + if (thing.properties[propertyName]?.readOnly !== true) { allReadOnly = false; } - if (!thing.properties[propertyName].writeOnly) { + if (thing.properties[propertyName].writeOnly !== true) { allWriteOnly = false; } } @@ -354,7 +355,7 @@ export default class HttpServer implements ProtocolServer { "writemultipleproperties", ]; } - if (!thing.forms) { + if (thing.forms == null) { thing.forms = []; } thing.forms.push(form); @@ -373,13 +374,13 @@ export default class HttpServer implements ProtocolServer { form, (tdTemplate.properties?.[propertyName] ?? {}) as PropertyElement ); - if (thing.properties[propertyName].readOnly) { + if (thing.properties[propertyName].readOnly === true) { form.op = ["readproperty"]; const hform: HttpForm = form; if (hform["htv:methodName"] === undefined) { hform["htv:methodName"] = "GET"; } - } else if (thing.properties[propertyName].writeOnly) { + } else if (thing.properties[propertyName].writeOnly === true) { form.op = ["writeproperty"]; const hform: HttpForm = form; if (hform["htv:methodName"] === undefined) { @@ -394,7 +395,7 @@ export default class HttpServer implements ProtocolServer { this.addUrlRewriteEndpoints(form, thing.properties[propertyName].forms); // if property is observable add an additional form with a observable href - if (thing.properties[propertyName].observable) { + if (thing.properties[propertyName].observable === true) { const href = base + "/" + @@ -475,7 +476,7 @@ export default class HttpServer implements ProtocolServer { case "basic": { const basic = bauth(req); if (basic === undefined) return false; - if (!credentials || credentials.length === 0) return false; + if (credentials == null || credentials.length === 0) return false; const basicCredentials = credentials as { username: string; password: string }[]; return basicCredentials.some((cred) => basic.name === cred.username && basic.pass === cred.password); @@ -510,7 +511,7 @@ export default class HttpServer implements ProtocolServer { const auth = req.headers.authorization.split(" "); if (auth.length !== 2 || auth[0] !== "Bearer") return false; - if (!credentials || credentials.length === 0) return false; + if (credentials == null || credentials.length === 0) return false; const bearerCredentials = credentials as { token: string }[]; return bearerCredentials.some((cred) => cred.token === auth[1]); @@ -522,14 +523,14 @@ export default class HttpServer implements ProtocolServer { private fillSecurityScheme(thing: ExposedThing) { // User selected one security scheme - if (thing.security) { + if (thing.security.length > 0) { // multiple security schemes are deprecated we are not supporting them const securityScheme = Helpers.toStringArray(thing.security)[0]; const secCandidate = Object.keys(thing.securityDefinitions).find((key) => { return key === securityScheme; }); - if (!secCandidate) { + if (secCandidate == null) { throw new Error( "Security scheme not found in thing security definitions. Thing security definitions: " + Object.keys(thing.securityDefinitions).join(", ") @@ -541,7 +542,7 @@ export default class HttpServer implements ProtocolServer { return thingScheme === supportedScheme.toLocaleLowerCase(); }); - if (!isSupported) { + if (isSupported == null) { throw new Error( "Servient does not support thing security schemes. Current scheme supported: " + this.supportedSecuritySchemes.join(", ") @@ -552,7 +553,7 @@ export default class HttpServer implements ProtocolServer { } // The user let the servient choose the security scheme - if (!thing.securityDefinitions || Object.keys(thing.securityDefinitions).length === 0) { + if (Object.keys(thing.securityDefinitions ?? {}).length === 0) { // We are using the first supported security scheme as default thing.securityDefinitions = { [this.supportedSecuritySchemes[0]]: { scheme: this.supportedSecuritySchemes[0] }, @@ -561,35 +562,33 @@ export default class HttpServer implements ProtocolServer { return; } - if (thing.securityDefinitions) { - // User provided a bunch of security schemes but no thing.security - // we select one for him. We select the first supported scheme. - const secCandidate = Object.keys(thing.securityDefinitions).find((key) => { - let scheme = thing.securityDefinitions[key].scheme; - // HTTP Authentication Scheme for OAuth does not contain the version number - // see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml - // remove version number for oauth2 schemes - scheme = scheme === "oauth2" ? scheme.split("2")[0] : scheme; - return this.supportedSecuritySchemes.includes(scheme.toLocaleLowerCase()); - }); + // User provided a bunch of security schemes but no thing.security + // we select one for him. We select the first supported scheme. + const secCandidate = Object.keys(thing.securityDefinitions).find((key) => { + let scheme = thing.securityDefinitions[key].scheme; + // HTTP Authentication Scheme for OAuth does not contain the version number + // see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml + // remove version number for oauth2 schemes + scheme = scheme === "oauth2" ? scheme.split("2")[0] : scheme; + return this.supportedSecuritySchemes.includes(scheme.toLocaleLowerCase()); + }); - if (!secCandidate) { - throw new Error( - "Servient does not support any of thing security schemes. Current scheme supported: " + - this.supportedSecuritySchemes.join(",") + - " thing security schemes: " + - Object.values(thing.securityDefinitions) - .map((schemeDef) => schemeDef.scheme) - .join(", ") - ); - } + if (secCandidate == null) { + throw new Error( + "Servient does not support any of thing security schemes. Current scheme supported: " + + this.supportedSecuritySchemes.join(",") + + " thing security schemes: " + + Object.values(thing.securityDefinitions) + .map((schemeDef) => schemeDef.scheme) + .join(", ") + ); + } - const selectedSecurityScheme = thing.securityDefinitions[secCandidate]; - thing.securityDefinitions = {}; - thing.securityDefinitions[secCandidate] = selectedSecurityScheme; + const selectedSecurityScheme = thing.securityDefinitions[secCandidate]; + thing.securityDefinitions = {}; + thing.securityDefinitions[secCandidate] = selectedSecurityScheme; - thing.security = [secCandidate]; - } + thing.security = [secCandidate]; } private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { diff --git a/packages/binding-http/src/oauth-token-validation.ts b/packages/binding-http/src/oauth-token-validation.ts index 2f3372569..0b78129e6 100644 --- a/packages/binding-http/src/oauth-token-validation.ts +++ b/packages/binding-http/src/oauth-token-validation.ts @@ -60,11 +60,11 @@ function extractTokenFromRequest(request: http.IncomingMessage) { const url = new URL(request.url ?? "", `http://${request.headers.host}`); const queryToken = url.searchParams.get("access_token"); - if (!headerToken && !queryToken) { + if (headerToken == null && queryToken == null) { throw new Error("Invalid request: only one authentication method is allowed"); } - if (queryToken) { + if (queryToken != null) { return queryToken; } @@ -84,9 +84,10 @@ export class EndpointValidator extends Validator { super(); this.config = config; const endpoint = config.endpoint; + const allowSelfSigned = config?.allowSelfSigned ?? false; this.agent = endpoint.startsWith("https") ? new SecureAgent({ - rejectUnauthorized: !config.allowSelfSigned, + rejectUnauthorized: !allowSelfSigned, }) : new http.Agent(); } @@ -134,7 +135,7 @@ export class EndpointValidator extends Validator { return true; } - if (!validationResult.scope) { + if (validationResult.scope == null) { // If the token doesn't have any scope and we already know that scopes.length > 0, // then the token is not valid return false; @@ -149,7 +150,7 @@ export class EndpointValidator extends Validator { if (!validScope) return false; // Check if the client was allowed in the servient configuration file - if (validationResult.client_id && !validationResult.client_id.match(clients)) { + if (!validationResult.client_id?.match(clients)) { return false; } diff --git a/packages/binding-http/src/routes/action.ts b/packages/binding-http/src/routes/action.ts index a07d928d0..2ed663e43 100644 --- a/packages/binding-http/src/routes/action.ts +++ b/packages/binding-http/src/routes/action.ts @@ -38,7 +38,7 @@ export default async function actionRoute( } const thing = this.getThings().get(_params.thing); - if (!thing) { + if (thing == null) { res.writeHead(404); res.end(); return; @@ -61,7 +61,7 @@ export default async function actionRoute( const action = thing.actions[_params.action]; - if (!action) { + if (action == null) { res.writeHead(404); res.end(); return; @@ -72,7 +72,7 @@ export default async function actionRoute( const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { - if (req.method === "OPTIONS" && req.headers.origin) { + if (req.method === "OPTIONS" && req.headers.origin != null) { corsPreflightWithCredentials = true; } else { res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); @@ -92,7 +92,7 @@ export default async function actionRoute( } try { const output = await thing.handleInvokeAction(_params.action, new Content(contentType, req), options); - if (output) { + if (output != null) { res.setHeader("Content-Type", output.type); res.writeHead(200); output.body.pipe(res); diff --git a/packages/binding-http/src/routes/common.ts b/packages/binding-http/src/routes/common.ts index 5a13c5bf5..b4d164fad 100644 --- a/packages/binding-http/src/routes/common.ts +++ b/packages/binding-http/src/routes/common.ts @@ -27,14 +27,17 @@ export function respondUnallowedMethod( if (!allowed.includes("OPTIONS")) { allowed += ", OPTIONS"; } - if (req.method === "OPTIONS" && req.headers.origin && req.headers["access-control-request-method"]) { + + const headers = req.headers; + const origin = headers.origin; + if (req.method === "OPTIONS" && origin != null && headers["access-control-request-method"] != null) { debug( `HttpServer received an CORS preflight request from ${Helpers.toUriLiteral(req.socket.remoteAddress)}:${ req.socket.remotePort }` ); if (corsPreflightWithCredentials) { - res.setHeader("Access-Control-Allow-Origin", req.headers.origin); + res.setHeader("Access-Control-Allow-Origin", origin); res.setHeader("Access-Control-Allow-Credentials", "true"); } else { res.setHeader("Access-Control-Allow-Origin", "*"); @@ -91,8 +94,10 @@ export function securitySchemeToHttpHeader(scheme: string): string { export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void { const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; // Set CORS headers - if (securityScheme !== "nosec" && req.headers.origin) { - res.setHeader("Access-Control-Allow-Origin", req.headers.origin); + + const origin = req.headers.origin; + if (securityScheme !== "nosec" && origin != null) { + res.setHeader("Access-Control-Allow-Origin", origin); res.setHeader("Access-Control-Allow-Credentials", "true"); } else { res.setHeader("Access-Control-Allow-Origin", "*"); diff --git a/packages/binding-http/src/routes/event.ts b/packages/binding-http/src/routes/event.ts index 6e134b985..91a29de3b 100644 --- a/packages/binding-http/src/routes/event.ts +++ b/packages/binding-http/src/routes/event.ts @@ -31,7 +31,7 @@ export default async function eventRoute( } const thing = this.getThings().get(_params.thing); - if (!thing) { + if (thing == null) { res.writeHead(404); res.end(); return; @@ -41,7 +41,7 @@ export default async function eventRoute( const contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader; const event = thing.events[_params.event]; - if (!event) { + if (event == null) { res.writeHead(404); res.end(); return; @@ -52,7 +52,7 @@ export default async function eventRoute( const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { - if (req.method === "OPTIONS" && req.headers.origin) { + if (req.method === "OPTIONS" && req.headers.origin != null) { corsPreflightWithCredentials = true; } else { res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); diff --git a/packages/binding-http/src/routes/properties.ts b/packages/binding-http/src/routes/properties.ts index c5f06eb56..a9bc34993 100644 --- a/packages/binding-http/src/routes/properties.ts +++ b/packages/binding-http/src/routes/properties.ts @@ -32,7 +32,7 @@ export default async function propertiesRoute( const thing = this.getThings().get(_params.thing); - if (!thing) { + if (thing == null) { res.writeHead(404); res.end(); return; @@ -44,7 +44,7 @@ export default async function propertiesRoute( const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { - if (req.method === "OPTIONS" && req.headers.origin) { + if (req.method === "OPTIONS" && req.headers.origin != null) { corsPreflightWithCredentials = true; } else { res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); diff --git a/packages/binding-http/src/routes/property-observe.ts b/packages/binding-http/src/routes/property-observe.ts index c7bf070e4..1c2aac2cc 100644 --- a/packages/binding-http/src/routes/property-observe.ts +++ b/packages/binding-http/src/routes/property-observe.ts @@ -32,7 +32,7 @@ export default async function propertyObserveRoute( const thing = this.getThings().get(_params.thing); - if (!thing) { + if (thing == null) { res.writeHead(404); res.end(); return; @@ -42,7 +42,7 @@ export default async function propertyObserveRoute( const contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader; const property = thing.properties[_params.property]; - if (!property) { + if (property == null) { res.writeHead(404); res.end(); return; @@ -62,7 +62,7 @@ export default async function propertyObserveRoute( const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { - if (req.method === "OPTIONS" && req.headers.origin) { + if (req.method === "OPTIONS" && req.headers.origin != null) { corsPreflightWithCredentials = true; } else { res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); diff --git a/packages/binding-http/src/routes/property.ts b/packages/binding-http/src/routes/property.ts index 4095fb3bd..6d808b87d 100644 --- a/packages/binding-http/src/routes/property.ts +++ b/packages/binding-http/src/routes/property.ts @@ -38,7 +38,7 @@ export default async function propertyRoute( const thing = this.getThings().get(_params.thing); - if (!thing) { + if (thing == null) { res.writeHead(404); res.end(); return; @@ -61,7 +61,7 @@ export default async function propertyRoute( const property = thing.properties[_params.property]; - if (!property) { + if (property == null) { res.writeHead(404); res.end(); return; @@ -82,7 +82,7 @@ export default async function propertyRoute( const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { - if (req.method === "OPTIONS" && req.headers.origin) { + if (req.method === "OPTIONS" && req.headers.origin != null) { corsPreflightWithCredentials = true; } else { res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); @@ -106,7 +106,7 @@ export default async function propertyRoute( res.end(message); } } else if (req.method === "PUT") { - if (!property.readOnly) { + if (property.readOnly !== true) { try { await thing.handleWriteProperty(_params.property, new Content(contentType, req), options); diff --git a/packages/binding-http/src/routes/thing-description.ts b/packages/binding-http/src/routes/thing-description.ts index 29188de5d..ef1c6618a 100644 --- a/packages/binding-http/src/routes/thing-description.ts +++ b/packages/binding-http/src/routes/thing-description.ts @@ -104,21 +104,24 @@ function resetMultiLangThing(thing: ThingDescription, prefLang: string) { * @param req */ function negotiateLanguage(td: ThingDescription, thing: ExposedThing, req: IncomingMessage) { - if (req.headers["accept-language"] && req.headers["accept-language"] !== "*") { - if (td.titles) { - const supportedLanguages = Object.keys(td.titles); // e.g., ['fr', 'en'] - // the loose option allows partial matching on supported languages (e.g., returns "de" for "de-CH") - const prefLang = acceptLanguageParser.pick(supportedLanguages, req.headers["accept-language"], { - loose: true, - }); - if (prefLang) { - // if a preferred language can be found use it - debug( - `TD language negotiation through the Accept-Language header field of HTTP leads to "${prefLang}"` - ); - // TODO: reset titles and descriptions to only contain the preferred language - resetMultiLangThing(td, prefLang); - } + const acceptLanguage = req.headers["accept-language"]; + const noPreference = acceptLanguage == null || acceptLanguage === "*"; + + if (noPreference) { + return; + } + + if (td.titles != null) { + const supportedLanguages = Object.keys(td.titles); // e.g., ['fr', 'en'] + // the loose option allows partial matching on supported languages (e.g., returns "de" for "de-CH") + const prefLang = acceptLanguageParser.pick(supportedLanguages, acceptLanguage, { + loose: true, + }); + if (prefLang != null) { + // if a preferred language can be found use it + debug(`TD language negotiation through the Accept-Language header field of HTTP leads to "${prefLang}"`); + // TODO: reset titles and descriptions to only contain the preferred language + resetMultiLangThing(td, prefLang); } } } @@ -136,7 +139,7 @@ export default async function thingDescriptionRoute( } const thing = this.getThings().get(_params.thing); - if (!thing) { + if (thing == null) { res.writeHead(404); res.end(); return; diff --git a/packages/binding-http/test/http-client-basic-test.ts b/packages/binding-http/test/http-client-basic-test.ts index b7e3bc106..3f322131f 100644 --- a/packages/binding-http/test/http-client-basic-test.ts +++ b/packages/binding-http/test/http-client-basic-test.ts @@ -33,7 +33,7 @@ function mockService(req: express.Request, res: express.Response) { const auth = { login: "admin", password: "password" }; // change this // parse login and password from headers - const b64auth = (req.headers.authorization || "").split(" ")[1] || ""; + const b64auth = (req.headers.authorization ?? "").split(" ")[1] ?? ""; const [login, password] = Buffer.from(b64auth, "base64").toString().split(":"); // Verify login and password are set and correct diff --git a/packages/binding-http/test/http-client-test.ts b/packages/binding-http/test/http-client-test.ts index e7ad419a0..1bb7067a4 100644 --- a/packages/binding-http/test/http-client-test.ts +++ b/packages/binding-http/test/http-client-test.ts @@ -21,7 +21,6 @@ import { suite, test } from "@testdeck/mocha"; import chai, { expect, should } from "chai"; import * as http from "http"; -import { AddressInfo } from "net"; import { Content, DefaultContent, ContentSerdes, createLoggers, ProtocolServer } from "@node-wot/core"; @@ -96,12 +95,19 @@ class TestHttpServer implements ProtocolServer { /** returns server port number and indicates that server is running when larger than -1 */ public getPort(): number { - if (this.server.address() && typeof this.server.address() === "object") { - return (this.server.address()).port; - } else { - // includes address() typeof "string" case, which is only for unix sockets + const address = this.server?.address(); + + if (typeof address === "object") { + return address?.port ?? -1; + } + + const port = parseInt(address); + + if (isNaN(port)) { return -1; } + + return port; } public async expose(thing: unknown): Promise {} @@ -111,8 +117,10 @@ class TestHttpServer implements ProtocolServer { } public setTestVector(vector: TestVector) { - if (!vector.op) throw new Error("No vector op given"); - if (!vector.form["htv:methodName"]) { + if (vector.op == null) { + throw new Error("No vector op given"); + } + if (vector.form["htv:methodName"] == null) { // TODO also check all array entries switch (vector.op[0]) { case "readproperty": diff --git a/packages/binding-http/test/http-server-test.ts b/packages/binding-http/test/http-server-test.ts index ac5e11ed6..e62e6e8d1 100644 --- a/packages/binding-http/test/http-server-test.ts +++ b/packages/binding-http/test/http-server-test.ts @@ -52,7 +52,7 @@ class HttpServerTest { @test async "should use middleware if provided"() { const middleware: MiddlewareRequestHandler = async (req, res, next) => { - if (req.url?.endsWith("testMiddleware")) { + if (req.url?.endsWith("testMiddleware") ?? false) { res.statusCode = 401; res.end("Unauthorized"); } else { diff --git a/packages/binding-http/test/memory-model.ts b/packages/binding-http/test/memory-model.ts index c8ebd118b..8c0901cbc 100644 --- a/packages/binding-http/test/memory-model.ts +++ b/packages/binding-http/test/memory-model.ts @@ -92,11 +92,11 @@ export default class InMemoryModel implements ClientCredentialsModel, PasswordMo * Get access token. */ - async getAccessToken(bearerToken: string, callback: Callback): Promise { + async getAccessToken(bearerToken: string, callback?: Callback): Promise { const tokens = this.tokens.filter(function (token) { return token.accessToken === bearerToken; }); - if (callback) { + if (callback != null) { callback(null, tokens[0]); } diff --git a/packages/binding-http/test/oauth-token-validation-tests.ts b/packages/binding-http/test/oauth-token-validation-tests.ts index 3f426141d..642eedf14 100644 --- a/packages/binding-http/test/oauth-token-validation-tests.ts +++ b/packages/binding-http/test/oauth-token-validation-tests.ts @@ -75,13 +75,13 @@ describe("OAuth2.0 Validator tests", () => { }); introspectEndpoint.use((req, res) => { - if (req.method !== "POST" || !req.is("application/x-www-form-urlencoded")) { + if (req.method !== "POST" || req.is("application/x-www-form-urlencoded") == null) { return res.status(400).end(); } const token = req.body.token; - if (!token) { + if (token == null) { return res.status(400).end(); } switch (token) { From e522eb42056b0bce0259b38ba8a98f358e759bb4 Mon Sep 17 00:00:00 2001 From: Cristiano Aguzzi Date: Wed, 27 Sep 2023 16:22:50 +0200 Subject: [PATCH 02/19] Update packages/binding-http/src/oauth-token-validation.ts Co-authored-by: Jan Romann --- packages/binding-http/src/oauth-token-validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/binding-http/src/oauth-token-validation.ts b/packages/binding-http/src/oauth-token-validation.ts index 0b78129e6..c3ff5286b 100644 --- a/packages/binding-http/src/oauth-token-validation.ts +++ b/packages/binding-http/src/oauth-token-validation.ts @@ -60,7 +60,7 @@ function extractTokenFromRequest(request: http.IncomingMessage) { const url = new URL(request.url ?? "", `http://${request.headers.host}`); const queryToken = url.searchParams.get("access_token"); - if (headerToken == null && queryToken == null) { + if (headerToken != null && queryToken != null) { throw new Error("Invalid request: only one authentication method is allowed"); } From c77a9fe6b5c55e1c0c963721af00b8254a8d8e65 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:09:00 +0200 Subject: [PATCH 03/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index e42b8dc96..0f0151e1c 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -252,14 +252,19 @@ export default class HttpServer implements ProtocolServer { /** returns server port number and indicates that server is running when larger than -1 */ public getPort(): number { - const address: AddressInfo | string | null = this.server?.address?.(); + const address = this.server?.address(); if (typeof address === "object") { return address?.port ?? -1; - } else { - // includes address() typeof "string" case, which is only for unix sockets + } + + const port = parseInt(address); + + if (isNaN(port)) { return -1; } + + return port; } public async expose(thing: ExposedThing, tdTemplate: WoT.ExposedThingInit = {}): Promise { From da707d7de10a4dd54a82352fd9e0b7feb71081ed Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:12:57 +0200 Subject: [PATCH 04/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 0f0151e1c..19f5988d0 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -45,7 +45,6 @@ import actionRoute from "./routes/action"; import eventRoute from "./routes/event"; import propertiesRoute from "./routes/properties"; import propertyObserveRoute from "./routes/property-observe"; -import { AddressInfo } from "net"; const { debug, info, warn, error } = createLoggers("binding-http", "http-server"); From 58e936afa5d3c4c4b6e6c0fb26804fc2640bf616 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:27:40 +0200 Subject: [PATCH 05/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions Co-authored-by: Cristiano Aguzzi --- packages/binding-http/src/http-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 19f5988d0..71c3df26b 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -378,13 +378,13 @@ export default class HttpServer implements ProtocolServer { form, (tdTemplate.properties?.[propertyName] ?? {}) as PropertyElement ); - if (thing.properties[propertyName].readOnly === true) { + if (thing.properties[propertyName].readOnly ?? false) { form.op = ["readproperty"]; const hform: HttpForm = form; if (hform["htv:methodName"] === undefined) { hform["htv:methodName"] = "GET"; } - } else if (thing.properties[propertyName].writeOnly === true) { + } else if (thing.properties[propertyName].writeOnly ?? false) { form.op = ["writeproperty"]; const hform: HttpForm = form; if (hform["htv:methodName"] === undefined) { From cef1203d3d6f45bf489e6447981e2e0ff24a9724 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:31:17 +0200 Subject: [PATCH 06/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 4 +-- packages/binding-http/src/routes/property.ts | 27 ++++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 71c3df26b..503f0accd 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -337,10 +337,10 @@ export default class HttpServer implements ProtocolServer { let anyProperties = false; for (const propertyName in thing.properties) { anyProperties = true; - if (thing.properties[propertyName]?.readOnly !== true) { + if (thing.properties[propertyName]?.readOnly ?? false) { allReadOnly = false; } - if (thing.properties[propertyName].writeOnly !== true) { + if (thing.properties[propertyName].writeOnly ?? false) { allWriteOnly = false; } } diff --git a/packages/binding-http/src/routes/property.ts b/packages/binding-http/src/routes/property.ts index 6d808b87d..b121ff23f 100644 --- a/packages/binding-http/src/routes/property.ts +++ b/packages/binding-http/src/routes/property.ts @@ -106,21 +106,22 @@ export default async function propertyRoute( res.end(message); } } else if (req.method === "PUT") { - if (property.readOnly !== true) { - try { - await thing.handleWriteProperty(_params.property, new Content(contentType, req), options); + if (property.readOnly ?? false) { + respondUnallowedMethod(req, res, "GET, PUT"); + return; + } - res.writeHead(204); - res.end("Changed"); - } catch (err) { - const message = err instanceof Error ? err.message : JSON.stringify(err); + try { + await thing.handleWriteProperty(_params.property, new Content(contentType, req), options); - error(`HttpServer on port ${this.getPort()} got internal error on invoke '${req.url}': ${message}`); - res.writeHead(500); - res.end(message); - } - } else { - respondUnallowedMethod(req, res, "GET, PUT"); + res.writeHead(204); + res.end("Changed"); + } catch (err) { + const message = err instanceof Error ? err.message : JSON.stringify(err); + + error(`HttpServer on port ${this.getPort()} got internal error on invoke '${req.url}': ${message}`); + res.writeHead(500); + res.end(message); } // resource found and response sent } else { From fd0a8d059be5b844f44109b49ee4320fff1d5885 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:35:05 +0200 Subject: [PATCH 07/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/codecs/tuya-codec.ts | 4 +++- packages/binding-http/src/credential.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/binding-http/src/codecs/tuya-codec.ts b/packages/binding-http/src/codecs/tuya-codec.ts index 3b0f54e52..82d4eaedd 100644 --- a/packages/binding-http/src/codecs/tuya-codec.ts +++ b/packages/binding-http/src/codecs/tuya-codec.ts @@ -32,7 +32,9 @@ export default class HttpTuyaCodec implements ContentCodec { bytesToValue(bytes: Buffer, schema: TD.DataSchema, parameters: { [key: string]: string }): DataSchemaValue { const parsedBody: TuyaOutput = JSON.parse(bytes.toString()); - if (parsedBody.success !== true) { + const success = parsedBody.success ?? false; + + if (!success) { throw new Error(parsedBody.msg != null ? parsedBody.msg : JSON.stringify(parsedBody)); } diff --git a/packages/binding-http/src/credential.ts b/packages/binding-http/src/credential.ts index a40ddad76..62841942b 100644 --- a/packages/binding-http/src/credential.ts +++ b/packages/binding-http/src/credential.ts @@ -214,7 +214,9 @@ export class TuyaCustomBearer extends Credential { url = `${this.baseUri}/token/${this.refreshToken}`; } const data: TokenResponse = await (await fetch(url, request)).json(); - if (data.success === true) { + const success = data.success ?? false; + + if (success) { this.token = data.result?.access_token; this.refreshToken = data.result?.refresh_token; From 291f3ab8eec7e52038969df1469835ef5d6af066 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:40:09 +0200 Subject: [PATCH 08/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 503f0accd..e09dc2bac 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -335,12 +335,12 @@ export default class HttpServer implements ProtocolServer { let allReadOnly = true; let allWriteOnly = true; let anyProperties = false; - for (const propertyName in thing.properties) { + for (const property of Object.values(thing.properties)) { anyProperties = true; - if (thing.properties[propertyName]?.readOnly ?? false) { + if (property.readOnly ?? false) { allReadOnly = false; } - if (thing.properties[propertyName].writeOnly ?? false) { + if (property.writeOnly ?? false) { allWriteOnly = false; } } From 9790543ad54660a44c8c54a03fd3eb50e41a62d4 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:45:45 +0200 Subject: [PATCH 09/19] fiuxp! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 36 +++++++++++------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index e09dc2bac..8fff4e480 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -332,11 +332,12 @@ export default class HttpServer implements ProtocolServer { public addEndpoint(thing: ExposedThing, tdTemplate: WoT.ExposedThingInit, base: string): void { for (const type of ContentSerdes.get().getOfferedMediaTypes()) { + const properties = Object.values(thing.properties); + let allReadOnly = true; let allWriteOnly = true; - let anyProperties = false; - for (const property of Object.values(thing.properties)) { - anyProperties = true; + + for (const property of properties) { if (property.readOnly ?? false) { allReadOnly = false; } @@ -344,7 +345,8 @@ export default class HttpServer implements ProtocolServer { allWriteOnly = false; } } - if (anyProperties) { + + if (properties.length > 0) { const href = base + "/" + this.PROPERTY_DIR; const form = new TD.Form(href, type); if (allReadOnly && !allWriteOnly) { @@ -366,10 +368,10 @@ export default class HttpServer implements ProtocolServer { this.addUrlRewriteEndpoints(form, thing.forms); } - for (const propertyName in thing.properties) { + for (const [propertyName, property] of Object.entries(thing.properties)) { const propertyNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( propertyName, - thing.properties[propertyName].uriVariables, + property.uriVariables, thing.uriVariables ); const href = base + "/" + this.PROPERTY_DIR + "/" + propertyNamePattern; @@ -378,28 +380,24 @@ export default class HttpServer implements ProtocolServer { form, (tdTemplate.properties?.[propertyName] ?? {}) as PropertyElement ); - if (thing.properties[propertyName].readOnly ?? false) { + if (property.readOnly ?? false) { form.op = ["readproperty"]; const hform: HttpForm = form; - if (hform["htv:methodName"] === undefined) { - hform["htv:methodName"] = "GET"; - } - } else if (thing.properties[propertyName].writeOnly ?? false) { + hform["htv:methodName"] ??= "GET"; + } else if (property.writeOnly ?? false) { form.op = ["writeproperty"]; const hform: HttpForm = form; - if (hform["htv:methodName"] === undefined) { - hform["htv:methodName"] = "PUT"; - } + hform["htv:methodName"] ??= "PUT"; } else { form.op = ["readproperty", "writeproperty"]; } - thing.properties[propertyName].forms.push(form); + property.forms.push(form); debug(`HttpServer on port ${this.getPort()} assigns '${href}' to Property '${propertyName}'`); - this.addUrlRewriteEndpoints(form, thing.properties[propertyName].forms); + this.addUrlRewriteEndpoints(form, property.forms); // if property is observable add an additional form with a observable href - if (thing.properties[propertyName].observable === true) { + if (property.observable === true) { const href = base + "/" + @@ -411,11 +409,11 @@ export default class HttpServer implements ProtocolServer { const form = new TD.Form(href, type); form.op = ["observeproperty", "unobserveproperty"]; form.subprotocol = "longpoll"; - thing.properties[propertyName].forms.push(form); + property.forms.push(form); debug( `HttpServer on port ${this.getPort()} assigns '${href}' to observable Property '${propertyName}'` ); - this.addUrlRewriteEndpoints(form, thing.properties[propertyName].forms); + this.addUrlRewriteEndpoints(form, property.forms); } } From 85a84f6d97cc91e87b489acc6d022dcaff067b83 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 19:47:53 +0200 Subject: [PATCH 10/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 8fff4e480..20a7b3c32 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -417,10 +417,10 @@ export default class HttpServer implements ProtocolServer { } } - for (const actionName in thing.actions) { + for (const [actionName, action] of Object.entries(thing.actions)) { const actionNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( actionName, - thing.actions[actionName].uriVariables, + action.uriVariables, thing.uriVariables ); const href = base + "/" + this.ACTION_DIR + "/" + actionNamePattern; @@ -431,18 +431,17 @@ export default class HttpServer implements ProtocolServer { ); form.op = ["invokeaction"]; const hform: HttpForm = form; - if (hform["htv:methodName"] === undefined) { - hform["htv:methodName"] = "POST"; - } - thing.actions[actionName].forms.push(form); + + hform["htv:methodName"] ??= "POST"; + action.forms.push(form); debug(`HttpServer on port ${this.getPort()} assigns '${href}' to Action '${actionName}'`); - this.addUrlRewriteEndpoints(form, thing.actions[actionName].forms); + this.addUrlRewriteEndpoints(form, action.forms); } - for (const eventName in thing.events) { + for (const [eventName, event] of Object.entries(thing.events)) { const eventNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( eventName, - thing.events[eventName].uriVariables, + event.uriVariables, thing.uriVariables ); const href = base + "/" + this.EVENT_DIR + "/" + eventNamePattern; @@ -453,9 +452,9 @@ export default class HttpServer implements ProtocolServer { ); form.subprotocol = "longpoll"; form.op = ["subscribeevent", "unsubscribeevent"]; - thing.events[eventName].forms.push(form); + event.forms.push(form); debug(`HttpServer on port ${this.getPort()} assigns '${href}' to Event '${eventName}'`); - this.addUrlRewriteEndpoints(form, thing.events[eventName].forms); + this.addUrlRewriteEndpoints(form, event.forms); } } } From c7356d39b4805386e8ba27c7d80da05575f28f7a Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 20:11:19 +0200 Subject: [PATCH 11/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 20a7b3c32..ed449d970 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -318,8 +318,7 @@ export default class HttpServer implements ProtocolServer { private addUrlRewriteEndpoints(form: TD.Form, forms: Array): void { if (this.urlRewrite != null) { - for (const inUri in this.urlRewrite) { - const toUri = this.urlRewrite[inUri]; + for (const [inUri, toUri] of Object.entries(this.urlRewrite)) { if (form.href.endsWith(toUri)) { const form2 = structuredClone(form); form2.href = form2.href.substring(0, form.href.lastIndexOf(toUri)) + inUri; From 414eb1f9ebc3f713494a9b5ae1ec1715d9e4a5c3 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 20:40:55 +0200 Subject: [PATCH 12/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index ed449d970..af79a500e 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -337,10 +337,13 @@ export default class HttpServer implements ProtocolServer { let allWriteOnly = true; for (const property of properties) { - if (property.readOnly ?? false) { + const readOnly = property.readOnly ?? false; + if (readOnly) { allReadOnly = false; } - if (property.writeOnly ?? false) { + + const writeOnly = property.writeOnly ?? false; + if (writeOnly) { allWriteOnly = false; } } From e2d09b8436830754e28eae29f3e530be1297077c Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 30 Sep 2023 20:49:49 +0200 Subject: [PATCH 13/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index af79a500e..29f0d6aba 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -337,12 +337,12 @@ export default class HttpServer implements ProtocolServer { let allWriteOnly = true; for (const property of properties) { - const readOnly = property.readOnly ?? false; + const readOnly: boolean = property.readOnly ?? false; if (readOnly) { allReadOnly = false; } - const writeOnly = property.writeOnly ?? false; + const writeOnly: boolean = property.writeOnly ?? false; if (writeOnly) { allWriteOnly = false; } From 123503136161b1eead5bda486bc7ff53e362d532 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 3 Oct 2023 09:40:01 +0200 Subject: [PATCH 14/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 29f0d6aba..3a5b34270 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -382,11 +382,15 @@ export default class HttpServer implements ProtocolServer { form, (tdTemplate.properties?.[propertyName] ?? {}) as PropertyElement ); - if (property.readOnly ?? false) { + + const readOnly: boolean = property.readOnly ?? false; + const writeOnly: boolean = property.writeOnly ?? false; + + if (readOnly) { form.op = ["readproperty"]; const hform: HttpForm = form; hform["htv:methodName"] ??= "GET"; - } else if (property.writeOnly ?? false) { + } else if (writeOnly) { form.op = ["writeproperty"]; const hform: HttpForm = form; hform["htv:methodName"] ??= "PUT"; From 3a76e8623412b2ab8edd8caf4de5230ac605b5d3 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 3 Oct 2023 09:46:43 +0200 Subject: [PATCH 15/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/routes/property.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/binding-http/src/routes/property.ts b/packages/binding-http/src/routes/property.ts index b121ff23f..916bc6a9b 100644 --- a/packages/binding-http/src/routes/property.ts +++ b/packages/binding-http/src/routes/property.ts @@ -106,7 +106,8 @@ export default async function propertyRoute( res.end(message); } } else if (req.method === "PUT") { - if (property.readOnly ?? false) { + const readOnly: boolean = property.readOnly ?? false; + if (readOnly) { respondUnallowedMethod(req, res, "GET, PUT"); return; } From ea16ef472730540b47b0dcfc601160195a221d21 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 3 Oct 2023 09:59:02 +0200 Subject: [PATCH 16/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 3a5b34270..c6694bca8 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -529,6 +529,11 @@ export default class HttpServer implements ProtocolServer { } private fillSecurityScheme(thing: ExposedThing) { + const security = thing.security; + if (typeof security === "string") { + thing.security = [security]; + } + // User selected one security scheme if (thing.security.length > 0) { // multiple security schemes are deprecated we are not supporting them @@ -559,7 +564,8 @@ export default class HttpServer implements ProtocolServer { return; } - // The user let the servient choose the security scheme + // The security array is empty – the user lets the servient choose the + // security scheme. if (Object.keys(thing.securityDefinitions ?? {}).length === 0) { // We are using the first supported security scheme as default thing.securityDefinitions = { From 43c8d26676a1528265bb7300a493214d80d0cd7f Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 3 Oct 2023 11:48:11 +0200 Subject: [PATCH 17/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index c6694bca8..e5f9a4dea 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -529,11 +529,6 @@ export default class HttpServer implements ProtocolServer { } private fillSecurityScheme(thing: ExposedThing) { - const security = thing.security; - if (typeof security === "string") { - thing.security = [security]; - } - // User selected one security scheme if (thing.security.length > 0) { // multiple security schemes are deprecated we are not supporting them From 99c10828209b93b42d07f6257c18d930f22750aa Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 3 Oct 2023 11:55:52 +0200 Subject: [PATCH 18/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index e5f9a4dea..c1730b44d 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -338,12 +338,12 @@ export default class HttpServer implements ProtocolServer { for (const property of properties) { const readOnly: boolean = property.readOnly ?? false; - if (readOnly) { + if (!readOnly) { allReadOnly = false; } const writeOnly: boolean = property.writeOnly ?? false; - if (writeOnly) { + if (!writeOnly) { allWriteOnly = false; } } From 4624a03f3b518a15c036c72c6fa026365c457f69 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 12 Oct 2023 22:04:00 +0200 Subject: [PATCH 19/19] fixup! chore(binding-http): enable eslint/strict-boolean-expressions --- packages/binding-http/src/http-server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index c1730b44d..e2b88f97a 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -319,7 +319,8 @@ export default class HttpServer implements ProtocolServer { private addUrlRewriteEndpoints(form: TD.Form, forms: Array): void { if (this.urlRewrite != null) { for (const [inUri, toUri] of Object.entries(this.urlRewrite)) { - if (form.href.endsWith(toUri)) { + const endsWithToUri: boolean = form.href.endsWith(toUri); + if (endsWithToUri) { const form2 = structuredClone(form); form2.href = form2.href.substring(0, form.href.lastIndexOf(toUri)) + inUri; forms.push(form2);