// Package ssh resolves local SSH hostname aliases. package ssh import ( "bufio" "net/url" "os/exec" "strings" "sync" "github.com/cli/safeexec" ) type Translator struct { cacheMap map[string]string cacheMu sync.RWMutex sshPath string sshPathErr error sshPathMu sync.Mutex lookPath func(string) (string, error) newCommand func(string, ...string) *exec.Cmd } // NewTranslator initializes a new Translator instance. func NewTranslator() *Translator { return &Translator{} } // Translate applies applicable SSH hostname aliases to the specified URL and returns the resulting URL. func (t *Translator) Translate(u *url.URL) *url.URL { if u.Scheme != "ssh" { return u } resolvedHost, err := t.resolve(u.Hostname()) if err != nil { return u } if strings.EqualFold(resolvedHost, "ssh.github.com") { resolvedHost = "github.com" } newURL, _ := url.Parse(u.String()) newURL.Host = resolvedHost return newURL } func (t *Translator) resolve(hostname string) (string, error) { t.cacheMu.RLock() cached, cacheFound := t.cacheMap[strings.ToLower(hostname)] t.cacheMu.RUnlock() if cacheFound { return cached, nil } var sshPath string t.sshPathMu.Lock() if t.sshPath == "" && t.sshPathErr == nil { lookPath := t.lookPath if lookPath == nil { lookPath = safeexec.LookPath } t.sshPath, t.sshPathErr = lookPath("ssh") } if t.sshPathErr != nil { defer t.sshPathMu.Unlock() return t.sshPath, t.sshPathErr } sshPath = t.sshPath t.sshPathMu.Unlock() t.cacheMu.Lock() defer t.cacheMu.Unlock() newCommand := t.newCommand if newCommand == nil { newCommand = exec.Command } sshCmd := newCommand(sshPath, "-G", hostname) stdout, err := sshCmd.StdoutPipe() if err != nil { return "", err } if err := sshCmd.Start(); err != nil { return "", err } var resolvedHost string s := bufio.NewScanner(stdout) for s.Scan() { line := s.Text() parts := strings.SplitN(line, " ", 2) if len(parts) == 2 && parts[0] == "hostname" { resolvedHost = parts[1] } } err = sshCmd.Wait() if err != nil || resolvedHost == "" { // handle failures by returning the original hostname unchanged resolvedHost = hostname } if t.cacheMap == nil { t.cacheMap = map[string]string{} } t.cacheMap[strings.ToLower(hostname)] = resolvedHost return resolvedHost, nil }