diff --git a/src/api/index.ts b/src/api/index.ts index 4e1d5dd2..1ba3aacb 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -20,11 +20,31 @@ const DEFAULT_SERVER_VERSION = "2016.2.0"; import * as Atelier from "./atelier"; import { isfsConfig } from "../utils/FileProviderUtil"; -// Map of the authRequest promises for each username@host:port/pathPrefix target to avoid concurrency issues +// Map of the authRequest promises for each `username@http(s)://host:port/pathPrefix` target to avoid concurrency issues const authRequestMap = new Map>(); -/** Map of `username@host:port/pathPrefix` to cookies */ -const cookiesMap = new Map(); +/** Map of `username@http(s)://host:port/pathPrefix` to cookies */ +export const cookiesMap = new Map(); + +/** Log out of CSP sessions specified in the `sessions` array, or all known sessions if no argument was passed. */ +export async function logoutOfSessions(sessions?: string[]): Promise { + const httpsAgent = new httpsModule.Agent({ + rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), + }); + return Promise.allSettled( + (sessions ?? Array.from(cookiesMap.keys())).map((session) => { + const cookie = cookiesMap.get(session); + if (!cookie) return; + const url = session.slice(session.indexOf("@") + 1); + return axios.head(`${url}/api/atelier/?CacheLogout=end`, { + headers: { + Cookie: cookie, + }, + httpsAgent, + }); + }) + ).then(() => {}); // Returned object isn't needed +} interface ConnectionSettings { serverName: string; @@ -175,10 +195,6 @@ export class AtelierAPI { return cookiesMap.get(this.mapKey()) ?? []; } - public clearCookies(): void { - cookiesMap.delete(this.mapKey()); - } - public xdebugUrl(): string { const { host, https, port, apiVersion, pathPrefix } = this.config; const proto = https ? "wss" : "ws"; @@ -208,13 +224,13 @@ export class AtelierAPI { } /** Return the key for getting values from connection-specific Maps for this connection */ - private mapKey(): string { - const { host, port, username } = this.config; + public mapKey(): string { + const { host, https, port, username } = this.config; let pathPrefix = this._config.pathPrefix || ""; if (pathPrefix.length && !pathPrefix.startsWith("/")) { pathPrefix = "/" + pathPrefix; } - return `${username}@${host}:${port}${pathPrefix}`; + return `${username}@http${https ? "s" : ""}://${host}:${port}${pathPrefix}`; } private setConnection(workspaceFolderName: string, namespace?: string): void { @@ -307,7 +323,7 @@ export class AtelierAPI { return serverName && serverName !== "" ? serverName : `${host}:${port}${pathPrefix}`; } - public async request( + private async request( minVersion: number, method: string, path?: string, @@ -457,7 +473,7 @@ export class AtelierAPI { } throw { statusCode: response.status, message: response.statusText }; } - await this.updateCookies(response.headers["set-cookie"] || []); + this.updateCookies(response.headers["set-cookie"] || []); if (method === "HEAD") { if (!originalPath) { authRequestMap.delete(mapKey); diff --git a/src/extension.ts b/src/extension.ts index a0a741e0..a6613af6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -71,7 +71,7 @@ import { ObjectScriptRoutineSymbolProvider } from "./providers/ObjectScriptRouti import { ObjectScriptCodeLensProvider } from "./providers/ObjectScriptCodeLensProvider"; import { XmlContentProvider } from "./providers/XmlContentProvider"; -import { AtelierAPI } from "./api"; +import { AtelierAPI, cookiesMap, logoutOfSessions } from "./api"; import { ObjectScriptDebugAdapterDescriptorFactory } from "./debug/debugAdapterFactory"; import { ObjectScriptConfigurationProvider } from "./debug/debugConfProvider"; import { ProjectsExplorerProvider } from "./explorer/projectsExplorer"; @@ -340,11 +340,7 @@ export function getResolvedConnectionSpec(key: string, dflt: any): any { /** The `api.serverId`s of all servers that are known to be inactive */ export const inactiveServerIds: Set = new Set(); -export async function checkConnection( - clearCookies = false, - uri?: vscode.Uri, - triggerRefreshes?: boolean -): Promise { +export async function checkConnection(clearState = false, uri?: vscode.Uri, triggerRefreshes?: boolean): Promise { // Do nothing if already checking the connection if (checkingConnection) { return; @@ -352,8 +348,8 @@ export async function checkConnection( const { apiTarget, configName } = connectionTarget(uri); const wsKey = configName.toLowerCase(); - if (clearCookies) { - /// clean-up cached values + if (clearState) { + // clean-up cached values await workspaceState.update(wsKey + ":host", undefined); await workspaceState.update(wsKey + ":port", undefined); await workspaceState.update(wsKey + ":superserverPort", undefined); @@ -422,10 +418,6 @@ export async function checkConnection( } } - if (clearCookies) { - api.clearCookies(); - } - // Before recreating the api object (in case something has updated connection details since we last fetched them?) // if this is an external server, remove it from the inactive list because its presence there would block the reconnect that we want to be attempting if (api.externalServer) { @@ -1521,38 +1513,57 @@ export async function activate(context: vscode.ExtensionContext): Promise { supportsMultipleEditorsPerDocument: false, }), vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => { - if (affectsConfiguration("objectscript.conn") || affectsConfiguration("intersystems.servers")) { - if (affectsConfiguration("intersystems.servers")) { - // Gather the server names previously resolved - const resolvedServers: string[] = []; - resolvedConnSpecs.forEach((v, k) => resolvedServers.push(k)); - // Clear the cache - resolvedConnSpecs.clear(); - // Resolve them again, sequentially in case user needs to be prompted for credentials - for await (const serverName of resolvedServers) { - await resolveConnectionSpec(serverName); - } - } - // Check connections sequentially for each workspace folder - let refreshFilesExplorer = false; - for await (const folder of vscode.workspace.workspaceFolders ?? []) { - if (schemas.includes(folder.uri.scheme)) { - refreshFilesExplorer = true; - } - try { - await checkConnection(true, folder.uri, true); - } catch (_) { - continue; + // Loop through all ws folder connections and see if any changed + // Find all "sessions" that are new or orphaned. + // For new ones, establish a connection? + // For orphans, logout and clear the cookies. + let refreshFilesExplorer = false; + const newConnections = new Set(); + const affectedWsFolders: vscode.WorkspaceFolder[] = []; + for (const wsFolder of vscode.workspace.workspaceFolders ?? []) { + const api = new AtelierAPI(wsFolder.uri); + newConnections.add(api.mapKey()); + const { serverName } = api.config; + if (notIsfs(wsFolder.uri) && affectsConfiguration("objectscript.conn", wsFolder)) { + // Connection info changed + affectedWsFolders.push(wsFolder); + } else if (serverName && affectsConfiguration("intersystems.servers", wsFolder)) { + const oldSpec = getResolvedConnectionSpec(serverName, undefined); + const newSpec = vscode.workspace.getConfiguration("intersystems.servers", wsFolder).get(serverName); + if ( + !oldSpec || + !( + oldSpec.webServer.host == newSpec.webServer.host && + oldSpec.webServer.port == newSpec.webServer.port && + oldSpec.webServer.scheme == newSpec.webServer.scheme && + oldSpec.webServer.pathPrefix == newSpec.webServer.pathPrefix + ) + ) { + // Connection info changed + resolvedConnSpecs.delete(serverName); + affectedWsFolders.push(wsFolder); + if (filesystemSchemas.includes(wsFolder.uri.scheme)) refreshFilesExplorer = true; } } - explorerProvider.refresh(); - projectsExplorerProvider.refresh(); - if (refreshFilesExplorer) { - // This unavoidably switches to the File Explorer view, so only do it if isfs folders were found - vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); + } + // Log out of any sessions that are orphaned + await logoutOfSessions(Array.from(cookiesMap.keys()).filter((k) => !newConnections.has(k))); + // Update the connection info for affected workspace folders + // This should create new CSP sessions if needed + for (const wsFolder of affectedWsFolders) { + try { + await checkConnection(true, wsFolder.uri, true); + } catch { + // Errors are handled by checkConnection() } - updateWebAndAbstractDocsCaches(vscode.workspace.workspaceFolders); } + explorerProvider.refresh(); + projectsExplorerProvider.refresh(); + if (refreshFilesExplorer) { + // This unavoidably switches to the File Explorer view, so only do it if isfs folders were found + vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); + } + updateWebAndAbstractDocsCaches(affectedWsFolders); if (affectsConfiguration("objectscript.commentToken")) { // Update the language configuration for "objectscript" and "objectscript-macros" macLangConf?.dispose(); @@ -2037,24 +2048,5 @@ export async function deactivate(): Promise { intLangConf?.dispose(); disposeDocumentIndex(); // Log out of all CSP sessions - const loggedOut: Set = new Set(); - const promises: Promise[] = []; - for (const f of vscode.workspace.workspaceFolders ?? []) { - const api = new AtelierAPI(f.uri); - if (!api.active || !api.cookies.length) continue; - const sessionCookie = api.cookies.find((c) => c.startsWith("CSPSESSIONID-")); - if (!sessionCookie || loggedOut.has(sessionCookie)) continue; - loggedOut.add(sessionCookie); - promises.push( - api.request( - 0, - "HEAD", - undefined, - undefined, - // Prefer IRISLogout for servers that support it - semver.lt(api.config.serverVersion, "2018.2.0") ? { CacheLogout: "end" } : { IRISLogout: "end" } - ) - ); - } - await Promise.allSettled(promises); + await logoutOfSessions(); }