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  }