mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-18 05:06:39 +01:00
Remove remaining deprecated URL parsing
Migrate the server, remote CLI, browser integration harness, and Seti update script from url.parse() to the WHATWG URL API while preserving repeated-query handling and rejecting malformed request targets.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
const minimatch = require('minimatch');
|
||||
|
||||
// list of languagesId not shipped with VSCode. The information is used to associate an icon with a language association
|
||||
@@ -97,10 +96,8 @@ function download(source) {
|
||||
return readFile(source);
|
||||
}
|
||||
return new Promise((c, e) => {
|
||||
const _url = url.parse(source);
|
||||
const options = { host: _url.host, port: _url.port, path: _url.path, headers: { 'User-Agent': 'NodeJS' } };
|
||||
let content = '';
|
||||
https.get(options, function (response) {
|
||||
https.get(new URL(source), { headers: { 'User-Agent': 'NodeJS' } }, function (response) {
|
||||
response.on('data', function (data) {
|
||||
content += data.toString();
|
||||
}).on('end', function () {
|
||||
@@ -474,4 +471,3 @@ if (path.basename(process.argv[1]) === 'update-icon-theme.js') {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import type * as http from 'http';
|
||||
import * as net from 'net';
|
||||
import { createRequire } from 'node:module';
|
||||
import { performance } from 'perf_hooks';
|
||||
import * as url from 'url';
|
||||
import { VSBuffer } from '../../base/common/buffer.js';
|
||||
import { CharCode } from '../../base/common/charCode.js';
|
||||
import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from '../../base/common/errors.js';
|
||||
@@ -42,6 +41,16 @@ import { setupServerServices, SocketServer } from './serverServices.js';
|
||||
import { CacheControl, serveError, serveFile, WebClientServer } from './webClientServer.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function parseRequestUrl(requestUrl: string): URL | undefined {
|
||||
try {
|
||||
return requestUrl.startsWith('/')
|
||||
? new URL(`http://localhost${requestUrl}`)
|
||||
: new URL(requestUrl);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace vsda {
|
||||
// the signer is a native module that for historical reasons uses a lower case class name
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -112,7 +121,10 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
|
||||
return serveError(req, res, 400, `Bad request.`);
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
const parsedUrl = parseRequestUrl(req.url);
|
||||
if (!parsedUrl) {
|
||||
return serveError(req, res, 400, `Bad request.`);
|
||||
}
|
||||
let pathname = parsedUrl.pathname;
|
||||
|
||||
if (!pathname) {
|
||||
@@ -141,7 +153,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
|
||||
return void res.end('OK');
|
||||
}
|
||||
|
||||
if (!httpRequestHasValidConnectionToken(this._connectionToken, req, parsedUrl)) {
|
||||
if (!httpRequestHasValidConnectionToken(this._connectionToken, req, parsedUrl.searchParams)) {
|
||||
// invalid connection token
|
||||
return serveError(req, res, 403, `Forbidden.`);
|
||||
}
|
||||
@@ -149,10 +161,11 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
|
||||
if (pathname === '/vscode-remote-resource') {
|
||||
// Handle HTTP requests for resources rendered in the rich client (images, fonts, etc.)
|
||||
// These resources could be files shipped with extensions or even workspace files.
|
||||
const desiredPath = parsedUrl.query['path'];
|
||||
if (typeof desiredPath !== 'string') {
|
||||
const desiredPaths = parsedUrl.searchParams.getAll('path');
|
||||
if (desiredPaths.length !== 1) {
|
||||
return serveError(req, res, 400, `Bad request.`);
|
||||
}
|
||||
const desiredPath = desiredPaths[0];
|
||||
|
||||
let filePath: string;
|
||||
try {
|
||||
@@ -195,14 +208,21 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
|
||||
let skipWebSocketFrames = false;
|
||||
|
||||
if (req.url) {
|
||||
const query = url.parse(req.url, true).query;
|
||||
if (typeof query.reconnectionToken === 'string') {
|
||||
reconnectionToken = query.reconnectionToken;
|
||||
const parsedUrl = parseRequestUrl(req.url);
|
||||
if (!parsedUrl) {
|
||||
this._logService.warn('WebSocket connection rejected: invalid request URL');
|
||||
socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
||||
return;
|
||||
}
|
||||
if (query.reconnection === 'true') {
|
||||
const query = parsedUrl.searchParams;
|
||||
const reconnectionTokens = query.getAll('reconnectionToken');
|
||||
if (reconnectionTokens.length === 1) {
|
||||
reconnectionToken = reconnectionTokens[0];
|
||||
}
|
||||
if (query.getAll('reconnection').length === 1 && query.get('reconnection') === 'true') {
|
||||
isReconnection = true;
|
||||
}
|
||||
if (query.skipWebSocketFrames === 'true') {
|
||||
if (query.getAll('skipWebSocketFrames').length === 1 && query.get('skipWebSocketFrames') === 'true') {
|
||||
skipWebSocketFrames = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as url from 'url';
|
||||
import { pathToFileURL } from 'url';
|
||||
import * as cp from 'child_process';
|
||||
import type * as http from 'http';
|
||||
import { cwd } from '../../base/common/process.js';
|
||||
@@ -394,7 +394,7 @@ async function openInBrowser(args: string[], verbose: boolean) {
|
||||
for (const location of args) {
|
||||
try {
|
||||
if (/^[a-z-]+:\/\/.+/.test(location)) {
|
||||
uris.push(url.parse(location).href);
|
||||
uris.push(new URL(location).href);
|
||||
} else {
|
||||
uris.push(pathToURI(location).href);
|
||||
}
|
||||
@@ -480,11 +480,11 @@ function fatal(message: string, err: unknown): void {
|
||||
|
||||
const preferredCwd = process.env.PWD || cwd(); // prefer process.env.PWD as it does not follow symlinks
|
||||
|
||||
function pathToURI(input: string): url.URL {
|
||||
function pathToURI(input: string): URL {
|
||||
input = input.trim();
|
||||
input = resolve(preferredCwd, input);
|
||||
|
||||
return url.pathToFileURL(input);
|
||||
return pathToFileURL(input);
|
||||
}
|
||||
|
||||
function translatePath(input: string, mapFileUri: (input: string) => string, folderURIS: string[], fileURIS: string[]) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import * as cookie from 'cookie';
|
||||
import * as fs from 'fs';
|
||||
import type * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import * as path from '../../base/common/path.js';
|
||||
import { generateUuid } from '../../base/common/uuid.js';
|
||||
import { connectionTokenCookieName, connectionTokenQueryName } from '../../base/common/network.js';
|
||||
@@ -120,9 +119,10 @@ export async function determineServerConnectionToken(args: ServerParsedArgs): Pr
|
||||
return parseServerConnectionToken(args, readOrGenerateConnectionToken);
|
||||
}
|
||||
|
||||
export function requestHasValidConnectionToken(connectionToken: ServerConnectionToken, req: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery) {
|
||||
export function requestHasValidConnectionToken(connectionToken: ServerConnectionToken, req: Pick<http.IncomingMessage, 'headers'>, searchParams: URLSearchParams) {
|
||||
// First check if there is a valid query parameter
|
||||
if (connectionToken.validate(parsedUrl.query[connectionTokenQueryName])) {
|
||||
const queryTokens = searchParams.getAll(connectionTokenQueryName);
|
||||
if (connectionToken.validate(queryTokens.length > 1 ? queryTokens : queryTokens[0])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { createReadStream, promises } from 'fs';
|
||||
import type * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import * as cookie from 'cookie';
|
||||
import * as crypto from 'crypto';
|
||||
import { isEqualOrParent } from '../../base/common/extpath.js';
|
||||
@@ -139,7 +138,7 @@ export class WebClientServer {
|
||||
* @param parsedUrl The URL to handle, including base and product path
|
||||
* @param pathname The pathname of the URL, without base and product path
|
||||
*/
|
||||
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery, pathname: string): Promise<void> {
|
||||
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: URL, pathname: string): Promise<void> {
|
||||
try {
|
||||
if (pathname.startsWith(STATIC_PATH) && pathname.charCodeAt(STATIC_PATH.length) === CharCode.Slash) {
|
||||
return this._handleStatic(req, res, pathname.substring(STATIC_PATH.length));
|
||||
@@ -257,7 +256,7 @@ export class WebClientServer {
|
||||
/**
|
||||
* Handle HTTP requests for /
|
||||
*/
|
||||
private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
|
||||
private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: URL): Promise<void> {
|
||||
|
||||
const getFirstHeader = (headerName: string) => {
|
||||
const val = req.headers[headerName];
|
||||
@@ -267,8 +266,9 @@ export class WebClientServer {
|
||||
// Prefix routes with basePath for clients
|
||||
const basePath = getFirstHeader('x-forwarded-prefix') || this._basePath;
|
||||
|
||||
const queryConnectionToken = parsedUrl.query[connectionTokenQueryName];
|
||||
if (typeof queryConnectionToken === 'string') {
|
||||
const queryConnectionTokens = parsedUrl.searchParams.getAll(connectionTokenQueryName);
|
||||
if (queryConnectionTokens.length === 1) {
|
||||
const queryConnectionToken = queryConnectionTokens[0];
|
||||
// We got a connection token as a query parameter.
|
||||
// We want to have a clean URL, so we strip it
|
||||
const responseHeaders: Record<string, string> = Object.create(null);
|
||||
@@ -281,13 +281,10 @@ export class WebClientServer {
|
||||
}
|
||||
);
|
||||
|
||||
const newQuery = Object.create(null);
|
||||
for (const key in parsedUrl.query) {
|
||||
if (key !== connectionTokenQueryName) {
|
||||
newQuery[key] = parsedUrl.query[key];
|
||||
}
|
||||
}
|
||||
const newLocation = url.format({ pathname: basePath, query: newQuery });
|
||||
const newQuery = new URLSearchParams(parsedUrl.searchParams);
|
||||
newQuery.delete(connectionTokenQueryName);
|
||||
const queryString = newQuery.toString();
|
||||
const newLocation = queryString ? `${basePath}?${queryString}` : basePath;
|
||||
responseHeaders['Location'] = newLocation;
|
||||
|
||||
res.writeHead(302, responseHeaders);
|
||||
|
||||
@@ -7,9 +7,10 @@ import assert from 'assert';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { join } from '../../../base/common/path.js';
|
||||
import { connectionTokenCookieName, connectionTokenQueryName } from '../../../base/common/network.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';
|
||||
import { getRandomTestPath } from '../../../base/test/node/testUtils.js';
|
||||
import { parseServerConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from '../../node/serverConnectionToken.js';
|
||||
import { MandatoryServerConnectionToken, parseServerConnectionToken, requestHasValidConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from '../../node/serverConnectionToken.js';
|
||||
import { ServerParsedArgs } from '../../node/serverEnvironmentService.js';
|
||||
|
||||
suite('parseServerConnectionToken', () => {
|
||||
@@ -70,3 +71,27 @@ suite('parseServerConnectionToken', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('requestHasValidConnectionToken', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
const connectionToken = new MandatoryServerConnectionToken('valid token');
|
||||
|
||||
test('validates a decoded query parameter', () => {
|
||||
const searchParams = new URLSearchParams(`${connectionTokenQueryName}=valid+token`);
|
||||
|
||||
assert.strictEqual(requestHasValidConnectionToken(connectionToken, { headers: {} }, searchParams), true);
|
||||
});
|
||||
|
||||
test('rejects repeated query parameters', () => {
|
||||
const searchParams = new URLSearchParams(`${connectionTokenQueryName}=valid+token&${connectionTokenQueryName}=valid+token`);
|
||||
|
||||
assert.strictEqual(requestHasValidConnectionToken(connectionToken, { headers: {} }, searchParams), false);
|
||||
});
|
||||
|
||||
test('falls back to a cookie', () => {
|
||||
const headers = { cookie: `${connectionTokenCookieName}=valid%20token` };
|
||||
|
||||
assert.strictEqual(requestHasValidConnectionToken(connectionToken, { headers }, new URLSearchParams()), true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ const height = 900;
|
||||
type BrowserType = 'chromium' | 'firefox' | 'webkit';
|
||||
type BrowserChannel = 'msedge' | 'chrome';
|
||||
|
||||
async function runTestsInBrowser(browserType: BrowserType, browserChannel: BrowserChannel, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise<void> {
|
||||
async function runTestsInBrowser(browserType: BrowserType, browserChannel: BrowserChannel, endpoint: URL, server: cp.ChildProcess): Promise<void> {
|
||||
const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), channel: browserChannel });
|
||||
const context = await browser.newContext();
|
||||
|
||||
@@ -177,7 +177,7 @@ function consoleLogFn(msg: playwright.ConsoleMessage) {
|
||||
return console.log;
|
||||
}
|
||||
|
||||
async function launchServer(browserType: BrowserType, browserChannel: BrowserChannel): Promise<{ endpoint: url.UrlWithStringQuery; server: cp.ChildProcess }> {
|
||||
async function launchServer(browserType: BrowserType, browserChannel: BrowserChannel): Promise<{ endpoint: URL; server: cp.ChildProcess }> {
|
||||
|
||||
// Ensure a tmp user-data-dir is used for the tests
|
||||
const tmpDir = tmp.dirSync({ prefix: 't' });
|
||||
@@ -241,7 +241,7 @@ async function launchServer(browserType: BrowserType, browserChannel: BrowserCha
|
||||
serverProcess.stdout!.on('data', data => {
|
||||
const matches = data.toString('ascii').match(/Web UI available at (.+)/);
|
||||
if (matches !== null) {
|
||||
c({ endpoint: url.parse(matches[1]), server: serverProcess });
|
||||
c({ endpoint: new URL(matches[1]), server: serverProcess });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user