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..82d4eaedd 100644 --- a/packages/binding-http/src/codecs/tuya-codec.ts +++ b/packages/binding-http/src/codecs/tuya-codec.ts @@ -17,17 +17,30 @@ 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()); + const success = parsedBody.success ?? false; + + if (!success) { + 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..62841942b 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,51 @@ 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(); + const success = data.success ?? false; + + if (success) { + 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..e2b88f97a 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, @@ -62,14 +60,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 +78,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 +121,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 +176,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,12 +251,19 @@ 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; - } 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: ExposedThing, tdTemplate: WoT.ExposedThingInit = {}): Promise { @@ -312,10 +317,10 @@ export default class HttpServer implements ProtocolServer { } private addUrlRewriteEndpoints(form: TD.Form, forms: Array): void { - if (this.urlRewrite) { - for (const inUri in this.urlRewrite) { - const toUri = this.urlRewrite[inUri]; - if (form.href.endsWith(toUri)) { + if (this.urlRewrite != null) { + for (const [inUri, toUri] of Object.entries(this.urlRewrite)) { + 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); @@ -327,19 +332,24 @@ 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 propertyName in thing.properties) { - anyProperties = true; - if (!thing.properties[propertyName].readOnly) { + + for (const property of properties) { + const readOnly: boolean = property.readOnly ?? false; + if (!readOnly) { allReadOnly = false; } - if (!thing.properties[propertyName].writeOnly) { + + const writeOnly: boolean = property.writeOnly ?? false; + if (!writeOnly) { allWriteOnly = false; } } - if (anyProperties) { + + if (properties.length > 0) { const href = base + "/" + this.PROPERTY_DIR; const form = new TD.Form(href, type); if (allReadOnly && !allWriteOnly) { @@ -354,17 +364,17 @@ export default class HttpServer implements ProtocolServer { "writemultipleproperties", ]; } - if (!thing.forms) { + if (thing.forms == null) { thing.forms = []; } thing.forms.push(form); 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; @@ -373,28 +383,28 @@ export default class HttpServer implements ProtocolServer { form, (tdTemplate.properties?.[propertyName] ?? {}) as PropertyElement ); - if (thing.properties[propertyName].readOnly) { + + const readOnly: boolean = property.readOnly ?? false; + const writeOnly: boolean = property.writeOnly ?? false; + + if (readOnly) { form.op = ["readproperty"]; const hform: HttpForm = form; - if (hform["htv:methodName"] === undefined) { - hform["htv:methodName"] = "GET"; - } - } else if (thing.properties[propertyName].writeOnly) { + hform["htv:methodName"] ??= "GET"; + } else if (writeOnly) { 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) { + if (property.observable === true) { const href = base + "/" + @@ -406,18 +416,18 @@ 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); } } - 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; @@ -428,18 +438,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; @@ -450,9 +459,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); } } } @@ -475,7 +484,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 +519,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 +531,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 +550,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(", ") @@ -551,8 +560,9 @@ export default class HttpServer implements ProtocolServer { return; } - // The user let the servient choose the security scheme - if (!thing.securityDefinitions || Object.keys(thing.securityDefinitions).length === 0) { + // 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 = { [this.supportedSecuritySchemes[0]]: { scheme: this.supportedSecuritySchemes[0] }, @@ -561,35 +571,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..c3ff5286b 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..916bc6a9b 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,21 +106,23 @@ export default async function propertyRoute( res.end(message); } } else if (req.method === "PUT") { - if (!property.readOnly) { - try { - await thing.handleWriteProperty(_params.property, new Content(contentType, req), options); + const readOnly: boolean = property.readOnly ?? false; + if (readOnly) { + 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 { 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) {