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