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 }