From 68bb5157fc25c946a34f65833758b2f96ba6f744 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Thu, 20 Jan 2022 00:53:32 +0100 Subject: [PATCH] Support http proxies --- dev-packages/cli/package.json | 4 +- dev-packages/cli/src/download-plugins.ts | 66 +++---- dev-packages/cli/src/theia.ts | 23 ++- dev-packages/cli/tsconfig.json | 3 + dev-packages/ovsx-client/package.json | 3 +- .../ovsx-client/src/ovsx-client.spec.ts | 3 +- dev-packages/ovsx-client/src/ovsx-client.ts | 32 ++-- dev-packages/ovsx-client/tsconfig.json | 6 +- dev-packages/request-service/.eslintrc.js | 10 ++ dev-packages/request-service/README.md | 29 +++ dev-packages/request-service/package.json | 36 ++++ dev-packages/request-service/src/index.ts | 17 ++ .../src/node-request-service.ts | 159 +++++++++++++++++ .../request-service/src/package.spec.ts | 28 +++ dev-packages/request-service/src/proxy.ts | 61 +++++++ .../request-service/src/request-service.ts | 92 ++++++++++ dev-packages/request-service/tsconfig.json | 12 ++ packages/core/README.md | 3 + packages/core/package.json | 14 ++ .../shared/@theia/request-service/index.d.ts | 1 + .../shared/@theia/request-service/index.js | 1 + packages/core/src/browser/core-preferences.ts | 38 ++++ .../browser/frontend-application-module.ts | 5 + .../core/src/browser/json-schema-store.ts | 8 +- .../browser/request/browser-request-module.ts | 23 +++ .../request/browser-request-service.ts | 167 ++++++++++++++++++ .../electron-browser-request-module.ts | 26 +++ .../electron-backend-request-module.ts | 23 +++ .../electron-backend-request-service.ts | 58 ++++++ .../src/node/backend-application-module.ts | 11 ++ .../node/request/backend-request-facade.ts | 46 +++++ .../node/request/backend-request-module.ts | 25 +++ .../node/request/proxy-cli-contribution.ts | 65 +++++++ packages/core/src/node/request/proxy.ts | 61 +++++++ packages/plugin-ext/package.json | 1 + .../src/hosted/node/plugin-host-proxy.ts | 92 ++++++++++ .../src/hosted/node/plugin-host-rpc.ts | 2 + .../abstract-resource-preference-provider.ts | 14 +- .../browser/util/preference-tree-generator.ts | 1 + .../src/browser/vsx-extensions-model.ts | 6 + .../browser/vsx-registry-frontend-module.ts | 3 +- .../src/common/ovsx-client-provider.ts | 5 +- .../src/node/vsx-registry-backend-module.ts | 3 +- tsconfig.json | 3 + yarn.lock | 75 +++++++- 45 files changed, 1296 insertions(+), 68 deletions(-) create mode 100644 dev-packages/request-service/.eslintrc.js create mode 100644 dev-packages/request-service/README.md create mode 100644 dev-packages/request-service/package.json create mode 100644 dev-packages/request-service/src/index.ts create mode 100644 dev-packages/request-service/src/node-request-service.ts create mode 100644 dev-packages/request-service/src/package.spec.ts create mode 100644 dev-packages/request-service/src/proxy.ts create mode 100644 dev-packages/request-service/src/request-service.ts create mode 100644 dev-packages/request-service/tsconfig.json create mode 100644 packages/core/shared/@theia/request-service/index.d.ts create mode 100644 packages/core/shared/@theia/request-service/index.js create mode 100644 packages/core/src/browser/request/browser-request-module.ts create mode 100644 packages/core/src/browser/request/browser-request-service.ts create mode 100644 packages/core/src/electron-browser/request/electron-browser-request-module.ts create mode 100644 packages/core/src/electron-node/request/electron-backend-request-module.ts create mode 100644 packages/core/src/electron-node/request/electron-backend-request-service.ts create mode 100644 packages/core/src/node/request/backend-request-facade.ts create mode 100644 packages/core/src/node/request/backend-request-module.ts create mode 100644 packages/core/src/node/request/proxy-cli-contribution.ts create mode 100644 packages/core/src/node/request/proxy.ts create mode 100644 packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json index 1a126ff1429ff..1f26235cab325 100644 --- a/dev-packages/cli/package.json +++ b/dev-packages/cli/package.json @@ -35,6 +35,7 @@ "@theia/ffmpeg": "1.23.0", "@theia/localization-manager": "1.23.0", "@theia/ovsx-client": "1.23.0", + "@theia/request-service": "1.23.0", "@types/chai": "^4.2.7", "@types/mocha": "^5.2.7", "@types/node-fetch": "^2.5.7", @@ -42,10 +43,7 @@ "chai": "^4.2.0", "chalk": "4.0.0", "decompress": "^4.2.1", - "https-proxy-agent": "^5.0.0", "mocha": "^7.0.0", - "node-fetch": "^2.6.7", - "proxy-from-env": "^1.1.0", "puppeteer": "^2.0.0", "puppeteer-to-istanbul": "^1.2.2", "temp": "^0.9.1", diff --git a/dev-packages/cli/src/download-plugins.ts b/dev-packages/cli/src/download-plugins.ts index 7ae7927595c2d..4d8c04959064c 100644 --- a/dev-packages/cli/src/download-plugins.ts +++ b/dev-packages/cli/src/download-plugins.ts @@ -26,17 +26,12 @@ declare global { import { OVSXClient } from '@theia/ovsx-client/lib/ovsx-client'; import * as chalk from 'chalk'; import * as decompress from 'decompress'; -import { createWriteStream, promises as fs } from 'fs'; -import { HttpsProxyAgent } from 'https-proxy-agent'; -import fetch, { RequestInit, Response } from 'node-fetch'; +import { promises as fs } from 'fs'; import * as path from 'path'; -import { getProxyForUrl } from 'proxy-from-env'; -import * as stream from 'stream'; import * as temp from 'temp'; -import { promisify } from 'util'; +import { NodeRequestService } from '@theia/request-service/lib/node-request-service'; import { DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package/lib/api'; - -const pipelineAsPromised = promisify(stream.pipeline); +import { RequestContext } from '@theia/request-service'; temp.track(); @@ -66,16 +61,31 @@ export interface DownloadPluginsOptions { * The open-vsx registry API url. */ apiUrl?: string; + + proxyUrl?: string; + proxyAuthorization?: string; + strictSsl?: boolean; } +const requestService = new NodeRequestService(); + export default async function downloadPlugins(options: DownloadPluginsOptions = {}): Promise { const { packed = false, ignoreErrors = false, apiVersion = DEFAULT_SUPPORTED_API_VERSION, - apiUrl = 'https://open-vsx.org/api' + apiUrl = 'https://open-vsx.org/api', + proxyUrl, + proxyAuthorization, + strictSsl } = options; + requestService.configure({ + proxyUrl, + proxyAuthorization, + strictSSL: strictSsl + }); + // Collect the list of failures to be appended at the end of the script. const failures: string[] = []; @@ -111,7 +121,7 @@ export default async function downloadPlugins(options: DownloadPluginsOptions = const extensionPacks = await collectExtensionPacks(pluginsDir, excludedIds); if (extensionPacks.size > 0) { console.warn(`--- resolving ${extensionPacks.size} extension-packs ---`); - const client = new OVSXClient({ apiVersion, apiUrl }); + const client = new OVSXClient({ apiVersion, apiUrl }, requestService); // De-duplicate extension ids to only download each once: const ids = new Set(Array.from(extensionPacks.values()).flat()); await Promise.all(Array.from(ids, async id => { @@ -127,7 +137,7 @@ export default async function downloadPlugins(options: DownloadPluginsOptions = const pluginDependencies = await collectPluginDependencies(pluginsDir, excludedIds); if (pluginDependencies.length > 0) { console.warn(`--- resolving ${pluginDependencies.length} extension dependencies ---`); - const client = new OVSXClient({ apiVersion, apiUrl }); + const client = new OVSXClient({ apiVersion, apiUrl }, requestService); // De-duplicate extension ids to only download each once: const ids = new Set(pluginDependencies); await Promise.all(Array.from(ids, async id => { @@ -187,7 +197,7 @@ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl let attempts: number; let lastError: Error | undefined; - let response: Response | undefined; + let response: RequestContext | undefined; for (attempts = 0; attempts < maxAttempts; attempts++) { if (attempts > 0) { @@ -195,12 +205,15 @@ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl } lastError = undefined; try { - response = await xfetch(pluginUrl); + response = await requestService.request({ + url: pluginUrl + }); } catch (error) { lastError = error; continue; } - const retry = response.status === 439 || response.status >= 500; + const status = response.res.statusCode; + const retry = status && (status === 439 || status >= 500); if (!retry) { break; } @@ -213,20 +226,19 @@ async function downloadPluginAsync(failures: string[], plugin: string, pluginUrl failures.push(chalk.red(`x ${plugin}: failed to download (unknown reason)`)); return; } - if (response.status !== 200) { - failures.push(chalk.red(`x ${plugin}: failed to download with: ${response.status} ${response.statusText}`)); + if (response.res.statusCode !== 200) { + failures.push(chalk.red(`x ${plugin}: failed to download with: ${response.res.statusCode}`)); return; } if ((fileExt === '.vsix' || fileExt === '.theia') && packed === true) { // Download .vsix without decompressing. - const file = createWriteStream(targetPath); - await pipelineAsPromised(response.body, file); + await fs.writeFile(targetPath, response.buffer.buffer); } else { await fs.mkdir(targetPath, { recursive: true }); - const tempFile = temp.createWriteStream('theia-plugin-download'); - await pipelineAsPromised(response.body, tempFile); - await decompress(tempFile.path, targetPath); + const tempFile = temp.path('theia-plugin-download'); + await fs.writeFile(tempFile, Buffer.from(response.buffer.buffer)); + await decompress(tempFile, targetPath); } console.warn(chalk.green(`+ ${plugin}${version ? `@${version}` : ''}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`)); @@ -242,18 +254,6 @@ async function isDownloaded(filePath: string): Promise { return fs.stat(filePath).then(() => true, () => false); } -/** - * Follow HTTP(S)_PROXY, ALL_PROXY and NO_PROXY environment variables. - */ -export function xfetch(url: string, options?: RequestInit): Promise { - const proxiedOptions: RequestInit = { ...options }; - const proxy = getProxyForUrl(url); - if (!proxiedOptions.agent && proxy !== '') { - proxiedOptions.agent = new HttpsProxyAgent(proxy); - } - return fetch(url, proxiedOptions); -} - /** * Walk the plugin directory and collect available extension paths. * @param pluginDir the plugin directory. diff --git a/dev-packages/cli/src/theia.ts b/dev-packages/cli/src/theia.ts index 67702fb3285e0..44be4eb72b785 100644 --- a/dev-packages/cli/src/theia.ts +++ b/dev-packages/cli/src/theia.ts @@ -197,7 +197,13 @@ async function theiaCli(): Promise { } }) .command<{ - packed: boolean + packed: boolean, + ignoreErrors: boolean, + apiVersion?: string, + apiUrl?: string, + proxyUrl?: string, + proxyAuthentification?: string, + strictSsl: boolean }>({ command: 'download:plugins', describe: 'Download defined external plugins', @@ -223,10 +229,21 @@ async function theiaCli(): Promise { alias: 'u', describe: 'Open-VSX Registry API URL', default: 'https://open-vsx.org/api' + }, + 'proxy-url': { + describe: 'Proxy URL' + }, + 'proxy-authentification': { + describe: 'Proxy authentification information' + }, + 'strict-ssl': { + describe: 'Whether to enable strict SSL mode', + boolean: true, + default: false } }, - handler: async ({ packed }) => { - await downloadPlugins({ packed }); + handler: async args => { + await downloadPlugins(args); }, }) .command<{ diff --git a/dev-packages/cli/tsconfig.json b/dev-packages/cli/tsconfig.json index 40b4dcb4a6cea..9de5d33701b32 100644 --- a/dev-packages/cli/tsconfig.json +++ b/dev-packages/cli/tsconfig.json @@ -23,6 +23,9 @@ }, { "path": "../ovsx-client" + }, + { + "path": "../request-service" } ] } diff --git a/dev-packages/ovsx-client/package.json b/dev-packages/ovsx-client/package.json index 73620101c119c..3c8719030174d 100644 --- a/dev-packages/ovsx-client/package.json +++ b/dev-packages/ovsx-client/package.json @@ -29,8 +29,7 @@ "watch": "theiaext watch" }, "dependencies": { - "@types/bent": "^7.0.1", - "bent": "^7.1.0", + "@theia/request-service": "1.23.0", "semver": "^5.4.1" } } diff --git a/dev-packages/ovsx-client/src/ovsx-client.spec.ts b/dev-packages/ovsx-client/src/ovsx-client.spec.ts index 23acb6d8af2b5..efa7d80353d5f 100644 --- a/dev-packages/ovsx-client/src/ovsx-client.spec.ts +++ b/dev-packages/ovsx-client/src/ovsx-client.spec.ts @@ -16,6 +16,7 @@ import * as chai from 'chai'; import { OVSXClient } from './ovsx-client'; +import { NodeRequestService } from '@theia/request-service/lib/node-request-service'; import { VSXSearchParam } from './ovsx-types'; const expect = chai.expect; @@ -31,7 +32,7 @@ describe('OVSX Client', () => { client = new OVSXClient({ apiVersion, apiUrl - }); + }, new NodeRequestService()); }); describe('isEngineValid', () => { diff --git a/dev-packages/ovsx-client/src/ovsx-client.ts b/dev-packages/ovsx-client/src/ovsx-client.ts index c964fce383984..11bce0e20e421 100644 --- a/dev-packages/ovsx-client/src/ovsx-client.ts +++ b/dev-packages/ovsx-client/src/ovsx-client.ts @@ -14,7 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as bent from 'bent'; import * as semver from 'semver'; import { VSXAllVersions, @@ -26,9 +25,7 @@ import { VSXSearchParam, VSXSearchResult } from './ovsx-types'; - -const fetchText = bent('GET', 'string', 200); -const fetchJson = bent('GET', { 'Accept': 'application/json' }, 'json', 200); +import { RequestContext, RequestService } from '@theia/request-service'; export interface OVSXClientOptions { apiVersion: string @@ -37,11 +34,19 @@ export interface OVSXClientOptions { export class OVSXClient { - constructor(readonly options: OVSXClientOptions) { } + constructor(readonly options: OVSXClientOptions, readonly request: RequestService) { } async search(param?: VSXSearchParam): Promise { const searchUri = await this.buildSearchUri(param); - return this.fetchJson(searchUri); + try { + return await this.fetchJson(searchUri); + } catch (err) { + return { + error: err?.message || String(err), + offset: 0, + extensions: [] + }; + } } protected async buildSearchUri(param?: VSXSearchParam): Promise { @@ -99,12 +104,19 @@ export class OVSXClient { throw new Error(`Extension with id ${id} not found at ${apiUri}`); } - protected fetchJson(url: string): Promise { - return fetchJson(url) as Promise; + protected async fetchJson(url: string): Promise { + const e = await this.request.request({ + url, + headers: { 'Accept': 'application/json' } + }); + return RequestContext.asJson(e); } - fetchText(url: string): Promise { - return fetchText(url); + async fetchText(url: string): Promise { + const e = await this.request.request({ + url + }); + return RequestContext.asText(e); } /** diff --git a/dev-packages/ovsx-client/tsconfig.json b/dev-packages/ovsx-client/tsconfig.json index b973ddbc673a2..527c38f25ff36 100644 --- a/dev-packages/ovsx-client/tsconfig.json +++ b/dev-packages/ovsx-client/tsconfig.json @@ -8,5 +8,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../request-service" + } + ] } diff --git a/dev-packages/request-service/.eslintrc.js b/dev-packages/request-service/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/dev-packages/request-service/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/dev-packages/request-service/README.md b/dev-packages/request-service/README.md new file mode 100644 index 0000000000000..21f8db4d18f3c --- /dev/null +++ b/dev-packages/request-service/README.md @@ -0,0 +1,29 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - REQUEST SERVICE

+ +
+ +
+ +## Description + +The `@theia/request-service` package is used to send proxy-aware http requests to other service. + +## Additional Information + +- [Theia - GitHub](https://github.com/eclipse-theia/theia) +- [Theia - Website](https://theia-ide.org/) + +## License + +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) + +## Trademark +"Theia" is a trademark of the Eclipse Foundation +https://www.eclipse.org/theia diff --git a/dev-packages/request-service/package.json b/dev-packages/request-service/package.json new file mode 100644 index 0000000000000..821a3389b2978 --- /dev/null +++ b/dev-packages/request-service/package.json @@ -0,0 +1,36 @@ +{ + "name": "@theia/request-service", + "version": "1.23.0", + "description": "Theia Proxy-Aware Request Service", + "publishConfig": { + "access": "public" + }, + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "vscode-languageserver-protocol": "~3.15.3" + } +} diff --git a/dev-packages/request-service/src/index.ts b/dev-packages/request-service/src/index.ts new file mode 100644 index 0000000000000..21f4226d3fb9c --- /dev/null +++ b/dev-packages/request-service/src/index.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export * from './request-service'; diff --git a/dev-packages/request-service/src/node-request-service.ts b/dev-packages/request-service/src/node-request-service.ts new file mode 100644 index 0000000000000..76b9548fe5205 --- /dev/null +++ b/dev-packages/request-service/src/node-request-service.ts @@ -0,0 +1,159 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as http from 'http'; +import * as https from 'https'; +import { parse as parseUrl } from 'url'; +import { getProxyAgent, ProxyAgent } from './proxy'; +import { Headers, RequestConfiguration, RequestContext, RequestOptions, RequestService } from './request-service'; +import { CancellationToken } from 'vscode-languageserver-protocol'; + +export interface RawRequestFunction { + (options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest; +} + +export interface NodeRequestOptions extends RequestOptions { + agent?: ProxyAgent; + strictSSL?: boolean; + getRawRequest?(options: NodeRequestOptions): RawRequestFunction; +}; + +export class NodeRequestService implements RequestService { + + protected proxyUrl?: string; + protected strictSSL?: boolean; + protected authorization?: string; + + protected getNodeRequest(options: RequestOptions): RawRequestFunction { + const endpoint = parseUrl(options.url!); + const module = endpoint.protocol === 'https:' ? https : http; + return module.request; + } + + protected async getProxyUrl(url: string): Promise { + return this.proxyUrl; + } + + async configure(config: RequestConfiguration): Promise { + if ('proxyUrl' in config) { + this.proxyUrl = config.proxyUrl; + } + if ('strictSSL' in config) { + this.strictSSL = config.strictSSL; + } + if ('proxyAuthorization' in config) { + this.authorization = config.proxyAuthorization; + } + } + + protected async processOptions(options: NodeRequestOptions): Promise { + const { strictSSL } = this; + const agent = options.agent ? options.agent : getProxyAgent(options.url || '', process.env, { + proxyUrl: await this.getProxyUrl(options.url), + strictSSL + }); + + options.agent = agent; + options.strictSSL = options.strictSSL ?? strictSSL; + + const authorization = options.proxyAuthorization || this.authorization; + if (authorization) { + options.headers = { + ...(options.headers || {}), + 'Proxy-Authorization': authorization + }; + } + + return options; + } + + request(options: NodeRequestOptions, token = CancellationToken.None): Promise { + return new Promise(async (resolve, reject) => { + options = await this.processOptions(options); + + const endpoint = parseUrl(options.url); + const rawRequest = options.getRawRequest + ? options.getRawRequest(options) + : this.getNodeRequest(options); + + const opts: https.RequestOptions = { + hostname: endpoint.hostname, + port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80), + protocol: endpoint.protocol, + path: endpoint.path, + method: options.type || 'GET', + headers: options.headers, + agent: options.agent, + rejectUnauthorized: !!options.strictSSL + }; + + if (options.user && options.password) { + opts.auth = options.user + ':' + options.password; + } + + const req = rawRequest(opts, async res => { + const followRedirects: number = options.followRedirects ?? 3; + if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && followRedirects > 0 && res.headers['location']) { + this.request({ + ...options, + url: res.headers['location'], + followRedirects: followRedirects - 1 + }, token).then(resolve, reject); + } else { + const chunks: Uint8Array[] = []; + + res.on('data', chunk => { + chunks.push(new Uint8Array(chunk)); + }); + + res.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve({ + res: { + headers: res.headers as Headers, + statusCode: res.statusCode + }, + buffer + }); + }); + + res.on('error', reject); + } + }); + + req.on('error', reject); + + if (options.timeout) { + req.setTimeout(options.timeout); + } + + if (options.data) { + req.write(options.data); + } + + req.end(); + + token.onCancellationRequested(() => { + req.abort(); + reject(); + }); + }); + } + + async resolveProxy(url: string): Promise { + return undefined; + } +} diff --git a/dev-packages/request-service/src/package.spec.ts b/dev-packages/request-service/src/package.spec.ts new file mode 100644 index 0000000000000..827513c5c68f9 --- /dev/null +++ b/dev-packages/request-service/src/package.spec.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('request-service package', () => { + + it('should support code coverage statistics', () => true); +}); diff --git a/dev-packages/request-service/src/proxy.ts b/dev-packages/request-service/src/proxy.ts new file mode 100644 index 0000000000000..5c4c496f16fc9 --- /dev/null +++ b/dev-packages/request-service/src/proxy.ts @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { parse as parseUrl, Url } from 'url'; +import * as httpAgent from 'http-proxy-agent'; +import * as httpsAgent from 'https-proxy-agent'; + +export type ProxyAgent = httpAgent.HttpProxyAgent | httpsAgent.HttpsProxyAgent; + +function getSystemProxyURI(requestURL: Url, env: typeof process.env): string | undefined { + if (requestURL.protocol === 'http:') { + return env.HTTP_PROXY || env.http_proxy; + } else if (requestURL.protocol === 'https:') { + return env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy; + } + + return undefined; +} + +export interface ProxySettings { + proxyUrl?: string; + strictSSL?: boolean; +} + +export function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: ProxySettings = {}): ProxyAgent | undefined { + const requestURL = parseUrl(rawRequestURL); + const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL, env); + + if (!proxyURL) { + return undefined; + } + + const proxyEndpoint = parseUrl(proxyURL); + + if (!/^https?:$/.test(proxyEndpoint.protocol || '')) { + return undefined; + } + + const opts = { + host: proxyEndpoint.hostname || '', + port: proxyEndpoint.port || (proxyEndpoint.protocol === 'https' ? '443' : '80'), + auth: proxyEndpoint.auth, + rejectUnauthorized: !!options.strictSSL, + }; + + const createAgent = requestURL.protocol === 'http:' ? httpAgent : httpsAgent; + return createAgent(opts); +} diff --git a/dev-packages/request-service/src/request-service.ts b/dev-packages/request-service/src/request-service.ts new file mode 100644 index 0000000000000..27a4eae356151 --- /dev/null +++ b/dev-packages/request-service/src/request-service.ts @@ -0,0 +1,92 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { CancellationToken } from 'vscode-languageserver-protocol'; + +const textDecoder = typeof TextDecoder !== 'undefined' ? new TextDecoder() : undefined; + +export interface Headers { + [header: string]: string; +} + +export interface RequestOptions { + type?: string; + url: string; + user?: string; + password?: string; + headers?: Headers; + timeout?: number; + data?: string; + followRedirects?: number; + proxyAuthorization?: string; +} + +export interface RequestContext { + res: { + headers: Headers; + statusCode?: number; + }; + buffer: Uint8Array; +} + +export namespace RequestContext { + export function isSuccess(context: RequestContext): boolean { + return (context.res.statusCode && context.res.statusCode >= 200 && context.res.statusCode < 300) || context.res.statusCode === 1223; + } + + function hasNoContent(context: RequestContext): boolean { + return context.res.statusCode === 204; + } + + export function asText(context: RequestContext): string { + if (!isSuccess(context)) { + throw new Error('Server returned ' + context.res.statusCode); + } + if (hasNoContent(context)) { + return ''; + } + if (textDecoder) { + return textDecoder.decode(context.buffer); + } else { + return context.buffer.toString(); + } + } + + export function asJson(context: RequestContext): T { + const str = asText(context); + try { + return JSON.parse(str); + } catch (err) { + err.message += ':\n' + str; + throw err; + } + } +} + +export interface RequestConfiguration { + proxyUrl?: string; + proxyAuthorization?: string; + strictSSL?: boolean; +} +export interface RequestService { + configure(config: RequestConfiguration): Promise; + request(options: RequestOptions, token?: CancellationToken): Promise; + resolveProxy(url: string): Promise +} + +export const RequestService = Symbol('RequestService'); +export const BackendRequestService = Symbol('BackendRequestService'); +export const REQUEST_SERVICE_PATH = '/services/request-service'; diff --git a/dev-packages/request-service/tsconfig.json b/dev-packages/request-service/tsconfig.json new file mode 100644 index 0000000000000..b973ddbc673a2 --- /dev/null +++ b/dev-packages/request-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [] +} diff --git a/packages/core/README.md b/packages/core/README.md index ce8407ed67b47..c36722f9cdc26 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -89,6 +89,9 @@ export class SomeClass { - `@theia/application-package` (from [`@theia/application-package@1.23.0`](https://www.npmjs.com/package/@theia/application-package/v/1.23.0)) - `@theia/application-package/lib/api` (from [`@theia/application-package@1.23.0`](https://www.npmjs.com/package/@theia/application-package/v/1.23.0)) - `@theia/application-package/lib/environment` (from [`@theia/application-package@1.23.0`](https://www.npmjs.com/package/@theia/application-package/v/1.23.0)) + - `@theia/request-service` (from [`@theia/request-service@1.23.0`](https://www.npmjs.com/package/@theia/request-service/v/1.23.0)) + - `@theia/request-service/lib/proxy` (from [`@theia/request-service@1.23.0`](https://www.npmjs.com/package/@theia/request-service/v/1.23.0)) + - `@theia/request-service/lib/node-request-service` (from [`@theia/request-service@1.23.0`](https://www.npmjs.com/package/@theia/request-service/v/1.23.0)) - `fs-extra` (from [`fs-extra@^4.0.2`](https://www.npmjs.com/package/fs-extra)) - `fuzzy` (from [`fuzzy@^0.1.3`](https://www.npmjs.com/package/fuzzy)) - `inversify` (from [`inversify@^5.1.1`](https://www.npmjs.com/package/inversify)) diff --git a/packages/core/package.json b/packages/core/package.json index 7c9da715a9a06..c79c24f61d0ea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,6 +18,7 @@ "@phosphor/widgets": "1", "@primer/octicons-react": "^9.0.0", "@theia/application-package": "1.23.0", + "@theia/request-service": "1.23.0", "@types/body-parser": "^1.16.4", "@types/cookie": "^0.3.3", "@types/dompurify": "^2.2.2", @@ -45,6 +46,8 @@ "font-awesome": "^4.7.0", "fs-extra": "^4.0.2", "fuzzy": "^0.1.3", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", "iconv-lite": "^0.6.0", "inversify": "^5.1.1", "jschardet": "^2.1.1", @@ -102,6 +105,9 @@ "@theia/application-package", "@theia/application-package/lib/api", "@theia/application-package/lib/environment", + "@theia/request-service", + "@theia/request-service/lib/proxy", + "@theia/request-service/lib/node-request-service", "fs-extra", "fuzzy", "inversify", @@ -149,6 +155,14 @@ { "backend": "lib/node/hosting/backend-hosting-module", "backendElectron": "lib/electron-node/hosting/electron-backend-hosting-module" + }, + { + "frontend": "lib/browser/request/browser-request-module", + "frontendElectron": "lib/electron-browser/request/electron-browser-request-module" + }, + { + "backend": "lib/node/request/backend-request-module", + "backendElectron": "lib/electron-node/request/electron-backend-request-module" } ], "keywords": [ diff --git a/packages/core/shared/@theia/request-service/index.d.ts b/packages/core/shared/@theia/request-service/index.d.ts new file mode 100644 index 0000000000000..3382f7bea029b --- /dev/null +++ b/packages/core/shared/@theia/request-service/index.d.ts @@ -0,0 +1 @@ +export * from '@theia/request-service'; diff --git a/packages/core/shared/@theia/request-service/index.js b/packages/core/shared/@theia/request-service/index.js new file mode 100644 index 0000000000000..8fbf0570f6e2d --- /dev/null +++ b/packages/core/shared/@theia/request-service/index.js @@ -0,0 +1 @@ +module.exports = require('@theia/request-service'); diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index fb32ef4493cb2..373c5125a75b5 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -75,6 +75,44 @@ export const corePreferenceSchema: PreferenceSchema = { markdownDescription: nls.localizeByDefault("Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and a single press of the Alt key will show it. By default, the menu bar will be visible, unless the window is full screen."), included: !isOSX }, + 'http.proxy': { + type: 'string', + pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', + // eslint-disable-next-line max-len + markdownDescription: nls.localizeByDefault('The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables.'), + scope: 'application' + }, + 'http.proxyStrictSSL': { + type: 'boolean', + default: true, + description: nls.localizeByDefault('Controls whether the proxy server certificate should be verified against the list of supplied CAs.'), + scope: 'application' + }, + 'http.proxyAuthorization': { + type: 'string', + markdownDescription: nls.localizeByDefault('The value to send as the `Proxy-Authorization` header for every network request.'), + scope: 'application' + }, + 'http.proxySupport': { + type: 'string', + enum: ['off', 'on', 'fallback', 'override'], + enumDescriptions: [ + nls.localizeByDefault('Disable proxy support for extensions.'), + nls.localizeByDefault('Enable proxy support for extensions.'), + nls.localize('theia/core/proxySupportFallback', 'Enable proxy support for extensions, fall back to request options, when no proxy found.'), + nls.localizeByDefault('Enable proxy support for extensions, override request options.'), + ], + default: 'override', + description: nls.localizeByDefault('Use the proxy support for extensions.'), + scope: 'application' + }, + 'http.systemCertificates': { + type: 'boolean', + default: true, + // eslint-disable-next-line max-len + description: nls.localizeByDefault('Controls whether CA certificates should be loaded from the OS. (On Windows and macOS a reload of the window is required after turning this off.)'), + scope: 'application' + }, 'workbench.list.openMode': { type: 'string', enum: [ diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 0953a71339ace..f17032767a75f 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -119,6 +119,7 @@ import { } from './breadcrumbs'; import { RendererHost } from './widgets'; import { TooltipService, TooltipServiceImpl } from './tooltip-service'; +import { BackendRequestService, RequestService, REQUEST_SERVICE_PATH } from '@theia/request-service'; import { bindFrontendStopwatch, bindBackendStopwatch } from './performance'; import { SaveResourceService } from './save-resource-service'; @@ -394,6 +395,10 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo return child.get(BreadcrumbPopupContainer); }); + bind(BackendRequestService).toDynamicValue(ctx => + WebSocketConnectionProvider.createProxy(ctx.container, REQUEST_SERVICE_PATH) + ).inSingletonScope(); + bindFrontendStopwatch(bind); bindBackendStopwatch(bind); diff --git a/packages/core/src/browser/json-schema-store.ts b/packages/core/src/browser/json-schema-store.ts index 994d9250f5542..efae90e2b7ced 100644 --- a/packages/core/src/browser/json-schema-store.ts +++ b/packages/core/src/browser/json-schema-store.ts @@ -20,6 +20,7 @@ import { FrontendApplicationContribution } from './frontend-application'; import { MaybePromise } from '../common'; import { Endpoint } from './endpoint'; import { timeout, Deferred } from '../common/promise-util'; +import { RequestContext, RequestService } from '@theia/request-service'; export interface JsonSchemaConfiguration { fileMatch: string | string[]; @@ -95,10 +96,13 @@ export class JsonSchemaStore implements FrontendApplicationContribution { @injectable() export class DefaultJsonSchemaContribution implements JsonSchemaContribution { + @inject(RequestService) + protected readonly requestService: RequestService; + async registerSchemas(context: JsonSchemaRegisterContext): Promise { const url = `${new Endpoint().httpScheme}//schemastore.azurewebsites.net/api/json/catalog.json`; - const response = await fetch(url); - const schemas: DefaultJsonSchemaContribution.SchemaData[] = (await response.json()).schemas!; + const response = await this.requestService.request({ url }); + const schemas = (await RequestContext.asJson<{ schemas: DefaultJsonSchemaContribution.SchemaData[] }>(response)).schemas; for (const s of schemas) { if (s.fileMatch) { context.registerSchema({ diff --git a/packages/core/src/browser/request/browser-request-module.ts b/packages/core/src/browser/request/browser-request-module.ts new file mode 100644 index 0000000000000..dd9067af14db8 --- /dev/null +++ b/packages/core/src/browser/request/browser-request-module.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { RequestService } from '@theia/request-service'; +import { XHRBrowserRequestService } from './browser-request-service'; + +export default new ContainerModule(bind => { + bind(RequestService).to(XHRBrowserRequestService).inSingletonScope(); +}); diff --git a/packages/core/src/browser/request/browser-request-service.ts b/packages/core/src/browser/request/browser-request-service.ts new file mode 100644 index 0000000000000..3f4c9172ba4b8 --- /dev/null +++ b/packages/core/src/browser/request/browser-request-service.ts @@ -0,0 +1,167 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable, postConstruct } from 'inversify'; +import { CancellationToken } from 'vscode-languageserver-protocol'; +import { BackendRequestService, RequestConfiguration, RequestContext, RequestOptions, RequestService } from '@theia/request-service'; +import { PreferenceService } from '../preferences/preference-service'; + +@injectable() +export class DefaultBrowserRequestService implements RequestService { + + @inject(BackendRequestService) + protected readonly backendRequestService: RequestService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + protected configurePromise: Promise = Promise.resolve(); + + @postConstruct() + protected init(): void { + this.preferenceService.onPreferencesChanged(e => { + const config: RequestConfiguration = {}; + if ('http.proxy' in e) { + config.proxyUrl = e['http.proxy'].newValue; + } + if ('http.proxyAuthorization' in e) { + config.proxyAuthorization = e['proxyAuthorization'].newValue; + } + if ('http.proxyStrictSSL' in e) { + config.strictSSL = e['http.proxyStrictSSL'].newValue; + } + this.configurePromise = this.configure(config); + }); + } + + configure(config: RequestConfiguration): Promise { + return this.backendRequestService.configure(config); + } + + resolveProxy(url: string): Promise { + return this.backendRequestService.resolveProxy(url); + } + + protected transformBackendResponse(context: RequestContext): RequestContext { + // In the `backend-request-facade` we transform the binary buffer into a base64 string to save space + // We need to tranform it back into a binary buffer here + const transferedBuffer = context.buffer as unknown as string; + context.buffer = Uint8Array.from(atob(transferedBuffer), c => c.charCodeAt(0)); + return context; + } + + async request(options: RequestOptions): Promise { + // Wait for both the preferences and the configuration of the backend service + await this.preferenceService.ready; + await this.configurePromise; + const backendResult = await this.backendRequestService.request(options); + return this.transformBackendResponse(backendResult); + } +} + +@injectable() +export class XHRBrowserRequestService extends DefaultBrowserRequestService { + + protected authorization?: string; + + override configure(config: RequestConfiguration): Promise { + if ('proxyAuthorization' in config) { + this.authorization = config.proxyAuthorization; + } + return super.configure(config); + } + + override async request(options: RequestOptions, token = CancellationToken.None): Promise { + try { + const xhrResult = await this.xhrRequest(options, token); + const statusCode = xhrResult.res.statusCode ?? 200; + if (statusCode >= 400) { + return super.request(options); + } + return xhrResult; + } catch { + return super.request(options); + } + } + + protected xhrRequest(options: RequestOptions, token: CancellationToken): Promise { + const authorization = this.authorization || options.proxyAuthorization; + if (authorization) { + options.headers = { + ...(options.headers || {}), + 'Proxy-Authorization': authorization + }; + } + + const xhr = new XMLHttpRequest(); + return new Promise((resolve, reject) => { + + xhr.open(options.type || 'GET', options.url || '', true, options.user, options.password); + this.setRequestHeaders(xhr, options); + + xhr.responseType = 'arraybuffer'; + xhr.onerror = () => reject(new Error(xhr.statusText && ('XHR failed: ' + xhr.statusText) || 'XHR failed')); + xhr.onload = () => { + resolve({ + res: { + statusCode: xhr.status, + headers: this.getResponseHeaders(xhr) + }, + buffer: new Uint8Array(xhr.response) + }); + }; + xhr.ontimeout = e => reject(new Error(`XHR timeout: ${options.timeout}ms`)); + + if (options.timeout) { + xhr.timeout = options.timeout; + } + + xhr.send(options.data); + + // cancel + token.onCancellationRequested(() => { + xhr.abort(); + reject(); + }); + }); + } + + protected setRequestHeaders(xhr: XMLHttpRequest, options: RequestOptions): void { + if (options.headers) { + outer: for (const k of Object.keys(options.headers)) { + switch (k) { + case 'User-Agent': + case 'Accept-Encoding': + case 'Content-Length': + // unsafe headers + continue outer; + } + xhr.setRequestHeader(k, options.headers[k]); + } + } + } + + protected getResponseHeaders(xhr: XMLHttpRequest): { [name: string]: string } { + const headers: { [name: string]: string } = {}; + for (const line of xhr.getAllResponseHeaders().split(/\r\n|\n|\r/g)) { + if (line) { + const idx = line.indexOf(':'); + headers[line.substring(0, idx).trim().toLowerCase()] = line.substring(idx + 1).trim(); + } + } + return headers; + } +} diff --git a/packages/core/src/electron-browser/request/electron-browser-request-module.ts b/packages/core/src/electron-browser/request/electron-browser-request-module.ts new file mode 100644 index 0000000000000..f26d06a029787 --- /dev/null +++ b/packages/core/src/electron-browser/request/electron-browser-request-module.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { DefaultBrowserRequestService } from '../../browser/request/browser-request-service'; +import { RequestService } from '@theia/request-service'; + +export default new ContainerModule(bind => { + // We bind the non-xhr request service here. This will always proxy every request through the backend. + // We do this since the backend currently cannot automatically resolve proxies, but the frontend can. + // We try to avoid confusion with this where some (frontend) requests successfully go through the proxy, but some others (backend) don't. + bind(RequestService).to(DefaultBrowserRequestService).inSingletonScope(); +}); diff --git a/packages/core/src/electron-node/request/electron-backend-request-module.ts b/packages/core/src/electron-node/request/electron-backend-request-module.ts new file mode 100644 index 0000000000000..bf5376353c4c3 --- /dev/null +++ b/packages/core/src/electron-node/request/electron-backend-request-module.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { RequestService } from '@theia/request-service'; +import { ElectronBackendRequestService } from './electron-backend-request-service'; + +export default new ContainerModule(bind => { + bind(RequestService).to(ElectronBackendRequestService).inSingletonScope(); +}); diff --git a/packages/core/src/electron-node/request/electron-backend-request-service.ts b/packages/core/src/electron-node/request/electron-backend-request-service.ts new file mode 100644 index 0000000000000..9156db4ba8b75 --- /dev/null +++ b/packages/core/src/electron-node/request/electron-backend-request-service.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import 'reflect-metadata'; +import { decorate, injectable } from 'inversify'; +import { NodeRequestService } from '@theia/request-service/lib/node-request-service'; + +decorate(injectable(), NodeRequestService); + +@injectable() +export class ElectronBackendRequestService extends NodeRequestService { + + override async getProxyUrl(url: string): Promise { + if (this.proxyUrl) { + return this.proxyUrl; + } + try { + const proxy = await this.resolveProxy(url); + if (proxy && proxy !== 'DIRECT') { + const proxyHost = proxy.split(' ')[1]; + return this.buildProxyUrl(url, proxyHost); + } + } catch (e) { + console.error('Could not resolve electron proxy.', e); + } + return super.getProxyUrl(url); + } + + override async resolveProxy(url: string): Promise { + // TODO: Implement IPC to the backend to access the Electron proxy resolver + return undefined; + } + + protected buildProxyUrl(url: string, proxyHost: string): string { + if (proxyHost.startsWith('http://') || proxyHost.startsWith('https://')) { + return proxyHost; + } + if (url.startsWith('http://')) { + return 'http://' + proxyHost; + } else if (url.startsWith('https://')) { + return 'https://' + proxyHost; + } + return proxyHost; + } +} diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index 49a0dedad2b18..dba8f03634308 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -16,6 +16,7 @@ import { ContainerModule, decorate, injectable } from 'inversify'; import { ApplicationPackage } from '@theia/application-package'; +import { REQUEST_SERVICE_PATH } from '@theia/request-service'; import { bindContributionProvider, MessageService, MessageClient, ConnectionHandler, JsonRpcConnectionHandler, CommandService, commandServicePath, messageServicePath @@ -35,7 +36,9 @@ import { KeytarServiceImpl } from './keytar-server'; import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter'; import { EnvironmentUtils } from './environment-utils'; import { ProcessUtils } from './process-utils'; +import { ProxyCliContribution } from './request/proxy-cli-contribution'; import { bindNodeStopwatch, bindBackendStopwatchServer } from './performance'; +import { BackendRequestFacade } from './request/backend-request-facade'; decorate(injectable(), ApplicationPackage); @@ -111,6 +114,14 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(EnvironmentUtils).toSelf().inSingletonScope(); bind(ProcessUtils).toSelf().inSingletonScope(); + bind(ProxyCliContribution).toSelf().inSingletonScope(); + bind(CliContribution).toService(ProxyCliContribution); + + bind(BackendRequestFacade).toSelf().inSingletonScope(); + bind(ConnectionHandler).toDynamicValue( + ctx => new JsonRpcConnectionHandler(REQUEST_SERVICE_PATH, () => ctx.container.get(BackendRequestFacade)) + ).inSingletonScope(); + bindNodeStopwatch(bind); bindBackendStopwatchServer(bind); }); diff --git a/packages/core/src/node/request/backend-request-facade.ts b/packages/core/src/node/request/backend-request-facade.ts new file mode 100644 index 0000000000000..1091297a0885c --- /dev/null +++ b/packages/core/src/node/request/backend-request-facade.ts @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { CancellationToken } from 'vscode-languageserver-protocol'; +import { RequestConfiguration, RequestContext, RequestOptions, RequestService } from '@theia/request-service'; + +@injectable() +export class BackendRequestFacade implements RequestService { + + @inject(RequestService) + protected readonly requestService: RequestService; + + configure(config: RequestConfiguration): Promise { + return this.requestService.configure(config); + } + + async request(options: RequestOptions, token?: CancellationToken): Promise { + const context = await this.requestService.request(options, token); + // Convert the buffer to base64 before sending it to the frontend + // This reduces the amount of JSON data transferred massively + const base64Data = Buffer.from(context.buffer).toString('base64'); + Object.assign(context, { + buffer: base64Data + }); + return context; + } + + resolveProxy(url: string): Promise { + return this.requestService.resolveProxy(url); + } + +} diff --git a/packages/core/src/node/request/backend-request-module.ts b/packages/core/src/node/request/backend-request-module.ts new file mode 100644 index 0000000000000..fc1ff158946b9 --- /dev/null +++ b/packages/core/src/node/request/backend-request-module.ts @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule, decorate, injectable } from 'inversify'; +import { RequestService } from '@theia/request-service'; +import { NodeRequestService } from '@theia/request-service/lib/node-request-service'; + +decorate(injectable(), NodeRequestService); + +export default new ContainerModule(bind => { + bind(RequestService).to(NodeRequestService).inSingletonScope(); +}); diff --git a/packages/core/src/node/request/proxy-cli-contribution.ts b/packages/core/src/node/request/proxy-cli-contribution.ts new file mode 100644 index 0000000000000..6798f4f9f9f18 --- /dev/null +++ b/packages/core/src/node/request/proxy-cli-contribution.ts @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { MaybePromise } from '../../common/types'; +import { RequestConfiguration, RequestService } from '@theia/request-service'; +import { Argv, Arguments } from 'yargs'; +import { CliContribution } from '../cli'; + +export const ProxyUrl = 'proxy-url'; +export const ProxyAuthorization = 'proxy-authorization'; +export const StrictSSL = 'strict-ssl'; + +@injectable() +export class ProxyCliContribution implements CliContribution { + + @inject(RequestService) + protected readonly requestService: RequestService; + + configure(conf: Argv): void { + conf.option(ProxyUrl, { + description: 'Sets the proxy URL for outgoing requests.', + type: 'string' + }); + conf.option(ProxyAuthorization, { + description: 'Sets the proxy authorization header for outgoing requests.', + type: 'string' + }); + conf.option(StrictSSL, { + description: 'Detemines whether SSL is strictly set for outgoing requests.', + type: 'boolean' + }); + } + + setArguments(args: Arguments): MaybePromise { + const proxyUrl = args[ProxyUrl]; + const authorization = args[ProxyAuthorization]; + const strictSSL = args[StrictSSL]; + const config: RequestConfiguration = {}; + if (typeof proxyUrl === 'string') { + config.proxyUrl = proxyUrl.trim(); + } + if (typeof authorization === 'string') { + config.proxyAuthorization = authorization; + } + if (typeof strictSSL === 'boolean') { + config.strictSSL = strictSSL; + } + this.requestService.configure(config); + } + +} diff --git a/packages/core/src/node/request/proxy.ts b/packages/core/src/node/request/proxy.ts new file mode 100644 index 0000000000000..5c4c496f16fc9 --- /dev/null +++ b/packages/core/src/node/request/proxy.ts @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { parse as parseUrl, Url } from 'url'; +import * as httpAgent from 'http-proxy-agent'; +import * as httpsAgent from 'https-proxy-agent'; + +export type ProxyAgent = httpAgent.HttpProxyAgent | httpsAgent.HttpsProxyAgent; + +function getSystemProxyURI(requestURL: Url, env: typeof process.env): string | undefined { + if (requestURL.protocol === 'http:') { + return env.HTTP_PROXY || env.http_proxy; + } else if (requestURL.protocol === 'https:') { + return env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy; + } + + return undefined; +} + +export interface ProxySettings { + proxyUrl?: string; + strictSSL?: boolean; +} + +export function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: ProxySettings = {}): ProxyAgent | undefined { + const requestURL = parseUrl(rawRequestURL); + const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL, env); + + if (!proxyURL) { + return undefined; + } + + const proxyEndpoint = parseUrl(proxyURL); + + if (!/^https?:$/.test(proxyEndpoint.protocol || '')) { + return undefined; + } + + const opts = { + host: proxyEndpoint.hostname || '', + port: proxyEndpoint.port || (proxyEndpoint.protocol === 'https' ? '443' : '80'), + auth: proxyEndpoint.auth, + rejectUnauthorized: !!options.strictSSL, + }; + + const createAgent = requestURL.protocol === 'http:' ? httpAgent : httpsAgent; + return createAgent(opts); +} diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index c6e3bd86e52a9..3cdd3fc2eff4b 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -41,6 +41,7 @@ "uuid": "^8.0.0", "vhost": "^3.0.2", "vscode-debugprotocol": "^1.32.0", + "vscode-proxy-agent": "^0.11.0", "vscode-textmate": "^4.0.1" }, "publishConfig": { diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts b/packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts new file mode 100644 index 0000000000000..1da89237c89ba --- /dev/null +++ b/packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts @@ -0,0 +1,92 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as http from 'http'; +import * as https from 'https'; +import * as tls from 'tls'; + +import { createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting } from 'vscode-proxy-agent'; +import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry'; + +export function connectProxyResolver(configProvider: PreferenceRegistryExtImpl): void { + const resolveProxy = createProxyResolver({ + resolveProxy: async url => url, + getHttpProxySetting: () => configProvider.getConfiguration('http').get('proxy'), + log: () => { }, + getLogLevel: () => 0, + proxyResolveTelemetry: () => { }, + useHostProxy: true, + env: process.env, + }); + const lookup = createPatchedModules(configProvider, resolveProxy); + return configureModuleLoading(lookup); +} + +interface PatchedModules { + http: Record; + https: Record; + tls: typeof tls; +} + +function createPatchedModules(configProvider: PreferenceRegistryExtImpl, resolveProxy: ReturnType): PatchedModules { + const proxySetting = { + config: 'off' as ProxySupportSetting + }; + const certSetting = { + config: false + }; + configProvider.onDidChangeConfiguration(() => { + const httpConfig = configProvider.getConfiguration('http'); + proxySetting.config = httpConfig?.get('proxySupport') || 'off'; + certSetting.config = !!httpConfig?.get('systemCertificates'); + }); + + return { + http: { + off: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'off' }, certSetting, true)), + on: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'on' }, certSetting, true)), + override: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'override' }, certSetting, true)), + onRequest: Object.assign({}, http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, true)), + default: Object.assign(http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, false)) // run last + }, + https: { + off: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'off' }, certSetting, true)), + on: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'on' }, certSetting, true)), + override: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'override' }, certSetting, true)), + onRequest: Object.assign({}, https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, true)), + default: Object.assign(https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, false)) // run last + }, + tls: Object.assign(tls, createTlsPatch(tls)) + }; +} + +function configureModuleLoading(lookup: PatchedModules): void { + const node_module = require('module'); + const original = node_module._load; + node_module._load = function (request: string): typeof tls | typeof http | typeof https { + if (request === 'tls') { + return lookup.tls; + } + + if (request !== 'http' && request !== 'https') { + return original.apply(this, arguments); + } + + // Create a shallow copy of the http(s) module to workaround extensions which apply changes to the modules + // See for more info: https://github.com/microsoft/vscode/issues/93167 + return { ...lookup[request].default }; + }; +} diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index dcd4365b8aa2b..d63b22587419c 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -34,6 +34,7 @@ import { WebviewsExtImpl } from '../../plugin/webviews'; import { TerminalServiceExtImpl } from '../../plugin/terminal-ext'; import { SecretsExtImpl } from '../../plugin/secrets-ext'; import { BackendInitializationFn } from '../../common'; +import { connectProxyResolver } from './plugin-host-proxy'; /** * Handle the RPC calls. @@ -81,6 +82,7 @@ export class PluginHostRPC { clipboardExt, webviewExt ); + connectProxyResolver(preferenceRegistryExt); } async terminate(): Promise { diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index 4e270e3ff7311..29915e8b58c28 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -157,10 +157,10 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi this.fileExists = false; return { value: '' }; }); - this.readPreferencesFromContent(content.value); + await this.readPreferencesFromContent(content.value); } - protected readPreferencesFromContent(content: string): void { + protected async readPreferencesFromContent(content: string): Promise { let preferencesInJson; try { preferencesInJson = this.parse(content); @@ -168,7 +168,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi preferencesInJson = {}; } const parsedPreferences = this.getParsedContent(preferencesInJson); - this.handlePreferenceChanges(parsedPreferences); + await this.handlePreferenceChanges(parsedPreferences); } protected parse(content: string): any { @@ -180,7 +180,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi return jsoncparser.parse(strippedContent); } - protected handlePreferenceChanges(newPrefs: { [key: string]: any }): void { + protected async handlePreferenceChanges(newPrefs: { [key: string]: any }): Promise { const oldPrefs = Object.assign({}, this.preferences); this.preferences = newPrefs; const prefNames = new Set([...Object.keys(oldPrefs), ...Object.keys(newPrefs)]); @@ -206,11 +206,11 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } if (prefChanges.length > 0) { - this.emitPreferencesChangedEvent(prefChanges); + await this.emitPreferencesChangedEvent(prefChanges); } } - protected reset(): void { + protected async reset(): Promise { const preferences = this.preferences; this.preferences = {}; const changes: PreferenceProviderDataChange[] = []; @@ -223,7 +223,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } } if (changes.length > 0) { - this.emitPreferencesChangedEvent(changes); + await this.emitPreferencesChangedEvent(changes); } } } diff --git a/packages/preferences/src/browser/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts index 3f3715cb3077d..42cb914229e63 100644 --- a/packages/preferences/src/browser/util/preference-tree-generator.ts +++ b/packages/preferences/src/browser/util/preference-tree-generator.ts @@ -58,6 +58,7 @@ export class PreferenceTreeGenerator { ['files', 'editor'], ['hosted-plugin', 'features'], ['keyboard', 'application'], + ['http', 'application'], ['output', 'features'], ['problems', 'features'], ['preview', 'features'], diff --git a/packages/vsx-registry/src/browser/vsx-extensions-model.ts b/packages/vsx-registry/src/browser/vsx-extensions-model.ts index 6a73ca1c865e6..03fb0fefbe0e8 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-model.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-model.ts @@ -118,6 +118,11 @@ export class VSXExtensionsModel { return this._installed.has(id); } + protected _searchError?: string; + get searchError(): string | undefined { + return this._searchError; + } + protected _searchResult = new Set(); get searchResult(): IterableIterator { return this._searchResult.values(); @@ -173,6 +178,7 @@ export class VSXExtensionsModel { } const client = await this.clientProvider(); const result = await client.search(param); + this._searchError = result.error; if (token.isCancellationRequested) { return; } diff --git a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts index b921ea2065e3b..9c3dfb3d9ddd6 100644 --- a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts +++ b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts @@ -35,10 +35,11 @@ import { bindExtensionPreferences } from './recommended-extensions/recommended-e import { bindPreferenceProviderOverrides } from './recommended-extensions/preference-provider-overrides'; import { OVSXClientProvider, createOVSXClient } from '../common/ovsx-client-provider'; import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment'; +import { RequestService } from '@theia/core/shared/@theia/request-service'; export default new ContainerModule((bind, unbind) => { bind(OVSXClientProvider).toDynamicValue(ctx => { - const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment)); + const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment), ctx.container.get(RequestService)); return () => clientPromise; }).inSingletonScope(); bind(VSXEnvironment).toDynamicValue( diff --git a/packages/vsx-registry/src/common/ovsx-client-provider.ts b/packages/vsx-registry/src/common/ovsx-client-provider.ts index cc597bb8b0192..07ecbde8783fb 100644 --- a/packages/vsx-registry/src/common/ovsx-client-provider.ts +++ b/packages/vsx-registry/src/common/ovsx-client-provider.ts @@ -14,16 +14,17 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { RequestService } from '@theia/core/shared/@theia/request-service'; import { OVSXClient } from '@theia/ovsx-client'; import { VSXEnvironment } from './vsx-environment'; export const OVSXClientProvider = Symbol('OVSXClientProvider'); export type OVSXClientProvider = () => Promise; -export async function createOVSXClient(vsxEnvironment: VSXEnvironment): Promise { +export async function createOVSXClient(vsxEnvironment: VSXEnvironment, requestService: RequestService): Promise { const [apiVersion, apiUrl] = await Promise.all([ vsxEnvironment.getVscodeApiVersion(), vsxEnvironment.getRegistryApiUri() ]); - return new OVSXClient({ apiVersion, apiUrl: apiUrl.toString() }); + return new OVSXClient({ apiVersion, apiUrl: apiUrl.toString() }, requestService); } diff --git a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts index 9e9c19e2477b6..93d247765880e 100644 --- a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts +++ b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts @@ -21,10 +21,11 @@ import { OVSXClientProvider, createOVSXClient } from '../common/ovsx-client-prov import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment'; import { VSXEnvironmentImpl } from './vsx-environment-impl'; import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; +import { RequestService } from '@theia/core/shared/@theia/request-service'; export default new ContainerModule(bind => { bind(OVSXClientProvider).toDynamicValue(ctx => { - const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment)); + const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment), ctx.container.get(RequestService)); return () => clientPromise; }).inSingletonScope(); bind(VSXEnvironment).to(VSXEnvironmentImpl).inSingletonScope(); diff --git a/tsconfig.json b/tsconfig.json index b9997bb80a9d2..940cf0544fcf4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,9 @@ { "path": "dev-packages/private-re-exports" }, + { + "path": "dev-packages/request-service" + }, { "path": "examples/api-samples" }, diff --git a/yarn.lock b/yarn.lock index 9f56f314ef37e..da681b5acd7c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2157,11 +2157,16 @@ resolved "https://registry.yarnpkg.com/@theia/monaco-editor-core/-/monaco-editor-core-1.65.2.tgz#91bc9ce2afe1b6011789ce83a5bee898f0153430" integrity sha512-2UmGjcEW/YpZ2DsFuVevKR3CBMe44Rd6DgwP/5s4pyOe6K/s6TKY7Sh24lO0BXetQKofAEx3zh+ldEvjwhNwDw== -"@tootallnate/once@1": +"@tootallnate/once@1", "@tootallnate/once@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@types/bent@^7.0.1": version "7.3.2" resolved "https://registry.yarnpkg.com/@types/bent/-/bent-7.3.2.tgz#07b4f7bcec577be27cdb9e9034eb0de0242481a7" @@ -4520,6 +4525,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== + data-urls@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" @@ -5668,6 +5678,11 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +file-uri-to-path@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" + integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== + filename-reserved-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" @@ -5927,6 +5942,14 @@ fstream@^1.0.12: mkdirp ">=0.5 0" rimraf "2" +ftp@^0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" + integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0= + dependencies: + readable-stream "1.1.x" + xregexp "2.0.0" + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -6049,6 +6072,18 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-uri@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-3.0.2.tgz#f0ef1356faabc70e1f9404fa3b66b2ba9bfc725c" + integrity sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg== + dependencies: + "@tootallnate/once" "1" + data-uri-to-buffer "3" + debug "4" + file-uri-to-path "2" + fs-extra "^8.1.0" + ftp "^0.3.10" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -6406,6 +6441,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -8152,7 +8196,7 @@ node-addon-api@*: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-addon-api@^3.0.0, node-addon-api@^3.1.0: +node-addon-api@^3.0.0, node-addon-api@^3.0.2, node-addon-api@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== @@ -11549,6 +11593,21 @@ vscode-oniguruma@^1.6.1: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz#aeb9771a2f1dbfc9083c8a7fdd9cccaa3f386607" integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== +vscode-proxy-agent@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.11.0.tgz#9dc8d2bb9d448f1e33bb1caef97a741289660f2f" + integrity sha512-Y5mHjDGq/OKOvKG0IwCYfj25cvQ2cLEil8ce8n55IZHRAP9RF3e1sKU4ZUNDB8X2NIpKwyltrWpK9tFFE/kc3g== + dependencies: + "@tootallnate/once" "^1.1.2" + agent-base "^6.0.2" + debug "^4.3.1" + get-uri "^3.0.2" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + socks-proxy-agent "^5.0.0" + optionalDependencies: + vscode-windows-ca-certs "^0.3.0" + vscode-ripgrep@^1.2.4: version "1.13.2" resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.13.2.tgz#8ccebc33f14d54442c4b11962aead163c55b506e" @@ -11574,6 +11633,13 @@ vscode-uri@^2.1.1: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c" integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A== +vscode-windows-ca-certs@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/vscode-windows-ca-certs/-/vscode-windows-ca-certs-0.3.0.tgz#324e1f8ba842bbf048a39e7c0ee8fe655e9adfcc" + integrity sha512-CYrpCEKmAFQJoZNReOrelNL+VKyebOVRCqL9evrBlVcpWQDliliJgU5RggGS8FPGtQ3jAKLQt9frF0qlxYYPKA== + dependencies: + node-addon-api "^3.0.2" + vscode-ws-jsonrpc@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-0.2.0.tgz#5e9c26e10da54a1a235da7d59e74508bbcb8edd9" @@ -11952,6 +12018,11 @@ xmlhttprequest-ssl@~2.0.0: resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== +xregexp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" + integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= + xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"