github.com/bshelton229/agent@v3.5.4+incompatible/bootstrap/knownhosts.go (about)

     1  package bootstrap
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/buildkite/agent/bootstrap/shell"
    12  	homedir "github.com/mitchellh/go-homedir"
    13  	"github.com/pkg/errors"
    14  	"golang.org/x/crypto/ssh/knownhosts"
    15  )
    16  
    17  type knownHosts struct {
    18  	Shell *shell.Shell
    19  	Path  string
    20  }
    21  
    22  func findKnownHosts(sh *shell.Shell) (*knownHosts, error) {
    23  	userHomePath, err := homedir.Dir()
    24  	if err != nil {
    25  		return nil, fmt.Errorf("Could not find the current users home directory (%s)", err)
    26  	}
    27  
    28  	// Construct paths to the known_hosts file
    29  	sshDirectory := filepath.Join(userHomePath, ".ssh")
    30  	knownHostPath := filepath.Join(sshDirectory, "known_hosts")
    31  
    32  	// Ensure ssh directory exists
    33  	if err := os.MkdirAll(sshDirectory, 0700); err != nil {
    34  		return nil, err
    35  	}
    36  
    37  	// Ensure file exists
    38  	if _, err := os.Stat(knownHostPath); err != nil {
    39  		f, err := os.OpenFile(knownHostPath, os.O_CREATE|os.O_WRONLY, 0600)
    40  		if err != nil {
    41  			return nil, errors.Wrapf(err, "Could not create %q", knownHostPath)
    42  		}
    43  		if err = f.Close(); err != nil {
    44  			return nil, err
    45  		}
    46  	}
    47  
    48  	return &knownHosts{Shell: sh, Path: knownHostPath}, nil
    49  }
    50  
    51  func (kh *knownHosts) Contains(host string) (bool, error) {
    52  	file, err := os.Open(kh.Path)
    53  	if err != nil {
    54  		return false, err
    55  	}
    56  	defer file.Close()
    57  
    58  	normalized := knownhosts.Normalize(host)
    59  
    60  	// There don't appear to be any libraries to parse known_hosts that don't also want to
    61  	// validate the IP's and host keys. Shelling out to ssh-keygen doesn't support custom ports
    62  	// so I guess we'll do it ourselves.
    63  	//
    64  	// known_host format is defined at https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT
    65  	// A basic example is:
    66  	// # Comments allowed at start of line
    67  	// closenet,...,192.0.2.53 1024 37 159...93 closenet.example.net
    68  	// cvs.example.net,192.0.2.10 ssh-rsa AAAA1234.....=
    69  	// # A hashed hostname
    70  	// |1|JfKTdBh7rNbXkVAQCRp4OQoPfmI=|USECr3SWf1JUPsms5AqfD5QfxkM= ssh-rsa
    71  	// AAAA1234.....=
    72  	// # A revoked key
    73  	// @revoked * ssh-rsa AAAAB5W...
    74  	// # A CA key, accepted for any host in *.mydomain.com or *.mydomain.org
    75  	// @cert-authority *.mydomain.org,*.mydomain.com ssh-rsa AAAAB5W...
    76  	scanner := bufio.NewScanner(file)
    77  	for scanner.Scan() {
    78  		fields := strings.Split(scanner.Text(), " ")
    79  		if len(fields) != 3 {
    80  			continue
    81  		}
    82  		for _, addr := range strings.Split(fields[0], ",") {
    83  			if addr == normalized || addr == knownhosts.HashHostname(normalized) {
    84  				return true, nil
    85  			}
    86  		}
    87  	}
    88  
    89  	return false, nil
    90  }
    91  
    92  func (kh *knownHosts) Add(host string) error {
    93  	// Use a lockfile to prevent parallel processes stepping on each other
    94  	lock, err := kh.Shell.LockFile(kh.Path+".lock", time.Second*30)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	defer func() {
    99  		if err := lock.Unlock(); err != nil {
   100  			kh.Shell.Warningf("Failed to release known_hosts file lock: %#v", err)
   101  		}
   102  	}()
   103  
   104  	// If the keygen output already contains the host, we can skip!
   105  	if contains, _ := kh.Contains(host); contains {
   106  		kh.Shell.Commentf("Host %q already in list of known hosts at \"%s\"", host, kh.Path)
   107  		return nil
   108  	}
   109  
   110  	// Scan the key and then write it to the known_host file
   111  	keyscanOutput, err := sshKeyScan(kh.Shell, host)
   112  	if err != nil {
   113  		return errors.Wrap(err, "Could not perform `ssh-keyscan`")
   114  	}
   115  
   116  	// Try and open the existing hostfile in (append_only) mode
   117  	f, err := os.OpenFile(kh.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700)
   118  	if err != nil {
   119  		return errors.Wrapf(err, "Could not open %q for appending", kh.Path)
   120  	}
   121  	defer f.Close()
   122  
   123  	if _, err = fmt.Fprintf(f, "%s\n", keyscanOutput); err != nil {
   124  		return errors.Wrapf(err, "Could not write to %q", kh.Path)
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  // AddFromRepository takes a git repo url, extracts the host and adds it
   131  func (kh *knownHosts) AddFromRepository(repository string) error {
   132  	u, err := parseGittableURL(repository)
   133  	if err != nil {
   134  		kh.Shell.Warningf("Could not parse %q as a URL - skipping adding host to SSH known_hosts", repository)
   135  		return err
   136  	}
   137  
   138  	// We only need to keyscan ssh repository urls
   139  	if u.Scheme != "ssh" {
   140  		return nil
   141  	}
   142  
   143  	host := stripAliasesFromGitHost(u.Host)
   144  
   145  	if err = kh.Add(host); err != nil {
   146  		return errors.Wrapf(err, "Failed to add `%s` to known_hosts file `%s`", host, u)
   147  	}
   148  
   149  	return nil
   150  }