Skip to content

Commit

Permalink
Merge pull request #1048 from relu91/fix-tsconfig-http
Browse files Browse the repository at this point in the history
Strict checking for http-binding package
  • Loading branch information
relu91 authored Aug 3, 2023
2 parents 56875c4 + 5469a3a commit db4b2c1
Show file tree
Hide file tree
Showing 23 changed files with 214 additions and 184 deletions.
31 changes: 12 additions & 19 deletions packages/binding-http/src/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface BasicCredentialConfiguration {
export class BasicCredential extends Credential {
private readonly username: string;
private readonly password: string;
private readonly options: BasicSecurityScheme;
private readonly options?: BasicSecurityScheme;
/**
*
*/
Expand Down Expand Up @@ -114,7 +114,7 @@ export class BasicKeyCredential extends Credential {

export class OAuthCredential extends Credential {
private token: Token | Promise<Token>;
private readonly refresh: () => Promise<Token>;
private readonly refresh?: () => Promise<Token>;

/**
*
Expand Down Expand Up @@ -167,9 +167,9 @@ export class TuyaCustomBearer extends Credential {
protected key: string;
protected secret: string;
protected baseUri: string;
protected token: string;
protected refreshToken: string;
protected expireTime: Date;
protected token?: string;
protected refreshToken?: string;
protected expireTime?: Date;

constructor(credentials: TuyaCustomBearerCredentialConfiguration, scheme: TuyaCustomBearerSecurityScheme) {
super();
Expand All @@ -187,11 +187,11 @@ export class TuyaCustomBearer extends Credential {
const body = request.body ? request.body.read().toString() : "";
const headers = this.getHeaders(true, request.headers.raw(), body, url, request.method);
Object.assign(headers, request.headers.raw());
return new Request(url, { method: request.method, body: body !== "" ? body : null, headers: headers });
return new Request(url, { method: request.method, body: body !== "" ? body : undefined, headers: headers });
}

protected async requestAndRefreshToken(refresh: boolean): Promise<void> {
const headers = this.getHeaders(false, {}, "", null, null);
const headers = this.getHeaders(false, {}, "");
const request = {
headers: headers,
method: "GET",
Expand All @@ -210,11 +210,11 @@ export class TuyaCustomBearer extends Credential {
}
}

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}`, "") : null;
const sign = this.requestSign(NormalRequest, requestTime, body, headers, _url, method);
const _url = url ? url.replace(`${replaceUri}`, "") : undefined;
const sign = this.requestSign(NormalRequest, requestTime, body, _url, method);
return {
t: requestTime,
client_id: this.key,
Expand All @@ -224,21 +224,14 @@ export class TuyaCustomBearer extends Credential {
};
}

private requestSign(
NormalRequest: boolean,
requestTime: string,
body: string,
headers: unknown,
path: string,
method: string
): string {
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";
if (NormalRequest) {
useToken = this.token;
useToken = this.token ?? "";
const pathQuery = queryString.parse(path.split("?")[1]);
let query: Record<string, string> = {};
query = Object.assign(query, pathQuery);
Expand Down
4 changes: 2 additions & 2 deletions packages/binding-http/src/http-client-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ const { debug, warn } = createLoggers("binding-http", "http-client-factory");

export default class HttpClientFactory implements ProtocolClientFactory {
public readonly scheme: string = "http";
private config: HttpConfig = null;
private config: HttpConfig | null = null;
private oAuthManager: OAuthManager = new OAuthManager();

constructor(config: HttpConfig = null) {
constructor(config: HttpConfig | null = null) {
this.config = config;
}

Expand Down
19 changes: 11 additions & 8 deletions packages/binding-http/src/http-client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Subscription } from "rxjs/Subscription";
import * as TD from "@node-wot/td-tools";
// for Security definition

import { ProtocolClient, Content, ProtocolHelpers, createLoggers } from "@node-wot/core";
import { ProtocolClient, Content, ProtocolHelpers, createLoggers, ContentSerdes } from "@node-wot/core";
import { HttpForm, HttpHeader, HttpConfig, HTTPMethodName, TuyaCustomBearerSecurityScheme } from "./http";
import fetch, { Request, RequestInit, Response } from "node-fetch";
import { Buffer } from "buffer";
Expand All @@ -50,15 +50,15 @@ const { debug, warn, error } = createLoggers("binding-http", "http-client-impl")
export default class HttpClient implements ProtocolClient {
private readonly agent: http.Agent;
private readonly provider: "https" | "http";
private proxyRequest: Request = null;
private proxyRequest: Request | null = null;
private allowSelfSigned = false;
private oauth: OAuthManager;

private credential: Credential = null;
private credential: Credential | null = null;

private activeSubscriptions = new Map<string, InternalSubscription>();

constructor(config: HttpConfig = null, secure = false, oauthManager: OAuthManager = new OAuthManager()) {
constructor(config: HttpConfig | null = null, secure = false, oauthManager: OAuthManager = new OAuthManager()) {
// config proxy by client side (not from TD)
if (config !== null && config.proxy && config.proxy.href) {
this.proxyRequest = new Request(HttpClient.fixLocalhostName(config.proxy.href));
Expand Down Expand Up @@ -125,7 +125,7 @@ export default class HttpClient implements ProtocolClient {
// in browsers node-fetch uses the native fetch, which returns a ReadableStream
// not complaint with node. Therefore we have to force the conversion here.
const body = ProtocolHelpers.toNodeStream(result.body as Readable);
return new Content(result.headers.get("content-type"), body);
return new Content(result.headers.get("content-type") ?? ContentSerdes.DEFAULT, body);
}

public async writeResource(form: HttpForm, content: Content): Promise<void> {
Expand Down Expand Up @@ -162,6 +162,9 @@ export default class HttpClient implements ProtocolClient {
} else if (form.subprotocol === "sse") {
// server sent events
internalSubscription = new SSESubscription(form);
} else {
reject(new Error(`HttpClient does not support subprotocol ${form.subprotocol}`));
return;
}

internalSubscription
Expand Down Expand Up @@ -202,15 +205,15 @@ export default class HttpClient implements ProtocolClient {
// in browsers node-fetch uses the native fetch, which returns a ReadableStream
// not complaint with node. Therefore we have to force the conversion here.
const body = ProtocolHelpers.toNodeStream(result.body as Readable);
return new Content(result.headers.get("content-type"), body);
return new Content(result.headers.get("content-type") ?? ContentSerdes.DEFAULT, body);
}

public async unlinkResource(form: HttpForm): Promise<void> {
debug(`HttpClient (unlinkResource) ${form.href}`);
const internalSub = this.activeSubscriptions.get(form.href);

if (internalSub) {
this.activeSubscriptions.get(form.href).close();
internalSub.close();
} else {
warn(`HttpClient cannot unlink ${form.href} no subscription found`);
}
Expand Down Expand Up @@ -399,7 +402,7 @@ export default class HttpClient implements ProtocolClient {
}
}

private static isOAuthTokenExpired(result: Response, credential: Credential) {
private static isOAuthTokenExpired(result: Response, credential: Credential | null) {
return result.status === 401 && credential instanceof OAuthCredential;
}

Expand Down
46 changes: 27 additions & 19 deletions packages/binding-http/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,16 @@ export default class HttpServer implements ProtocolServer {
// 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<string, string> = undefined;
private readonly address?: string = undefined;
private readonly baseUri?: string = undefined;
private readonly urlRewrite?: Record<string, string> = undefined;
private readonly httpSecurityScheme: string = "NoSec"; // HTTP header compatible string
private readonly validOAuthClients: RegExp = /.*/g;
private readonly server: http.Server | https.Server = null;
private readonly middleware: MiddlewareRequestHandler = null;
private readonly server: http.Server | https.Server;
private readonly middleware: MiddlewareRequestHandler | null = null;
private readonly things: Map<string, ExposedThing> = new Map<string, ExposedThing>();
private servient: Servient = null;
private oAuthValidator: Validator;
private servient: Servient | null = null;
private oAuthValidator?: Validator = undefined;
private router: Router.Instance<Router.HTTPVersion.V1>;

constructor(config: HttpConfig = {}) {
Expand All @@ -88,11 +88,11 @@ export default class HttpServer implements ProtocolServer {
.map((envVar) => {
return { key: envVar, value: process.env[envVar] };
})
.find((envObj) => envObj.value != null);
.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 = +environmentObj.value;
this.port = parseInt(environmentObj.value);
}

if (config.address !== undefined) {
Expand All @@ -115,7 +115,7 @@ export default class HttpServer implements ProtocolServer {
const pathname = req.url;
if (config.urlRewrite) {
const entryUrl = pathname;
const internalUrl = config.urlRewrite[entryUrl];
const internalUrl = config.urlRewrite[entryUrl ?? "/"];
if (internalUrl) {
req.url = internalUrl;
router.lookup(req, res, this);
Expand Down Expand Up @@ -301,7 +301,7 @@ export default class HttpServer implements ProtocolServer {
}
}

public async expose(thing: ExposedThing, tdTemplate?: WoT.ExposedThingInit): Promise<void> {
public async expose(thing: ExposedThing, tdTemplate: WoT.ExposedThingInit = {}): Promise<void> {
let urlPath = slugify(thing.title, { lower: true });

if (this.things.has(urlPath)) {
Expand Down Expand Up @@ -336,7 +336,7 @@ export default class HttpServer implements ProtocolServer {
public destroy(thingId: string): Promise<boolean> {
debug(`HttpServer on port ${this.getPort()} destroying thingId '${thingId}'`);
return new Promise<boolean>((resolve, reject) => {
let removedThing: ExposedThing;
let removedThing: ExposedThing | undefined;
for (const name of Array.from(this.things.keys())) {
const expThing = this.things.get(name);
if (expThing?.id === thingId) {
Expand Down Expand Up @@ -413,7 +413,7 @@ export default class HttpServer implements ProtocolServer {
const form = new TD.Form(href, type);
ProtocolHelpers.updatePropertyFormWithTemplate(
form,
(tdTemplate?.properties[propertyName] ?? {}) as PropertyElement
(tdTemplate.properties?.[propertyName] ?? {}) as PropertyElement
);
if (thing.properties[propertyName].readOnly) {
form.op = ["readproperty"];
Expand Down Expand Up @@ -466,7 +466,7 @@ export default class HttpServer implements ProtocolServer {
const form = new TD.Form(href, type);
ProtocolHelpers.updateActionFormWithTemplate(
form,
(tdTemplate?.actions[actionName] ?? {}) as ActionElement
(tdTemplate.actions?.[actionName] ?? {}) as ActionElement
);
form.op = ["invokeaction"];
const hform: HttpForm = form;
Expand All @@ -488,7 +488,7 @@ export default class HttpServer implements ProtocolServer {
const form = new TD.Form(href, type);
ProtocolHelpers.updateEventFormWithTemplate(
form,
(tdTemplate?.events[eventName] ?? {}) as EventElement
(tdTemplate.events?.[eventName] ?? {}) as EventElement
);
form.subprotocol = "longpoll";
form.op = ["subscribeevent", "unsubscribeevent"];
Expand All @@ -502,6 +502,10 @@ export default class HttpServer implements ProtocolServer {
public async checkCredentials(thing: ExposedThing, req: http.IncomingMessage): Promise<boolean> {
debug(`HttpServer on port ${this.getPort()} checking credentials for '${thing.id}'`);

if (this.servient === null) {
throw new Error("Servient not set");
}

const creds = this.servient.getCredentials(thing.id);

switch (this.httpSecurityScheme) {
Expand All @@ -526,13 +530,17 @@ export default class HttpServer implements ProtocolServer {
const scopes = Helpers.toStringArray(oAuthScheme.scopes); // validate call requires array of strings while oAuthScheme.scopes can be string or array of strings
let valid = false;

if (!this.oAuthValidator) {
throw new Error("OAuth validator not set. Cannot validate request.");
}

try {
valid = await this.oAuthValidator.validate(req, scopes, this.validOAuthClients);
} catch (error) {
} catch (err) {
// TODO: should we answer differently to the client if something went wrong?
error("OAuth authorization error; sending unauthorized response error");
error("this was possibly caused by a misconfiguration of the server");
error(`${error}`);
error(`${err}`);
}

return valid;
Expand Down Expand Up @@ -583,7 +591,7 @@ export default class HttpServer implements ProtocolServer {
}

private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
const requestUri = new URL(req.url, `${this.scheme}://${req.headers.host}`);
const requestUri = new URL(req.url ?? "", `${this.scheme}://${req.headers.host}`);

debug(
`HttpServer on port ${this.getPort()} received '${req.method} ${
Expand All @@ -606,7 +614,7 @@ export default class HttpServer implements ProtocolServer {
res.setHeader("Access-Control-Allow-Origin", "*");
}

const contentTypeHeader: string | string[] = req.headers["content-type"];
const contentTypeHeader = req.headers["content-type"];
let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;

if (req.method === "PUT" || req.method === "POST") {
Expand Down
4 changes: 2 additions & 2 deletions packages/binding-http/src/https-client-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ const { debug, warn } = createLoggers("binding-http", "https-client-factory");

export default class HttpsClientFactory implements ProtocolClientFactory {
public readonly scheme: string = "https";
private config: HttpConfig = null;
private config: HttpConfig | null = null;

constructor(config: HttpConfig = null) {
constructor(config: HttpConfig | null = null) {
this.config = config;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/binding-http/src/oauth-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function createRequestFunction(rejectUnauthorized: boolean) {
});
response.on("end", () => {
resolve({
status: response.statusCode,
status: response.statusCode ?? 500, // we are not expecting undefined status codes
body: body.toString(),
});
});
Expand Down
7 changes: 3 additions & 4 deletions packages/binding-http/src/oauth-token-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export abstract class Validator {

function extractTokenFromRequest(request: http.IncomingMessage) {
const headerToken = request.headers.authorization;
const url = new URL(request.url, `http://${request.headers.host}`);
const url = new URL(request.url ?? "", `http://${request.headers.host}`);
const queryToken = url.searchParams.get("access_token");

if (!headerToken && !queryToken) {
Expand All @@ -68,7 +68,7 @@ function extractTokenFromRequest(request: http.IncomingMessage) {
return queryToken;
}

const matches = headerToken.match(/Bearer\s(\S+)/);
const matches = headerToken?.match(/Bearer\s(\S+)/);

if (!matches) {
throw new Error("Invalid request: malformed authorization header");
Expand Down Expand Up @@ -112,8 +112,7 @@ export class EndpointValidator extends Validator {
throw new Error("Introspection endpoint error: " + response.statusText);
}

let contentType = response.headers.get("content-type");
contentType = response.headers.get("content-type")?.split(";")[0];
const contentType = response.headers.get("content-type")?.split(";")[0];

if (contentType !== "application/json") {
throw new Error(
Expand Down
8 changes: 5 additions & 3 deletions packages/binding-http/src/routes/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default async function actionRoute(
return;
}

const contentTypeHeader: string | string[] = req.headers["content-type"];
const contentTypeHeader = req.headers["content-type"];
let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
try {
contentType = validOrDefaultRequestContentType(req, res, contentType);
Expand Down Expand Up @@ -87,9 +87,11 @@ export default async function actionRoute(
res.end();
}
} catch (err) {
error(`HttpServer on port ${this.getPort()} got internal error on invoke '${req.url}': ${err.message}`);
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(err.message);
res.end(message);
}
} else {
// may have been OPTIONS that failed the credentials check
Expand Down
Loading

0 comments on commit db4b2c1

Please sign in to comment.