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 }