github.com/devops-filetransfer/sshego@v7.0.4+incompatible/knownhosts.go (about) 1 package sshego 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "net" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 "sync" 15 16 "github.com/glycerine/sshego/xendor/github.com/glycerine/xcryptossh" 17 ) 18 19 // KnownHosts represents in Hosts a hash map of host identifier (ip or name) 20 // and the corresponding public key for the server. It corresponds to the 21 // ~/.ssh/known_hosts file. 22 type KnownHosts struct { 23 Hosts map[string]*ServerPubKey 24 curHost *ServerPubKey 25 curStatus HostState 26 27 // FilepathPrefix doesn't have the .json.snappy suffix on it. 28 FilepathPrefix string 29 30 PersistFormatSuffix string 31 32 // PersistFormat is the format indicator 33 PersistFormat KnownHostsPersistFormat 34 35 // NoSave means we don't touch the files we read from 36 NoSave bool 37 38 Mut sync.Mutex 39 } 40 41 // ServerPubKey stores the RSA public keys for a particular known server. This 42 // structure is stored in KnownHosts.Hosts. 43 type ServerPubKey struct { 44 Hostname string 45 46 // HumanKey is a serialized and readable version of Key, the key for Hosts map in KnownHosts. 47 HumanKey string 48 ServerBanned bool 49 //OurAcctKeyPair ssh.Signer 50 51 remote net.Addr // unmarshalled form of Hostname 52 53 //key ssh.PublicKey // unmarshalled form of HumanKey 54 55 // reading ~/.ssh/known_hosts 56 Markers string 57 Hostnames string 58 SplitHostnames map[string]bool 59 Keytype string 60 Base64EncodededPublicKey string 61 Comment string 62 Port string 63 LineInFileOneBased int 64 65 // if AlreadySaved, then we don't need to append. 66 AlreadySaved bool 67 68 // lock around SplitHostnames access 69 Mut sync.Mutex 70 } 71 72 type KnownHostsPersistFormat int 73 74 const ( 75 KHJson KnownHostsPersistFormat = 0 76 KHGob KnownHostsPersistFormat = 1 77 KHSsh KnownHostsPersistFormat = 2 78 ) 79 80 // NewKnownHosts creats a new KnownHosts structure. 81 // filepathPrefix does not include the 82 // PersistFormat suffix. If filepathPrefix + defaultFileFormat() 83 // exists as a file on disk, then we read the 84 // contents of that file into the new KnownHosts. 85 // 86 // The returned KnownHosts will remember the 87 // filepathPrefix for future saves. 88 // 89 func NewKnownHosts(filepath string, format KnownHostsPersistFormat) (*KnownHosts, error) { 90 p("NewKnownHosts called, with filepath = '%s', format='%v'", filepath, format) 91 92 h := &KnownHosts{ 93 PersistFormat: format, 94 } 95 96 h.FilepathPrefix = filepath 97 fn := filepath 98 switch format { 99 case KHJson: 100 h.PersistFormatSuffix = ".json.snappy" 101 case KHGob: 102 h.PersistFormatSuffix = ".gob.snappy" 103 } 104 fn += h.PersistFormatSuffix 105 106 var err error 107 if fileExists(fn) { 108 //pp("fn '%s' exists in NewKnownHosts(). format = %v\n", fn, format) 109 110 switch format { 111 case KHJson: 112 err = h.readJSONSnappy(fn) 113 if err != nil { 114 return nil, err 115 } 116 case KHGob: 117 err = h.readGobSnappy(fn) 118 if err != nil { 119 return nil, err 120 } 121 case KHSsh: 122 h, err = LoadSshKnownHosts(fn) 123 if err != nil { 124 return nil, err 125 } 126 default: 127 return nil, fmt.Errorf("unknown persistence format: %v", format) 128 } 129 130 //pp("after reading from file, h = '%#v'\n", h) 131 132 } else { 133 //pp("fn '%s' does not exist already in NewKnownHosts()\n", fn) 134 //pp("making h.Hosts in NewKnownHosts()\n") 135 h.Hosts = make(map[string]*ServerPubKey) 136 } 137 138 return h, nil 139 } 140 141 // KnownHostsEqual compares two instances of KnownHosts structures for equality. 142 func KnownHostsEqual(a, b *KnownHosts) (bool, error) { 143 a.Mut.Lock() 144 defer a.Mut.Unlock() 145 b.Mut.Lock() 146 defer b.Mut.Unlock() 147 148 for k, v := range a.Hosts { 149 v2, ok := b.Hosts[k] 150 if !ok { 151 return false, fmt.Errorf("KnownHostsEqual detected difference at key '%s': a.Hosts had this key, but b.Hosts did not have this key", k) 152 } 153 if v.HumanKey != v2.HumanKey { 154 return false, fmt.Errorf("KnownHostsEqual detected difference at key '%s': a.HumanKey = '%s' but b.HumanKey = '%s'", k, v.HumanKey, v2.HumanKey) 155 } 156 if v.Hostname != v2.Hostname { 157 return false, fmt.Errorf("KnownHostsEqual detected difference at key '%s': a.Hostname = '%s' but b.Hostname = '%s'", k, v.Hostname, v2.Hostname) 158 } 159 if v.ServerBanned != v2.ServerBanned { 160 return false, fmt.Errorf("KnownHostsEqual detected difference at key '%s': a.ServerBanned = '%v' but b.ServerBanned = '%v'", k, v.ServerBanned, v2.ServerBanned) 161 } 162 } 163 for k := range b.Hosts { 164 _, ok := a.Hosts[k] 165 if !ok { 166 return false, fmt.Errorf("KnownHostsEqual detected difference at key '%s': b.Hosts had this key, but a.Hosts did not have this key", k) 167 } 168 } 169 return true, nil 170 } 171 172 // Sync writes the contents of the KnownHosts structure to the 173 // file h.FilepathPrefix + h.PersistFormat (for json/gob); to 174 // just h.FilepathPrefix for "ssh_known_hosts" format. 175 func (h *KnownHosts) Sync() (err error) { 176 fn := h.FilepathPrefix + h.PersistFormatSuffix 177 switch h.PersistFormat { 178 case KHJson: 179 err = h.saveJSONSnappy(fn) 180 panicOn(err) 181 case KHGob: 182 err = h.saveGobSnappy(fn) 183 panicOn(err) 184 case KHSsh: 185 err = h.saveSshKnownHosts() 186 panicOn(err) 187 default: 188 panic(fmt.Sprintf("unknown persistence format: %v", h.PersistFormat)) 189 } 190 return 191 } 192 193 // Close cleans up and prepares for shutdown. It calls h.Sync() to write 194 // the state to disk. 195 func (h *KnownHosts) Close() { 196 h.Sync() 197 } 198 199 // LoadSshKnownHosts reads a ~/.ssh/known_hosts style 200 // file from path, see the SSH_KNOWN_HOSTS FILE FORMAT 201 // section of http://manpages.ubuntu.com/manpages/zesty/en/man8/sshd.8.html 202 // or the local sshd(8) man page. 203 func LoadSshKnownHosts(path string) (*KnownHosts, error) { 204 //pp("top of LoadSshKnownHosts for path = '%s'", path) 205 206 h := &KnownHosts{ 207 Hosts: make(map[string]*ServerPubKey), 208 FilepathPrefix: path, 209 PersistFormat: KHSsh, 210 } 211 212 if !fileExists(path) { 213 return nil, fmt.Errorf("path '%s' does not exist", path) 214 } 215 216 by, err := ioutil.ReadFile(path) 217 if err != nil { 218 return nil, err 219 } 220 221 killRightBracket := strings.NewReplacer("]", "") 222 223 lines := strings.Split(string(by), "\n") 224 for i := range lines { 225 line := strings.Trim(lines[i], " ") 226 // skip comments 227 if line == "" || line[0] == '#' { 228 continue 229 } 230 // skip hashed hostnames 231 if line[0] == '|' { 232 continue 233 } 234 splt := strings.Split(line, " ") 235 //pp("for line i = %v, splt = %#v\n", i, splt) 236 n := len(splt) 237 if n < 3 || n > 5 { 238 return nil, fmt.Errorf("known_hosts file '%s' did not have 3/4/5 fields on line %v: '%s'", path, i+1, lines[i]) 239 } 240 b := 0 241 markers := "" 242 if splt[0][0] == '@' { 243 markers = splt[0] 244 b = 1 245 if strings.Contains(markers, "@revoked") { 246 log.Printf("ignoring @revoked host key at line %v of path '%s': '%s'", i+1, path, lines[i]) 247 continue 248 } 249 if strings.Contains(markers, "@cert-authority") { 250 log.Printf("ignoring @cert-authority host key at line %v of path '%s': '%s'", i+1, path, lines[i]) 251 continue 252 } 253 } 254 comment := "" 255 if b+3 < n { 256 comment = splt[b+3] 257 } 258 pubkey := ServerPubKey{ 259 Markers: markers, 260 Hostnames: splt[b], 261 Keytype: splt[b+1], 262 Base64EncodededPublicKey: splt[b+2], 263 Comment: comment, 264 Port: "22", 265 SplitHostnames: make(map[string]bool), 266 } 267 hosts := strings.Split(pubkey.Hostnames, ",") 268 269 // 2 passes: first fill all the SplitHostnames, then each indiv. 270 271 // a) fill all the SplitHostnames 272 for k := range hosts { 273 hst := hosts[k] 274 //pp("processing hst = '%s'\n", hst) 275 if hst[0] == '[' { 276 hst = hst[1:] 277 hst = killRightBracket.Replace(hst) 278 //pp("after killing [], hst = '%s'\n", hst) 279 } 280 hostport := strings.Split(hst, ":") 281 //pp("hostport = '%#v'\n", hostport) 282 if len(hostport) > 1 { 283 hst = hostport[0] 284 pubkey.Port = hostport[1] 285 } 286 pubkey.Hostname = hst + ":" + pubkey.Port 287 pubkey.SplitHostnames[pubkey.Hostname] = true 288 } 289 290 // b) each individual name 291 for k := range hosts { 292 293 // copy pubkey so we can modify 294 ourpubkey := pubkey 295 296 hst := hosts[k] 297 //pp("processing hst = '%s'\n", hst) 298 if hst[0] == '[' { 299 hst = hst[1:] 300 hst = killRightBracket.Replace(hst) 301 //pp("after killing [], hst = '%s'\n", hst) 302 } 303 hostport := strings.Split(hst, ":") 304 //p("hostport = '%#v'\n", hostport) 305 if len(hostport) > 1 { 306 hst = hostport[0] 307 ourpubkey.Port = hostport[1] 308 } 309 ourpubkey.Hostname = hst + ":" + ourpubkey.Port 310 311 // unbase64 the public key to get []byte, then string() that 312 // to get the key of h.Hosts 313 pub := []byte(ourpubkey.Base64EncodededPublicKey) 314 expandedMaxSize := base64.StdEncoding.DecodedLen(len(pub)) 315 expand := make([]byte, expandedMaxSize) 316 n, err := base64.StdEncoding.Decode(expand, []byte(ourpubkey.Base64EncodededPublicKey)) 317 if err != nil { 318 log.Printf("warning: ignoring entry in known_hosts file '%s' on line %v: '%s' we find the following error: could not base64 decode the public key field. detailed error: '%s'", path, i+1, lines[i], err) 319 continue 320 } 321 expand = expand[:n] 322 323 xkey, err := ssh.ParsePublicKey(expand) 324 if err != nil { 325 log.Printf("warning: ignoring entry in known_hosts file '%s' on line %v: '%s' we find the following error: could not ssh.ParsePublicKey(). detailed error: '%s'", path, i+1, lines[i], err) 326 continue 327 } 328 se := string(ssh.MarshalAuthorizedKey(xkey)) 329 330 ourpubkey.LineInFileOneBased = i + 1 331 /* don't resolve now, this may be slow: 332 ourpubkey.remote, err = net.ResolveTCPAddr("tcp", ourpubkey.Hostname+":"+ourpubkey.Port) 333 if err != nil { 334 log.Printf("warning: ignoring entry known_hosts file '%s' on line %v: '%s' we find the following error: could not resolve the hostname '%s'. detailed error: '%s'", path, i+1, lines[i], ourpubkey.Hostname, err) 335 } 336 */ 337 ourpubkey.AlreadySaved = true 338 ourpubkey.HumanKey = se 339 // check for existing that we need to combine... 340 prior, already := h.Hosts[se] 341 if !already { 342 h.Hosts[se] = &ourpubkey 343 //pp("saved known hosts: key '%s' -> value: %#v\n", se, ourpubkey) 344 } else { 345 // need to combine under this key... 346 //pp("have prior entry for se='%s': %#v\n", se, prior) 347 prior.AddHostPort(ourpubkey.Hostname) 348 prior.AlreadySaved = true // reading from file, all are saved already. 349 } 350 } 351 } 352 353 return h, nil 354 } 355 356 func (s *KnownHosts) saveSshKnownHosts() error { 357 s.Mut.Lock() 358 defer s.Mut.Unlock() 359 360 if s.NoSave { 361 return nil 362 } 363 364 fn := s.FilepathPrefix 365 mkpath(fn) 366 367 // backups 368 exec.Command("mv", fn+".prev", fn+".prev.prev").Run() 369 exec.Command("cp", "-p", fn, fn+".prev").Run() 370 371 f, err := os.OpenFile(fn, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) 372 if err != nil { 373 return fmt.Errorf("could not open file '%s' for appending: '%s'", fn, err) 374 } 375 defer f.Close() 376 377 for _, v := range s.Hosts { 378 if v.AlreadySaved { 379 continue 380 } 381 382 hostname := "" 383 if len(v.SplitHostnames) == 1 { 384 hn := v.Hostname 385 hp := strings.Split(hn, ":") 386 //pp("hn='%v', hp='%#v'", hn, hp) 387 if hp[1] != "22" { 388 hn = "[" + hp[0] + "]:" + hp[1] 389 } 390 hostname = hn 391 } else { 392 // put all hostnames under this one key. 393 k := 0 394 for tmp := range v.SplitHostnames { 395 hp := strings.Split(tmp, ":") 396 if len(hp) != 2 { 397 panic(fmt.Sprintf("must be 2 parts here, but we got '%s'", tmp)) 398 } 399 hn := "[" + hp[0] + "]:" + hp[1] 400 if k == 0 { 401 hostname = hn 402 } else { 403 hostname += "," + hn 404 } 405 k++ 406 } 407 } 408 409 _, err = fmt.Fprintf(f, "%s %s %s %s\n", 410 hostname, 411 v.Keytype, 412 v.Base64EncodededPublicKey, 413 v.Comment) 414 if err != nil { 415 return fmt.Errorf("could not append to file '%s': '%s'", fn, err) 416 } 417 v.AlreadySaved = true 418 } 419 420 return nil 421 } 422 423 func Base64ofPublicKey(key ssh.PublicKey) string { 424 b := &bytes.Buffer{} 425 e := base64.NewEncoder(base64.StdEncoding, b) 426 e.Write(key.Marshal()) 427 e.Close() 428 return b.String() 429 430 } 431 432 func (prior *ServerPubKey) AddHostPort(hp string) { 433 //pp("AddHostPort called with hp = '%v'", hp) 434 prior.Mut.Lock() 435 436 _, already2 := prior.SplitHostnames[hp] 437 prior.SplitHostnames[hp] = true 438 if !already2 { 439 prior.AlreadySaved = false 440 } 441 prior.Mut.Unlock() 442 } 443 444 func mkpath(fn string) { 445 os.MkdirAll(filepath.Dir(fn), 0700) 446 }