github.com/scaleway/scaleway-cli@v1.11.1/pkg/utils/utils.go (about) 1 // Copyright (C) 2015 Scaleway. All rights reserved. 2 // Use of this source code is governed by a MIT-style 3 // license that can be found in the LICENSE.md file. 4 5 // scw helpers 6 7 // Package utils contains helpers 8 package utils 9 10 import ( 11 "crypto/md5" 12 "errors" 13 "fmt" 14 "io" 15 "net" 16 "os" 17 "os/exec" 18 "path" 19 "path/filepath" 20 "reflect" 21 "regexp" 22 "strings" 23 "time" 24 25 "golang.org/x/crypto/ssh" 26 27 "github.com/Sirupsen/logrus" 28 log "github.com/Sirupsen/logrus" 29 "github.com/mattn/go-isatty" 30 "github.com/moul/gotty-client" 31 "github.com/scaleway/scaleway-cli/pkg/sshcommand" 32 ) 33 34 // SpawnRedirection is used to redirects the fluxes 35 type SpawnRedirection struct { 36 Stdin io.Reader 37 Stdout io.Writer 38 Stderr io.Writer 39 } 40 41 // SSHExec executes a command over SSH and redirects file-descriptors 42 func SSHExec(publicIPAddress, privateIPAddress, user string, port int, command []string, checkConnection bool, gateway string) error { 43 gatewayUser := "root" 44 gatewayIPAddress := gateway 45 if strings.Contains(gateway, "@") { 46 parts := strings.Split(gatewayIPAddress, "@") 47 if len(parts) != 2 { 48 return fmt.Errorf("gateway: must be like root@IP") 49 } 50 gatewayUser = parts[0] 51 gatewayIPAddress = parts[1] 52 gateway = gatewayUser + "@" + gatewayIPAddress 53 } 54 55 if publicIPAddress == "" && gatewayIPAddress == "" { 56 return errors.New("server does not have public IP") 57 } 58 if privateIPAddress == "" && gatewayIPAddress != "" { 59 return errors.New("server does not have private IP") 60 } 61 62 if checkConnection { 63 useGateway := gatewayIPAddress != "" 64 if useGateway && !IsTCPPortOpen(fmt.Sprintf("%s:22", gatewayIPAddress)) { 65 return errors.New("gateway is not available, try again later") 66 } 67 if !useGateway && !IsTCPPortOpen(fmt.Sprintf("%s:%d", publicIPAddress, port)) { 68 return errors.New("server is not ready, try again later") 69 } 70 } 71 72 sshCommand := NewSSHExecCmd(publicIPAddress, privateIPAddress, user, port, isatty.IsTerminal(os.Stdin.Fd()), command, gateway) 73 74 log.Debugf("Executing: %s", sshCommand) 75 76 spawn := exec.Command("ssh", sshCommand.Slice()[1:]...) 77 spawn.Stdout = os.Stdout 78 spawn.Stdin = os.Stdin 79 spawn.Stderr = os.Stderr 80 return spawn.Run() 81 } 82 83 // NewSSHExecCmd computes execve compatible arguments to run a command via ssh 84 func NewSSHExecCmd(publicIPAddress, privateIPAddress, user string, port int, allocateTTY bool, command []string, gatewayIPAddress string) *sshcommand.Command { 85 quiet := os.Getenv("DEBUG") != "1" 86 secureExec := os.Getenv("SCW_SECURE_EXEC") == "1" 87 sshCommand := &sshcommand.Command{ 88 AllocateTTY: allocateTTY, 89 Command: command, 90 Host: publicIPAddress, 91 Quiet: quiet, 92 SkipHostKeyChecking: !secureExec, 93 User: user, 94 NoEscapeCommand: true, 95 Port: port, 96 } 97 if gatewayIPAddress != "" { 98 sshCommand.Host = privateIPAddress 99 sshCommand.Gateway = &sshcommand.Command{ 100 Host: gatewayIPAddress, 101 SkipHostKeyChecking: !secureExec, 102 AllocateTTY: allocateTTY, 103 Quiet: quiet, 104 User: user, 105 Port: port, 106 } 107 } 108 109 return sshCommand 110 } 111 112 // GeneratingAnSSHKey generates an SSH key 113 func GeneratingAnSSHKey(cfg SpawnRedirection, path string, name string) (string, error) { 114 args := []string{ 115 "-t", 116 "rsa", 117 "-b", 118 "4096", 119 "-f", 120 filepath.Join(path, name), 121 "-N", 122 "", 123 "-C", 124 "", 125 } 126 log.Infof("Executing commands %v", args) 127 spawn := exec.Command("ssh-keygen", args...) 128 spawn.Stdout = cfg.Stdout 129 spawn.Stdin = cfg.Stdin 130 spawn.Stderr = cfg.Stderr 131 return args[5], spawn.Run() 132 } 133 134 // WaitForTCPPortOpen calls IsTCPPortOpen in a loop 135 func WaitForTCPPortOpen(dest string) error { 136 for { 137 if IsTCPPortOpen(dest) { 138 break 139 } 140 time.Sleep(1 * time.Second) 141 } 142 return nil 143 } 144 145 // IsTCPPortOpen returns true if a TCP communication with "host:port" can be initialized 146 func IsTCPPortOpen(dest string) bool { 147 conn, err := net.DialTimeout("tcp", dest, time.Duration(2000)*time.Millisecond) 148 if err == nil { 149 defer conn.Close() 150 } 151 return err == nil 152 } 153 154 // TruncIf ensures the input string does not exceed max size if cond is met 155 func TruncIf(str string, max int, cond bool) string { 156 if cond && len(str) > max { 157 return str[:max] 158 } 159 return str 160 } 161 162 // Wordify convert complex name to a single word without special shell characters 163 func Wordify(str string) string { 164 str = regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAllString(str, "_") 165 str = regexp.MustCompile(`__+`).ReplaceAllString(str, "_") 166 str = strings.Trim(str, "_") 167 return str 168 } 169 170 // PathToTARPathparts returns the two parts of a unix path 171 func PathToTARPathparts(fullPath string) (string, string) { 172 fullPath = strings.TrimRight(fullPath, "/") 173 return path.Dir(fullPath), path.Base(fullPath) 174 } 175 176 // RemoveDuplicates transforms an array into a unique array 177 func RemoveDuplicates(elements []string) []string { 178 encountered := map[string]bool{} 179 180 // Create a map of all unique elements. 181 for v := range elements { 182 encountered[elements[v]] = true 183 } 184 185 // Place all keys from the map into a slice. 186 result := []string{} 187 for key := range encountered { 188 result = append(result, key) 189 } 190 return result 191 } 192 193 // AttachToSerial tries to connect to server serial using 'gotty-client' and fallback with a help message 194 func AttachToSerial(serverID, apiToken, url string) (*gottyclient.Client, chan bool, error) { 195 gottyURL := os.Getenv("SCW_GOTTY_URL") 196 if gottyURL == "" { 197 gottyURL = url 198 } 199 URL := fmt.Sprintf("%s?arg=%s&arg=%s", gottyURL, apiToken, serverID) 200 201 logrus.Debug("Connection to ", URL) 202 gottycli, err := gottyclient.NewClient(URL) 203 if err != nil { 204 return nil, nil, err 205 } 206 207 if os.Getenv("SCW_TLSVERIFY") == "0" { 208 gottycli.SkipTLSVerify = true 209 } 210 211 gottycli.UseProxyFromEnv = true 212 213 if err = gottycli.Connect(); err != nil { 214 return nil, nil, err 215 } 216 done := make(chan bool) 217 218 fmt.Println("You are connected, type 'Ctrl+q' to quit.") 219 go func() { 220 gottycli.Loop() 221 gottycli.Close() 222 done <- true 223 }() 224 return gottycli, done, nil 225 } 226 227 func rfc4716hex(data []byte) string { 228 fingerprint := "" 229 230 for i := 0; i < len(data); i++ { 231 fingerprint = fmt.Sprintf("%s%0.2x", fingerprint, data[i]) 232 if i != len(data)-1 { 233 fingerprint = fingerprint + ":" 234 } 235 } 236 return fingerprint 237 } 238 239 // SSHGetFingerprint returns the fingerprint of an SSH key 240 func SSHGetFingerprint(key []byte) (string, error) { 241 publicKey, comment, _, _, err := ssh.ParseAuthorizedKey(key) 242 if err != nil { 243 return "", err 244 } 245 switch reflect.TypeOf(publicKey).String() { 246 case "*ssh.rsaPublicKey", "*ssh.dsaPublicKey", "*ssh.ecdsaPublicKey": 247 md5sum := md5.Sum(publicKey.Marshal()) 248 return publicKey.Type() + " " + rfc4716hex(md5sum[:]) + " " + comment, nil 249 default: 250 return "", errors.New("Can't handle this key") 251 } 252 }