Added password prompt with asterisks displayed while typing
This commit is contained in:
@ -17,6 +17,7 @@
|
|||||||
- `1` runtime/SFTP error
|
- `1` runtime/SFTP error
|
||||||
- `2` argument/usage error
|
- `2` argument/usage error
|
||||||
- Missing `--host`, `--user`, `--password` should still support interactive prompt mode.
|
- 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.
|
- `--skip` and `--skip-delete` matching semantics should remain stable.
|
||||||
- Rules without wildcard characters (`*`, `?`) use legacy exact matching.
|
- Rules without wildcard characters (`*`, `?`) use legacy exact matching.
|
||||||
- Exact rules without slash match any path segment; exact rules with slash match a relative subpath.
|
- 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
|
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
|
## CLI Usage
|
||||||
|
|
||||||
|
|||||||
211
src/SFTPsync.php
211
src/SFTPsync.php
@ -1213,7 +1213,7 @@ function askForMissingRequiredOptions(array $config): array
|
|||||||
$config['user'] = promptValue('user');
|
$config['user'] = promptValue('user');
|
||||||
}
|
}
|
||||||
if ($config['password'] === '') {
|
if ($config['password'] === '') {
|
||||||
$config['password'] = promptValue('password');
|
$config['password'] = readPassword('Password: ');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $config;
|
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
|
function isInteractiveStdin(): bool
|
||||||
{
|
{
|
||||||
if (!defined('STDIN')) {
|
if (!defined('STDIN')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user