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  }