X Tutup
Skip to content

Commit dc8698e

Browse files
alissonbrunosamislav
authored andcommitted
Make ssh parser to parse included config files
1 parent cee4f85 commit dc8698e

File tree

6 files changed

+175
-49
lines changed

6 files changed

+175
-49
lines changed

git/ssh_config.go

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package git
22

33
import (
44
"bufio"
5-
"io"
65
"net/url"
76
"os"
87
"path/filepath"
@@ -13,12 +12,10 @@ import (
1312
)
1413

1514
var (
16-
sshHostRE,
1715
sshTokenRE *regexp.Regexp
1816
)
1917

2018
func init() {
21-
sshHostRE = regexp.MustCompile("(?i)^[ \t]*(host|hostname)[ \t]+(.+)$")
2219
sshTokenRE = regexp.MustCompile(`%[%h]`)
2320
}
2421

@@ -45,55 +42,46 @@ func (m SSHAliasMap) Translator() func(*url.URL) *url.URL {
4542
}
4643
}
4744

48-
// ParseSSHConfig constructs a map of SSH hostname aliases based on user and
49-
// system configuration files
50-
func ParseSSHConfig() SSHAliasMap {
51-
configFiles := []string{
52-
"/etc/ssh_config",
53-
"/etc/ssh/ssh_config",
54-
}
55-
if homedir, err := homedir.Dir(); err == nil {
56-
userConfig := filepath.Join(homedir, ".ssh", "config")
57-
configFiles = append([]string{userConfig}, configFiles...)
58-
}
59-
60-
openFiles := make([]io.Reader, 0, len(configFiles))
61-
for _, file := range configFiles {
62-
f, err := os.Open(file)
63-
if err != nil {
64-
continue
65-
}
66-
defer f.Close()
67-
openFiles = append(openFiles, f)
68-
}
69-
return sshParse(openFiles...)
45+
type parser struct {
46+
aliasMap SSHAliasMap
7047
}
7148

72-
func sshParse(r ...io.Reader) SSHAliasMap {
73-
config := make(SSHAliasMap)
74-
for _, file := range r {
75-
_ = sshParseConfig(config, file)
49+
func (p *parser) read(fileName string) error {
50+
file, err := os.Open(fileName)
51+
if err != nil {
52+
return err
7653
}
77-
return config
78-
}
54+
defer file.Close()
7955

80-
func sshParseConfig(c SSHAliasMap, file io.Reader) error {
8156
hosts := []string{"*"}
8257
scanner := bufio.NewScanner(file)
8358
for scanner.Scan() {
8459
line := scanner.Text()
85-
match := sshHostRE.FindStringSubmatch(line)
86-
if match == nil {
60+
fields := strings.Fields(line)
61+
62+
if len(fields) < 2 {
8763
continue
8864
}
8965

90-
names := strings.Fields(match[2])
91-
if strings.EqualFold(match[1], "host") {
92-
hosts = names
93-
} else {
66+
directive, params := fields[0], fields[1:]
67+
switch {
68+
case strings.EqualFold(directive, "Host"):
69+
hosts = params
70+
case strings.EqualFold(directive, "Hostname"):
9471
for _, host := range hosts {
95-
for _, name := range names {
96-
c[host] = sshExpandTokens(name, host)
72+
for _, name := range params {
73+
p.aliasMap[host] = sshExpandTokens(name, host)
74+
}
75+
}
76+
case strings.EqualFold(directive, "Include"):
77+
for _, path := range absolutePaths(fileName, params) {
78+
fileNames, err := filepath.Glob(path)
79+
if err != nil {
80+
continue
81+
}
82+
83+
for _, fileName := range fileNames {
84+
_ = p.read(fileName)
9785
}
9886
}
9987
}
@@ -102,6 +90,55 @@ func sshParseConfig(c SSHAliasMap, file io.Reader) error {
10290
return scanner.Err()
10391
}
10492

93+
func isSystem(path string) bool {
94+
return strings.HasPrefix(path, "/etc/ssh")
95+
}
96+
97+
func absolutePaths(parentFile string, paths []string) []string {
98+
absPaths := make([]string, len(paths))
99+
100+
for i, path := range paths {
101+
switch {
102+
case filepath.IsAbs(path):
103+
absPaths[i] = path
104+
case strings.HasPrefix(path, "~"):
105+
absPaths[i], _ = homedir.Expand(path)
106+
case isSystem(parentFile):
107+
absPaths[i] = filepath.Join("/etc", "ssh", path)
108+
default:
109+
dir, _ := homedir.Dir()
110+
absPaths[i] = filepath.Join(dir, ".ssh", path)
111+
}
112+
}
113+
114+
return absPaths
115+
}
116+
117+
func parse(files ...string) SSHAliasMap {
118+
p := parser{aliasMap: make(SSHAliasMap)}
119+
120+
for _, file := range files {
121+
_ = p.read(file)
122+
}
123+
124+
return p.aliasMap
125+
}
126+
127+
// ParseSSHConfig constructs a map of SSH hostname aliases based on user and
128+
// system configuration files
129+
func ParseSSHConfig() SSHAliasMap {
130+
configFiles := []string{
131+
"/etc/ssh_config",
132+
"/etc/ssh/ssh_config",
133+
}
134+
if homedir, err := homedir.Dir(); err == nil {
135+
userConfig := filepath.Join(homedir, ".ssh", "config")
136+
configFiles = append([]string{userConfig}, configFiles...)
137+
}
138+
139+
return parse(configFiles...)
140+
}
141+
105142
func sshExpandTokens(text, host string) string {
106143
return sshTokenRE.ReplaceAllStringFunc(text, func(match string) string {
107144
switch match {

git/ssh_config_test.go

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package git
22

33
import (
4+
"fmt"
5+
"io/ioutil"
46
"net/url"
7+
"os"
8+
"path/filepath"
59
"reflect"
6-
"strings"
710
"testing"
11+
12+
"github.com/mitchellh/go-homedir"
813
)
914

1015
// TODO: extract assertion helpers into a shared package
@@ -15,19 +20,96 @@ func eq(t *testing.T, got interface{}, expected interface{}) {
1520
}
1621
}
1722

18-
func Test_sshParse(t *testing.T) {
19-
m := sshParse(strings.NewReader(`
20-
Host foo bar
21-
HostName example.com
22-
`), strings.NewReader(`
23-
Host bar baz
24-
hostname %%%h.net%%
25-
`))
23+
func createTempFile(t *testing.T, prefix string) *os.File {
24+
t.Helper()
25+
26+
dir, err := homedir.Dir()
27+
if err != nil {
28+
t.Errorf("Could not find homedir: %s", err)
29+
}
30+
31+
tempFile, err := ioutil.TempFile(filepath.Join(dir, ".ssh"), prefix)
32+
if err != nil {
33+
t.Errorf("Could create a temp file: %s", err)
34+
}
35+
36+
t.Cleanup(func() {
37+
tempFile.Close()
38+
os.Remove(tempFile.Name())
39+
})
40+
41+
return tempFile
42+
}
43+
44+
func Test_parse(t *testing.T) {
45+
includedTempFile := createTempFile(t, "included")
46+
includedConfigFile := `
47+
Host webapp
48+
HostName webapp.example.com
49+
`
50+
fmt.Fprint(includedTempFile, includedConfigFile)
51+
52+
m := parse(
53+
"testdata/ssh_config1.conf",
54+
"testdata/ssh_config2.conf",
55+
"testdata/ssh_config3.conf",
56+
)
57+
2658
eq(t, m["foo"], "example.com")
2759
eq(t, m["bar"], "%bar.net%")
2860
eq(t, m["nonexistent"], "")
2961
}
3062

63+
func Test_absolutePaths(t *testing.T) {
64+
dir, err := homedir.Dir()
65+
if err != nil {
66+
t.Errorf("Could not find homedir: %s", err)
67+
}
68+
69+
tests := map[string]struct {
70+
parentFile string
71+
Input []string
72+
Want []string
73+
}{
74+
"absolute path": {
75+
parentFile: "/etc/ssh/ssh_config",
76+
Input: []string{"/etc/ssh/config"},
77+
Want: []string{"/etc/ssh/config"},
78+
},
79+
"system relative path": {
80+
parentFile: "/etc/ssh/config",
81+
Input: []string{"configs/*.conf"},
82+
Want: []string{"/etc/ssh/configs/*.conf"},
83+
},
84+
"user relative path": {
85+
parentFile: filepath.Join(dir, ".ssh", "ssh_config"),
86+
Input: []string{"configs/*.conf"},
87+
Want: []string{filepath.Join(dir, ".ssh", "configs/*.conf")},
88+
},
89+
"shell-like ~ rerefence": {
90+
parentFile: filepath.Join(dir, ".ssh", "ssh_config"),
91+
Input: []string{"~/.ssh/*.conf"},
92+
Want: []string{filepath.Join(dir, ".ssh", "*.conf")},
93+
},
94+
}
95+
96+
for name, test := range tests {
97+
t.Run(name, func(t *testing.T) {
98+
paths := absolutePaths(test.parentFile, test.Input)
99+
100+
if len(paths) != len(test.Input) {
101+
t.Errorf("Expected %d, got %d", len(test.Input), len(paths))
102+
}
103+
104+
for i, path := range paths {
105+
if path != test.Want[i] {
106+
t.Errorf("Expected %q, got %q", test.Want[i], path)
107+
}
108+
}
109+
})
110+
}
111+
}
112+
31113
func Test_Translator(t *testing.T) {
32114
m := SSHAliasMap{
33115
"gh": "github.com",

git/testdata/included.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Host webapp
2+
HostName webapp.example.com

git/testdata/ssh_config1.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Host foo bar
2+
HostName example.com

git/testdata/ssh_config2.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Host bar baz
2+
hostname %%%h.net%%

git/testdata/ssh_config3.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Include ~/.ssh/included*

0 commit comments

Comments
 (0)
X Tutup