Added password prompt with asterisks displayed while typing
This commit is contained in:
@ -17,6 +17,7 @@
|
||||
- `1` runtime/SFTP error
|
||||
- `2` argument/usage error
|
||||
- Missing `--host`, `--user`, `--password` should still support interactive prompt mode.
|
||||
- Interactive password prompts should avoid echoing the password, mask typed characters when possible, and restore terminal settings after input errors.
|
||||
- `--skip` and `--skip-delete` matching semantics should remain stable.
|
||||
- Rules without wildcard characters (`*`, `?`) use legacy exact matching.
|
||||
- Exact rules without slash match any path segment; exact rules with slash match a relative subpath.
|
||||
|
||||
@ -25,7 +25,7 @@ From repository root:
|
||||
php src/SFTPsync.php --host example.com --user myuser --password mypass --sync ./local /var/www/app
|
||||
```
|
||||
|
||||
If `--host`, `--user`, or `--password` is missing, the script asks for it interactively (TTY only).
|
||||
If `--host`, `--user`, or `--password` is missing, the script asks for it interactively (TTY only). Interactive password input is hidden or masked when the terminal supports it.
|
||||
|
||||
## CLI Usage
|
||||
|
||||
|
||||
211
src/SFTPsync.php
211
src/SFTPsync.php
@ -1213,7 +1213,7 @@ function askForMissingRequiredOptions(array $config): array
|
||||
$config['user'] = promptValue('user');
|
||||
}
|
||||
if ($config['password'] === '') {
|
||||
$config['password'] = promptValue('password');
|
||||
$config['password'] = readPassword('Password: ');
|
||||
}
|
||||
|
||||
return $config;
|
||||
@ -1241,6 +1241,215 @@ function promptValue(string $name): string
|
||||
}
|
||||
}
|
||||
|
||||
function readPassword(string $prompt = 'Password: '): string
|
||||
{
|
||||
if (!isInteractiveStdin()) {
|
||||
throw new CliUsageException('Missing required option --password and no interactive input available.');
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$password = trim(readPasswordOnce($prompt));
|
||||
if ($password !== '') {
|
||||
return $password;
|
||||
}
|
||||
|
||||
logError('Value for --password cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
function readPasswordOnce(string $prompt): string
|
||||
{
|
||||
$password = readPasswordFromRawTerminal($prompt);
|
||||
if ($password !== null) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
$password = readPasswordFromWindowsConsole($prompt);
|
||||
if ($password !== null) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
$password = readPasswordWithEchoDisabled($prompt);
|
||||
if ($password !== null) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
// Last-resort fallback for platforms where PHP cannot safely switch the terminal
|
||||
// to no-echo or character-by-character input. This may echo the password.
|
||||
fwrite(STDOUT, $prompt);
|
||||
$line = fgets(STDIN);
|
||||
if ($line === false) {
|
||||
throw new CliUsageException('Could not read --password from interactive input.');
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
function readPasswordFromRawTerminal(string $prompt): ?string
|
||||
{
|
||||
if (PHP_OS_FAMILY === 'Windows' || !function_exists('shell_exec')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$originalStty = @shell_exec('stty -g 2>/dev/null');
|
||||
if ($originalStty === null || trim($originalStty) === '') {
|
||||
return null;
|
||||
}
|
||||
$originalStty = trim($originalStty);
|
||||
|
||||
fwrite(STDOUT, $prompt);
|
||||
@shell_exec('stty -icanon -echo min 1 time 0 2>/dev/null');
|
||||
|
||||
$password = '';
|
||||
try {
|
||||
while (true) {
|
||||
$char = fgetc(STDIN);
|
||||
if ($char === false) {
|
||||
throw new CliUsageException('Could not read --password from interactive input.');
|
||||
}
|
||||
|
||||
if ($char === "\n" || $char === "\r") {
|
||||
fwrite(STDOUT, PHP_EOL);
|
||||
return $password;
|
||||
}
|
||||
|
||||
if ($char === "\x03") {
|
||||
throw new CliUsageException('Password input interrupted.');
|
||||
}
|
||||
|
||||
if ($char === "\x04") {
|
||||
throw new CliUsageException('Could not read --password from interactive input.');
|
||||
}
|
||||
|
||||
if ($char === "\x08" || $char === "\x7f") {
|
||||
if ($password !== '') {
|
||||
$password = substr($password, 0, -1);
|
||||
fwrite(STDOUT, "\x08 \x08");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === "\x1b") {
|
||||
readPasswordHandleEscapeSequence($password);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ord($char) < 32) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$password .= $char;
|
||||
fwrite(STDOUT, '*');
|
||||
}
|
||||
} finally {
|
||||
@shell_exec('stty ' . escapeshellarg($originalStty) . ' 2>/dev/null');
|
||||
}
|
||||
}
|
||||
|
||||
function readPasswordHandleEscapeSequence(string &$password): void
|
||||
{
|
||||
$second = fgetc(STDIN);
|
||||
$third = fgetc(STDIN);
|
||||
if ($second !== '[' || $third !== '3') {
|
||||
return;
|
||||
}
|
||||
|
||||
$fourth = fgetc(STDIN);
|
||||
if ($fourth === '~' && $password !== '') {
|
||||
$password = substr($password, 0, -1);
|
||||
fwrite(STDOUT, "\x08 \x08");
|
||||
}
|
||||
}
|
||||
|
||||
function readPasswordFromWindowsConsole(string $prompt): ?string
|
||||
{
|
||||
if (PHP_OS_FAMILY !== 'Windows' || !function_exists('proc_open')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$promptLiteral = "'" . str_replace("'", "''", $prompt) . "'";
|
||||
$script = '$PromptText = ' . $promptLiteral . PHP_EOL . <<<'POWERSHELL'
|
||||
[Console]::Error.Write($PromptText)
|
||||
$password = New-Object System.Text.StringBuilder
|
||||
|
||||
while ($true) {
|
||||
$key = [Console]::ReadKey($true)
|
||||
if ($key.Key -eq [ConsoleKey]::Enter) {
|
||||
[Console]::Error.WriteLine()
|
||||
break
|
||||
}
|
||||
if ($key.Key -eq [ConsoleKey]::Backspace -or $key.Key -eq [ConsoleKey]::Delete) {
|
||||
if ($password.Length -gt 0) {
|
||||
[void]$password.Remove($password.Length - 1, 1)
|
||||
[Console]::Error.Write("`b `b")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ($key.KeyChar -ne [char]0) {
|
||||
[void]$password.Append($key.KeyChar)
|
||||
[Console]::Error.Write("*")
|
||||
}
|
||||
}
|
||||
|
||||
[Console]::Out.Write($password.ToString())
|
||||
POWERSHELL;
|
||||
|
||||
$process = @proc_open(
|
||||
['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', $script],
|
||||
[
|
||||
0 => STDIN,
|
||||
1 => ['pipe', 'w'],
|
||||
2 => STDERR,
|
||||
],
|
||||
$pipes
|
||||
);
|
||||
if (!is_resource($process)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$password = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
$exitCode = proc_close($process);
|
||||
} catch (Throwable) {
|
||||
proc_terminate($process);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($exitCode !== 0 || $password === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
function readPasswordWithEchoDisabled(string $prompt): ?string
|
||||
{
|
||||
if (PHP_OS_FAMILY === 'Windows' || !function_exists('shell_exec')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$originalStty = @shell_exec('stty -g 2>/dev/null');
|
||||
if ($originalStty === null || trim($originalStty) === '') {
|
||||
return null;
|
||||
}
|
||||
$originalStty = trim($originalStty);
|
||||
|
||||
fwrite(STDOUT, $prompt);
|
||||
@shell_exec('stty -echo 2>/dev/null');
|
||||
try {
|
||||
$line = fgets(STDIN);
|
||||
if ($line === false) {
|
||||
throw new CliUsageException('Could not read --password from interactive input.');
|
||||
}
|
||||
fwrite(STDOUT, PHP_EOL);
|
||||
|
||||
return $line;
|
||||
} finally {
|
||||
@shell_exec('stty ' . escapeshellarg($originalStty) . ' 2>/dev/null');
|
||||
}
|
||||
}
|
||||
|
||||
function isInteractiveStdin(): bool
|
||||
{
|
||||
if (!defined('STDIN')) {
|
||||
|
||||
Reference in New Issue
Block a user