Skip to content

Commit 152d643

Browse files
authored
Merge pull request #139 from ekcasey/server-alias-wincred
make docker-credential-wincred work like docker-credential-osxkeychain
2 parents 74636a1 + 77e30bd commit 152d643

File tree

9 files changed

+333
-94
lines changed

9 files changed

+333
-94
lines changed

osxkeychain/osxkeychain_darwin.go

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ package osxkeychain
1010
import "C"
1111
import (
1212
"errors"
13-
"net/url"
1413
"strconv"
15-
"strings"
1614
"unsafe"
1715

1816
"github.com/docker/docker-credential-helpers/credentials"
17+
"github.com/docker/docker-credential-helpers/registryurl"
1918
)
2019

2120
// errCredentialsNotFound is the specific error message returned by OS X
@@ -139,7 +138,7 @@ func (h Osxkeychain) List() (map[string]string, error) {
139138
}
140139

141140
func splitServer(serverURL string) (*C.struct_Server, error) {
142-
u, err := parseURL(serverURL)
141+
u, err := registryurl.Parse(serverURL)
143142
if err != nil {
144143
return nil, err
145144
}
@@ -149,7 +148,7 @@ func splitServer(serverURL string) (*C.struct_Server, error) {
149148
proto = C.kSecProtocolTypeHTTP
150149
}
151150
var port int
152-
p := getPort(u)
151+
p := registryurl.GetPort(u)
153152
if p != "" {
154153
port, err = strconv.Atoi(p)
155154
if err != nil {
@@ -159,7 +158,7 @@ func splitServer(serverURL string) (*C.struct_Server, error) {
159158

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

osxkeychain/osxkeychain_darwin_test.go

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package osxkeychain
22

33
import (
4-
"errors"
54
"fmt"
6-
"github.com/docker/docker-credential-helpers/credentials"
75
"testing"
6+
7+
"github.com/docker/docker-credential-helpers/credentials"
88
)
99

1010
func TestOSXKeychainHelper(t *testing.T) {
@@ -56,46 +56,6 @@ func TestOSXKeychainHelper(t *testing.T) {
5656
}
5757
}
5858

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-
9959
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
10060
// through variations on the URL
10161
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"
6+
url "net/url"
77
"strings"
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: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package wincred
22

33
import (
44
"bytes"
5+
"net/url"
56
"strings"
67

78
winc "github.com/danieljoos/wincred"
89
"github.com/docker/docker-credential-helpers/credentials"
10+
"github.com/docker/docker-credential-helpers/registryurl"
911
)
1012

1113
// Wincred handles secrets using the Windows credential service.
@@ -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,71 @@ 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+
var targets []string
76+
for i := range creds {
77+
attrs := creds[i].Attributes
78+
for _, attr := range attrs {
79+
if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
80+
targets = append(targets, creds[i].TargetName)
81+
}
82+
}
83+
}
84+
85+
if target, found := findMatch(s, targets, exactMatch); found {
86+
return target, nil
87+
}
88+
89+
if target, found := findMatch(s, targets, approximateMatch); found {
90+
return target, nil
91+
}
92+
93+
return "", nil
94+
}
95+
96+
func findMatch(serverUrl *url.URL, targets []string, matches func(url.URL, url.URL) bool) (string, bool) {
97+
for _, target := range targets {
98+
tURL, err := registryurl.Parse(target)
99+
if err != nil {
100+
continue
101+
}
102+
if matches(*serverUrl, *tURL) {
103+
return target, true
104+
}
105+
}
106+
return "", false
107+
}
108+
109+
func exactMatch(serverURL, target url.URL) bool {
110+
return serverURL.String() == target.String()
111+
}
112+
113+
func approximateMatch(serverURL, target url.URL) bool {
114+
//if scheme is missing assume it is the same as target
115+
if serverURL.Scheme == "" {
116+
serverURL.Scheme = target.Scheme
117+
}
118+
//if port is missing assume it is the same as target
119+
if serverURL.Port() == "" && target.Port() != "" {
120+
serverURL.Host = serverURL.Host + ":" + target.Port()
121+
}
122+
//if path is missing assume it is the same as target
123+
if serverURL.Path == "" {
124+
serverURL.Path = target.Path
125+
}
126+
return serverURL.String() == target.String()
127+
}
128+
54129
// List returns the stored URLs and corresponding usernames for a given credentials label.
55130
func (h Wincred) List() (map[string]string, error) {
56131
creds, err := winc.List()

0 commit comments

Comments
 (0)