Fix win execution with whitespace in username #351
This commit addresses the issue where scripts fail to execute on Windows environments with usernames containing spaces. The problem stemmed from PowerShell and cmd shell's handling of spaces in quoted arguments. The solution involves encoding PowerShell commands before execution, which mitigates the quoting issues previously causing script failures. This approach is now integrated into the execution flow, ensuring that commands are correctly handled irrespective of user names or other variables that may include spaces. Changes: - Implement encoding for PowerShell commands to handle spaces in usernames and other similar scenarios. - Update script documentation URLs to reflect changes in directory structure. Fixes #351
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvokeShellCommandCreator';
|
||||
|
||||
/**
|
||||
Encoding PowerShell commands resolve issues with quote handling.
|
||||
|
||||
There are known problems with PowerShell's handling of double quotes in command line arguments:
|
||||
- Quote stripping in PowerShell command line arguments: https://web.archive.org/web/20240507102706/https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments
|
||||
- privacy.sexy double quotes issue when calling PowerShell from command line: https://web.archive.org/web/20240507102841/https://github.com/undergroundwires/privacy.sexy/issues/351
|
||||
- Challenges with single quotes in PowerShell command line: https://web.archive.org/web/20240507102047/https://stackoverflow.com/questions/20958388/command-line-escaping-single-quote-for-powershell
|
||||
|
||||
Using the `EncodedCommand` parameter is recommended by Microsoft for handling
|
||||
complex quoting scenarios. This approach helps avoid issues by encoding the entire
|
||||
command as a Base64 string:
|
||||
- Microsoft's documentation on using the `EncodedCommand` parameter: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||
*/
|
||||
export class EncodedPowerShellInvokeCmdCommandCreator
|
||||
implements PowerShellInvokeShellCommandCreator {
|
||||
public createCommandToInvokePowerShell(powerShellScript: string): string {
|
||||
return generateEncodedPowershellCommand(powerShellScript);
|
||||
}
|
||||
}
|
||||
|
||||
function generateEncodedPowershellCommand(powerShellScript: string): string {
|
||||
const encodedCommand = encodeForPowershellExecution(powerShellScript);
|
||||
return `PowerShell -EncodedCommand ${encodedCommand}`;
|
||||
}
|
||||
|
||||
function encodeForPowershellExecution(script: string): string {
|
||||
// The string must be formatted using UTF-16LE character encoding, see: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||
const buffer = Buffer.from(script, 'utf16le');
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface PowerShellInvokeShellCommandCreator {
|
||||
createCommandToInvokePowerShell(powerShellCommand: string): string;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||
|
||||
export class CmdShellArgumentEscaper implements ShellArgumentEscaper {
|
||||
public escapePathArgument(pathArgument: string): string {
|
||||
return cmdShellPathArgumentEscape(pathArgument);
|
||||
}
|
||||
}
|
||||
|
||||
function cmdShellPathArgumentEscape(pathArgument: string): string {
|
||||
// - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe)
|
||||
// to correctly handle paths containing spaces.
|
||||
// - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped.
|
||||
return `"${pathArgument}"`;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||
|
||||
export class PowerShellArgumentEscaper implements ShellArgumentEscaper {
|
||||
public escapePathArgument(pathArgument: string): string {
|
||||
return powerShellPathArgumentEscape(pathArgument);
|
||||
}
|
||||
}
|
||||
|
||||
function powerShellPathArgumentEscape(pathArgument: string): string {
|
||||
// - Encloses the path in single quotes to handle spaces and most special characters.
|
||||
// - Single quotes are used in PowerShell to ensure the string is treated as a literal string.
|
||||
// - Paths in Windows can include single quotes ('), so any internal single quotes are escaped
|
||||
// using double quotes.
|
||||
return `'${pathArgument.replace(/'/g, "''")}'`;
|
||||
}
|
||||
@@ -1,22 +1,29 @@
|
||||
import { CmdShellArgumentEscaper } from './ShellArgument/CmdShellArgumentEscaper';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { PowerShellArgumentEscaper } from './ShellArgument/PowerShellArgumentEscaper';
|
||||
import { EncodedPowerShellInvokeCmdCommandCreator } from './PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator';
|
||||
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||
|
||||
export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
||||
constructor(
|
||||
private readonly escaper: ShellArgumentEscaper = new CmdShellArgumentEscaper(),
|
||||
private readonly escaper: ShellArgumentEscaper = new PowerShellArgumentEscaper(),
|
||||
private readonly powershellCommandCreator: PowerShellInvokeShellCommandCreator
|
||||
= new EncodedPowerShellInvokeCmdCommandCreator(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public buildShellCommand(filePath: string): string {
|
||||
const command = [
|
||||
'PowerShell',
|
||||
const powershellCommand = [
|
||||
'Start-Process',
|
||||
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
||||
`-FilePath ${this.escaper.escapePathArgument(filePath)}`,
|
||||
].join(' ');
|
||||
return command;
|
||||
/*
|
||||
📝 Options:
|
||||
Running PowerShell command is preferred due to its flexibility and the way it provides
|
||||
GUI sudo prompt through `RunAs` argument.
|
||||
Other options considered:
|
||||
`child_process.execFile()`
|
||||
"path", `cmd.exe /c "path"`
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
@@ -36,6 +43,8 @@ export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||
*/
|
||||
this.logger.info(`Building command for PowerShell execution:\n\tCommand: ${powershellCommand}`);
|
||||
return this.powershellCommandCreator.createCommandToInvokePowerShell(powershellCommand);
|
||||
}
|
||||
|
||||
public isExecutionTerminatedExternally(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user