Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/authResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@ export function getRemoteAuthority(host: string) {
return `${REMOTE_SSH_AUTHORITY}+${host}`;
}

/**
* Convert a glob-style pattern (used in SendEnv) to a regular expression.
* Supports * (matches any characters) and ? (matches single character).
*/
function globToRegex(pattern: string): RegExp {
// Escape special regex characters except * and ?
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
// Convert glob wildcards to regex
const regexPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
return new RegExp(`^${regexPattern}$`);
}

/**
* Filter environment variables based on SendEnv patterns.
* Patterns can include wildcards like LC_* or exact names like LANG.
*/
function filterEnvBySendEnv(sendEnvPatterns: string[]): Record<string, string> {
const result: Record<string, string> = {};

for (const pattern of sendEnvPatterns) {
const regex = globToRegex(pattern);
for (const [key, value] of Object.entries(process.env)) {
if (regex.test(key) && value !== undefined) {
result[key] = value;
}
}
}

return result;
}

class TunnelInfo implements vscode.Disposable {
constructor(
readonly localPort: number,
Expand Down Expand Up @@ -110,6 +141,22 @@ export class RemoteSSHResolver implements vscode.RemoteAuthorityResolver, vscode
const identitiesOnly = (sshHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes';
const identityKeys = await gatherIdentityFiles(identityFiles, this.sshAgentSock, identitiesOnly, this.logger);

// Extract SendEnv configuration and filter local environment variables
this.logger.trace(`SSH config raw SendEnv value: ${JSON.stringify(sshHostConfig['SendEnv'])}`);
const sendEnvRaw = sshHostConfig['SendEnv'];
const sendEnvPatterns: string[] = sendEnvRaw
? (Array.isArray(sendEnvRaw) ? sendEnvRaw : [sendEnvRaw])
: [];
this.logger.trace(`SendEnv patterns after normalization: ${JSON.stringify(sendEnvPatterns)}`);
const sendEnvVars = sendEnvPatterns.length > 0 ? filterEnvBySendEnv(sendEnvPatterns) : {};
if (sendEnvPatterns.length > 0) {
if (Object.keys(sendEnvVars).length > 0) {
this.logger.info(`SendEnv: Sending ${Object.keys(sendEnvVars).length} environment variable(s) to remote: ${Object.keys(sendEnvVars).join(', ')}`);
} else {
this.logger.trace(`SendEnv: No local environment variables matched patterns: ${sendEnvPatterns.join(', ')}`);
}
}

// Create proxy jump connections if any
let proxyStream: ssh2.ClientChannel | stream.Duplex | undefined;
if (sshHostConfig['ProxyJump']) {
Expand Down Expand Up @@ -191,7 +238,7 @@ export class RemoteSSHResolver implements vscode.RemoteAuthorityResolver, vscode
envVariables['SSH_AUTH_SOCK'] = null;
}

const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger);
const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), sendEnvVars, remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger);

for (const key of Object.keys(envVariables)) {
if (installResult[key] !== undefined) {
Expand Down
14 changes: 9 additions & 5 deletions src/serverSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ServerInstallOptions {
release?: string; // vscodium specific
extensionIds: string[];
envVariables: string[];
sendEnvVars: Record<string, string>;
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
Expand All @@ -37,7 +38,7 @@ export class ServerInstallError extends Error {

const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/VSCodium/vscodium/releases/download/${version}.${release}/vscodium-reh-${os}-${arch}-${version}.${release}.tar.gz';

export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], sendEnvVars: Record<string, string>, platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
let shell = 'powershell';

// detect platform and shell for windows
Expand Down Expand Up @@ -78,6 +79,7 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe
release: vscodeServerConfig.release,
extensionIds,
envVariables,
sendEnvVars,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
Expand Down Expand Up @@ -198,12 +200,13 @@ function parseServerInstallOutput(str: string, scriptId: string): { [k: string]:
return resultMap;
}

function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, sendEnvVars, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const sendEnvExports = Object.entries(sendEnvVars).map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`).join('\n');
return `
# Server installation script

TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
${sendEnvExports ? sendEnvExports + '\n' : ''}TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"

DISTRO_VERSION="${version}"
DISTRO_COMMIT="${commit}"
Expand Down Expand Up @@ -422,8 +425,9 @@ print_install_results_and_exit 0
`;
}

function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, sendEnvVars, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const sendEnvExports = Object.entries(sendEnvVars).map(([key, value]) => `$env:${key}="${value.replace(/"/g, '`"')}"`).join('\n');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
Expand All @@ -435,7 +439,7 @@ function generatePowerShellInstallScript({ id, quality, version, commit, release
return `
# Server installation script

$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
${sendEnvExports ? sendEnvExports + '\n' : ''}$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
$ProgressPreference = "SilentlyContinue"

$DISTRO_VERSION="${version}"
Expand Down
1 change: 1 addition & 0 deletions src/ssh/sshConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const SSH_CONFIG_PROPERTIES: Record<string, string> = {
'preferredauthentications': 'PreferredAuthentications',
'proxyjump': 'ProxyJump',
'proxycommand': 'ProxyCommand',
'sendenv': 'SendEnv',
'include': 'Include',
};

Expand Down
Loading