Feature:sudo support for scp (#3421)

This commit is contained in:
榴莲榴莲
2023-06-29 14:55:25 +08:00
committed by GitHub
parent fedcf555b5
commit 69f2fdd742
4 changed files with 186 additions and 5 deletions
+12 -2
View File
@@ -64,12 +64,22 @@ func (c *Client) CmdToString(host, cmd, sep string) (string, error) {
}
func (c *Client) newClientAndSftpClient(host string) (*ssh.Client, *sftp.Client, error) {
sshClient, err := c.connect(host)
var (
sshClient *ssh.Client
sftpClient *sftp.Client
err error
)
sshClient, err = c.connect(host)
if err != nil {
return nil, nil, err
}
// create sftp client
sftpClient, err := sftp.NewClient(sshClient)
if c.Option.sudo || c.Option.user != defaultUsername {
sftpClient, err = NewSudoSftpClient(sshClient, c.password)
} else {
sftpClient, err = sftp.NewClient(sshClient)
}
return sshClient, sftpClient, err
}
+4
View File
@@ -145,6 +145,10 @@ func newOptionFromSSH(ssh *v2.SSH, isStdout bool) *Option {
if len(ssh.PkData) > 0 {
opts = append(opts, WithRawPrivateKeyDataAndPhrase(ssh.PkData, ssh.PkPasswd))
}
if ssh.User != defaultUsername {
opts = append(opts, WithSudoEnable(true))
}
opt := NewOption()
for i := range opts {
opts[i](opt)
+1 -3
View File
@@ -42,13 +42,11 @@ func (c *Client) Ping(host string) error {
return client.Close()
}
// wrapCommands just placeholder for now
func (c *Client) wrapCommands(cmds ...string) string {
if !c.Option.sudo || c.Option.user == defaultUsername {
return strings.Join(cmds, "; ")
}
// run as user provided and set HOME env, then run `bash -c` in a sub-shell
return fmt.Sprintf("sudo -u %s -H bash -c '%s'", c.Option.user, strings.Join(cmds, "; "))
return fmt.Sprintf("sudo -E /bin/bash -c '%s'", strings.Join(cmds, "; "))
}
func (c *Client) CmdAsyncWithContext(ctx context.Context, host string, cmds ...string) error {
+169
View File
@@ -0,0 +1,169 @@
/*
Copyright 2023 ghostloda
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ssh
import (
"fmt"
"io"
"strings"
"github.com/pkg/errors"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
const (
promptpwd string = "sudo password"
promptsuccess string = "sudo success!"
prompterror string = "sudo failed!!"
promptlength int = 13
sshdConfigPath = "/etc/ssh/sshd_config"
)
func NewSudoSftpClient(conn *ssh.Client, sudopwd string, opts ...sftp.ClientOption) (*sftp.Client, error) {
s, err := conn.NewSession()
if err != nil {
return nil, err
}
sftpServerPath, err := getSftpServerPath(conn, sudopwd)
if err != nil {
return nil, err
}
// sudo -p specifies the password prompt from sudo. The prompt is sent to stderr
// sudo -S dictates that password should be read from stdin
// sudo -u specifies the user
// sh -c 'cmd' passes cmd to sh
// >&2 echo "%s" echos promptsucess to stderr before starting sftp server ( see https://stackoverflow.com/a/23550347 )
// & %s is the final command to be executed is sudo is successful, which is the path to the sftp-server binary. It is a list of possible binary paths and it will pick the first one that exists. At the end, it will look in $PATH.
// || sh -c '>&2 echo "%s" & exit 1' -> runs if sudo failed and ensures that prompterror is written to stderr before erroring
// In total this means that we can read from stderr and write to stdin to
cmd := fmt.Sprintf(
`sudo -p '%s' -S sh -c '>&2 echo "%s" & %s' || sh -c '>&2 echo "%s" & exit 1'`,
promptpwd+"\n",
promptsuccess,
sftpServerPath,
prompterror,
)
stdin, err := s.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := s.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := s.StderrPipe()
if err != nil {
return nil, err
}
err = s.Start(cmd)
if err != nil {
return nil, err
}
// Sudo might output a lecture, so looping for either password, error or success prompt
prompt, err := getPrompt(stderr)
if err != nil {
return nil, errors.Wrap(err, "could not get err prompt")
}
switch prompt {
case prompterror:
return nil, errors.New("sudo failed")
case promptpwd:
// ignore any writer error
_, _ = stdin.Write([]byte(sudopwd + "\n"))
case promptsuccess:
return sftp.NewClientPipe(stdout, stdin, opts...)
}
// Second read is only after pwd has been entered.
// If it returns pwd prompt again, it means that it was the wrong password.
// Error means the user is not allowed to sudo or something else went wrong.
prompt, err = getPrompt(stderr)
if err != nil {
return nil, errors.Wrap(err, "second stderr read failed")
}
switch prompt {
case prompterror:
return nil, errors.New("sudo failed or no sudo rights")
case promptpwd:
return nil, fmt.Errorf("wrong sudo password")
}
return sftp.NewClientPipe(stdout, stdin, opts...)
}
// getPrompt is a handy method for reading a prompt from stderr
// prompt should be at the end of the read, minus a newline
func getPrompt(rd io.Reader) (string, error) {
buf := make([]byte, 2048)
var prompt string
for {
n, err := rd.Read(buf)
if err != nil {
return "", err
}
if n <= promptlength+1 {
prompt = string(buf[:n-1])
} else {
prompt = string(buf[n-promptlength-1 : n-1])
}
if prompt == prompterror || prompt == promptpwd || prompt == promptsuccess {
return prompt, nil
}
}
}
func getSftpServerPath(conn *ssh.Client, sudopwd string) (string, error) {
ses, err := conn.NewSession()
if err != nil {
return "", err
}
defer ses.Close()
cmd := fmt.Sprintf(`sudo -S grep -oP "Subsystem\s+sftp\s+\K.*" %s`, sshdConfigPath)
in, err := ses.StdinPipe()
if err != nil {
return "", err
}
b := autoAnswerWriter{
in: in,
answer: []byte(sudopwd + "\n"),
condition: isSudoPrompt,
}
ses.Stdout = &b
ses.Stderr = &b
err = ses.Run(cmd)
if err != nil {
return "", fmt.Errorf("failed to find sftp-server binary path on %s: %v", sshdConfigPath, err)
}
sftpServerPath := strings.ReplaceAll(b.b.String(), "\r", "")
return sftpServerPath, nil
}