英文:
Golang Enter SSH Sudo Password on Prompt (or exit)
问题
我正在尝试在我的Go程序中通过SSH包运行一个脚本(到目前为止我已经成功了)。
我的问题是,如果用户具有sudo权限,脚本会尝试使用sudo运行命令,这会导致bash脚本暂停,直到用户输入密码。
例如:
[ERROR ] Install cs-server: Checking dependencies: missing: lib32gcc1
# 它尝试使用sudo安装缺失的依赖项,但在这里暂停
[sudo] password for guest: 
在我的Go程序中,我编写了类似于以下代码:
// 连接到SSH并获取会话...
out, err := session.StdoutPipe()
if err != nil {
    log.Fatal(err)
}
go func(out io.Reader) {
    r := bufio.NewScanner(out)
    for r.Scan() {
        fmt.Println(r.Text())
    }
}(out)
// 执行ssh命令...
我收到的输出与上面的示例完全相同,只是在这种情况下,我甚至看不到[sudo] password for guest: 这一行...它只打印到[ERROR ] Install cs-server: Checking dependencies: missing: lib32gcc1并永远暂停。
如何绕过这个暂停?我的选择是要么从我的Go程序自动输入密码,要么结束ssh执行并只接收输出。
英文:
I'm trying to run a script via the SSH package in my Go program (so far I've had success).
My issue is, the script attempts to run a command with sudo if the user has sudo privileges, and this causes the bash script to pause until a password is entered by the user.
For example:
[ERROR ] Install cs-server: Checking dependencies: missing: lib32gcc1
# It attempts to install the missing dependencies with sudo but pauses here
[sudo] password for guest: 
In my Go program, I have written something that looks similar to this:
// Connect to SSH and retreive session...
out, err := session.StdoutPipe()
if err != nil {
	log.Fatal(err)
}
go func(out io.Reader) {
	r := bufio.NewScanner(out)
	for r.Scan() {
		fmt.Println(r.Text())
	}
}(out)
// Execute ssh command...
And I receive the exact same output as the example above, only in this case, I don't even see the line [sudo] password for guest: ... it only prints up to [ERROR ] Install cs-server: Checking dependencies: missing: lib32gcc1 and pauses forever.
How can I bypass this pause? My options are to either enter the password from my Go program automatically, or end the ssh execution and just receive the output.
答案1
得分: 10
我使用session.StdoutPipe()和session.StdinPipe()解决了这个问题。我编写了一个Go协程,它扫描每个字节,并检查最后一行是否以"[sudo] password for "开头,以": "结尾。它将password + "\n"写入session.StdinPipe(),以继续执行脚本。
以下是我为此问题编写的所有代码。
package ssh
import (
	"bufio"
	"io"
	"log"
	"net"
	"strings"
	"golang.org/x/crypto/ssh"
)
type Connection struct {
	*ssh.Client
	password string
}
func Connect(addr, user, password string) (*Connection, error) {
	sshConfig := &ssh.ClientConfig{
		User: user,
		Auth: []ssh.AuthMethod{
			ssh.Password(password),
		},
		HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
	}
	conn, err := ssh.Dial("tcp", addr, sshConfig)
	if err != nil {
		return nil, err
	}
	return &Connection{conn, password}, nil
}
func (conn *Connection) SendCommands(cmds ...string) ([]byte, error) {
	session, err := conn.NewSession()
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()
	modes := ssh.TerminalModes{
		ssh.ECHO:          0,     // disable echoing
		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
	}
	err = session.RequestPty("xterm", 80, 40, modes)
	if err != nil {
		return []byte{}, err
	}
	in, err := session.StdinPipe()
	if err != nil {
		log.Fatal(err)
	}
	out, err := session.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	var output []byte
	go func(in io.WriteCloser, out io.Reader, output *[]byte) {
		var (
			line string
			r    = bufio.NewReader(out)
		)
		for {
			b, err := r.ReadByte()
			if err != nil {
				break
			}
			*output = append(*output, b)
			if b == byte('\n') {
				line = ""
				continue
			}
			line += string(b)
			if strings.HasPrefix(line, "[sudo] password for ") && strings.HasSuffix(line, ": ") {
				_, err = in.Write([]byte(conn.password + "\n"))
				if err != nil {
					break
				}
			}
		}
	}(in, out, &output)
	cmd := strings.Join(cmds, "; ")
	_, err = session.Output(cmd)
	if err != nil {
		return []byte{}, err
	}
	return output, nil
}
以下是如何使用它的示例。
// ssh是上面自定义的包
conn, err := ssh.Connect("0.0.0.0:22", "username", "password")
if err != nil {
	log.Fatal(err)
}
output, err := conn.SendCommands("sleep 2", "echo Hello!")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(output))
英文:
I managed to fix this issue by making use of the session.StdoutPipe() and session.StdinPipe(). I wrote a go routine which scans each byte and checks if the last written line starts with "[sudo] password for " and ends with ": ". It will write the password + "\n" to the session.StdinPipe() which continues execution of the script.
Here's all of the code I have for this.
package ssh
import (
"bufio"
"io"
"log"
"net"
"strings"
"golang.org/x/crypto/ssh"
)
type Connection struct {
*ssh.Client
password string
}
func Connect(addr, user, password string) (*Connection, error) {
sshConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
}
conn, err := ssh.Dial("tcp", addr, sshConfig)
if err != nil {
return nil, err
}
return &Connection{conn, password}, nil
}
func (conn *Connection) SendCommands(cmds ...string) ([]byte, error) {
session, err := conn.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
modes := ssh.TerminalModes{
ssh.ECHO:          0,     // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
err = session.RequestPty("xterm", 80, 40, modes)
if err != nil {
return []byte{}, err
}
in, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
out, err := session.StdoutPipe()
if err != nil {
log.Fatal(err)
}
var output []byte
go func(in io.WriteCloser, out io.Reader, output *[]byte) {
var (
line string
r    = bufio.NewReader(out)
)
for {
b, err := r.ReadByte()
if err != nil {
break
}
*output = append(*output, b)
if b == byte('\n') {
line = ""
continue
}
line += string(b)
if strings.HasPrefix(line, "[sudo] password for ") && strings.HasSuffix(line, ": ") {
_, err = in.Write([]byte(conn.password + "\n"))
if err != nil {
break
}
}
}
}(in, out, &output)
cmd := strings.Join(cmds, "; ")
_, err = session.Output(cmd)
if err != nil {
return []byte{}, err
}
return output, nil
}
And an example of how you could use it.
// ssh refers to the custom package above
conn, err := ssh.Connect("0.0.0.0:22", "username", "password")
if err != nil {
log.Fatal(err)
}
output, err := conn.SendCommands("sleep 2", "echo Hello!")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
答案2
得分: 4
这是一个问题,@acidic的代码无法完全捕获输出流。更新后的代码如下:
package main
import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"golang.org/x/crypto/ssh"
)
type Connection struct {
	*ssh.Client
	password string
}
func Connect(addr, user, password string) (*Connection, error) {
	sshConfig := &ssh.ClientConfig{
		User: user,
		Auth: []ssh.AuthMethod{
			ssh.Password(password),
		},
		HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
	}
	conn, err := ssh.Dial("tcp", addr, sshConfig)
	if err != nil {
		return nil, err
	}
	return &Connection{conn, password}, nil
}
func (conn *Connection) SendCommands(cmds string) ([]byte, error) {
	session, err := conn.NewSession()
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()
	modes := ssh.TerminalModes{
		ssh.ECHO:          0,     // 禁用回显
		ssh.TTY_OP_ISPEED: 14400, // 输入速度 = 14.4kbaud
		ssh.TTY_OP_OSPEED: 14400, // 输出速度 = 14.4kbaud
	}
	err = session.RequestPty("xterm", 80, 40, modes)
	if err != nil {
		return []byte{}, err
	}
	stdoutB := new(bytes.Buffer)
	session.Stdout = stdoutB
	in, _ := session.StdinPipe()
	go func(in io.Writer, output *bytes.Buffer) {
		for {
			if strings.Contains(string(output.Bytes()), "[sudo] password for ") {
				_, err = in.Write([]byte(conn.password + "\n"))
				if err != nil {
					break
				}
				fmt.Println("输入密码 --- 结束。")
				break
			}
		}
	}(in, stdoutB)
	err = session.Run(cmds)
	if err != nil {
		return []byte{}, err
	}
	return stdoutB.Bytes(), nil
}
func main() {
	// ssh 是上面自定义的包
	conn, err := Connect("0.0.0.0:22", "username", "password")
	if err != nil {
		log.Fatal(err)
	}
	output, err := conn.SendCommands("sudo docker ps")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(output))
}
希望对你有帮助!
英文:
This is an issue that output stream can't be fully captured for @acidic's code.
The updated code is as following
package main
import (
"bytes"
"fmt"
"io"
"log"
"net"
"strings"
"golang.org/x/crypto/ssh"
)
type Connection struct {
*ssh.Client
password string
}
func Connect(addr, user, password string) (*Connection, error) {
sshConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
}
conn, err := ssh.Dial("tcp", addr, sshConfig)
if err != nil {
return nil, err
}
return &Connection{conn, password}, nil
}
func (conn *Connection) SendCommands(cmds string) ([]byte, error) {
session, err := conn.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
modes := ssh.TerminalModes{
ssh.ECHO:          0,     // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
err = session.RequestPty("xterm", 80, 40, modes)
if err != nil {
return []byte{}, err
}
stdoutB := new(bytes.Buffer)
session.Stdout = stdoutB
in, _ := session.StdinPipe()
go func(in io.Writer, output *bytes.Buffer) {
for {
if strings.Contains(string(output.Bytes()), "[sudo] password for ") {
_, err = in.Write([]byte(conn.password + "\n"))
if err != nil {
break
}
fmt.Println("put the password ---  end .")
break
}
}
}(in, stdoutB)
err = session.Run(cmds)
if err != nil {
return []byte{}, err
}
return stdoutB.Bytes(), nil
}
func main() {
// ssh refers to the custom package above
conn, err := Connect("0.0.0.0:22", "username", "password")
if err != nil {
log.Fatal(err)
}
output, err := conn.SendCommands("sudo docker ps")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
}
答案3
得分: 0
一个解决方法是将 sudo [cmd] 转换为 echo 
 | sudo -S [cmd],这不是很好,但对我有效。
英文:
A work around is converting sudo [cmd] to echo 
 | sudo -S [cmd], it is not good, but working for me.
答案4
得分: 0
另一种解决方法,如果你不想使用ssh库,可以使用pty库创建一个伪终端。以下是一个非常简单的示例:
import (
	"io"
	"os"
	"os/exec"
	"time"
	"github.com/creack/pty"
)
func main() {
	c := exec.Command("ssh", "<user>@<IP>")
	f, err := pty.Start(c)
	if err != nil {
		panic(err)
	}
	time.Sleep(2 * time.Second)
	f.Write([]byte("1234\n"))
	io.Copy(os.Stdout, f)
}
这段代码使用pty库创建了一个伪终端,并通过ssh命令连接到指定的用户和IP地址。然后,它等待2秒钟,向伪终端写入"1234\n",并将伪终端的输出复制到标准输出。
英文:
Another workaround if you dont want to use ssh library is to make a pseudo terminal using pty library. An extremely simple example as above
    import (
"io"
"os"
"os/exec"
"time"
"github.com/creack/pty"
)
func main() {
c := exec.Command("ssh", "<user>@<IP>")
f, err := pty.Start(c)
if err != nil {
panic(err)
}
time.Sleep(2 * time.Second)
f.Write([]byte("1234\n"))
io.Copy(os.Stdout, f)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论