Skip to content

Commit 6f4b0a7

Browse files
ekcaseyDanny Joyce
authored andcommitted
make docker-credential-wincred work like docker-credential-osxkeychain
* fetch credentials for server with matching hostname if scheme, path, or port are not provided * if the credential request includes specific scheme, path, or port that does not match entry, don't return * extract url helpers into a package Signed-off-by: Emily Casey <[email protected]> Signed-off-by: Danny Joyce <[email protected]>
1 parent ecb0113 commit 6f4b0a7

File tree

9 files changed

+332
-93
lines changed

9 files changed

+332
-93
lines changed

osxkeychain/osxkeychain_darwin.go

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ package osxkeychain
1010
import "C"
1111
import (
1212
"errors"
13-
"net/url"
13+
"github.com/docker/docker-credential-helpers/registryurl"
1414
"strconv"
15-
"strings"
1615
"unsafe"
1716

1817
"github.com/docker/docker-credential-helpers/credentials"
@@ -135,7 +134,7 @@ func (h Osxkeychain) List() (map[string]string, error) {
135134
}
136135

137136
func splitServer(serverURL string) (*C.struct_Server, error) {
138-
u, err := parseURL(serverURL)
137+
u, err := registryurl.Parse(serverURL)
139138
if err != nil {
140139
return nil, err
141140
}
@@ -145,7 +144,7 @@ func splitServer(serverURL string) (*C.struct_Server, error) {
145144
proto = C.kSecProtocolTypeHTTP
146145
}
147146
var port int
148-
p := getPort(u)
147+
p := registryurl.GetPort(u)
149148
if p != "" {
150149
port, err = strconv.Atoi(p)
151150
if err != nil {
@@ -155,7 +154,7 @@ func splitServer(serverURL string) (*C.struct_Server, error) {
155154

156155
return &C.struct_Server{
157156
proto: C.SecProtocolType(proto),
158-
host: C.CString(getHostname(u)),
157+
host: C.CString(registryurl.GetHostname(u)),
159158
port: C.uint(port),
160159
path: C.CString(u.Path),
161160
}, nil
@@ -165,32 +164,3 @@ func freeServer(s *C.struct_Server) {
165164
C.free(unsafe.Pointer(s.host))
166165
C.free(unsafe.Pointer(s.path))
167166
}
168-
169-
// parseURL parses and validates a given serverURL to an url.URL, and
170-
// returns an error if validation failed. Querystring parameters are
171-
// omitted in the resulting URL, because they are not used in the helper.
172-
//
173-
// If serverURL does not have a valid scheme, `//` is used as scheme
174-
// before parsing. This prevents the hostname being used as path,
175-
// and the credentials being stored without host.
176-
func parseURL(serverURL string) (*url.URL, error) {
177-
// Check if serverURL has a scheme, otherwise add `//` as scheme.
178-
if !strings.Contains(serverURL, "://") && !strings.HasPrefix(serverURL, "//") {
179-
serverURL = "//" + serverURL
180-
}
181-
182-
u, err := url.Parse(serverURL)
183-
if err != nil {
184-
return nil, err
185-
}
186-
187-
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
188-
return nil, errors.New("unsupported scheme: " + u.Scheme)
189-
}
190-
if getHostname(u) == "" {
191-
return nil, errors.New("no hostname in URL")
192-
}
193-
194-
u.RawQuery = ""
195-
return u, nil
196-
}

osxkeychain/osxkeychain_darwin_test.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package osxkeychain
22

33
import (
4-
"errors"
54
"fmt"
65
"github.com/docker/docker-credential-helpers/credentials"
76
"testing"
@@ -56,46 +55,6 @@ func TestOSXKeychainHelper(t *testing.T) {
5655
}
5756
}
5857

59-
// TestOSXKeychainHelperParseURL verifies that a // "scheme" is added to URLs,
60-
// and that invalid URLs produce an error.
61-
func TestOSXKeychainHelperParseURL(t *testing.T) {
62-
tests := []struct {
63-
url string
64-
expectedURL string
65-
err error
66-
}{
67-
{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
68-
{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
69-
{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
70-
{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
71-
{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
72-
{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
73-
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
74-
{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
75-
{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
76-
}
77-
78-
for _, te := range tests {
79-
u, err := parseURL(te.url)
80-
81-
if te.err == nil && err != nil {
82-
t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
83-
continue
84-
}
85-
if te.err != nil && err == nil {
86-
t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
87-
continue
88-
}
89-
if te.err != nil && err.Error() != te.err.Error() {
90-
t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
91-
continue
92-
}
93-
if u != nil && u.String() != te.expectedURL {
94-
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
95-
}
96-
}
97-
}
98-
9958
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
10059
// through variations on the URL
10160
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {

osxkeychain/url_go18.go

Lines changed: 0 additions & 13 deletions
This file was deleted.

registryurl/parse.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package registryurl
2+
3+
import (
4+
"errors"
5+
"net/url"
6+
"strings"
7+
)
8+
9+
// Parse parses and validates a given serverURL to an url.URL, and
10+
// returns an error if validation failed. Querystring parameters are
11+
// omitted in the resulting URL, because they are not used in the helper.
12+
//
13+
// If serverURL does not have a valid scheme, `//` is used as scheme
14+
// before parsing. This prevents the hostname being used as path,
15+
// and the credentials being stored without host.
16+
func Parse(registryURL string) (*url.URL, error) {
17+
// Check if registryURL has a scheme, otherwise add `//` as scheme.
18+
if !strings.Contains(registryURL, "://") && !strings.HasPrefix(registryURL, "//") {
19+
registryURL = "//" + registryURL
20+
}
21+
22+
u, err := url.Parse(registryURL)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
28+
return nil, errors.New("unsupported scheme: " + u.Scheme)
29+
}
30+
31+
if GetHostname(u) == "" {
32+
return nil, errors.New("no hostname in URL")
33+
}
34+
35+
u.RawQuery = ""
36+
return u, nil
37+
}

registryurl/parse_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package registryurl
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
// TestHelperParseURL verifies that a // "scheme" is added to URLs,
9+
// and that invalid URLs produce an error.
10+
func TestHelperParseURL(t *testing.T) {
11+
tests := []struct {
12+
url string
13+
expectedURL string
14+
err error
15+
}{
16+
{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
17+
{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
18+
{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
19+
{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
20+
{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
21+
{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
22+
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
23+
{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
24+
{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
25+
}
26+
27+
for _, te := range tests {
28+
u, err := Parse(te.url)
29+
30+
if te.err == nil && err != nil {
31+
t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
32+
continue
33+
}
34+
if te.err != nil && err == nil {
35+
t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
36+
continue
37+
}
38+
if te.err != nil && err.Error() != te.err.Error() {
39+
t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
40+
continue
41+
}
42+
if u != nil && u.String() != te.expectedURL {
43+
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
44+
}
45+
}
46+
}

registryurl/url_go18.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//+build go1.8
2+
3+
package registryurl
4+
5+
import (
6+
url "net/url"
7+
)
8+
9+
func GetHostname(u *url.URL) string {
10+
return u.Hostname()
11+
}
12+
13+
func GetPort(u *url.URL) string {
14+
return u.Port()
15+
}

osxkeychain/url_non_go18.go renamed to registryurl/url_non_go18.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
//+build !go1.8
22

3-
package osxkeychain
3+
package registryurl
44

55
import (
6-
"net/url"
76
"strings"
7+
url "net/url"
88
)
99

10-
func getHostname(u *url.URL) string {
10+
func GetHostname(u *url.URL) string {
1111
return stripPort(u.Host)
1212
}
1313

14-
func getPort(u *url.URL) string {
14+
func GetPort(u *url.URL) string {
1515
return portOnly(u.Host)
1616
}
1717

wincred/wincred_windows.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package wincred
22

33
import (
44
"bytes"
5+
"github.com/docker/docker-credential-helpers/registryurl"
6+
"net/url"
57
"strings"
68

79
winc "github.com/danieljoos/wincred"
@@ -37,10 +39,18 @@ func (h Wincred) Delete(serverURL string) error {
3739

3840
// Get retrieves credentials from the windows credentials manager.
3941
func (h Wincred) Get(serverURL string) (string, string, error) {
40-
g, _ := winc.GetGenericCredential(serverURL)
42+
target, err := getTarget(serverURL)
43+
if err != nil {
44+
return "", "", err
45+
} else if target == "" {
46+
return "", "", credentials.NewErrCredentialsNotFound()
47+
}
48+
49+
g, _ := winc.GetGenericCredential(target)
4150
if g == nil {
4251
return "", "", credentials.NewErrCredentialsNotFound()
4352
}
53+
4454
for _, attr := range g.Attributes {
4555
if strings.Compare(attr.Keyword, "label") == 0 &&
4656
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
@@ -51,6 +61,72 @@ func (h Wincred) Get(serverURL string) (string, string, error) {
5161
return "", "", credentials.NewErrCredentialsNotFound()
5262
}
5363

64+
func getTarget(serverURL string) (string, error) {
65+
s, err := registryurl.Parse(serverURL)
66+
if err != nil {
67+
return serverURL, nil
68+
}
69+
70+
creds, err := winc.List()
71+
if err != nil {
72+
return "", err
73+
}
74+
75+
targets := make([]string, 0)
76+
for i := range creds {
77+
attrs := creds[i].Attributes
78+
for _, attr := range attrs {
79+
if strings.Compare(attr.Keyword, "label") == 0 &&
80+
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
81+
targets = append(targets, creds[i].TargetName)
82+
}
83+
}
84+
}
85+
86+
if target, found := findMatch(s, targets, exactMatch); found {
87+
return target, nil
88+
}
89+
90+
if target, found := findMatch(s, targets, approximateMatch); found {
91+
return target, nil
92+
}
93+
94+
return "", nil
95+
}
96+
97+
func findMatch(serverUrl *url.URL, targets []string, matches func(url.URL, url.URL) bool) (string, bool) {
98+
for _, target := range targets {
99+
tURL, err := registryurl.Parse(target)
100+
if err != nil {
101+
continue
102+
}
103+
if matches(*serverUrl, *tURL) {
104+
return target, true
105+
}
106+
}
107+
return "", false
108+
}
109+
110+
func exactMatch(serverURL, target url.URL) bool {
111+
return serverURL.String() == target.String()
112+
}
113+
114+
func approximateMatch(serverURL, target url.URL) bool {
115+
//if scheme is missing assume it is the same as target
116+
if serverURL.Scheme == "" {
117+
serverURL.Scheme = target.Scheme
118+
}
119+
//if port is missing assume it is the same as target
120+
if serverURL.Port() == "" && target.Port() != "" {
121+
serverURL.Host = serverURL.Host + ":" + target.Port()
122+
}
123+
//if path is missing assume it is the same as target
124+
if serverURL.Path == "" {
125+
serverURL.Path = target.Path
126+
}
127+
return serverURL.String() == target.String()
128+
}
129+
54130
// List returns the stored URLs and corresponding usernames for a given credentials label.
55131
func (h Wincred) List() (map[string]string, error) {
56132
creds, err := winc.List()

0 commit comments

Comments
 (0)