Skip to content

Commit

Permalink
Inject VS Code proxy settings into JVM args
Browse files Browse the repository at this point in the history
Interprets the `http.proxyHost` and `http.proxyAuthorization` settings
in order to set up the JVM arguments or environment variables
in order to configure the proxy for the language server.

JVM arguments are used when running the Java server, and environment
variables are used when running the binary server.

This means if the proxy is configured through these settings in VS Code,
then vscode-xml will download schemas through the proxy.

While running the Java server,
if the proxy is already configured in `xml.server.vmargs`,
then the `http.proxyHost` and `http.proxyAuthorization` are ignored.

Limitations:
 * If `http.proxyAuthorization` is used, it is assumed to be
   [Basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization#directives),
   with a username/password pair, since the JVM expects a username/password pair.

Requires eclipse-lemminx/lemminx#1012

Closes redhat-developer#416

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Apr 6, 2021
1 parent 621b11a commit 1cdc71a
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 7 deletions.
16 changes: 10 additions & 6 deletions src/server/binary/binaryServerStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import * as https from 'https';
import * as os from 'os';
import * as path from 'path';
import { Readable } from 'stream';
import { ExtensionContext, extensions, window, WorkspaceConfiguration, ProgressOptions, ProgressLocation, Progress, CancellationToken } from "vscode";
import { ExtensionContext, extensions, ProgressLocation, ProgressOptions, window, WorkspaceConfiguration } from "vscode";
import { Executable } from "vscode-languageclient";
import * as yauzl from 'yauzl';
import { addTrustedHash, getTrustedHashes } from './binaryHashManager';
import { getProxySettings, getProxySettingsAsEnvironmentVariables } from '../../settings/proxySettings';
import { getXMLConfiguration } from "../../settings/settings";
import { Telemetry } from '../../telemetry';
import { addTrustedHash, getTrustedHashes } from './binaryHashManager';
const glob = require('glob');

const HTTPS_PATTERN = /^https:\/\//;
Expand All @@ -26,13 +27,16 @@ export const ABORTED_ERROR: Error = new Error('XML Language Server download canc
* @returns Returns the executable to launch LemMinX (the XML Language Server) as a binary
*/
export async function prepareBinaryExecutable(context: ExtensionContext): Promise<Executable> {
const binaryOptions: string = getXMLConfiguration().get("server.binary.args");
const binaryArgs: string = getXMLConfiguration().get("server.binary.args");
let binaryExecutable: Executable;
return getServerBinaryPath()
.then((binaryPath: string) => {
binaryExecutable = {
args: [binaryOptions],
command: binaryPath
args: [binaryArgs],
command: binaryPath,
options: {
env: getProxySettingsAsEnvironmentVariables(getProxySettings())
}
} as Executable;
return binaryPath;
})
Expand Down Expand Up @@ -386,4 +390,4 @@ async function acceptBinaryDownloadResponse(response: http.IncomingMessage): Pro
});
response.pipe(serverBinaryFileStream);
});
}
}
9 changes: 8 additions & 1 deletion src/server/java/javaServerStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ExtensionContext, workspace } from 'vscode';
import { Executable } from 'vscode-languageclient';
import { RequirementsData } from '../requirements';
import { getJavaagentFlag, getKey, getXMLConfiguration, IS_WORKSPACE_VMARGS_XML_ALLOWED, xmlServerVmargs } from '../../settings/settings';
import { getProxySettings, getProxySettingsAsJVMArgs, ProxySettings, jvmArgsContainsProxySettings } from '../../settings/proxySettings';
const glob = require('glob');

declare var v8debug;
Expand Down Expand Up @@ -44,12 +45,18 @@ function prepareParams(requirements: RequirementsData, xmlJavaExtensions: string
} else {
vmargsCheck = getXMLConfiguration().get('server.vmargs');
}
let vmargs;
let vmargs: string;
if (vmargsCheck !== undefined) {
vmargs = vmargsCheck + '';
} else {
vmargs = '';
}

const proxySettings: ProxySettings = getProxySettings();
if (proxySettings && !jvmArgsContainsProxySettings(vmargs)) {
vmargs += getProxySettingsAsJVMArgs(proxySettings);
}

if (os.platform() == 'win32') {
const watchParentProcess = '-DwatchParentProcess=';
if (vmargs.indexOf(watchParentProcess) < 0) {
Expand Down
175 changes: 175 additions & 0 deletions src/settings/proxySettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* Copyright (c) 2021 Red Hat, Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*/
import { workspace } from 'vscode';

/**
* Represents the information needed to communicate through a proxy
*/
export class ProxySettings {

private _host: string;
private _port: string;
private _auth?: ProxyAuthorization;

constructor(host: string, port: string, auth?: ProxyAuthorization) {
this._host = host;
this._port = port;
this._auth = auth;
}

get host(): string {
return this._host;
}

get port(): string {
return this._port;
}

get auth(): ProxyAuthorization {
return this._auth;
}

}

/**
* Represents the information needed to authenticate with the proxy
*/
export class ProxyAuthorization {

private _username: string;
private _password: string;

constructor(username: string, password: string) {
this._username = username;
this._password = password;
}

get username(): string {
return this._username
}

get password(): string {
return this._password;
}

}


/**
* Returns the proxy settings that are declared in the VS Code settings, or null if no proxy is configured
*
* @throws if the proxy settings aren't in the expected format
* @returns the proxy settings that are declared in the VS Code settings, or null if no proxy is configured
*/
export function getProxySettings(): ProxySettings {
const proxyAddress = getProxyAddress();
if (!proxyAddress) {
return null;
}
const regexResult = HOST_AND_PORT_EXTRACTOR.exec(proxyAddress);
if (!regexResult[1]) {
return null;
}
const host: string = regexResult[1];
const port: string = regexResult[2] ? regexResult[2] : "80";

const proxyAuthorizationString = getProxyAuthorization();
if (!proxyAuthorizationString) {
return new ProxySettings(host, port);
}
if (proxyAuthorizationString.indexOf(' ') === -1) {
throw new Error('A space is expected in the Authorization header between the authorization method and encoded username/password');
}

const encodedUserAndPass = proxyAuthorizationString.split(' ').pop();
const decodedUserAndPass: string = Buffer.from(encodedUserAndPass, 'base64').toString('utf8');
if (decodedUserAndPass.indexOf(':') === -1) {
throw new Error('Authorization header is not in the expected format');
}
const [uriEncodedUsername, uriEncodedPassword] = decodedUserAndPass.split(':');
const proxyAuthorization = new ProxyAuthorization(decodeURIComponent(uriEncodedUsername), decodeURIComponent(uriEncodedPassword));
return new ProxySettings(host, port, proxyAuthorization);
}

/**
* Returns the proxy settings as arguments for the JVM
*
* eg. -Dhttp.proxyHost=<proxy_host> -Dhttp.proxyPort=<proxy_port> -Dhttp.proxyUser=<user> -Dhttp.proxyPassword=<password>
*
* @param proxySettings the proxy settings to convert into JVM args
*/
export function getProxySettingsAsJVMArgs(proxySettings: ProxySettings): string {
// Java doesn't recognize localhost in the proxy settings
const adaptedHostName = 'localhost'.startsWith(proxySettings.host) ? '127.0.0.1' : proxySettings.host;
let proxyJVMArgs: string = ` -Dhttp.proxyHost=${adaptedHostName} -Dhttp.proxyPort=${proxySettings.port} `;
if (proxySettings.auth) {
proxyJVMArgs += ` -Dhttp.proxyUser=${proxySettings.auth.username} -Dhttp.proxyPassword=${proxySettings.auth.password} `;
}
return proxyJVMArgs;
}

/**
* Returns the proxy settings as environment variables for LemMinX
*
* @param proxySettings the proxy settings to convert into environment variables
* @returns the proxy settings as environment variables for LemMinX
*/
export function getProxySettingsAsEnvironmentVariables(proxySettings: ProxySettings): any {
let proxyEnv: any = {};

proxyEnv['HTTP_PROXY_HOST'] = proxySettings.host;
proxyEnv['HTTP_PROXY_PORT'] = proxySettings.port;

if (proxySettings.auth) {
proxyEnv['HTTP_PROXY_USERNAME'] = proxySettings.auth.username;
proxyEnv['HTTP_PROXY_PASSWORD'] = proxySettings.auth.password;
}

return proxyEnv;
}

/**
* Checks if the given JVM arguments contain any proxy configuration
*
* @param jvmArgs the arguments being passed to the JVM
*/
export function jvmArgsContainsProxySettings(jvmArgs: string): boolean {
return (
[JVM_PROXY_HOST, JVM_PROXY_PORT, JVM_PROXY_USER, JVM_PROXY_PASS] //
.map(prop => jvmArgs.indexOf(`-D${prop}`) !== -1)
.reduce((a, b, _index, _array) => a || b, false)
);
}

const HOST_AND_PORT_EXTRACTOR = /https?:\/\/([^:/]+)(?::([0-9]+))?/;

const JVM_PROXY_HOST = 'http.proxyHost';
const JVM_PROXY_PORT = 'http.proxyPort';
const JVM_PROXY_USER = 'http.proxyUser';
const JVM_PROXY_PASS = 'http.proxyPassword';

/**
* Returns the address of the proxy
*
* @returns the address of the proxy
*/
function getProxyAddress(): string {
return workspace.getConfiguration('http').get('proxy', undefined);
}

/**
* Returns the Proxy-Authorization to use to access the proxy
*
* @returns The Proxy-Authorization to use to access the proxy
*/
function getProxyAuthorization(): string {
return workspace.getConfiguration('http').get('proxyAuthorization', undefined);
}
1 change: 1 addition & 0 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';
import { commands, Extension, extensions, window, workspace, WorkspaceConfiguration } from "vscode";
import { getVariableSubstitutedAssociations } from "./variableSubstitution";
import { jvmArgsContainsProxySettings, getProxySettings, ProxySettings, getProxySettingsAsJVMArgs } from './proxySettings';

export interface ScopeInfo {
scope: "default" | "global" | "workspace" | "folder";
Expand Down

0 comments on commit 1cdc71a

Please sign in to comment.