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  }