...

Source file src/github.com/cli/go-gh/v2/pkg/ssh/ssh.go

Documentation: github.com/cli/go-gh/v2/pkg/ssh

     1  // Package ssh resolves local SSH hostname aliases.
     2  package ssh
     3  
     4  import (
     5  	"bufio"
     6  	"net/url"
     7  	"os/exec"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/cli/safeexec"
    12  )
    13  
    14  type Translator struct {
    15  	cacheMap   map[string]string
    16  	cacheMu    sync.RWMutex
    17  	sshPath    string
    18  	sshPathErr error
    19  	sshPathMu  sync.Mutex
    20  
    21  	lookPath   func(string) (string, error)
    22  	newCommand func(string, ...string) *exec.Cmd
    23  }
    24  
    25  // NewTranslator initializes a new Translator instance.
    26  func NewTranslator() *Translator {
    27  	return &Translator{}
    28  }
    29  
    30  // Translate applies applicable SSH hostname aliases to the specified URL and returns the resulting URL.
    31  func (t *Translator) Translate(u *url.URL) *url.URL {
    32  	if u.Scheme != "ssh" {
    33  		return u
    34  	}
    35  	resolvedHost, err := t.resolve(u.Hostname())
    36  	if err != nil {
    37  		return u
    38  	}
    39  	if strings.EqualFold(resolvedHost, "ssh.github.com") {
    40  		resolvedHost = "github.com"
    41  	}
    42  	newURL, _ := url.Parse(u.String())
    43  	newURL.Host = resolvedHost
    44  	return newURL
    45  }
    46  
    47  func (t *Translator) resolve(hostname string) (string, error) {
    48  	t.cacheMu.RLock()
    49  	cached, cacheFound := t.cacheMap[strings.ToLower(hostname)]
    50  	t.cacheMu.RUnlock()
    51  	if cacheFound {
    52  		return cached, nil
    53  	}
    54  
    55  	var sshPath string
    56  	t.sshPathMu.Lock()
    57  	if t.sshPath == "" && t.sshPathErr == nil {
    58  		lookPath := t.lookPath
    59  		if lookPath == nil {
    60  			lookPath = safeexec.LookPath
    61  		}
    62  		t.sshPath, t.sshPathErr = lookPath("ssh")
    63  	}
    64  	if t.sshPathErr != nil {
    65  		defer t.sshPathMu.Unlock()
    66  		return t.sshPath, t.sshPathErr
    67  	}
    68  	sshPath = t.sshPath
    69  	t.sshPathMu.Unlock()
    70  
    71  	t.cacheMu.Lock()
    72  	defer t.cacheMu.Unlock()
    73  
    74  	newCommand := t.newCommand
    75  	if newCommand == nil {
    76  		newCommand = exec.Command
    77  	}
    78  	sshCmd := newCommand(sshPath, "-G", hostname)
    79  	stdout, err := sshCmd.StdoutPipe()
    80  	if err != nil {
    81  		return "", err
    82  	}
    83  
    84  	if err := sshCmd.Start(); err != nil {
    85  		return "", err
    86  	}
    87  
    88  	var resolvedHost string
    89  	s := bufio.NewScanner(stdout)
    90  	for s.Scan() {
    91  		line := s.Text()
    92  		parts := strings.SplitN(line, " ", 2)
    93  		if len(parts) == 2 && parts[0] == "hostname" {
    94  			resolvedHost = parts[1]
    95  		}
    96  	}
    97  
    98  	err = sshCmd.Wait()
    99  	if err != nil || resolvedHost == "" {
   100  		// handle failures by returning the original hostname unchanged
   101  		resolvedHost = hostname
   102  	}
   103  
   104  	if t.cacheMap == nil {
   105  		t.cacheMap = map[string]string{}
   106  	}
   107  	t.cacheMap[strings.ToLower(hostname)] = resolvedHost
   108  	return resolvedHost, nil
   109  }
   110  

View as plain text