Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Strict checking for http-binding package #1048

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading