Skip to content

Commit 62d95ea

Browse files
authored
Adding CustomHosts options (#453)
* Adding custom hosts options * Adding unit tests * adding custom host check in telemetry handler * Update to comments * Function to check valid custom host * unit test for invalid hostname
1 parent b763af5 commit 62d95ea

File tree

9 files changed

+167
-8
lines changed

9 files changed

+167
-8
lines changed

src/GraphRequest.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,15 +283,15 @@ export class GraphRequest {
283283
*/
284284
private parseQueryParamenterString(queryParameter: string): void {
285285
/* The query key-value pair must be split on the first equals sign to avoid errors in parsing nested query parameters.
286-
Example-> "/me?$expand=home($select=city)" */
286+
Example-> "/me?$expand=home($select=city)" */
287287
if (this.isValidQueryKeyValuePair(queryParameter)) {
288288
const indexOfFirstEquals = queryParameter.indexOf("=");
289289
const paramKey = queryParameter.substring(0, indexOfFirstEquals);
290290
const paramValue = queryParameter.substring(indexOfFirstEquals + 1);
291291
this.setURLComponentsQueryParamater(paramKey, paramValue);
292292
} else {
293293
/* Push values which are not of key-value structure.
294-
Example-> Handle an invalid input->.query(test), .query($select($select=name)) and let the Graph API respond with the error in the URL*/
294+
Example-> Handle an invalid input->.query(test), .query($select($select=name)) and let the Graph API respond with the error in the URL*/
295295
this.urlComponents.otherURLQueryOptions.push(queryParameter);
296296
}
297297
}
@@ -367,12 +367,15 @@ export class GraphRequest {
367367
let rawResponse: Response;
368368
const middlewareControl = new MiddlewareControl(this._middlewareOptions);
369369
this.updateRequestOptions(options);
370+
const customHosts = this.config?.customHosts;
370371
try {
371372
const context: Context = await this.httpClient.sendRequest({
372373
request,
373374
options,
374375
middlewareControl,
376+
customHosts,
375377
});
378+
376379
rawResponse = context.response;
377380
const response: any = await GraphResponseHandler.getResponse(rawResponse, this._responseType, callback);
378381
return response;

src/GraphRequestUtil.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* @module GraphRequestUtil
1010
*/
1111
import { GRAPH_URLS } from "./Constants";
12+
import { GraphClientError } from "./GraphClientError";
1213
/**
1314
* To hold list of OData query params
1415
*/
@@ -65,6 +66,27 @@ export const serializeContent = (content: any): any => {
6566
* @returns {boolean} - Returns true if the url is a Graph URL
6667
*/
6768
export const isGraphURL = (url: string): boolean => {
69+
return isValidEndpoint(url);
70+
};
71+
72+
/**
73+
* Checks if the url is for one of the custom hosts provided during client initialization
74+
* @param {string} url - The url to be verified
75+
* @param {Set} customHosts - The url to be verified
76+
* @returns {boolean} - Returns true if the url is a for a custom host
77+
*/
78+
export const isCustomHost = (url: string, customHosts: Set<string>): boolean => {
79+
customHosts.forEach((host) => isCustomHostValid(host));
80+
return isValidEndpoint(url, customHosts);
81+
};
82+
83+
/**
84+
* Checks if the url is for one of the provided hosts.
85+
* @param {string} url - The url to be verified
86+
* @param {Set<string>} allowedHosts - A set of hosts.
87+
* @returns {boolean} - Returns true is for one of the provided endpoints.
88+
*/
89+
const isValidEndpoint = (url: string, allowedHosts: Set<string> = GRAPH_URLS): boolean => {
6890
// Valid Graph URL pattern - https://graph.microsoft.com/{version}/{resource}?{query-parameters}
6991
// Valid Graph URL example - https://graph.microsoft.com/v1.0/
7092
url = url.toLowerCase();
@@ -79,13 +101,23 @@ export const isGraphURL = (url: string): boolean => {
79101
if (endOfHostStrPos !== -1) {
80102
if (startofPortNoPos !== -1 && startofPortNoPos < endOfHostStrPos) {
81103
hostName = url.substring(0, startofPortNoPos);
82-
return GRAPH_URLS.has(hostName);
104+
return allowedHosts.has(hostName);
83105
}
84106
// Parse out the host
85107
hostName = url.substring(0, endOfHostStrPos);
86-
return GRAPH_URLS.has(hostName);
108+
return allowedHosts.has(hostName);
87109
}
88110
}
89111

90112
return false;
91113
};
114+
115+
/**
116+
* Throws error if the string is not a valid host/hostname and contains other url parts.
117+
* @param {string} url - The host to be verified
118+
*/
119+
const isCustomHostValid = (host: string) => {
120+
if (host.indexOf("/") !== -1) {
121+
throw new GraphClientError("Please add only hosts or hostnames to the CustomHosts config. If the url is `http://example.com:3000/`, host is `example:3000`");
122+
}
123+
};

src/IClientOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ import { Middleware } from "./middleware/IMiddleware";
1818
* @property {string} [defaultVersion] - The default version that needs to be used while making graph api request
1919
* @property {FetchOptions} [fetchOptions] - The options for fetch request
2020
* @property {Middleware| Middleware[]} [middleware] - The first middleware of the middleware chain or an array of the Middleware handlers
21+
* @property {Set<string>}[customHosts] - A set of custom host names. Should contain hostnames only.
2122
*/
23+
2224
export interface ClientOptions {
2325
authProvider?: AuthenticationProvider;
2426
baseUrl?: string;
2527
debugLogging?: boolean;
2628
defaultVersion?: string;
2729
fetchOptions?: FetchOptions;
2830
middleware?: Middleware | Middleware[];
31+
/**
32+
* Example - If URL is "https://test_host/v1.0", then set property "customHosts" as "customHosts: Set<string>(["test_host"])"
33+
*/
34+
customHosts?: Set<string>;
2935
}

src/IContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ import { MiddlewareControl } from "./middleware/MiddlewareControl";
1414
* @property {FetchOptions} [options] - The options for the request
1515
* @property {Response} [response] - The response content
1616
* @property {MiddlewareControl} [middlewareControl] - The options for the middleware chain
17+
* @property {Set<string>}[customHosts] - A set of custom host names. Should contain hostnames only.
18+
*
1719
*/
1820

1921
export interface Context {
2022
request: RequestInfo;
2123
options?: FetchOptions;
2224
response?: Response;
2325
middlewareControl?: MiddlewareControl;
26+
/**
27+
* Example - If URL is "https://test_host", then set property "customHosts" as "customHosts: Set<string>(["test_host"])"
28+
*/
29+
customHosts?: Set<string>;
2430
}

src/IOptions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ import { FetchOptions } from "./IFetchOptions";
1616
* @property {boolean} [debugLogging] - The boolean to enable/disable debug logging
1717
* @property {string} [defaultVersion] - The default version that needs to be used while making graph api request
1818
* @property {FetchOptions} [fetchOptions] - The options for fetch request
19+
* @property {Set<string>}[customHosts] - A set of custom host names. Should contain hostnames only.
1920
*/
2021
export interface Options {
2122
authProvider: AuthProvider;
2223
baseUrl?: string;
2324
debugLogging?: boolean;
2425
defaultVersion?: string;
2526
fetchOptions?: FetchOptions;
27+
/**
28+
* Example - If URL is "https://test_host/v1.0", then set property "customHosts" as "customHosts: Set<string>(["test_host"])"
29+
*/
30+
customHosts?: Set<string>;
2631
}

src/middleware/AuthenticationHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @module AuthenticationHandler
1010
*/
1111

12-
import { isGraphURL } from "../GraphRequestUtil";
12+
import { isCustomHost, isGraphURL } from "../GraphRequestUtil";
1313
import { AuthenticationProvider } from "../IAuthenticationProvider";
1414
import { AuthenticationProviderOptions } from "../IAuthenticationProviderOptions";
1515
import { Context } from "../IContext";
@@ -62,7 +62,7 @@ export class AuthenticationHandler implements Middleware {
6262
*/
6363
public async execute(context: Context): Promise<void> {
6464
const url = typeof context.request === "string" ? context.request : context.request.url;
65-
if (isGraphURL(url)) {
65+
if (isGraphURL(url) || (context.customHosts && isCustomHost(url, context.customHosts))) {
6666
let options: AuthenticationHandlerOptions;
6767
if (context.middlewareControl instanceof MiddlewareControl) {
6868
options = context.middlewareControl.getMiddlewareOptions(AuthenticationHandlerOptions) as AuthenticationHandlerOptions;

src/middleware/TelemetryHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
/**
99
* @module TelemetryHandler
1010
*/
11-
import { isGraphURL } from "../GraphRequestUtil";
11+
import { isCustomHost, isGraphURL } from "../GraphRequestUtil";
1212
import { Context } from "../IContext";
1313
import { PACKAGE_VERSION } from "../Version";
1414
import { Middleware } from "./IMiddleware";
@@ -65,7 +65,7 @@ export class TelemetryHandler implements Middleware {
6565
*/
6666
public async execute(context: Context): Promise<void> {
6767
const url = typeof context.request === "string" ? context.request : context.request.url;
68-
if (isGraphURL(url)) {
68+
if (isGraphURL(url) || (context.customHosts && isCustomHost(url, context.customHosts))) {
6969
// Add telemetry only if the request url is a Graph URL.
7070
// Errors are reported as in issue #265 if headers are present when redirecting to a non Graph URL
7171
let clientRequestId: string = getRequestHeader(context.request, context.options, TelemetryHandler.CLIENT_REQUEST_ID_HEADER);

test/common/core/Client.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import "isomorphic-fetch";
99

1010
import { assert } from "chai";
11+
import * as sinon from "sinon";
1112

1213
import { CustomAuthenticationProvider, TelemetryHandler } from "../../../src";
1314
import { Client } from "../../../src/Client";
@@ -148,6 +149,63 @@ describe("Client.ts", () => {
148149
assert.equal(error.customError, customError);
149150
}
150151
});
152+
153+
it("Init middleware with custom hosts", async () => {
154+
const accessToken = "DUMMY_TOKEN";
155+
const provider: AuthProvider = (done) => {
156+
done(null, "DUMMY_TOKEN");
157+
};
158+
159+
const options = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Testing chained middleware array", 200, 100, "");
160+
const chaosHandler = new ChaosHandler(options);
161+
162+
const authHandler = new AuthenticationHandler(new CustomAuthenticationProvider(provider));
163+
164+
const telemetry = new TelemetryHandler();
165+
const middleware = [authHandler, telemetry, chaosHandler];
166+
167+
const customHost = "test_custom";
168+
const customHosts = new Set<string>([customHost]);
169+
const client = Client.initWithMiddleware({ middleware, customHosts });
170+
171+
const spy = sinon.spy(telemetry, "execute");
172+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
173+
const response = await client.api(`https://${customHost}/v1.0/me`).get();
174+
const context = spy.getCall(0).args[0];
175+
176+
assert.equal(context.options.headers["Authorization"], `Bearer ${accessToken}`);
177+
});
178+
179+
it("Pass invalid custom hosts", async () => {
180+
try {
181+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
182+
const accessToken = "DUMMY_TOKEN";
183+
const provider: AuthProvider = (done) => {
184+
done(null, "DUMMY_TOKEN");
185+
};
186+
187+
const options = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Testing chained middleware array", 200, 100, "");
188+
const chaosHandler = new ChaosHandler(options);
189+
190+
const authHandler = new AuthenticationHandler(new CustomAuthenticationProvider(provider));
191+
192+
const telemetry = new TelemetryHandler();
193+
const middleware = [authHandler, telemetry, chaosHandler];
194+
195+
const customHost = "https://test_custom";
196+
const customHosts = new Set<string>([customHost]);
197+
const client = Client.initWithMiddleware({ middleware, customHosts });
198+
199+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
200+
const response = await client.api(`https://${customHost}/v1.0/me`).get();
201+
202+
throw new Error("Test fails - Error expected when custom host is not valid");
203+
} catch (error) {
204+
assert.isDefined(error);
205+
assert.isDefined(error.message);
206+
assert.equal(error.message, "Please add only hosts or hostnames to the CustomHosts config. If the url is `http://example.com:3000/`, host is `example:3000`");
207+
}
208+
});
151209
});
152210

153211
describe("init", () => {

test/common/middleware/AuthenticationHandler.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77

88
import { assert } from "chai";
99

10+
import { ChaosHandler, ChaosHandlerOptions, ChaosStrategy } from "../../../src";
11+
import { GRAPH_BASE_URL } from "../../../src/Constants";
12+
import { Context } from "../../../src/IContext";
1013
import { AuthenticationHandler } from "../../../src/middleware/AuthenticationHandler";
1114
import { DummyAuthenticationProvider } from "../../DummyAuthenticationProvider";
1215

1316
const dummyAuthProvider = new DummyAuthenticationProvider();
1417
const authHandler = new AuthenticationHandler(dummyAuthProvider);
18+
const chaosHandler = new ChaosHandler(new ChaosHandlerOptions(ChaosStrategy.MANUAL, "TEST_MESSAGE", 200));
1519

1620
describe("AuthenticationHandler.ts", async () => {
1721
describe("Constructor", () => {
@@ -20,4 +24,49 @@ describe("AuthenticationHandler.ts", async () => {
2024
assert.equal(authHandler["authenticationProvider"], dummyAuthProvider);
2125
});
2226
});
27+
describe("Auth Headers", () => {
28+
it("Should delete Auth header when Request object is passed with non Graph URL", async () => {
29+
const request = new Request("test_url");
30+
const context: Context = {
31+
request,
32+
options: {
33+
headers: {
34+
Authorization: "TEST_VALUE",
35+
},
36+
},
37+
};
38+
authHandler.setNext(chaosHandler);
39+
await authHandler.execute(context);
40+
assert.equal(context.options.headers["Authorization"], undefined);
41+
});
42+
43+
it("Should contain Auth header when Request object is passed with custom URL", async () => {
44+
const request = new Request("https://custom/");
45+
const context: Context = {
46+
request,
47+
customHosts: new Set<string>(["custom"]),
48+
options: {
49+
headers: {},
50+
},
51+
};
52+
const accessToken = "Bearer DUMMY_TOKEN";
53+
54+
await authHandler.execute(context);
55+
assert.equal((request as Request).headers.get("Authorization"), accessToken);
56+
});
57+
58+
it("Should contain Auth header when Request object is passed with a valid Graph URL", async () => {
59+
const request = new Request(GRAPH_BASE_URL);
60+
const context: Context = {
61+
request,
62+
customHosts: new Set<string>(["custom"]),
63+
options: {
64+
headers: {},
65+
},
66+
};
67+
const accessToken = "Bearer DUMMY_TOKEN";
68+
await authHandler.execute(context);
69+
assert.equal((request as Request).headers.get("Authorization"), accessToken);
70+
});
71+
});
2372
});

0 commit comments

Comments
 (0)