diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json index 2bef9332c527c..d2a1554dce42d 100644 --- a/dev-packages/cli/package.json +++ b/dev-packages/cli/package.json @@ -35,6 +35,7 @@ "@theia/ffmpeg": "1.25.0", "@theia/localization-manager": "1.25.0", "@theia/ovsx-client": "1.25.0", + "@theia/request": "1.25.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 4cfc228677666..9a0cee36b730c 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/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'; temp.track(); @@ -71,6 +66,10 @@ export interface DownloadPluginsOptions { * Fetch plugins in parallel */ parallel?: boolean; + + proxyUrl?: string; + proxyAuthorization?: string; + strictSsl?: boolean; } interface PluginDownload { @@ -79,15 +78,26 @@ interface PluginDownload { version?: string | undefined } +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', - parallel = true + parallel = true, + 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[] = []; @@ -133,7 +143,7 @@ export default async function downloadPlugins(options: DownloadPluginsOptions = await downloader(pluginsToDownload); const handleDependencyList = async (dependencies: Array) => { - 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(dependencies.flat()); await parallelOrSequence(...Array.from(ids, id => async () => { @@ -210,7 +220,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) { @@ -218,12 +228,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; } @@ -236,20 +249,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); } 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, response.buffer); + await decompress(tempFile, targetPath); } console.warn(chalk.green(`+ ${plugin}${version ? `@${version}` : ''}: downloaded successfully ${attempts > 1 ? `(after ${attempts} attempts)` : ''}`)); @@ -265,18 +277,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 d3507eea1e844..6d266c519fe8f 100644 --- a/dev-packages/cli/src/theia.ts +++ b/dev-packages/cli/src/theia.ts @@ -197,11 +197,14 @@ async function theiaCli(): Promise { } }) .command<{ - packed: boolean, - ignoreErrors: boolean, - apiVersion: string, - apiUrl: string, + packed: boolean + ignoreErrors: boolean + apiVersion: string + apiUrl: string parallel: boolean + proxyUrl?: string + proxyAuthentification?: string + strictSsl: boolean }>({ command: 'download:plugins', describe: 'Download defined external plugins', @@ -232,10 +235,21 @@ async function theiaCli(): Promise { describe: 'Download in parallel', boolean: true, default: true + }, + '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, ignoreErrors, apiVersion, apiUrl, parallel }) => { - await downloadPlugins({ packed, ignoreErrors, apiVersion, apiUrl, parallel }); + handler: async args => { + await downloadPlugins(args); }, }) .command<{ diff --git a/dev-packages/cli/tsconfig.json b/dev-packages/cli/tsconfig.json index 40b4dcb4a6cea..306d9ef99b167 100644 --- a/dev-packages/cli/tsconfig.json +++ b/dev-packages/cli/tsconfig.json @@ -23,6 +23,9 @@ }, { "path": "../ovsx-client" + }, + { + "path": "../request" } ] } diff --git a/dev-packages/ovsx-client/package.json b/dev-packages/ovsx-client/package.json index 1d6c591c92379..9c5e1d90fd65d 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": "1.25.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..8e49f6efba1f6 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/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..d625cd0477bf5 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'; export interface OVSXClientOptions { apiVersion: string @@ -37,11 +34,19 @@ export interface OVSXClientOptions { export class OVSXClient { - constructor(readonly options: OVSXClientOptions) { } + constructor(readonly options: OVSXClientOptions, protected 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,17 @@ 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 requestContext = await this.request.request({ + url, + headers: { 'Accept': 'application/json' } + }); + return RequestContext.asJson(requestContext); } - fetchText(url: string): Promise { - return fetchText(url); + async fetchText(url: string): Promise { + const requestContext = await this.request.request({ url }); + return RequestContext.asText(requestContext); } /** diff --git a/dev-packages/ovsx-client/tsconfig.json b/dev-packages/ovsx-client/tsconfig.json index b973ddbc673a2..1699f09aff7b5 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" + } + ] } diff --git a/dev-packages/request/.eslintrc.js b/dev-packages/request/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/dev-packages/request/.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/README.md b/dev-packages/request/README.md new file mode 100644 index 0000000000000..bd9f1db2623c1 --- /dev/null +++ b/dev-packages/request/README.md @@ -0,0 +1,29 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - REQUEST

+ +
+ +
+ +## Description + +The `@theia/request` package is used to send proxy-aware http requests to other services. + +## 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/package.json b/dev-packages/request/package.json new file mode 100644 index 0000000000000..1d31433bb3d2d --- /dev/null +++ b/dev-packages/request/package.json @@ -0,0 +1,35 @@ +{ + "name": "@theia/request", + "version": "1.25.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" + } +} diff --git a/dev-packages/request/src/common-request-service.ts b/dev-packages/request/src/common-request-service.ts new file mode 100644 index 0000000000000..e56c0a05f1b01 --- /dev/null +++ b/dev-packages/request/src/common-request-service.ts @@ -0,0 +1,127 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +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 { + url: string; + res: { + headers: Headers; + statusCode?: number; + }; + /** + * Contains the data returned by the request. + * + * If the request was transferred from the backend to the frontend, the buffer has been compressed into a string. In every case the buffer is an {@link Uint8Array}. + */ + buffer: Uint8Array | string; +} + +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 code ${context.res.statusCode} for request to '${context.url}'`); + } + if (hasNoContent(context)) { + return ''; + } + // Ensures that the buffer is an Uint8Array + context = decompress(context); + if (textDecoder) { + return textDecoder.decode(context.buffer as Uint8Array); + } 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; + } + } + + /** + * Convert the buffer to base64 before sending it to the frontend. + * This reduces the amount of JSON data transferred massively. + * Does nothing if the buffer is already compressed. + */ + export function compress(context: RequestContext): RequestContext { + if (context.buffer instanceof Uint8Array && Buffer !== undefined) { + context.buffer = Buffer.from(context.buffer).toString('base64'); + } + return context; + } + + /** + * Decompresses a base64 buffer into a normal array buffer + * Does nothing if the buffer is not compressed. + */ + export function decompress(context: RequestContext): RequestContext { + const buffer = context.buffer; + if (typeof buffer === 'string' && typeof atob === 'function') { + context.buffer = Uint8Array.from(atob(buffer), c => c.charCodeAt(0)); + } + return context; + } +} + +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'; + +export interface CancellationToken { + readonly isCancellationRequested: boolean; + readonly onCancellationRequested: (listener: () => void) => void; +} diff --git a/dev-packages/request/src/index.ts b/dev-packages/request/src/index.ts new file mode 100644 index 0000000000000..d0518a4332d0f --- /dev/null +++ b/dev-packages/request/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 './common-request-service'; diff --git a/dev-packages/request/src/node-request-service.ts b/dev-packages/request/src/node-request-service.ts new file mode 100644 index 0000000000000..06a8ac6b0d5bf --- /dev/null +++ b/dev-packages/request/src/node-request-service.ts @@ -0,0 +1,157 @@ +/******************************************************************************** + * 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 { getProxyAgent, ProxyAgent } from './proxy'; +import { Headers, RequestConfiguration, RequestContext, RequestOptions, RequestService, CancellationToken } from './common-request-service'; + +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 = new URL(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 (config.proxyUrl !== undefined) { + this.proxyUrl = config.proxyUrl; + } + if (config.strictSSL !== undefined) { + this.strictSSL = config.strictSSL; + } + if (config.proxyAuthorization !== undefined) { + this.authorization = config.proxyAuthorization; + } + } + + protected async processOptions(options: NodeRequestOptions): Promise { + const { strictSSL } = this; + options.strictSSL = options.strictSSL ?? strictSSL; + const agent = options.agent ? options.agent : getProxyAgent(options.url || '', process.env, { + proxyUrl: await this.getProxyUrl(options.url), + strictSSL: options.strictSSL + }); + options.agent = agent; + + const authorization = options.proxyAuthorization || this.authorization; + if (authorization) { + options.headers = { + ...(options.headers || {}), + 'Proxy-Authorization': authorization + }; + } + + return options; + } + + request(options: NodeRequestOptions, token?: CancellationToken): Promise { + return new Promise(async (resolve, reject) => { + options = await this.processOptions(options); + + const endpoint = new URL(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.pathname + endpoint.search, + 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 = 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(chunk); + }); + + res.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve({ + url: options.url, + 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/src/package.spec.ts b/dev-packages/request/src/package.spec.ts new file mode 100644 index 0000000000000..8bdd97ff579bf --- /dev/null +++ b/dev-packages/request/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 package', () => { + + it('should support code coverage statistics', () => true); +}); diff --git a/dev-packages/request/src/proxy.ts b/dev-packages/request/src/proxy.ts new file mode 100644 index 0000000000000..5c4c496f16fc9 --- /dev/null +++ b/dev-packages/request/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/tsconfig.json b/dev-packages/request/tsconfig.json new file mode 100644 index 0000000000000..b973ddbc673a2 --- /dev/null +++ b/dev-packages/request/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 64537785cf2f5..0764fe91fd53f 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.25.0`](https://www.npmjs.com/package/@theia/application-package/v/1.25.0)) - `@theia/application-package/lib/api` (from [`@theia/application-package@1.25.0`](https://www.npmjs.com/package/@theia/application-package/v/1.25.0)) - `@theia/application-package/lib/environment` (from [`@theia/application-package@1.25.0`](https://www.npmjs.com/package/@theia/application-package/v/1.25.0)) + - `@theia/request` (from [`@theia/request@1.25.0`](https://www.npmjs.com/package/@theia/request/v/1.25.0)) + - `@theia/request/lib/proxy` (from [`@theia/request@1.25.0`](https://www.npmjs.com/package/@theia/request/v/1.25.0)) + - `@theia/request/lib/node-request-service` (from [`@theia/request@1.25.0`](https://www.npmjs.com/package/@theia/request/v/1.25.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 733a8b75400f9..b3b62b1e9ae17 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,6 +17,7 @@ "@phosphor/virtualdom": "1", "@phosphor/widgets": "1", "@theia/application-package": "1.25.0", + "@theia/request": "1.25.0", "@types/body-parser": "^1.16.4", "@types/cookie": "^0.3.3", "@types/dompurify": "^2.2.2", @@ -44,6 +45,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", @@ -101,6 +104,9 @@ "@theia/application-package", "@theia/application-package/lib/api", "@theia/application-package/lib/environment", + "@theia/request", + "@theia/request/lib/proxy", + "@theia/request/lib/node-request-service", "fs-extra", "fuzzy", "inversify", @@ -148,6 +154,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/index.d.ts b/packages/core/shared/@theia/request/index.d.ts new file mode 100644 index 0000000000000..3b1a868b68f48 --- /dev/null +++ b/packages/core/shared/@theia/request/index.d.ts @@ -0,0 +1 @@ +export * from '@theia/request'; diff --git a/packages/core/shared/@theia/request/index.js b/packages/core/shared/@theia/request/index.js new file mode 100644 index 0000000000000..1e4a387d72e64 --- /dev/null +++ b/packages/core/shared/@theia/request/index.js @@ -0,0 +1 @@ +module.exports = require('@theia/request'); diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index 5ec1b9de1226b..8dc8ab4bb15fd 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 55cfb684e7c48..667e52624aa94 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'; import { bindFrontendStopwatch, bindBackendStopwatch } from './performance'; import { SaveResourceService } from './save-resource-service'; import { UserWorkingDirectoryProvider } from './user-working-directory-provider'; @@ -407,6 +408,10 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is 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..06e30099453f7 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'; 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 = RequestContext.asJson<{ schemas: DefaultJsonSchemaContribution.SchemaData[] }>(response).schemas; for (const s of schemas) { if (s.fileMatch) { context.registerSchema({ @@ -120,4 +124,3 @@ export namespace DefaultJsonSchemaContribution { schema: any; } } - 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..836c80c8bbb02 --- /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'; +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..78e9e26594d60 --- /dev/null +++ b/packages/core/src/browser/request/browser-request-service.ts @@ -0,0 +1,172 @@ +/******************************************************************************** + * 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 { BackendRequestService, RequestConfiguration, RequestContext, RequestOptions, RequestService, CancellationToken } from '@theia/request'; +import { PreferenceService } from '../preferences/preference-service'; + +@injectable() +export abstract class AbstractBrowserRequestService implements RequestService { + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + protected configurePromise: Promise = Promise.resolve(); + + @postConstruct() + protected init(): void { + this.configurePromise = this.preferenceService.ready.then(() => { + const proxyUrl = this.preferenceService.get('http.proxy') as string; + const proxyAuthorization = this.preferenceService.get('http.proxyAuthorization') as string; + const strictSSL = this.preferenceService.get('http.proxyStrictSSL') as boolean; + return this.configure({ + proxyUrl, + proxyAuthorization, + strictSSL + }); + }); + this.preferenceService.onPreferencesChanged(e => { + this.configurePromise.then(() => this.configure({ + proxyUrl: e['http.proxy']?.newValue, + proxyAuthorization: e['http.proxyAuthorization']?.newValue, + strictSSL: e['http.proxyStrictSSL']?.newValue + })); + }); + } + + abstract configure(config: RequestConfiguration): Promise; + abstract request(options: RequestOptions, token?: CancellationToken): Promise; + abstract resolveProxy(url: string): Promise; + +} + +@injectable() +export class ProxyingBrowserRequestService extends AbstractBrowserRequestService { + + @inject(BackendRequestService) + protected readonly backendRequestService: RequestService; + + configure(config: RequestConfiguration): Promise { + return this.backendRequestService.configure(config); + } + + resolveProxy(url: string): Promise { + return this.backendRequestService.resolveProxy(url); + } + + async request(options: RequestOptions): Promise { + // Wait for both the preferences and the configuration of the backend service + await this.configurePromise; + const backendResult = await this.backendRequestService.request(options); + return RequestContext.decompress(backendResult); + } +} + +@injectable() +export class XHRBrowserRequestService extends ProxyingBrowserRequestService { + + protected authorization?: string; + + override configure(config: RequestConfiguration): Promise { + if (config.proxyAuthorization !== undefined) { + this.authorization = config.proxyAuthorization; + } + return super.configure(config); + } + + override async request(options: RequestOptions, token?: CancellationToken): Promise { + try { + const xhrResult = await this.xhrRequest(options, token); + const statusCode = xhrResult.res.statusCode ?? 200; + if (statusCode >= 400) { + // We might've been blocked by the firewall + // Try it through the backend request service + 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({ + url: options.url, + 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); + + token?.onCancellationRequested(() => { + xhr.abort(); + reject(); + }); + }); + } + + protected setRequestHeaders(xhr: XMLHttpRequest, options: RequestOptions): void { + if (options.headers) { + for (const k of Object.keys(options.headers)) { + switch (k) { + case 'User-Agent': + case 'Accept-Encoding': + case 'Content-Length': + // unsafe headers + continue; + } + 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..d64de16afbd06 --- /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 { ProxyingBrowserRequestService } from '../../browser/request/browser-request-service'; +import { RequestService } from '@theia/request'; + +export default new ContainerModule(bind => { + // This version of the request service 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(ProxyingBrowserRequestService).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..fce38d11cbeaa --- /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'; +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..562f6b6d2b1cc --- /dev/null +++ b/packages/core/src/electron-node/request/electron-backend-request-service.ts @@ -0,0 +1,57 @@ +/******************************************************************************** + * 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 { decorate, injectable } from 'inversify'; +import { NodeRequestService } from '@theia/request/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 9fe09a305b551..c463498902441 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'; import { bindContributionProvider, MessageService, MessageClient, ConnectionHandler, JsonRpcConnectionHandler, CommandService, commandServicePath, messageServicePath @@ -35,8 +36,10 @@ 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 { OSBackendApplicationContribution } from './os-backend-application-contribution'; +import { BackendRequestFacade } from './request/backend-request-facade'; decorate(injectable(), ApplicationPackage); @@ -115,6 +118,14 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(OSBackendApplicationContribution).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(OSBackendApplicationContribution); + 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..b212772b33489 --- /dev/null +++ b/packages/core/src/node/request/backend-request-facade.ts @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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 { RequestConfiguration, RequestContext, RequestOptions, RequestService } from '@theia/request'; + +@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): Promise { + const context = await this.requestService.request(options); + return RequestContext.compress(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..10397317ddd1b --- /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'; +import { NodeRequestService } from '@theia/request/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..4b034d4d14780 --- /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'; +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/plugin-ext/package.json b/packages/plugin-ext/package.json index b36217af489d0..03f78a838a1bf 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..02fb06d621dd2 --- /dev/null +++ b/packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts @@ -0,0 +1,80 @@ +/******************************************************************************** + * 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); + configureModuleLoading(lookup); +} + +interface PatchedModules { + http: typeof http; + https: typeof https; + 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: Object.assign(http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, true)), + https: Object.assign(https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, true)), + 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 work around extensions that apply changes to the modules + // See for more info: https://github.com/microsoft/vscode/issues/93167 + return { ...lookup[request] }; + }; +} 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 44a24aa0ef737..b1658b427b248 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/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts index 3f3715cb3077d..7aa8f7ef120cf 100644 --- a/packages/preferences/src/browser/util/preference-tree-generator.ts +++ b/packages/preferences/src/browser/util/preference-tree-generator.ts @@ -57,6 +57,7 @@ export class PreferenceTreeGenerator { ['extensions', 'features'], ['files', 'editor'], ['hosted-plugin', 'features'], + ['http', 'application'], ['keyboard', 'application'], ['output', 'features'], ['problems', 'features'], diff --git a/packages/vsx-registry/package.json b/packages/vsx-registry/package.json index b7a233cc27d76..d6c4ef522bff1 100644 --- a/packages/vsx-registry/package.json +++ b/packages/vsx-registry/package.json @@ -12,7 +12,6 @@ "@theia/workspace": "1.25.0", "@types/showdown": "^1.7.1", "p-debounce": "^2.1.0", - "requestretry": "^7.0.0", "semver": "^5.4.1", "showdown": "^1.9.1", "uuid": "^8.0.0" @@ -51,8 +50,7 @@ "watch": "theiaext watch" }, "devDependencies": { - "@theia/ext-scripts": "1.25.0", - "@types/requestretry": "^1.12.8" + "@theia/ext-scripts": "1.25.0" }, "nyc": { "extends": "../../configs/nyc.json" 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..4331e9c9a9f92 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'; 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..c7edb6454d953 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'; 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-extension-resolver.ts b/packages/vsx-registry/src/node/vsx-extension-resolver.ts index 3b8e06f0ebb0d..8d4e7b42ede83 100644 --- a/packages/vsx-registry/src/node/vsx-extension-resolver.ts +++ b/packages/vsx-registry/src/node/vsx-extension-resolver.ts @@ -19,13 +19,13 @@ import * as path from 'path'; import * as semver from 'semver'; import * as fs from '@theia/core/shared/fs-extra'; import { v4 as uuidv4 } from 'uuid'; -import * as requestretry from 'requestretry'; import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { PluginDeployerHandler, PluginDeployerResolver, PluginDeployerResolverContext } from '@theia/plugin-ext/lib/common/plugin-protocol'; import { VSXExtensionUri } from '../common/vsx-extension-uri'; import { OVSXClientProvider } from '../common/ovsx-client-provider'; import { VSXExtensionRaw } from '@theia/ovsx-client'; +import { RequestService } from '@theia/core/shared/@theia/request'; @injectable() export class VSXExtensionResolver implements PluginDeployerResolver { @@ -36,6 +36,9 @@ export class VSXExtensionResolver implements PluginDeployerResolver { @inject(PluginDeployerHandler) protected pluginDeployerHandler: PluginDeployerHandler; + @inject(RequestService) + protected requestService: RequestService; + protected readonly downloadPath: string; constructor() { @@ -95,23 +98,14 @@ export class VSXExtensionResolver implements PluginDeployerResolver { } protected async download(downloadUrl: string, downloadPath: string): Promise { - return new Promise((resolve, reject) => { - requestretry(downloadUrl, { - method: 'GET', - maxAttempts: 5, - retryDelay: 2000, - retryStrategy: requestretry.RetryStrategies.HTTPOrNetworkError - }, (err, response) => { - if (err) { - reject(err); - } else if (response && response.statusCode === 404) { - resolve(false); - } else if (response && response.statusCode !== 200) { - reject(new Error(response.statusMessage)); - } - }).pipe(fs.createWriteStream(downloadPath)) - .on('error', reject) - .on('close', () => resolve(true)); - }); + const context = await this.requestService.request({ url: downloadUrl }); + if (context.res.statusCode === 404) { + return false; + } else if (context.res.statusCode !== 200) { + throw new Error('Request returned status code: ' + context.res.statusCode); + } else { + await fs.writeFile(downloadPath, context.buffer); + return true; + } } } 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..3b1908d23d39f 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'; 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..0662e48a95f62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,9 @@ { "path": "dev-packages/private-re-exports" }, + { + "path": "dev-packages/request" + }, { "path": "examples/api-samples" }, diff --git a/yarn.lock b/yarn.lock index 52802cb3c200a..f44ba5149dd53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2142,11 +2142,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" @@ -2563,7 +2568,7 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/request@*", "@types/request@^2.0.3": +"@types/request@^2.0.3": version "2.48.8" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ== @@ -2573,14 +2578,6 @@ "@types/tough-cookie" "*" form-data "^2.5.0" -"@types/requestretry@^1.12.8": - version "1.12.8" - resolved "https://registry.yarnpkg.com/@types/requestretry/-/requestretry-1.12.8.tgz#d3f02ee0b4d18e1cee15324506704052a25e1ebd" - integrity sha512-NKdv2WmEe6/V1PmJrflfvGVN9bvuOsnHmNRC4m8TFXPMQnRFKvnnkFN4I6AxAtw4hUi2h3A+z/aH+ir2HYTNOg== - dependencies: - "@types/node" "*" - "@types/request" "*" - "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -4538,6 +4535,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" @@ -5704,6 +5706,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" @@ -5963,6 +5970,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" @@ -6090,6 +6105,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" @@ -6465,6 +6492,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" @@ -8214,7 +8250,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== @@ -11627,6 +11663,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" @@ -11652,6 +11703,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" @@ -12030,6 +12088,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"