Skip to content

Commit e1d4c01

Browse files
thaJeztahn4ss
authored andcommitted
Fix storing URLs without scheme (#72)
* Fix storing URLs without scheme If secrets are stored without specifying a scheme (https://), the keychain-helper would interpret the hostname as _path_, causing lookup of secrets to fail. This patch makes sure that a scheme is added (if missing). If no scheme is specified, https:// is used as a default. Signed-off-by: Sebastiaan van Stijn <[email protected]> * Have pre go1.8 compiler able to compile Signed-off-by: Tibor Vass <[email protected]> * Fix URL parsing with port and no scheme Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]> * Improve parseURL comment Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]>
1 parent 94be56b commit e1d4c01

File tree

7 files changed

+289
-22
lines changed

7 files changed

+289
-22
lines changed

credentials/credentials_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ func TestStore(t *testing.T) {
7676
func TestStoreMissingServerURL(t *testing.T) {
7777
creds := &Credentials{
7878
ServerURL: "",
79-
Username: "foo",
80-
Secret: "bar",
79+
Username: "foo",
80+
Secret: "bar",
8181
}
8282

8383
b, err := json.Marshal(creds)
@@ -96,8 +96,8 @@ func TestStoreMissingServerURL(t *testing.T) {
9696
func TestStoreMissingUsername(t *testing.T) {
9797
creds := &Credentials{
9898
ServerURL: "https://index.docker.io/v1/",
99-
Username: "",
100-
Secret: "bar",
99+
Username: "",
100+
Secret: "bar",
101101
}
102102

103103
b, err := json.Marshal(creds)

credentials/error.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const (
88
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
99
// invalid credentials or credentials management operations
1010
errCredentialsMissingServerURLMessage = "no credentials server URL"
11-
errCredentialsMissingUsernameMessage = "no credentials username"
11+
errCredentialsMissingUsernameMessage = "no credentials username"
1212
)
1313

1414
// errCredentialsNotFound represents an error
@@ -43,7 +43,6 @@ func IsErrCredentialsNotFoundMessage(err string) bool {
4343
return err == errCredentialsNotFoundMessage
4444
}
4545

46-
4746
// errCredentialsMissingServerURL represents an error raised
4847
// when the credentials object has no server URL or when no
4948
// server URL is provided to a credentials operation requiring
@@ -64,7 +63,6 @@ func (errCredentialsMissingUsername) Error() string {
6463
return errCredentialsMissingUsernameMessage
6564
}
6665

67-
6866
// NewErrCredentialsMissingServerURL creates a new error for
6967
// errCredentialsMissingServerURL.
7068
func NewErrCredentialsMissingServerURL() error {
@@ -77,7 +75,6 @@ func NewErrCredentialsMissingUsername() error {
7775
return errCredentialsMissingUsername{}
7876
}
7977

80-
8178
// IsCredentialsMissingServerURL returns true if the error
8279
// was an errCredentialsMissingServerURL.
8380
func IsCredentialsMissingServerURL(err error) bool {

osxkeychain/osxkeychain_darwin.go

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -135,30 +135,27 @@ func (h Osxkeychain) List() (map[string]string, error) {
135135
}
136136

137137
func splitServer(serverURL string) (*C.struct_Server, error) {
138-
u, err := url.Parse(serverURL)
138+
u, err := parseURL(serverURL)
139139
if err != nil {
140140
return nil, err
141141
}
142142

143-
hostAndPort := strings.Split(u.Host, ":")
144-
host := hostAndPort[0]
143+
proto := C.kSecProtocolTypeHTTPS
144+
if u.Scheme == "http" {
145+
proto = C.kSecProtocolTypeHTTP
146+
}
145147
var port int
146-
if len(hostAndPort) == 2 {
147-
p, err := strconv.Atoi(hostAndPort[1])
148+
p := getPort(u)
149+
if p != "" {
150+
port, err = strconv.Atoi(p)
148151
if err != nil {
149152
return nil, err
150153
}
151-
port = p
152-
}
153-
154-
proto := C.kSecProtocolTypeHTTPS
155-
if u.Scheme != "https" {
156-
proto = C.kSecProtocolTypeHTTP
157154
}
158155

159156
return &C.struct_Server{
160157
proto: C.SecProtocolType(proto),
161-
host: C.CString(host),
158+
host: C.CString(getHostname(u)),
162159
port: C.uint(port),
163160
path: C.CString(u.Path),
164161
}, nil
@@ -168,3 +165,32 @@ func freeServer(s *C.struct_Server) {
168165
C.free(unsafe.Pointer(s.host))
169166
C.free(unsafe.Pointer(s.path))
170167
}
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: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package osxkeychain
22

33
import (
4+
"errors"
5+
"fmt"
46
"github.com/docker/docker-credential-helpers/credentials"
57
"testing"
68
)
@@ -54,6 +56,194 @@ func TestOSXKeychainHelper(t *testing.T) {
5456
}
5557
}
5658

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+
99+
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
100+
// through variations on the URL
101+
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
102+
tests := []struct {
103+
storeURL string
104+
readURL string
105+
}{
106+
// stored with port, retrieved without
107+
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
108+
109+
// stored as https, retrieved without scheme
110+
{"https://foobar.docker.io:2376", "foobar.docker.io"},
111+
112+
// stored with path, retrieved without
113+
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234"},
114+
}
115+
116+
helper := Osxkeychain{}
117+
defer func() {
118+
for _, te := range tests {
119+
helper.Delete(te.storeURL)
120+
}
121+
}()
122+
123+
// Clean store before testing.
124+
for _, te := range tests {
125+
helper.Delete(te.storeURL)
126+
}
127+
128+
for _, te := range tests {
129+
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
130+
if err := helper.Add(c); err != nil {
131+
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
132+
continue
133+
}
134+
if _, _, err := helper.Get(te.readURL); err != nil {
135+
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
136+
}
137+
helper.Delete(te.storeURL)
138+
}
139+
}
140+
141+
// TestOSXKeychainHelperRetrieveStrict verifies that only matching secrets are
142+
// returned.
143+
func TestOSXKeychainHelperRetrieveStrict(t *testing.T) {
144+
tests := []struct {
145+
storeURL string
146+
readURL string
147+
}{
148+
// stored as https, retrieved using http
149+
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
150+
151+
// stored as http, retrieved using https
152+
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
153+
154+
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
155+
{"http://foobar.docker.io", "foobar.docker.io:5678"},
156+
157+
// non-matching ports
158+
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
159+
160+
// non-matching ports TODO is this desired behavior? The other way round does work
161+
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
162+
163+
// non-matching paths
164+
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
165+
}
166+
167+
helper := Osxkeychain{}
168+
defer func() {
169+
for _, te := range tests {
170+
helper.Delete(te.storeURL)
171+
}
172+
}()
173+
174+
// Clean store before testing.
175+
for _, te := range tests {
176+
helper.Delete(te.storeURL)
177+
}
178+
179+
for _, te := range tests {
180+
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
181+
if err := helper.Add(c); err != nil {
182+
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
183+
continue
184+
}
185+
if _, _, err := helper.Get(te.readURL); err == nil {
186+
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
187+
}
188+
helper.Delete(te.storeURL)
189+
}
190+
}
191+
192+
// TestOSXKeychainHelperStoreRetrieve verifies that secrets stored in the
193+
// the keychain can be read back using the URL that was used to store them.
194+
func TestOSXKeychainHelperStoreRetrieve(t *testing.T) {
195+
tests := []struct {
196+
url string
197+
}{
198+
{url: "foobar.docker.io"},
199+
{url: "foobar.docker.io:2376"},
200+
{url: "//foobar.docker.io:2376"},
201+
{url: "https://foobar.docker.io:2376"},
202+
{url: "http://foobar.docker.io:2376"},
203+
{url: "https://foobar.docker.io:2376/some/path"},
204+
{url: "https://foobar.docker.io:2376/some/other/path"},
205+
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
206+
}
207+
208+
helper := Osxkeychain{}
209+
defer func() {
210+
for _, te := range tests {
211+
helper.Delete(te.url)
212+
}
213+
}()
214+
215+
// Clean store before testing.
216+
for _, te := range tests {
217+
helper.Delete(te.url)
218+
}
219+
220+
// Note that we don't delete between individual tests here, to verify that
221+
// subsequent stores/overwrites don't affect storing / retrieving secrets.
222+
for i, te := range tests {
223+
c := &credentials.Credentials{
224+
ServerURL: te.url,
225+
Username: fmt.Sprintf("user-%d", i),
226+
Secret: fmt.Sprintf("secret-%d", i),
227+
}
228+
229+
if err := helper.Add(c); err != nil {
230+
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
231+
continue
232+
}
233+
user, secret, err := helper.Get(te.url)
234+
if err != nil {
235+
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
236+
continue
237+
}
238+
if user != c.Username {
239+
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
240+
}
241+
if secret != c.Secret {
242+
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
243+
}
244+
}
245+
}
246+
57247
func TestMissingCredentials(t *testing.T) {
58248
helper := Osxkeychain{}
59249
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")

osxkeychain/url_go18.go

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

osxkeychain/url_non_go18.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//+build !go1.8
2+
3+
package osxkeychain
4+
5+
import (
6+
"net/url"
7+
"strings"
8+
)
9+
10+
func getHostname(u *url.URL) string {
11+
return stripPort(u.Host)
12+
}
13+
14+
func getPort(u *url.URL) string {
15+
return portOnly(u.Host)
16+
}
17+
18+
func stripPort(hostport string) string {
19+
colon := strings.IndexByte(hostport, ':')
20+
if colon == -1 {
21+
return hostport
22+
}
23+
if i := strings.IndexByte(hostport, ']'); i != -1 {
24+
return strings.TrimPrefix(hostport[:i], "[")
25+
}
26+
return hostport[:colon]
27+
}
28+
29+
func portOnly(hostport string) string {
30+
colon := strings.IndexByte(hostport, ':')
31+
if colon == -1 {
32+
return ""
33+
}
34+
if i := strings.Index(hostport, "]:"); i != -1 {
35+
return hostport[i+len("]:"):]
36+
}
37+
if strings.Contains(hostport, "]") {
38+
return ""
39+
}
40+
return hostport[colon+len(":"):]
41+
}

0 commit comments

Comments
 (0)