Skip to content

Commit f9ec369

Browse files
git: Handle scp-style git addresses in detector, not getter
The "scp-style" remote URL scheme in git is ambiguous with the standard URL syntax in our previous design where we'd add/accept the ssh:// prefix in both cases; ssh://[email protected]:222/foo could be understood as either: - connect to example.com on port 222 and clone from path "foo" - connect to example.com on port 22 and clone from path "222/foo" Originally we used the first interpretation. but in #129 we switched to the second in order to support a strange hybrid form where no port number was specified, but in the process we broke handling of the standard URL form when a non-standard port is required, as is sometimes the case for internal git servers running on machines where port 22 is already used for administrative access. git itself resolves this ambiguity by not permitting the ssh:// scheme to be used in conjunction with the scp-like form. Here we mimic that by supporting the scp-like form _only_ in the detector, and only allowing it when the ssh:// prefix isn't present. That allows for the following two scp-like forms to be accepted: - [email protected]:foo/bar when the username is "git" - git::[email protected]:foo/bar for any other username We no longer support using the scp-like form with the ssh:// prefix, so git::ssh://anything.example.com:foo/bar is invalid. To use that form, the ssh:// part must be omitted, which is consistent with git's own behavior. The detector already rewrites the scp-like form into the standard URL form, when no ssh: scheme is given so the getter does not need to do anything special to deal with it. In the git getter, we now also check to see if the port number seems to be non-decimal and return a more actionable error if so, since that is likely to be caused by someone incorrectly using the ssh:// scheme with a scp-style address.
1 parent 763ab89 commit f9ec369

File tree

4 files changed

+172
-32
lines changed

4 files changed

+172
-32
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,16 @@ None
280280
* `depth` - The Git clone depth. The provided number specifies the last `n`
281281
revisions to clone from the repository.
282282

283+
284+
The `git` getter accepts both URL-style SSH addresses like
285+
`git::ssh://[email protected]/foo/bar`, and "scp-style" addresses like
286+
`git::[email protected]/foo/bar`. In the latter case, omitting the `git::`
287+
force prefix is allowed if the username prefix is exactly `git@`.
288+
289+
The "scp-style" addresses _cannot_ be used in conjunction with the `ssh://`
290+
scheme prefix, because in that case the colon is used to mark an optional
291+
port number to connect on, rather than to delimit the path from the host.
292+
283293
### Mercurial (`hg`)
284294

285295
* `rev` - The Mercurial revision to checkout.

detect_git_test.go

+29-12
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ func TestGitDetector(t *testing.T) {
99
Input string
1010
Output string
1111
}{
12-
{"[email protected]:hashicorp/foo.git", "git::ssh://[email protected]/hashicorp/foo.git"},
12+
{
13+
"[email protected]:hashicorp/foo.git",
14+
"git::ssh://[email protected]/hashicorp/foo.git",
15+
},
1316
{
1417
"[email protected]:org/project.git?ref=test-branch",
1518
"git::ssh://[email protected]/org/project.git?ref=test-branch",
@@ -38,21 +41,35 @@ func TestGitDetector(t *testing.T) {
3841
"[email protected]:org/project.git//module/a?ref=test-branch",
3942
"git::ssh://[email protected]/org/project.git//module/a?ref=test-branch",
4043
},
44+
{
45+
// Using the scp-like form with the ssh:// prefix is invalid, so
46+
// it passes through as-is.
47+
"git::ssh://[email protected]:org/project.git",
48+
"git::ssh://[email protected]:org/project.git",
49+
},
50+
{
51+
// Already in the canonical form, so no rewriting required
52+
// When the ssh: protocol is used explicitly, we recognize it as
53+
// URL form rather than SCP-like form, so the part after the colon
54+
// is a port number, not part of the path.
55+
"git::ssh://[email protected]:2222/hashicorp/foo.git",
56+
"git::ssh://[email protected]:2222/hashicorp/foo.git",
57+
},
4158
}
4259

4360
pwd := "/pwd"
4461
f := new(GitDetector)
45-
for i, tc := range cases {
46-
output, ok, err := f.Detect(tc.Input, pwd)
47-
if err != nil {
48-
t.Fatalf("err: %s", err)
49-
}
50-
if !ok {
51-
t.Fatal("not ok")
52-
}
62+
ds := []Detector{f}
63+
for _, tc := range cases {
64+
t.Run(tc.Input, func (t *testing.T) {
65+
output, err := Detect(tc.Input, pwd, ds)
66+
if err != nil {
67+
t.Fatalf("unexpected error: %s", err)
68+
}
5369

54-
if output != tc.Output {
55-
t.Fatalf("%d: bad: %#v", i, output)
56-
}
70+
if output != tc.Output {
71+
t.Errorf("wrong result\ninput: %s\ngot: %s\nwant: %s", tc.Input, output, tc.Output)
72+
}
73+
})
5774
}
5875
}

get_git.go

+9-20
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ func (g *GitGetter) Get(dst string, u *url.URL) error {
3434
return fmt.Errorf("git must be available and on the PATH")
3535
}
3636

37+
// The port number must be parseable as an integer. If not, the user
38+
// was probably trying to use a scp-style address, in which case the
39+
// ssh:// prefix must be removed to indicate that.
40+
if portStr := u.Port(); portStr != "" {
41+
if _, err := strconv.ParseUint(portStr, 10, 16); err != nil {
42+
return fmt.Errorf("invalid port number %q; if using the \"scp-like\" git address scheme where a colon introduces the path instead, remove the ssh:// portion and use just the git:: prefix", portStr)
43+
}
44+
}
45+
3746
// Extract some query parameters we use
3847
var ref, sshKey string
3948
var depth int
@@ -90,26 +99,6 @@ func (g *GitGetter) Get(dst string, u *url.URL) error {
9099
}
91100
}
92101

93-
// For SSH-style URLs, if they use the SCP syntax of host:path, then
94-
// the URL will be mangled. We detect that here and correct the path.
95-
// Example: host:path/bar will turn into host/path/bar
96-
if u.Scheme == "ssh" {
97-
if idx := strings.Index(u.Host, ":"); idx > -1 {
98-
// Copy the URL so we don't modify the input
99-
var newU url.URL = *u
100-
u = &newU
101-
102-
// Path includes the part after the ':'.
103-
u.Path = u.Host[idx+1:] + u.Path
104-
if u.Path[0] != '/' {
105-
u.Path = "/" + u.Path
106-
}
107-
108-
// Host trims up to the :
109-
u.Host = u.Host[:idx]
110-
}
111-
}
112-
113102
// Clone or update the repository
114103
_, err := os.Stat(dst)
115104
if err != nil && !os.IsNotExist(err) {

get_git_test.go

+124
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,130 @@ func TestGitGetter_sshKey(t *testing.T) {
298298
}
299299
}
300300

301+
func TestGitGetter_sshSCPStyle(t *testing.T) {
302+
if !testHasGit {
303+
t.Skip("git not found, skipping")
304+
}
305+
306+
g := new(GitGetter)
307+
dst := tempDir(t)
308+
309+
encodedKey := base64.StdEncoding.EncodeToString([]byte(testGitToken))
310+
311+
// avoid getting locked by a github authenticity validation prompt
312+
os.Setenv("GIT_SSH_COMMAND", "ssh -o StrictHostKeyChecking=no")
313+
defer os.Setenv("GIT_SSH_COMMAND", "")
314+
315+
// This test exercises the combination of the git detector and the
316+
// git getter, to make sure that together they make scp-style URLs work.
317+
client := &Client{
318+
Src: "[email protected]:hashicorp/test-private-repo?sshkey=" + encodedKey,
319+
Dst: dst,
320+
Pwd: ".",
321+
322+
Mode: ClientModeDir,
323+
324+
Detectors: []Detector{
325+
new(GitDetector),
326+
},
327+
Getters: map[string]Getter{
328+
"git": g,
329+
},
330+
}
331+
332+
if err := client.Get(); err != nil {
333+
t.Fatalf("client.Get failed: %s", err)
334+
}
335+
336+
readmePath := filepath.Join(dst, "README.md")
337+
if _, err := os.Stat(readmePath); err != nil {
338+
t.Fatalf("err: %s", err)
339+
}
340+
}
341+
342+
func TestGitGetter_sshExplicitPort(t *testing.T) {
343+
if !testHasGit {
344+
t.Skip("git not found, skipping")
345+
}
346+
347+
g := new(GitGetter)
348+
dst := tempDir(t)
349+
350+
encodedKey := base64.StdEncoding.EncodeToString([]byte(testGitToken))
351+
352+
// avoid getting locked by a github authenticity validation prompt
353+
os.Setenv("GIT_SSH_COMMAND", "ssh -o StrictHostKeyChecking=no")
354+
defer os.Setenv("GIT_SSH_COMMAND", "")
355+
356+
// This test exercises the combination of the git detector and the
357+
// git getter, to make sure that together they make scp-style URLs work.
358+
client := &Client{
359+
Src: "git::ssh://[email protected]:22/hashicorp/test-private-repo?sshkey=" + encodedKey,
360+
Dst: dst,
361+
Pwd: ".",
362+
363+
Mode: ClientModeDir,
364+
365+
Detectors: []Detector{
366+
new(GitDetector),
367+
},
368+
Getters: map[string]Getter{
369+
"git": g,
370+
},
371+
}
372+
373+
if err := client.Get(); err != nil {
374+
t.Fatalf("client.Get failed: %s", err)
375+
}
376+
377+
readmePath := filepath.Join(dst, "README.md")
378+
if _, err := os.Stat(readmePath); err != nil {
379+
t.Fatalf("err: %s", err)
380+
}
381+
}
382+
383+
384+
func TestGitGetter_sshSCPStyleInvalidScheme(t *testing.T) {
385+
if !testHasGit {
386+
t.Skip("git not found, skipping")
387+
}
388+
389+
g := new(GitGetter)
390+
dst := tempDir(t)
391+
392+
encodedKey := base64.StdEncoding.EncodeToString([]byte(testGitToken))
393+
394+
// avoid getting locked by a github authenticity validation prompt
395+
os.Setenv("GIT_SSH_COMMAND", "ssh -o StrictHostKeyChecking=no")
396+
defer os.Setenv("GIT_SSH_COMMAND", "")
397+
398+
// This test exercises the combination of the git detector and the
399+
// git getter, to make sure that together they make scp-style URLs work.
400+
client := &Client{
401+
Src: "git::ssh://[email protected]:hashicorp/test-private-repo?sshkey=" + encodedKey,
402+
Dst: dst,
403+
Pwd: ".",
404+
405+
Mode: ClientModeDir,
406+
407+
Detectors: []Detector{
408+
new(GitDetector),
409+
},
410+
Getters: map[string]Getter{
411+
"git": g,
412+
},
413+
}
414+
415+
err := client.Get()
416+
if err == nil {
417+
t.Fatalf("get succeeded; want error")
418+
}
419+
420+
if got, want := err.Error(), `invalid port number "hashicorp"`; !strings.Contains(got, want) {
421+
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
422+
}
423+
}
424+
301425
func TestGitGetter_submodule(t *testing.T) {
302426
if !testHasGit {
303427
t.Skip("git not found, skipping")

0 commit comments

Comments
 (0)