diff --git a/README.cn.md b/README.cn.md index eed37f7..661dc35 100644 --- a/README.cn.md +++ b/README.cn.md @@ -572,6 +572,14 @@ trzsz-ssh ( tssh ) 设计为 ssh 客户端的直接替代品,提供与 openssh encotp636f64653a20 77b4ce85d087b39909e563efb165659b22b9ea700a537f1258bdf56ce6fdd6ea70bc7591ea5c01918537a65433133bc0bd5ed3e4 ``` +- 可以自己实现获取动态密码的程序,指定 `%q` 参数可以得到问题内容,将动态密码输出到 stdout 并正常退出即可,调试信息可以输出到 stderr ( `tssh --debug` 运行时可以看到 )。配置举例(序号代表第几个问题,一般只有一个问题,只需配置 `OtpCommand1` 即可): + + ``` + Host custom_otp_command + #!! OtpCommand1 /path/to/your_own_program %q + #!! OtpCommand2 python C:\your_python_code.py %q + ``` + - 如果启用了 `ControlMaster` 多路复用,或者是在 `Warp` 终端,请参考前面 `自动交互` 加 `Ctrl` 前缀来实现。 ``` diff --git a/README.en.md b/README.en.md index 64b2b02..038829f 100644 --- a/README.en.md +++ b/README.en.md @@ -572,6 +572,14 @@ trzsz-ssh ( tssh ) is an ssh client designed as a drop-in replacement for the op encotp636f64653a20 77b4ce85d087b39909e563efb165659b22b9ea700a537f1258bdf56ce6fdd6ea70bc7591ea5c01918537a65433133bc0bd5ed3e4 ``` +- You can write a program to obtain the one-time password. Specify the `%q` argument if you want to get the question. Just output the one-time password to stdout and exit with 0, and the debugging information can be output to stderr (you can see it when running `tssh --debug`). Configuration example (the serial number represents the number of questions, generally there is only one question, just configure `OtpCommand1`): + + ``` + Host custom_otp_command + #!! OtpCommand1 /path/to/your_own_program %q + #!! OtpCommand2 python C:\your_python_code.py %q + ``` + - If `ControlMaster` multiplexing is enabled or using `Warp` terminal, you will need to use the `Automated Interaction` mentioned earlier to achieve remembering answers. ``` diff --git a/tssh/expect.go b/tssh/expect.go index d8e0457..66ed4a7 100644 --- a/tssh/expect.go +++ b/tssh/expect.go @@ -357,19 +357,19 @@ func (e *sshExpect) wrapOutput(reader io.Reader, writer io.Writer, ch chan []byt } } -func (e *sshExpect) waitForPattern(pattern string, caseSends *caseSendList) error { +func (e *sshExpect) waitForPattern(pattern string, caseSends *caseSendList) (string, error) { expr := quoteExpectPattern(pattern) re, err := regexp.Compile(expr) if err != nil { warning("compile expect expr [%s] failed: %v", expr, err) - return err + return "", err } var builder strings.Builder for { var buf []byte select { case <-e.ctx.Done(): - return e.ctx.Err() + return "", e.ctx.Err() case buf = <-e.out: case buf = <-e.err: } @@ -387,7 +387,7 @@ func (e *sshExpect) waitForPattern(pattern string, caseSends *caseSendList) erro case <-e.out: case <-e.err: default: - return nil + return builder.String(), nil } } } else { @@ -396,7 +396,7 @@ func (e *sshExpect) waitForPattern(pattern string, caseSends *caseSendList) erro } } -func (e *sshExpect) getExpectSender(idx int) *expectSender { +func (e *sshExpect) getExpectSender(idx int, question string) *expectSender { if pass := getExConfig(e.args.Destination, fmt.Sprintf("%sExpectSendPass%d", e.pre, idx)); pass != "" { secret, err := decodeSecret(pass) if err != nil { @@ -425,7 +425,7 @@ func (e *sshExpect) getExpectSender(idx int) *expectSender { warning("decode %sExpectSendEncOtp%d [%s] failed: %v", e.pre, idx, encOtp, err) return nil } - return newPassSender(e, getOtpCommandOutput(command)) + return newPassSender(e, getOtpCommandOutput(command, question)) } if secret := getExConfig(e.args.Destination, fmt.Sprintf("%sExpectSendTotp%d", e.pre, idx)); secret != "" { @@ -433,7 +433,7 @@ func (e *sshExpect) getExpectSender(idx int) *expectSender { } if command := getExConfig(e.args.Destination, fmt.Sprintf("%sExpectSendOtp%d", e.pre, idx)); command != "" { - return newPassSender(e, getOtpCommandOutput(command)) + return newPassSender(e, getOtpCommandOutput(command, question)) } return nil @@ -458,13 +458,14 @@ func (e *sshExpect) execInteractions(writer io.Writer, expectCount int) { warning("Invalid ExpectCaseSendText%d: %v", idx, err) } } - if err := e.waitForPattern(pattern, caseSends); err != nil { + question, err := e.waitForPattern(pattern, caseSends) + if err != nil { return } if e.ctx.Err() != nil { return } - sender := e.getExpectSender(idx) + sender := e.getExpectSender(idx, question) if !sender.sendInput(writer, strconv.Itoa(idx)) { return } diff --git a/tssh/login.go b/tssh/login.go index b420a2b..ca5b73c 100644 --- a/tssh/login.go +++ b/tssh/login.go @@ -568,7 +568,7 @@ func readQuestionAnswerConfig(dest string, idx int, question string) string { } if command := getSecretConfig(dest, "otp"+qhex); command != "" { - if answer := getOtpCommandOutput(command); answer != "" { + if answer := getOtpCommandOutput(command, question); answer != "" { return answer } } @@ -590,7 +590,7 @@ func readQuestionAnswerConfig(dest string, idx int, question string) string { qcmd := fmt.Sprintf("OtpCommand%d", idx) debug("the otp command key for question '%s' is %s", question, qcmd) if command := getSecretConfig(dest, qcmd); command != "" { - if answer := getOtpCommandOutput(command); answer != "" { + if answer := getOtpCommandOutput(command, question); answer != "" { return answer } } diff --git a/tssh/otp.go b/tssh/otp.go index 400dec6..dda7aa1 100644 --- a/tssh/otp.go +++ b/tssh/otp.go @@ -33,18 +33,21 @@ import ( "github.com/pquerna/otp/totp" ) -func getOtpCommandOutput(command string) string { +func getOtpCommandOutput(command, question string) string { argv, err := splitCommandLine(command) if err != nil || len(argv) == 0 { warning("split otp command failed: %v", err) return "" } - if enableDebugLogging { - for i, arg := range argv { - debug("otp command argv[%d] = %s", i, arg) + var args []string + for i, arg := range argv { + if i > 0 && arg == "%q" { + arg = question } + debug("otp command argv[%d] = %s", i, arg) + args = append(args, arg) } - cmd := exec.Command(argv[0], argv[1:]...) + cmd := exec.Command(args[0], args[1:]...) var outBuf, errBuf bytes.Buffer cmd.Stdout = &outBuf cmd.Stderr = &errBuf @@ -56,6 +59,9 @@ func getOtpCommandOutput(command string) string { } return "" } + if enableDebugLogging && errBuf.Len() > 0 { + debug("otp command stderr output: %s", errBuf.String()) + } return strings.TrimSpace(outBuf.String()) }