diff --git a/package-lock.json b/package-lock.json index 766bb1e0..cc18b8b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "moment-timezone": "^0.5.44", "node-fetch": "^2.6.6", "node-q": "^2.6.1", + "pick-port": "^2.0.1", "picomatch": "^3.0.1", "semver": "^7.5.3", "vscode-extension-telemetry": "^0.4.5", @@ -4198,6 +4199,17 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, + "node_modules/pick-port": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-2.0.1.tgz", + "integrity": "sha512-8GSFuOCelctwcW0CkNGFahrGJyKxx2hMOmzDxVaHTknsikh4Zi+edCFUMgfAcBLRkV0GYQT64QivSi4SxDeKdA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/picomatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", @@ -8323,6 +8335,14 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, + "pick-port": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-2.0.1.tgz", + "integrity": "sha512-8GSFuOCelctwcW0CkNGFahrGJyKxx2hMOmzDxVaHTknsikh4Zi+edCFUMgfAcBLRkV0GYQT64QivSi4SxDeKdA==", + "requires": { + "debug": "^4.3.4" + } + }, "picomatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", diff --git a/package.json b/package.json index 7b87a858..7ebb7a96 100644 --- a/package.json +++ b/package.json @@ -733,6 +733,7 @@ "moment-timezone": "^0.5.44", "node-fetch": "^2.6.6", "node-q": "^2.6.1", + "pick-port": "^2.0.1", "picomatch": "^3.0.1", "semver": "^7.5.3", "vscode-extension-telemetry": "^0.4.5", diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 237b28f5..9d08c7fa 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -103,7 +103,6 @@ export namespace ext { export const insightsAuthUrls = { authURL: "auth/realms/insights/protocol/openid-connect/auth ", - callbackURL: "http://localhost:9010/redirect", revoke: "auth/realms/insights/protocol/openid-connect/revoke", tokenURL: "auth/realms/insights/protocol/openid-connect/token", scratchpadURL: "servicebroker/scratchpad/display", diff --git a/src/services/kdbInsights/codeFlowLogin.ts b/src/services/kdbInsights/codeFlowLogin.ts index f1cb27a8..1ddb8d4b 100644 --- a/src/services/kdbInsights/codeFlowLogin.ts +++ b/src/services/kdbInsights/codeFlowLogin.ts @@ -20,7 +20,7 @@ import * as querystring from "querystring"; import * as url from "url"; import { ext } from "../../extensionVariables"; import { Uri, env } from "vscode"; -import { Socket } from "net"; +import { pickPort } from "pick-port"; interface IDeferred { resolve: (result: T | Promise) => void; @@ -34,59 +34,35 @@ export interface IToken { refreshToken: string; } -const defaultTimeout = 5 * 60 * 1000; // 5 min +const defaultTimeout = 3 * 60 * 1000; // 3 min const closeTimeout = 10 * 1000; // 10 sec const commonRequestParams = { client_id: "insights-app", - redirect_uri: ext.insightsAuthUrls.callbackURL, }; -let flow: { server: http.Server; codePromise: Promise } | undefined; - export async function signIn(insightsUrl: string) { - if (flow) { - flow.server.close(); - } - flow = createServer(); - - const { server, codePromise } = flow; - const sockets: Socket[] = []; - - server.on("connection", (socket) => { - sockets.push(socket); - socket.on("close", () => { - const index = sockets.indexOf(socket); - if (index > -1) { - sockets.splice(index, 1); - } - }); - }); - - server.on("close", () => { - for (const socket of sockets) { - socket.destroy(); - } - }); + const { server, codePromise } = createServer(); try { - await startServer(server); + const port = await startServer(server); const authParams = { response_type: "code", scope: "profile", + redirect_uri: `http://localhost:${port}/redirect`, state: crypto.randomBytes(20).toString("hex"), }; const authorizationUrl = new url.URL( ext.insightsAuthUrls.authURL, - insightsUrl + insightsUrl, ); authorizationUrl.search = queryString(authParams); const opened = await env.openExternal( - Uri.parse(authorizationUrl.toString()) + Uri.parse(authorizationUrl.toString()), ); if (opened) { @@ -96,16 +72,13 @@ export async function signIn(insightsUrl: string) { throw new Error("Error opening url"); } finally { - setImmediate(() => { - server.close(); - flow = undefined; - }); + setImmediate(() => server.close()); } } export async function signOut( insightsUrl: string, - token: string + token: string, ): Promise { const queryParams = queryString({ grant_type: ext.insightsGrantType.authorizationCode, @@ -126,7 +99,7 @@ export async function signOut( export async function refreshToken( insightsUrl: string, - token: string + token: string, ): Promise { return await tokenRequest(insightsUrl, { grant_type: ext.insightsGrantType.refreshToken, @@ -136,7 +109,7 @@ export async function refreshToken( export async function getCurrentToken( serverName: string, - serverAlias: string + serverAlias: string, ): Promise { if (serverName === "" || serverAlias === "") { return undefined; @@ -167,7 +140,7 @@ export async function getCurrentToken( async function getToken( insightsUrl: string, - code: string + code: string, ): Promise { return await tokenRequest(insightsUrl, { code, @@ -177,7 +150,7 @@ async function getToken( async function tokenRequest( insightsUrl: string, - params: any + params: any, ): Promise { const queryParams = queryString(params); const headers = { @@ -225,18 +198,18 @@ function queryString(options: any): string { function createServer() { let deferredCode: IDeferred; const codePromise = new Promise( - (resolve, reject) => (deferredCode = { resolve, reject }) + (resolve, reject) => (deferredCode = { resolve, reject }), ); const codeTimer = setTimeout( () => deferredCode.reject(new Error("Timeout waiting for code.")), - defaultTimeout + defaultTimeout, ); const cancelCodeTimer = () => clearTimeout(codeTimer); const server = http.createServer((req, res) => { const reqUrl = new url.URL( req.url!, - `${ext.networkProtocols.http}${ext.localhost}` + `${ext.networkProtocols.http}${ext.localhost}`, ); switch (reqUrl.pathname) { case "/": @@ -245,9 +218,9 @@ function createServer() { join( ext.context.asAbsolutePath("resources"), "codeFlowResult", - "index.html" + "index.html", ), - "text/html; charset=utf-8" + "text/html; charset=utf-8", ); break; case "/redirect": @@ -275,9 +248,9 @@ function createServer() { join( ext.context.asAbsolutePath("resources"), "codeFlowResult", - "main.css" + "main.css", ), - "text/css; charset=utf-8" + "text/css; charset=utf-8", ); break; default: @@ -296,37 +269,41 @@ function createServer() { } function startServer(server: http.Server): Promise { - let deferredCode: IDeferred; - const portPromise = new Promise( - (resolve, reject) => (deferredCode = { resolve, reject }) - ); - const portTimer = setTimeout( - () => deferredCode.reject(new Error("Timeout waiting for port")), - closeTimeout - ); - const cancelPortTimer = () => clearTimeout(portTimer); - - server.on("listening", () => deferredCode.resolve(9010)); - server.on("error", (error) => deferredCode.reject(error)); - server.on("close", () => deferredCode.reject(new Error("Closed"))); - - server.listen(9010, ext.localhost); - - portPromise.then(cancelPortTimer, cancelPortTimer); - - return new Promise((resolve) => { - env - .asExternalUri( - Uri.parse(`${ext.networkProtocols.http}${ext.localhost}:9010`) - ) - .then(() => resolve(portPromise)); + return new Promise((resolve, reject) => { + let deferredCode: IDeferred; + const portPromise = new Promise( + (resolve, reject) => (deferredCode = { resolve, reject }), + ); + const portTimer = setTimeout( + () => deferredCode.reject(new Error("Timeout waiting for port")), + closeTimeout, + ); + const cancelPortTimer = () => clearTimeout(portTimer); + + pickPort({ + ip: "127.0.0.1", + type: "tcp", + minPort: 9000, + maxPort: 9999, + }) + .then((port) => { + server.on("listening", () => deferredCode.resolve(port)); + server.on("error", (error) => deferredCode.reject(error)); + server.on("close", () => deferredCode.reject(new Error("Closed"))); + server.listen(port, ext.localhost); + portPromise.then(cancelPortTimer, cancelPortTimer); + env + .asExternalUri(Uri.parse(`http://localhost:${port}`)) + .then(() => resolve(portPromise)); + }) + .catch(reject); }); } function sendFile( res: http.ServerResponse, filePath: string, - contentType: string + contentType: string, ) { fs.readFile(filePath, (_err: any, _body: any) => { if (!_err) { diff --git a/test/suite/services.test.ts b/test/suite/services.test.ts index 8a348288..a84d5c1e 100644 --- a/test/suite/services.test.ts +++ b/test/suite/services.test.ts @@ -37,7 +37,6 @@ import { QueryHistoryProvider, QueryHistoryTreeItem, } from "../../src/services/queryHistoryProvider"; -import * as http from "http"; // eslint-disable-next-line @typescript-eslint/no-var-requires const codeFlow = require("../../src/services/kdbInsights/codeFlowLogin"); @@ -70,13 +69,13 @@ describe("kdbTreeProvider", () => { ["child1"], "testElement", servers["testServer"], - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); insightNode = new InsightsNode( ["child1"], "testElement", insights["testInsight"], - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); }); @@ -85,7 +84,7 @@ describe("kdbTreeProvider", () => { assert.notStrictEqual( kdbProvider, undefined, - "KdbTreeProvider should be created." + "KdbTreeProvider should be created.", ); }); @@ -95,7 +94,7 @@ describe("kdbTreeProvider", () => { assert.notStrictEqual( kdbProvider, undefined, - "KdbTreeProvider should be created." + "KdbTreeProvider should be created.", ); }); @@ -113,7 +112,7 @@ describe("kdbTreeProvider", () => { assert.notStrictEqual( kdbProvider, undefined, - "KdbTreeProvider should be created." + "KdbTreeProvider should be created.", ); }); @@ -128,7 +127,7 @@ describe("kdbTreeProvider", () => { assert.notStrictEqual( kdbProvider, undefined, - "KdbTreeProvider should be created." + "KdbTreeProvider should be created.", ); }); @@ -138,7 +137,7 @@ describe("kdbTreeProvider", () => { assert.strictEqual( element.label, kdbNode.label, - "Get kdb node element is incorrect" + "Get kdb node element is incorrect", ); }); @@ -148,7 +147,7 @@ describe("kdbTreeProvider", () => { assert.strictEqual( element.label, insightNode.label, - "Get insights node element is incorrect" + "Get insights node element is incorrect", ); }); @@ -177,7 +176,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); kdbNode.contextValue = "testServerAlias"; kdbProvider.getChildren(kdbNode); @@ -198,7 +197,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); kdbProvider.getChildren(kdbNode); const result = await kdbProvider.getChildren(kdbNode); @@ -218,7 +217,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); kdbNode.contextValue = "ns"; kdbProvider.getChildren(kdbNode); @@ -238,12 +237,12 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.strictEqual( kdbNode.label, "kdbnode1 [kdbserveralias]", - "KdbNode node creation failed" + "KdbNode node creation failed", ); }); @@ -259,12 +258,12 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.strictEqual( kdbNode.label, "kdbnode1", - "KdbNode node creation failed" + "KdbNode node creation failed", ); }); @@ -280,12 +279,12 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.strictEqual( kdbNode.label, "kdbnode1 [kdbserveralias]", - "KdbNode node creation failed" + "KdbNode node creation failed", ); }); @@ -301,7 +300,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); ext.connectionNode = kdbNode; @@ -317,13 +316,13 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.strictEqual( kdbNode1.label, "kdbnode1 [kdbserveralias] (connected)", - "KdbNode node creation failed" + "KdbNode node creation failed", ); }); @@ -341,7 +340,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.equal(ext.kdbNodesWithoutTls.length, 1); }); @@ -360,7 +359,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: true, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.equal(ext.kdbNodesWithoutTls, 0); }); @@ -379,7 +378,7 @@ describe("kdbTreeProvider", () => { auth: false, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.equal(ext.kdbNodesWithoutAuth.length, 1); }); @@ -398,7 +397,7 @@ describe("kdbTreeProvider", () => { auth: true, tls: false, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.equal(ext.kdbNodesWithoutAuth, 0); }); @@ -411,7 +410,7 @@ describe("kdbTreeProvider", () => { alias: "insightsserveralias", auth: true, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); ext.kdbinsightsNodes.pop(); @@ -419,7 +418,7 @@ describe("kdbTreeProvider", () => { assert.strictEqual( insightsNode.label, "insightsnode1", - "InsightsNode node creation failed" + "InsightsNode node creation failed", ); }); @@ -432,7 +431,7 @@ describe("kdbTreeProvider", () => { alias: "insightsserveralias", auth: true, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); ext.kdbinsightsNodes.pop(); @@ -440,7 +439,7 @@ describe("kdbTreeProvider", () => { assert.strictEqual( insightsNode.label, "insightsnode1", - "InsightsNode node creation failed" + "InsightsNode node creation failed", ); }); @@ -453,7 +452,7 @@ describe("kdbTreeProvider", () => { alias: "insightsserveralias", auth: true, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); ext.connectionNode = insightsNode; @@ -466,7 +465,7 @@ describe("kdbTreeProvider", () => { alias: "insightsserveralias", auth: true, }, - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); ext.kdbinsightsNodes.pop(); @@ -474,7 +473,7 @@ describe("kdbTreeProvider", () => { assert.strictEqual( insightsNode1.label, "insightsnode1 (connected)", - "InsightsNode node creation failed" + "InsightsNode node creation failed", ); }); @@ -484,12 +483,12 @@ describe("kdbTreeProvider", () => { "nsnode1", "nsnodedetails1", TreeItemCollapsibleState.None, - "nsfullname" + "nsfullname", ); assert.strictEqual( qNsNode.label, "nsnode1", - "QNamespaceNode node creation failed" + "QNamespaceNode node creation failed", ); }); @@ -499,12 +498,12 @@ describe("kdbTreeProvider", () => { "categorynode1", "categorynodedetails1", "categoryns", - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.strictEqual( qCategoryNode.label, "categorynode1", - "QCategoryNode node creation failed" + "QCategoryNode node creation failed", ); }); @@ -514,12 +513,12 @@ describe("kdbTreeProvider", () => { "servernode1", "servernodedetails1", TreeItemCollapsibleState.None, - "" + "", ); assert.strictEqual( qServerNode.label, "servernode1", - "QServer node creation failed" + "QServer node creation failed", ); }); }); @@ -552,12 +551,12 @@ describe("Code flow login service tests", () => { sinon.stub(axios, "post").resolves(Promise.resolve({ data: token })); const result = await refreshToken( "http://localhost", - JSON.stringify(token) + JSON.stringify(token), ); assert.strictEqual( result.accessToken, token.access_token, - "Token has not refreshed correctly" + "Token has not refreshed correctly", ); }); @@ -566,7 +565,7 @@ describe("Code flow login service tests", () => { assert.strictEqual( result, undefined, - "Should return undefined when server name is empty." + "Should return undefined when server name is empty.", ); }); @@ -575,7 +574,7 @@ describe("Code flow login service tests", () => { assert.strictEqual( result, undefined, - "Should return undefined when server alias is empty." + "Should return undefined when server alias is empty.", ); }); @@ -583,14 +582,6 @@ describe("Code flow login service tests", () => { sinon.stub(env, "openExternal").value(async () => false); await assert.rejects(() => signIn("http://127.0.0.1")); }); - - it("Should not sign in in case of error", async () => { - sinon.stub(env, "openExternal").value(async () => true); - setTimeout(async () => { - await axios.get("http://127.0.0.1:9010/redirect?error=1"); - }, 500); - await assert.rejects(() => signIn("http://127.0.0.1")); - }); }); describe("queryHistoryProvider", () => { @@ -627,7 +618,7 @@ describe("queryHistoryProvider", () => { assert.notStrictEqual( queryHistoryProvider, undefined, - "queryHistoryProvider should be created." + "queryHistoryProvider should be created.", ); }); it("Should refresh the provider", () => { @@ -636,7 +627,7 @@ describe("queryHistoryProvider", () => { assert.notStrictEqual( queryHistoryProvider, undefined, - "queryHistoryProvider should be created." + "queryHistoryProvider should be created.", ); }); @@ -644,14 +635,14 @@ describe("queryHistoryProvider", () => { const queryHistoryTreeItem = new QueryHistoryTreeItem( "testLabel", dummyQueryHistory[0], - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); const queryHistoryProvider = new QueryHistoryProvider(); const element = queryHistoryProvider.getTreeItem(queryHistoryTreeItem); assert.strictEqual( element.label, queryHistoryTreeItem.label, - "Get query history item is incorrect" + "Get query history item is incorrect", ); }); @@ -675,25 +666,25 @@ describe("queryHistoryProvider", () => { const queryHistoryTreeItem = new QueryHistoryTreeItem( "testLabel", dummyQueryHistory[0], - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); assert.strictEqual( queryHistoryTreeItem.label, "testLabel", - "QueryHistoryTreeItem node creation failed" + "QueryHistoryTreeItem node creation failed", ); }); it("Should return a new QueryHistoryTreeItem with sucess icom", () => { const queryHistoryTreeItem = new QueryHistoryTreeItem( "testLabel", dummyQueryHistory[0], - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); const result = queryHistoryTreeItem.defineQueryIcon(true); assert.strictEqual( result, sucessIcon, - "QueryHistoryTreeItem defineQueryIcon failed" + "QueryHistoryTreeItem defineQueryIcon failed", ); }); @@ -701,13 +692,13 @@ describe("queryHistoryProvider", () => { const queryHistoryTreeItem = new QueryHistoryTreeItem( "testLabel", dummyQueryHistory[0], - TreeItemCollapsibleState.None + TreeItemCollapsibleState.None, ); const result = queryHistoryTreeItem.defineQueryIcon(false); assert.strictEqual( result, failIcon, - "QueryHistoryTreeItem defineQueryIcon failed" + "QueryHistoryTreeItem defineQueryIcon failed", ); }); });