Skip to content

Commit

Permalink
Fix storing URLs without scheme
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
thaJeztah committed Jun 14, 2017
1 parent 94be56b commit 181227e
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 13 deletions.
50 changes: 37 additions & 13 deletions osxkeychain/osxkeychain_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import "C"
import (
"errors"
"net/url"
"regexp"
"strconv"
"strings"
"unsafe"

"github.com/docker/docker-credential-helpers/credentials"
Expand Down Expand Up @@ -135,30 +135,26 @@ func (h Osxkeychain) List() (map[string]string, error) {
}

func splitServer(serverURL string) (*C.struct_Server, error) {
u, err := url.Parse(serverURL)
u, err := parseURL(serverURL)
if err != nil {
return nil, err
}

hostAndPort := strings.Split(u.Host, ":")
host := hostAndPort[0]
proto := C.kSecProtocolTypeHTTPS
if u.Scheme == "http" {
proto = C.kSecProtocolTypeHTTP
}
var port int
if len(hostAndPort) == 2 {
p, err := strconv.Atoi(hostAndPort[1])
if u.Port() != "" {
port, err = strconv.Atoi(u.Port())
if err != nil {
return nil, err
}
port = p
}

proto := C.kSecProtocolTypeHTTPS
if u.Scheme != "https" {
proto = C.kSecProtocolTypeHTTP
}

return &C.struct_Server{
proto: C.SecProtocolType(proto),
host: C.CString(host),
host: C.CString(u.Hostname()),
port: C.uint(port),
path: C.CString(u.Path),
}, nil
Expand All @@ -168,3 +164,31 @@ func freeServer(s *C.struct_Server) {
C.free(unsafe.Pointer(s.host))
C.free(unsafe.Pointer(s.path))
}

// 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.
//
// If serverURL does not have a valid scheme, `//` is used as scheme
// 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.
// 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
}
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
}
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
return nil, errors.New("unsupported scheme: " + u.Scheme)
}
if u.Hostname() == "" {
return nil, errors.New("no hostname in URL")
}

u.RawQuery = ""
return u, nil
}
190 changes: 190 additions & 0 deletions osxkeychain/osxkeychain_darwin_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package osxkeychain

import (
"errors"
"fmt"
"github.com/docker/docker-credential-helpers/credentials"
"testing"
)
Expand Down Expand Up @@ -54,6 +56,194 @@ func TestOSXKeychainHelper(t *testing.T) {
}
}

// TestOSXKeychainHelperParseURL verifies that a // "scheme" is added to URLs,
// and that invalid URLs produce an error.
func TestOSXKeychainHelperParseURL(t *testing.T) {
tests := []struct {
url string
expectedURL string
err error
}{
{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
}

for _, te := range tests {
u, err := parseURL(te.url)

if te.err == nil && err != nil {
t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
continue
}
if te.err != nil && err == nil {
t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
continue
}
if te.err != nil && err.Error() != te.err.Error() {
t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
continue
}
if u != nil && u.String() != te.expectedURL {
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
}
}
}

// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
// through variations on the URL
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored with port, retrieved without
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},

// stored as https, retrieved without scheme
{"https://foobar.docker.io:2376", "foobar.docker.io"},

// stored with path, retrieved without
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234"},
}

helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()

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

for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err != nil {
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}

// TestOSXKeychainHelperRetrieveStrict verifies that only matching secrets are
// returned.
func TestOSXKeychainHelperRetrieveStrict(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored as https, retrieved using http
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},

// stored as http, retrieved using https
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},

// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
{"http://foobar.docker.io", "foobar.docker.io:5678"},

// non-matching ports
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},

// non-matching ports TODO is this desired behavior? The other way round does work
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},

// non-matching paths
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
}

helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()

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

for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err == nil {
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}

// TestOSXKeychainHelperStoreRetrieve verifies that secrets stored in the
// the keychain can be read back using the URL that was used to store them.
func TestOSXKeychainHelperStoreRetrieve(t *testing.T) {
tests := []struct {
url string
}{
{url: "foobar.docker.io"},
{url: "foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
}

helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.url)
}
}()

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

// Note that we don't delete between individual tests here, to verify that
// subsequent stores/overwrites don't affect storing / retrieving secrets.
for i, te := range tests {
c := &credentials.Credentials{
ServerURL: te.url,
Username: fmt.Sprintf("user-%d", i),
Secret: fmt.Sprintf("secret-%d", i),
}

if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
continue
}
user, secret, err := helper.Get(te.url)
if err != nil {
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
continue
}
if user != c.Username {
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
}
if secret != c.Secret {
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
}
}
}

func TestMissingCredentials(t *testing.T) {
helper := Osxkeychain{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
Expand Down

0 comments on commit 181227e

Please sign in to comment.