开源 issue: https://github.com/trzsz/trzsz-ssh/issues/46
复现 demo:
package main import ( "fmt" "os/exec" ) func main() { command := `"C:\WINDOWS\System32\OpenSSH\ssh.exe" -V` // command = `"C:\Program Files\ssh.exe" -V` cmd := exec.Command("cmd", "/c", command) output, _ := cmd.CombinedOutput() fmt.Println(string(output)) }
错误输出:
'\"C:\WINDOWS\System32\OpenSSH\ssh.exe\"' is not recognized as an internal or external command, operable program or batch file.
原始问题,用户在 ~/.ssh/config
中配置任意的 ProxyCommand
,要用 os/exec
来执行它,并且获取它的标准输出。用 cmd /c
是想避免解释每一个具体的参数是什么,有双引号,有空格,有转义,要准确地解释出每一个参数不简单啊。
大佬们有什么好想法吗?
![]() | 1 geelaw 2023-10-06 22:31:26 +08:00 via iPhone 为什么要多此一举透过 cmd ?另外从 go 的 API 设计可以看出它必须用 Unix 的方式传入 argv ,而不是 Windows 的 command line 。而 cmd 是按照 command line 而不是 argv 读取命令的,因为同一组 argv 有无限种不同的 command line 表示,而且这些会被 cmd 理解为不同的意思,所以不存在可靠的用 go 的 exec 调用 cmd 的方法。 如果你要调用 ssh 并传入 -V ,可以直接传入合适的 argv 。 |
![]() | 2 geelaw 2023-10-06 22:34:44 +08:00 via iPhone 例子: exec.Command("C:\\WINDOWS\\System32\|OpenSSH\\ssh.exe", "-V") go 和 ssh 理应 Windows 上能正确互转 command line 和 argv 。 |
![]() | 3 LonnyWong OP @geelaw 因为 command 是未知的,如果你想输入准确的 argv ,那就需要先从一个字符串中解释出准确的 argv 来。 只是简单的空格分隔是不够的,因为有些参数本身可能就存在空格,然后还会有双引号包起来,然后双引号自己又可能会存在转义。如何保证从字符串中解释出来的参数是准确的? |
![]() | 4 geelaw 2023-10-06 23:25:51 +08:00 via iPhone ![]() @LonnyWong Windows 上标准解析命令行的方式是有文档的,https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw 解析方法如下(我随便瞎写的,不一定准确): 0. 若字符串只有空格,则设置 argc 为 1 ,argv[0] 为空串,否则转到 1 1. 去掉开头的空格,若已经没有字符,则结束,否则设置状态为无 quote ,设置当前实参为空字符串,转到 2 2. 下一个字符 2.1. 如果现在是无 quote 且这个字符是空格,则把当前实参加入 argv 并转到 1 2.2. 如果接下来有 2n 个 \ 之后紧跟一个 ",则在当前实参中追加 n 个 \,反转 quote 状态,并转到 2 2.3. (2n+1) \ + ",追加 n 个 \ 和 1 个 ",转到 2 2.4. 其他字符,则追加当前字符并转到 2 3. 把最后一个实参加入 argv |
![]() | 5 geelaw 2023-10-06 23:32:07 +08:00 via iPhone 另外 cmd 本身还有自己的内部命令和转义,比如 ^、管道、重定向,除非你的目的就是完全按照 cmd 执行命令,否则用 cmd /c 是错误的。 如果你想用 cmd /c ,那就不能用 argv 传参,而要用直接可以 ShellExecute/CreateProcess 的 API ,这样才能确保命令行准确传递(而不是经过 argv 互转,这个转换对 command line 来说不“往返”,对 argv 往返)。 |
6 shimanooo 2023-10-06 23:51:17 +08:00 当你想要解析字符串,而不是结构化地处理的时候,你往往在做错误的事情。 尤其当是这个命令是外部传入的。看看 ImageMagick 2020 年的那个注入漏洞。 |
![]() | 7 LonnyWong OP @geelaw #5 ssh ProxyCommand 的本意就是原样执行,我抽空看看这个 ShellExecute/CreateProcess 是否适用, 需要操作 stdin 和 stdout 。 |
![]() | 9 tool2d 2023-10-07 00:12:06 +08:00 动态生成一个临时 bat, 包含空格和双引号,用 cmd /c 调用应该是最简单的方法。 |
10 patrickyoung 2023-10-07 00:17:16 +08:00 这个问题我之前写自己的玩具的时候遇到过,不过是在 linux 平台,我记得是 exec/cmd 的 godoc 里有写一些注意事项。首楼中 issue 提到的配置可用于测试吗?可以的话我试着调调看,有能力的话给你发个 PR |
![]() | 11 lianyue 2023-10-07 00:18:18 +08:00 cmd := exec.Command("cmd", "/c", "C:\WINDOWS\System32\OpenSSH\ssh.exe", "-V") |
![]() | 12 LonnyWong OP |
![]() | 14 LonnyWong OP |
![]() | 15 ysc3839 2023-10-07 03:24:29 +08:00 via Android Windows 和 Unix 进程的一大区别是,Unix 进程参数是字符串数组,而 Windows 进程参数只是一个字符串。因此 Unix 程序无需自行解析参数,参数是由 shell 解析成字符串数组的,而 Windows 则需要程序自己解析参数成字符串数组。楼上几位似乎都没提到这个根本区别。 @geelaw 有说法称 CommandLineToArgvW 和 MSVC CRT 内置的解析逻辑不同,cmd 的解析逻辑似乎也与前两者不同,我没有实际测试过情况如何,只是提醒一下可能遇到坑。 |
![]() | 16 geelaw 2023-10-07 03:57:19 +08:00 via iPhone @ysc3839 #15 我以为之前已经算是提到了这个区别了。cmd 有自己的转义,但 cmd 当然不负责外部命令如何理解命令行。MSVC CRT 解析 argv 不是 CommandLineToArgvW 我倒是不知道,另外我刚发现 CommandLineToArgvW 读取空格开头的字符串时会把第一个 argv 设置为空串 orz 除了提醒 lpCmdLine 不需要有 argv 的格式,还应该注意即使 lpCmdLine 解析为 argv ,第一个参数也不一定是程序的名字或者路径。 |
![]() | 17 Kisesy 2023-10-07 09:12:10 +08:00 ![]() 可以用 golang.org/x/sys/windows 下的 DecomposeCommandLine 函数,内部调用系统的 CommandLineToArgv 函数,所以兼容性非常高 |
18 jorneyr 2023-10-07 09:56:26 +08:00 ![]() 我是把要执行的命令写入 bat / sh 文件,然后执行文件,这样可以方便的支持管道等复杂命令。 |