105 lines
2.5 KiB
Go
105 lines
2.5 KiB
Go
//go:build linux
|
||
|
||
package cli
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"golang.org/x/sys/unix"
|
||
)
|
||
|
||
func openTestPTY(t *testing.T) (master, slave *os.File) {
|
||
t.Helper()
|
||
masterFD, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
|
||
if err != nil {
|
||
t.Skipf("当前 Linux 环境无法打开伪终端:%v", err)
|
||
}
|
||
master = os.NewFile(uintptr(masterFD), "pty-master")
|
||
t.Cleanup(func() { _ = master.Close() })
|
||
if err := unix.IoctlSetInt(masterFD, unix.TIOCSPTLCK, 0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ptyNumber, err := unix.IoctlGetInt(masterFD, unix.TIOCGPTN)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
slave, err = os.OpenFile(fmt.Sprintf("/dev/pts/%d", ptyNumber), os.O_RDWR|unix.O_NOCTTY, 0)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { _ = slave.Close() })
|
||
return master, slave
|
||
}
|
||
|
||
func TestReadSecretInputRejectsPipe(t *testing.T) {
|
||
_, err := readSecretInput(strings.NewReader("not-a-secret\n"), io.Discard, "请输入密码")
|
||
if err == nil || !strings.Contains(err.Error(), "交互式终端") {
|
||
t.Fatalf("管道输入未被拒绝:%v", err)
|
||
}
|
||
}
|
||
|
||
func TestReadSecretInputDisablesAndRestoresEcho(t *testing.T) {
|
||
master, slave := openTestPTY(t)
|
||
before, err := unix.IoctlGetTermios(int(slave.Fd()), unix.TCGETS)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var output bytes.Buffer
|
||
result := make(chan struct {
|
||
value string
|
||
err error
|
||
}, 1)
|
||
go func() {
|
||
value, err := readSecretInput(slave, &output, "请输入密码")
|
||
result <- struct {
|
||
value string
|
||
err error
|
||
}{value: value, err: err}
|
||
}()
|
||
|
||
deadline := time.Now().Add(2 * time.Second)
|
||
for {
|
||
current, ioctlErr := unix.IoctlGetTermios(int(slave.Fd()), unix.TCGETS)
|
||
if ioctlErr == nil && current.Lflag&(unix.ECHO|unix.ECHONL) == 0 {
|
||
break
|
||
}
|
||
if time.Now().After(deadline) {
|
||
t.Fatal("读取敏感输入时未关闭终端回显")
|
||
}
|
||
time.Sleep(5 * time.Millisecond)
|
||
}
|
||
|
||
if _, err := master.Write([]byte(" valid-secret \n")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
select {
|
||
case got := <-result:
|
||
if got.err != nil {
|
||
t.Fatalf("伪终端读取失败:%v", got.err)
|
||
}
|
||
if got.value != " valid-secret " {
|
||
t.Fatalf("合法首尾空格未保留:%q", got.value)
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("伪终端读取超时")
|
||
}
|
||
|
||
after, err := unix.IoctlGetTermios(int(slave.Fd()), unix.TCGETS)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if after.Lflag != before.Lflag {
|
||
t.Fatalf("终端状态未恢复:before=%#x after=%#x", before.Lflag, after.Lflag)
|
||
}
|
||
if strings.Contains(output.String(), "valid-secret") {
|
||
t.Fatal("敏感值出现在提示输出中")
|
||
}
|
||
}
|