github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/git/ssh_config.go (about) 1 package git 2 3 import ( 4 "bufio" 5 "io" 6 "net/url" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strings" 11 12 "github.com/abdfnx/gh-api/internal/config" 13 ) 14 15 var ( 16 sshConfigLineRE = regexp.MustCompile(`\A\s*(?P<keyword>[A-Za-z][A-Za-z0-9]*)(?:\s+|\s*=\s*)(?P<argument>.+)`) 17 sshTokenRE = regexp.MustCompile(`%[%h]`) 18 ) 19 20 // SSHAliasMap encapsulates the translation of SSH hostname aliases 21 type SSHAliasMap map[string]string 22 23 // Translator returns a function that applies hostname aliases to URLs 24 func (m SSHAliasMap) Translator() func(*url.URL) *url.URL { 25 return func(u *url.URL) *url.URL { 26 if u.Scheme != "ssh" { 27 return u 28 } 29 resolvedHost, ok := m[u.Hostname()] 30 if !ok { 31 return u 32 } 33 // FIXME: cleanup domain logic 34 if strings.EqualFold(u.Hostname(), "github.com") && strings.EqualFold(resolvedHost, "ssh.github.com") { 35 return u 36 } 37 newURL, _ := url.Parse(u.String()) 38 newURL.Host = resolvedHost 39 return newURL 40 } 41 } 42 43 type sshParser struct { 44 homeDir string 45 46 aliasMap SSHAliasMap 47 hosts []string 48 49 open func(string) (io.Reader, error) 50 glob func(string) ([]string, error) 51 } 52 53 func (p *sshParser) read(fileName string) error { 54 var file io.Reader 55 if p.open == nil { 56 f, err := os.Open(fileName) 57 if err != nil { 58 return err 59 } 60 defer f.Close() 61 file = f 62 } else { 63 var err error 64 file, err = p.open(fileName) 65 if err != nil { 66 return err 67 } 68 } 69 70 if len(p.hosts) == 0 { 71 p.hosts = []string{"*"} 72 } 73 74 scanner := bufio.NewScanner(file) 75 for scanner.Scan() { 76 m := sshConfigLineRE.FindStringSubmatch(scanner.Text()) 77 if len(m) < 3 { 78 continue 79 } 80 81 keyword, arguments := strings.ToLower(m[1]), m[2] 82 switch keyword { 83 case "host": 84 p.hosts = strings.Fields(arguments) 85 case "hostname": 86 for _, host := range p.hosts { 87 for _, name := range strings.Fields(arguments) { 88 if p.aliasMap == nil { 89 p.aliasMap = make(SSHAliasMap) 90 } 91 p.aliasMap[host] = sshExpandTokens(name, host) 92 } 93 } 94 case "include": 95 for _, arg := range strings.Fields(arguments) { 96 path := p.absolutePath(fileName, arg) 97 98 var fileNames []string 99 if p.glob == nil { 100 paths, _ := filepath.Glob(path) 101 for _, p := range paths { 102 if s, err := os.Stat(p); err == nil && !s.IsDir() { 103 fileNames = append(fileNames, p) 104 } 105 } 106 } else { 107 var err error 108 fileNames, err = p.glob(path) 109 if err != nil { 110 continue 111 } 112 } 113 114 for _, fileName := range fileNames { 115 _ = p.read(fileName) 116 } 117 } 118 } 119 } 120 121 return scanner.Err() 122 } 123 124 func (p *sshParser) absolutePath(parentFile, path string) string { 125 if filepath.IsAbs(path) || strings.HasPrefix(filepath.ToSlash(path), "/") { 126 return path 127 } 128 129 if strings.HasPrefix(path, "~") { 130 return filepath.Join(p.homeDir, strings.TrimPrefix(path, "~")) 131 } 132 133 if strings.HasPrefix(filepath.ToSlash(parentFile), "/etc/ssh") { 134 return filepath.Join("/etc/ssh", path) 135 } 136 137 return filepath.Join(p.homeDir, ".ssh", path) 138 } 139 140 // ParseSSHConfig constructs a map of SSH hostname aliases based on user and 141 // system configuration files 142 func ParseSSHConfig() SSHAliasMap { 143 configFiles := []string{ 144 "/etc/ssh_config", 145 "/etc/ssh/ssh_config", 146 } 147 148 p := sshParser{} 149 150 if sshDir, err := config.HomeDirPath(".ssh"); err == nil { 151 userConfig := filepath.Join(sshDir, "config") 152 configFiles = append([]string{userConfig}, configFiles...) 153 p.homeDir = filepath.Dir(sshDir) 154 } 155 156 for _, file := range configFiles { 157 _ = p.read(file) 158 } 159 return p.aliasMap 160 } 161 162 func sshExpandTokens(text, host string) string { 163 return sshTokenRE.ReplaceAllStringFunc(text, func(match string) string { 164 switch match { 165 case "%h": 166 return host 167 case "%%": 168 return "%" 169 } 170 return "" 171 }) 172 }