From 5ca74e31994776bd66806854ea71d3577cc6e67e Mon Sep 17 00:00:00 2001 From: gjsjohnmurray Date: Fri, 7 May 2021 18:03:15 +0100 Subject: [PATCH] Use REST to get a CSPCHD token with which to access Portal --- src/api/getPortalUriWithCredentials.ts | 34 ---------------------- src/api/getPortalUriWithToken.ts | 39 ++++++++++++++++++++++++++ src/extension.ts | 15 +++++----- src/makeRESTRequest.ts | 8 +++--- 4 files changed, 50 insertions(+), 46 deletions(-) delete mode 100644 src/api/getPortalUriWithCredentials.ts create mode 100644 src/api/getPortalUriWithToken.ts diff --git a/src/api/getPortalUriWithCredentials.ts b/src/api/getPortalUriWithCredentials.ts deleted file mode 100644 index d0e84ef..0000000 --- a/src/api/getPortalUriWithCredentials.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as vscode from 'vscode'; -import { Uri } from 'vscode'; -import { extensionId, ServerSpec } from '../extension'; - -export async function getPortalUriWithCredentials(name: string, scope?: vscode.ConfigurationScope): Promise { - - // Use our own API so that the Recent folder updates with our activity - const myApi = vscode.extensions.getExtension(extensionId)?.exports; - return myApi.getServerSpec(name, scope).then((spec) => { - if (typeof spec !== 'undefined') { - const webServer = spec.webServer; - let queryString = ''; - - // We can only pass credentials in cleartext as a queryparam, so only do this if user was willing to store password in cleartext in settings. - const settingsSpec: ServerSpec | undefined = vscode.workspace.getConfiguration('intersystems.servers', scope).get(name); - spec.password = settingsSpec?.password; - - if (spec?.password && spec?.username) { - // At this point we don't know if the target is IRIS or Cache, so add credentials in both formats. - // Deliberately put password before username, otherwise it is visible in VS Code's confirmation dialog triggered target domain - // hasn't been set as trusted. Likewise, deliberately put IRIS* after Cache* - const passwordEncoded = encodeURIComponent(spec.password); - queryString += `&CachePassword=${passwordEncoded}&IRISPassword=${passwordEncoded}`; - const usernameEncoded = encodeURIComponent(spec.username); - queryString += `&CacheUsername=${usernameEncoded}&IRISUsername=${usernameEncoded}`; - - // Add a cache-buster and push any credentials offscreen - queryString = '_=' + new Date().getTime().toString().padEnd(480,' ') + queryString; - } - - return vscode.Uri.parse(`${webServer.scheme}://${webServer.host}:${webServer.port}${webServer.pathPrefix}/csp/sys/UtilHome.csp?${queryString}`, true); - } - }) -} diff --git a/src/api/getPortalUriWithToken.ts b/src/api/getPortalUriWithToken.ts new file mode 100644 index 0000000..ed3fa98 --- /dev/null +++ b/src/api/getPortalUriWithToken.ts @@ -0,0 +1,39 @@ +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; +import { extensionId, ServerSpec } from '../extension'; +import { makeRESTRequest } from '../makeRESTRequest'; + +const allTokens = new Map(); + +export async function getPortalUriWithToken(name: string, scope?: vscode.ConfigurationScope): Promise { + + const PORTAL_HOME = '/csp/sys/UtilHome.csp'; + + // Use our own API so that the Recent folder updates with our activity + const myApi = vscode.extensions.getExtension(extensionId)?.exports; + + const spec: ServerSpec | undefined = await myApi.getServerSpec(name, scope); + if (typeof spec !== 'undefined') { + + // Retrieve previously cached token + let token = allTokens.get(name) || ''; + + // Revalidate and extend existing token, or obtain a new one + const response = await makeRESTRequest("POST", spec, { apiVersion: 1, namespace: '%SYS', path:'/action/query' }, { query: 'select %Atelier_v1_Utils.General_GetCSPToken(?, ?) token', parameters: [PORTAL_HOME, token]}); + + if (!response) { + // User will have to enter credentials + token = ''; + allTokens.delete(name); + } + else { + token = response.data?.result?.content[0]?.token || ''; + allTokens.set(name, token); + } + + const webServer = spec.webServer; + let queryString = token ? `CSPCHD=${encodeURIComponent(token)}` : ''; + + return vscode.Uri.parse(`${webServer.scheme}://${webServer.host}:${webServer.port}${webServer.pathPrefix}${PORTAL_HOME}?${queryString}`, true); + } +} diff --git a/src/extension.ts b/src/extension.ts index 4239e75..21b1c40 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,8 +9,8 @@ import { storePassword, clearPassword } from './commands/managePasswords'; import { importFromRegistry } from './commands/importFromRegistry'; import { ServerManagerView, ServerTreeItem, SMTreeItem } from './ui/serverManagerView'; import { addServer } from './api/addServer'; -import { getPortalUriWithCredentials } from './api/getPortalUriWithCredentials'; import { getServerSummary } from './api/getServerSummary'; +import { getPortalUriWithToken } from './api/getPortalUriWithToken'; export interface ServerName { name: string, @@ -78,9 +78,9 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand(`${extensionId}.openPortalExternal`, (server?: ServerTreeItem) => { if (server?.contextValue?.match(/\.server\./) && server.name) { - getPortalUriWithCredentials(server.name).then((uriWithCredentials) => { - if (uriWithCredentials) { - vscode.env.openExternal(uriWithCredentials); + getPortalUriWithToken(server.name).then((uriWithToken) => { + if (uriWithToken) { + vscode.env.openExternal(uriWithToken); } }); } @@ -89,13 +89,12 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand(`${extensionId}.openPortalTab`, (server?: ServerTreeItem) => { if (server?.contextValue?.match(/\.server\./) && server.name) { - getPortalUriWithCredentials(server.name).then((uriWithCredentials) => { - if (uriWithCredentials) { - //vscode.commands.executeCommand('simpleBrowser.api.open', uriWithCredentials); + getPortalUriWithToken(server.name).then((uriWithToken) => { + if (uriWithToken) { // // It is essential to pass skipEncoding=true when converting the uri to a string, // otherwise the encoding done within Simple Browser / webview causes double-encoding of the querystring. - vscode.commands.executeCommand('simpleBrowser.show', uriWithCredentials.toString(true)); + vscode.commands.executeCommand('simpleBrowser.show', uriWithToken.toString(true)); } }); } diff --git a/src/makeRESTRequest.ts b/src/makeRESTRequest.ts index 1d425cb..b71d30b 100644 --- a/src/makeRESTRequest.ts +++ b/src/makeRESTRequest.ts @@ -36,7 +36,7 @@ export interface AtelierRESTEndpoint { } url += "/api/atelier/"; if (endpoint) { - url += "/api/atelier/v" + String(endpoint.apiVersion) + "/" + endpoint.namespace + endpoint.path; + url += "v" + String(endpoint.apiVersion) + "/" + endpoint.namespace + endpoint.path; } // Make the request (SASchema support removed) @@ -65,7 +65,7 @@ export interface AtelierRESTEndpoint { respdata = await axios.request( { method: method, - url: url, + url: encodeURI(url), data: data, headers: { 'Content-Type': 'application/json' @@ -85,7 +85,7 @@ export interface AtelierRESTEndpoint { respdata = await axios.request( { method: method, - url: url, + url: encodeURI(url), withCredentials: true, jar: cookieJar, validateStatus: function (status) { @@ -99,7 +99,7 @@ export interface AtelierRESTEndpoint { respdata = await axios.request( { method: method, - url: url, + url: encodeURI(url), auth: { username: server.username, password: server.password