177 lines
4.6 KiB
Go
177 lines
4.6 KiB
Go
|
package sshfp
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/sirupsen/logrus"
|
||
|
)
|
||
|
|
||
|
type service struct {
|
||
|
Algorithms AlgorithmTypes
|
||
|
Hashes HashTypes
|
||
|
}
|
||
|
|
||
|
func NewService() Service {
|
||
|
//Based on: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
|
||
|
|
||
|
hashes := make(HashTypes)
|
||
|
hashes["sha1"] = "1"
|
||
|
hashes["sha256"] = "2"
|
||
|
|
||
|
algorithms := make(AlgorithmTypes)
|
||
|
algorithms["rsa"] = "1"
|
||
|
algorithms["dsa"] = "2"
|
||
|
algorithms["ecdsa"] = "3"
|
||
|
algorithms["ed25519"] = "4"
|
||
|
algorithms["ed449"] = "6"
|
||
|
|
||
|
return &service{Algorithms: algorithms, Hashes: hashes}
|
||
|
|
||
|
}
|
||
|
|
||
|
func (s *service) ParseConsulSSHRecord(key string, value string) (*SSHFPRecord, error) {
|
||
|
logrus.Debugf("SSHFP: ParseConsulSSHRecord: %s %s", key, value)
|
||
|
|
||
|
output := &SSHFPRecord{}
|
||
|
|
||
|
key = strings.ToLower(key)
|
||
|
value = strings.ToLower(value)
|
||
|
|
||
|
output.Fingerprint = value
|
||
|
|
||
|
splittedKey := strings.Split(key, "-")
|
||
|
|
||
|
if len(splittedKey) < 3 {
|
||
|
return nil, errors.New("incorrect format")
|
||
|
}
|
||
|
|
||
|
//Assumption: Last field is hash type
|
||
|
output.Type = s.Hashes[splittedKey[len(splittedKey)-1]]
|
||
|
|
||
|
//Check first field - for "ecdsa" we can return value, for "ssh", we have to check second field.
|
||
|
|
||
|
switch splittedKey[0] {
|
||
|
case "ecdsa":
|
||
|
output.Algorithm = s.Algorithms[splittedKey[0]]
|
||
|
|
||
|
case "ssh":
|
||
|
output.Algorithm = s.Algorithms[splittedKey[1]]
|
||
|
|
||
|
}
|
||
|
|
||
|
if output.Type == "" || output.Algorithm == "" || output.Fingerprint == "" {
|
||
|
return nil, errors.New("cannot parse record")
|
||
|
}
|
||
|
return output, nil
|
||
|
}
|
||
|
|
||
|
func (s *service) ParseConsulSSHRecords(records map[string]string) []*SSHFPRecord {
|
||
|
output := make([]*SSHFPRecord, 0)
|
||
|
|
||
|
for k, v := range records {
|
||
|
sshRecord, err := s.ParseConsulSSHRecord(k, v)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
output = append(output, sshRecord)
|
||
|
}
|
||
|
|
||
|
return output
|
||
|
}
|
||
|
|
||
|
func (s *service) PrepareConfiguration(hostname string, current []*SSHFPRecord, new []*SSHFPRecord) ConfigPlan {
|
||
|
logrus.Debug("SSHFP: PrepareConfiguration")
|
||
|
|
||
|
/* Config plan have to cover the following scenarios:
|
||
|
0. Both configs are empty - do nothing.
|
||
|
1. New config is empty - DELETE everything
|
||
|
2. Old config is empty - CREATE everything
|
||
|
3. Record in new config doesn't exist in old config - CREATE record
|
||
|
4. Record in new config has different fingerprint than the old one - UPDATE and remove from oldMap
|
||
|
5. Record in new config and record in old config are the same - just remove from oldMap to avoid unwanted removal
|
||
|
|
||
|
Records left in oldMap should be qualified for removal
|
||
|
*/
|
||
|
|
||
|
configPlan := make(ConfigPlan, 0)
|
||
|
|
||
|
// Scenario 0
|
||
|
if len(current) == 0 && len(new) == 0 {
|
||
|
logrus.Debug("Both configs are empty - do nothing")
|
||
|
return configPlan
|
||
|
}
|
||
|
|
||
|
//Scenario 1
|
||
|
if len(current) > 0 && len(new) == 0 {
|
||
|
logrus.Debug("New config is empty - remove config")
|
||
|
for _, v := range current {
|
||
|
configPlan = append(configPlan, ConfigPlanElement{Operation: DELETE, Record: v, Hostname: hostname})
|
||
|
}
|
||
|
return configPlan
|
||
|
}
|
||
|
|
||
|
//Scenario 2
|
||
|
if len(current) == 0 && len(new) > 0 {
|
||
|
logrus.Debug("Old config is empty - create config")
|
||
|
for _, v := range new {
|
||
|
configPlan = append(configPlan, ConfigPlanElement{Operation: CREATE, Record: v, Hostname: hostname})
|
||
|
}
|
||
|
return configPlan
|
||
|
}
|
||
|
|
||
|
//To handle scenarios 3-5 we have to create temporary maps with string "<algorithm><type>" as key.
|
||
|
//Assumption: Pair algorithm+hash type is unique
|
||
|
|
||
|
oldMap := make(map[string]*SSHFPRecord)
|
||
|
newMap := make(map[string]*SSHFPRecord)
|
||
|
|
||
|
//Create temporary maps for better searching
|
||
|
for _, v := range current {
|
||
|
oldMap[v.Algorithm+v.Type] = v
|
||
|
}
|
||
|
|
||
|
for _, v := range new {
|
||
|
newMap[v.Algorithm+v.Type] = v
|
||
|
}
|
||
|
|
||
|
for k := range newMap {
|
||
|
//Scenario 3
|
||
|
if _, ok := oldMap[k]; !ok {
|
||
|
logrus.Debugf("Record not found in current config: %s", k)
|
||
|
configPlan = append(configPlan, ConfigPlanElement{Operation: CREATE, Record: newMap[k], Hostname: hostname})
|
||
|
continue
|
||
|
}
|
||
|
//Scenario 4
|
||
|
if oldMap[k].Fingerprint != newMap[k].Fingerprint {
|
||
|
logrus.Debugf("Updating record in current config: %s", k)
|
||
|
newMap[k].RecordID = oldMap[k].RecordID
|
||
|
configPlan = append(configPlan, ConfigPlanElement{Operation: UPDATE, Record: newMap[k], Hostname: hostname})
|
||
|
delete(oldMap, k)
|
||
|
continue
|
||
|
}
|
||
|
//Scenario 5
|
||
|
if oldMap[k].Fingerprint == newMap[k].Fingerprint {
|
||
|
delete(oldMap, k)
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//Cleanup
|
||
|
for _, v := range oldMap {
|
||
|
configPlan = append(configPlan, ConfigPlanElement{Operation: DELETE, Record: v, Hostname: hostname})
|
||
|
}
|
||
|
return configPlan
|
||
|
}
|
||
|
func (s *service) PrintConfigPlan(configPlan ConfigPlan) {
|
||
|
logrus.Debug("SSHFP: PrintConfigPlan")
|
||
|
logrus.Infof("Config Plan:")
|
||
|
for _, v := range configPlan {
|
||
|
logrus.Infof("Hostname: %s", v.Hostname)
|
||
|
logrus.Infof("Operation: %v", v.Operation)
|
||
|
logrus.Infof("Record: %+v", v.Record)
|
||
|
logrus.Infof("---")
|
||
|
}
|
||
|
}
|