Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix storing URLs without scheme #72

Merged
merged 4 commits into from
Jun 15, 2017
Merged

Conversation

thaJeztah
Copy link
Member

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.

{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},

// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
// TODO is this desired behavior?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure about this one

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one looks good to me

{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},

// non-matching ports
// TODO is this desired behavior? The other way round does work
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure here either, I'd drop this test.

// parseURL parses and validates a given serverURL to an url.URL, and
// returns an error if validation failed. Querystring parameters are
// omitted in the resulting URL, because they are not used in the helper
func parseURL(serverURL string) (*url.URL, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitted this to a separate function, so that we can test independently 😄

return nil, errors.New("no hostname in URL")
}

u.RawQuery = ""
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly strip query-string, because we're not storing it, but let me know if you don't want this 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good catch!

{"https://foobar.docker.io:2376", "foobar.docker.io"},

// stored with path, retrieved without
// TODO is this the desired behavior?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure about this either

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path is not kept in the keychain, this is the desired behavior AAIK 👍

// returns an error if validation failed. Querystring parameters are
// omitted in the resulting URL, because they are not used in the helper
func parseURL(serverURL string) (*url.URL, error) {
if !regexp.MustCompile(`^([a-zA-Z][-+.a-zA-Z0-9]+:)?//`).MatchString(serverURL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we add a comment to know what this regex checks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, makes sense

return nil, errors.New("no hostname in URL")
}

u.RawQuery = ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good catch!

}

return &C.struct_Server{
proto: C.SecProtocolType(proto),
host: C.CString(host),
host: C.CString(u.Hostname()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is u.Hostname() different from u.Host ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{"https://foobar.docker.io:2376", "foobar.docker.io"},

// stored with path, retrieved without
// TODO is this the desired behavior?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path is not kept in the keychain, this is the desired behavior AAIK 👍

continue
}
if te.err != nil && err == nil {
t.Errorf("Error: expected parsing to fail for URL %q", te.url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include the expected error message of te.err?

{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},

// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
// TODO is this desired behavior?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one looks good to me

{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},

// non-matching ports
// TODO is this desired behavior? The other way round does work
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure here either, I'd drop this test.

Secret: fmt.Sprintf("secret-%d", i),
}

if err := helper.Add(c); err != nil {
Copy link
Contributor

@n4ss n4ss Jun 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helper.Delete before testing and defer helper.Delete in order to remove all the added credentials, otherwise it generates false positives/negatives with creds from previous tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did comment backward for this one sorry, added references to other spots where it should be done too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference; this was addressed through this part above;

// Clean store before testing.
for _, te := range tests {
	helper.Delete(te.url)
}

helper := Osxkeychain{}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here for the Delete -> Add -> defer Delete

helper := Osxkeychain{}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here for the Delete -> Add -> defer Delete

@thaJeztah thaJeztah force-pushed the fix-missing-scheme branch 2 times, most recently from 8d249de to 42d4782 Compare June 14, 2017 12:46
@n4ss
Copy link
Contributor

n4ss commented Jun 14, 2017

LGTM! travisCI is failing because it compiles with Golang 1.6, I don't have the rights to update I think.

@vdemeester can you review this please? ^_^

@n4ss
Copy link
Contributor

n4ss commented Jun 14, 2017

Follow up issue on this on the CLI side: docker/cli#190

We now need to fix the behavior of stripping the protocol when a user adds it to the URL.

Since we'll default to HTTPS, they'll have explicitly specify protocol: docker login http://... to use an insecure connection.

Copy link
Collaborator

@vdemeester vdemeester left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@thaJeztah
Copy link
Member Author

Hm, looks like it fails on Go 1.7 as well; were these options added in 1.8?

ok  	github.com/docker/docker-credential-helpers/credentials	0.224s
# github.com/docker/docker-credential-helpers/osxkeychain
osxkeychain/osxkeychain_darwin.go:148: u.Port undefined (type *url.URL has no field or method Port)
osxkeychain/osxkeychain_darwin.go:149: u.Port undefined (type *url.URL has no field or method Port)
osxkeychain/osxkeychain_darwin.go:157: u.Hostname undefined (type *url.URL has no field or method Hostname)
osxkeychain/osxkeychain_darwin.go:188: u.Hostname undefined (type *url.URL has no field or method Hostname)
# github.com/docker/docker-credential-helpers/osxkeychain
osxkeychain/osxkeychain_darwin.go:148: u.Port undefined (type *url.URL has no field or method Port)
osxkeychain/osxkeychain_darwin.go:149: u.Port undefined (type *url.URL has no field or method Port)
osxkeychain/osxkeychain_darwin.go:157: u.Hostname undefined (type *url.URL has no field or method Hostname)
osxkeychain/osxkeychain_darwin.go:188: u.Hostname undefined (type *url.URL has no field or method Hostname)
FAIL	github.com/docker/docker-credential-helpers/osxkeychain [build failed]
make: *** [test] Error 2

@thaJeztah
Copy link
Member Author

Ugh looks like it was added in Go 1.8 https://golang.org/doc/go1.8

screen shot 2017-06-14 at 18 47 57

}

u.RawQuery = ""
return u, nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of striping the query, and checking the scheme above what do you think about returning a different struct here instead of url.Url. It seems we're not really using much of the url.

type url struct {
    proto string
    host string
    port string
    path string
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using the Port() and Hostname() methods

Copy link

@dnephin dnephin Jun 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but those values could be written to this new struct before returning. Either way works fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that first, but decided to keep it simple

// Without a scheme, the hostname is seen as "path" by `url.Parse()`
if !regexp.MustCompile(`^([a-zA-Z][-+.a-zA-Z0-9]+:)?//`).MatchString(serverURL) {
serverURL = "//" + serverURL
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could call url.Parse() and if u.Scheme == "", then prepend // and call it again on the new string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, funny, that was my original proposal; #55 (comment) / https://play.golang.org/p/uSIHiJ55ir

func parseURL(serverURL string) (*url.URL, error) {
// Check if serverURL has a valid scheme, otherwise add `//` as scheme.
// Without a scheme, the hostname is seen as "path" by `url.Parse()`
if !regexp.MustCompile(`^([a-zA-Z][-+.a-zA-Z0-9]+:)?//`).MatchString(serverURL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no biggie, but if you update the PR, you could put this mustcompile in a package level var

var port int
if len(hostAndPort) == 2 {
p, err := strconv.Atoi(hostAndPort[1])
if u.Port() != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These go1.8 Port() and Hostname() methods are only useful for supporting ipv6, based on what I've read from their code. We could argue it's out of scope for this PR. Or we should provide pre-1.8 functions with !go1.8 buildtags

thaJeztah and others added 2 commits June 14, 2017 15:45
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 <github@gone.nl>
Signed-off-by: Tibor Vass <teabee89@gmail.com>
@tiborvass
Copy link
Contributor

ping @dnephin @thaJeztah

}
// Check if serverURL has a valid scheme, otherwise add `//` as scheme.
// Without a scheme, the hostname is seen as "path" by `url.Parse()`
if u.Scheme == "" {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not actually work; localhost:443 will be parsed as scheme: localhost, host: 443; https://play.golang.org/p/QRE7pqxjx1

Copy link
Member Author

@thaJeztah thaJeztah Jun 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify, we can use

if !strings.Contains(serverURL, "://") && !strings.HasPrefix(serverURL, "//") {
    serverURL = "//" + serverURL
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

@tiborvass
Copy link
Contributor

Another note: this should be a fix for all OSes and not specific to OSX. Fixing.

@n4ss
Copy link
Contributor

n4ss commented Jun 15, 2017

@tiborvass at the wrapper level? Then, we'll have to warn every folk implementing a credential helper.

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
@tiborvass
Copy link
Contributor

LGTM

"unsafe"

"github.com/docker/docker-credential-helpers/credentials"
"strings"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to gofmt it is :/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess it's because docker/docker follows the goimport conventions, in which case they are grouped, and sorted, but gofmt won't complain if they were not grouped.

gofmt follows a simple rule, which is to sort each group of imports,
where different groups are separated by a blank line.

goimports also follows a simple rule, which is to put stdlib imports
in one group and non-stdlib imports in a different group.

// before parsing. This prevents the hostname being used as path,
// and the credentials being stored without host.
func parseURL(serverURL string) (*url.URL, error) {
// Check if serverURL has a valid scheme, otherwise add `//` as scheme.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remove "valid" here in the comment? This is just a detection is a "possible" scheme is present

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
@@ -1,6 +1,8 @@
package osxkeychain

import (
"errors"
"fmt"
"github.com/docker/docker-credential-helpers/credentials"
"testing"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, not sure what formatting is used in this repo, but ideally would be;

import (
	"errors"
	"fmt"
	"testing"

	"github.com/docker/docker-credential-helpers/credentials"
)

Just a nit, so not a blocker

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll investigate that for a next PR :)

Copy link
Member Author

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one tiny nit, but LGTM

@n4ss n4ss merged commit e1d4c01 into docker:master Jun 15, 2017
@thaJeztah thaJeztah deleted the fix-missing-scheme branch June 15, 2017 09:15
@dnephin
Copy link

dnephin commented Jun 15, 2017

Someone just linked me this: https://github.com/goware/urlx

I think this is exactly what we want for the url parsing code.

@thaJeztah
Copy link
Member Author

@dnephin that looks neat indeed. Only issue I see is that it defaults to http, but perhaps we can make a proposal to make it configurable, or have a function that allows passing in defaults for missing components

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants