-
Notifications
You must be signed in to change notification settings - Fork 190
Description
The client package includes an example of how to use client.Store:
docker-credential-helpers/client/client_test.go
Lines 95 to 105 in b7a754b
| p := NewShellProgramFunc("docker-credential-pass") | |
| c := &credentials.Credentials{ | |
| ServerURL: "https://registry.example.com", | |
| Username: "exampleuser", | |
| Secret: "my super secret token", | |
| } | |
| if err := Store(p, c); err != nil { | |
| _, _ = fmt.Println(err) | |
| } |
The ServerURL field is set to a URL including a scheme here, which on the surface seems correct for a field named ServerURL. However, in practice it seems like existing callers like docker login set this field to include just a hostname instead, such as "registry.example.com".
I notice that at least some of the actual credential helper implementations in this repository use registryurl.Parse, which normalizes this difference away a little by turning a bare hostname into a schemeless URL:
docker-credential-helpers/registryurl/parse.go
Lines 9 to 37 in b7a754b
| // Parse 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 Parse(registryURL string) (*url.URL, error) { | |
| // Check if registryURL has a scheme, otherwise add `//` as scheme. | |
| if !strings.Contains(registryURL, "://") && !strings.HasPrefix(registryURL, "//") { | |
| registryURL = "//" + registryURL | |
| } | |
| u, err := url.Parse(registryURL) | |
| 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 | |
| } |
...but others, such as the pass helper, just take whatever they are given and use it without any normalization:
docker-credential-helpers/pass/pass.go
Lines 193 to 197 in b7a754b
| // encodeServerURL returns the serverURL in base64-URL encoding to use | |
| // as directory-name in pass storage. | |
| func encodeServerURL(serverURL string) string { | |
| return base64.URLEncoding.EncodeToString([]byte(serverURL)) | |
| } |
...so in order to interop with other implementations I presumably need to exactly match the way they would populate this field.
My main hope in opening this issue is to confirm that I've correctly understood that the code example for Store and the documentation in the main README of this repository are both incorrect and that the actual credential helper protocol (as expected by existing real implementations) is to send just a plain hostname wherever a "server URL" is expected.
Is that correct, or is the situation more subtle than that? 🤔
Given that many implementations just take "ServerURL" verbatim and use it as a key, I assume the caller is also responsible for normalizing the hostname for case-insensitive comparison, such as by using idna.Profile.ToUnicode on one of the four IDNA profiles. Is there any established convention for exactly what normalization is to be used here?
Thanks!