github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/utils/ssh/connect.go (about)

     1  // Copyright © 2021 Alibaba Group Holding Ltd.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ssh
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"net"
    21  	"os"
    22  	"path/filepath"
    23  	"regexp"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"github.com/sealerio/sealer/common"
    29  
    30  	"github.com/sealerio/sealer/utils/hash"
    31  
    32  	"github.com/pkg/sftp"
    33  	"golang.org/x/crypto/ssh"
    34  )
    35  
    36  const DefaultSSHPort = "22"
    37  
    38  func (s *SSH) connect(host net.IP) (*ssh.Client, error) {
    39  	if s.Encrypted {
    40  		passwd, err := hash.AesDecrypt([]byte(s.Password))
    41  		if err != nil {
    42  			return nil, err
    43  		}
    44  		s.Password = passwd
    45  		s.Encrypted = false
    46  	}
    47  	auth := s.sshAuthMethod(s.Password, s.PkFile, s.PkPassword)
    48  	config := ssh.Config{
    49  		Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},
    50  	}
    51  	DefaultTimeout := time.Duration(15) * time.Second
    52  	if s.Timeout == nil {
    53  		s.Timeout = &DefaultTimeout
    54  	}
    55  	clientConfig := &ssh.ClientConfig{
    56  		User:    s.User,
    57  		Auth:    auth,
    58  		Timeout: *s.Timeout,
    59  		Config:  config,
    60  		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
    61  			return nil
    62  		},
    63  	}
    64  	if s.Port == "" {
    65  		s.Port = DefaultSSHPort
    66  	}
    67  	return ssh.Dial("tcp", net.JoinHostPort(host.String(), s.Port), clientConfig)
    68  }
    69  
    70  func (s *SSH) Connect(host net.IP) (*ssh.Client, *ssh.Session, error) {
    71  	client, err := s.connect(host)
    72  	if err != nil {
    73  		return nil, nil, err
    74  	}
    75  
    76  	session, err := client.NewSession()
    77  	if err != nil {
    78  		_ = client.Close()
    79  		return nil, nil, err
    80  	}
    81  
    82  	modes := ssh.TerminalModes{
    83  		ssh.ECHO:          0,     //disable echoing
    84  		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
    85  		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
    86  	}
    87  
    88  	if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
    89  		_ = session.Close()
    90  		_ = client.Close()
    91  		return nil, nil, err
    92  	}
    93  
    94  	return client, session, nil
    95  }
    96  
    97  func (s *SSH) sshAuthMethod(password, pkFile, pkPasswd string) (auth []ssh.AuthMethod) {
    98  	if fileExist(pkFile) {
    99  		am, err := s.sshPrivateKeyMethod(pkFile, pkPasswd)
   100  		if err == nil {
   101  			auth = append(auth, am)
   102  		}
   103  	}
   104  	if password != "" {
   105  		auth = append(auth, s.sshPasswordMethod(password))
   106  	}
   107  	return auth
   108  }
   109  
   110  // Authentication with a private key,private key has password and no password to verify in this
   111  func (s *SSH) sshPrivateKeyMethod(pkFile, pkPassword string) (am ssh.AuthMethod, err error) {
   112  	pkData, err := os.ReadFile(filepath.Clean(pkFile))
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	var pk ssh.Signer
   118  	if pkPassword == "" {
   119  		pk, err = ssh.ParsePrivateKey(pkData)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  	} else {
   124  		bufPwd := []byte(pkPassword)
   125  		pk, err = ssh.ParsePrivateKeyWithPassphrase(pkData, bufPwd)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  	}
   130  	return ssh.PublicKeys(pk), nil
   131  }
   132  
   133  func (s *SSH) sshPasswordMethod(password string) ssh.AuthMethod {
   134  	return ssh.Password(password)
   135  }
   136  
   137  type Client struct {
   138  	SSHClient  *ssh.Client
   139  	SftpClient *sftp.Client
   140  }
   141  
   142  var sshClientMap = map[string]Client{}
   143  
   144  var getSSHClientLock = sync.Mutex{}
   145  
   146  func (s *SSH) sftpConnect(host net.IP) (*sftp.Client, error) {
   147  	getSSHClientLock.Lock()
   148  	defer getSSHClientLock.Unlock()
   149  
   150  	if ret, ok := sshClientMap[host.String()]; ok {
   151  		return ret.SftpClient, nil
   152  	}
   153  
   154  	var (
   155  		sshClient  *ssh.Client
   156  		sftpClient *sftp.Client
   157  		err        error
   158  	)
   159  
   160  	sshClient, err = s.connect(host)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	// create sftp client
   166  	if s.User != common.ROOT {
   167  		sftpClient, err = s.NewSudoSftpClient(sshClient)
   168  	} else {
   169  		sftpClient, err = sftp.NewClient(sshClient)
   170  	}
   171  
   172  	sshClientMap[host.String()] = Client{
   173  		SSHClient:  sshClient,
   174  		SftpClient: sftpClient,
   175  	}
   176  
   177  	return sftpClient, err
   178  }
   179  
   180  func (s *SSH) NewSudoSftpClient(conn *ssh.Client, opts ...sftp.ClientOption) (*sftp.Client, error) {
   181  	var (
   182  		cmd            string
   183  		err            error
   184  		ses, ses2      *ssh.Session
   185  		buff           []byte
   186  		sftpServerPath string
   187  	)
   188  
   189  	ses2, err = conn.NewSession()
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	defer ses2.Close()
   194  
   195  	cmd = `sudo grep -oP "Subsystem\s+sftp\s+\K.*" /etc/ssh/sshd_config`
   196  	buff, err = ses2.Output(cmd)
   197  	if err != nil {
   198  		return nil, fmt.Errorf("failed to execute cmd(%s): %v", cmd, err)
   199  	}
   200  
   201  	ses, err = conn.NewSession()
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	sftpServerPath = strings.ReplaceAll(string(buff), "\r", "")
   207  	if match, _ := regexp.MatchString(`^sudo `, sftpServerPath); !match {
   208  		sftpServerPath = SUDO + sftpServerPath
   209  	}
   210  
   211  	ok, err := ses.SendRequest("exec", true, ssh.Marshal(struct{ Command string }{sftpServerPath}))
   212  	if err == nil && !ok {
   213  		return nil, errors.New("ssh: failed to exec request")
   214  	}
   215  
   216  	pw, err := ses.StdinPipe()
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	pr, err := ses.StdoutPipe()
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	return sftp.NewClientPipe(pr, pw, opts...)
   226  }