When a endpoint is configured for http and has a tls configuration, always try to the tls connection and fallback to http when the tls connections fails from receiving an http response. This fixes an issue with default localhost endpoints which get defaulted to http with insecure tls also configured but are using tls. Signed-off-by: Derek McGowan <derek@mcg.dev>
895 lines
24 KiB
Go
895 lines
24 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package docker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/remotes"
|
|
"github.com/containerd/containerd/remotes/docker/auth"
|
|
digest "github.com/opencontainers/go-digest"
|
|
specs "github.com/opencontainers/image-spec/specs-go"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
func TestHTTPResolver(t *testing.T) {
|
|
s := func(h http.Handler) (string, ResolverOptions, func()) {
|
|
s := httptest.NewServer(h)
|
|
|
|
options := ResolverOptions{}
|
|
base := s.URL[7:] // strip "http://"
|
|
return base, options, s.Close
|
|
}
|
|
runBasicTest(t, "testname", s)
|
|
}
|
|
|
|
func TestHTTPSResolver(t *testing.T) {
|
|
runBasicTest(t, "testname", tlsServer)
|
|
}
|
|
|
|
func TestResolverOptionsRace(t *testing.T) {
|
|
header := http.Header{}
|
|
header.Set("X-Test", "test")
|
|
|
|
s := func(h http.Handler) (string, ResolverOptions, func()) {
|
|
s := httptest.NewServer(h)
|
|
|
|
options := ResolverOptions{
|
|
Headers: header,
|
|
}
|
|
base := s.URL[7:] // strip "http://"
|
|
return base, options, s.Close
|
|
}
|
|
|
|
for i := 0; i < 5; i++ {
|
|
t.Run(fmt.Sprintf("test ResolverOptions race %d", i), func(t *testing.T) {
|
|
// parallel sub tests so the race condition (if not handled) can be caught
|
|
// by race detector
|
|
t.Parallel()
|
|
runBasicTest(t, "testname", s)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBasicResolver(t *testing.T) {
|
|
basicAuth := func(h http.Handler) (string, ResolverOptions, func()) {
|
|
// Wrap with basic auth
|
|
wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
username, password, ok := r.BasicAuth()
|
|
if !ok || username != "user1" || password != "password1" {
|
|
rw.Header().Set("WWW-Authenticate", "Basic realm=localhost")
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
h.ServeHTTP(rw, r)
|
|
})
|
|
|
|
base, options, close := tlsServer(wrapped)
|
|
authorizer := NewDockerAuthorizer(
|
|
WithAuthClient(options.Client),
|
|
WithAuthCreds(func(host string) (string, string, error) {
|
|
return "user1", "password1", nil
|
|
}),
|
|
)
|
|
options.Hosts = ConfigureDefaultRegistries(
|
|
WithClient(options.Client),
|
|
WithAuthorizer(authorizer),
|
|
)
|
|
return base, options, close
|
|
}
|
|
runBasicTest(t, "testname", basicAuth)
|
|
}
|
|
|
|
func TestAnonymousTokenResolver(t *testing.T) {
|
|
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
rw.WriteHeader(http.StatusOK)
|
|
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
|
})
|
|
|
|
runBasicTest(t, "testname", withTokenServer(th, nil))
|
|
}
|
|
|
|
func TestBasicAuthTokenResolver(t *testing.T) {
|
|
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
rw.WriteHeader(http.StatusOK)
|
|
username, password, ok := r.BasicAuth()
|
|
if !ok || username != "user1" || password != "password1" {
|
|
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
|
} else {
|
|
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
|
}
|
|
})
|
|
creds := func(string) (string, string, error) {
|
|
return "user1", "password1", nil
|
|
}
|
|
|
|
runBasicTest(t, "testname", withTokenServer(th, creds))
|
|
}
|
|
|
|
func TestRefreshTokenResolver(t *testing.T) {
|
|
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
r.ParseForm()
|
|
if r.PostForm.Get("grant_type") != "refresh_token" || r.PostForm.Get("refresh_token") != "somerefreshtoken" {
|
|
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
|
} else {
|
|
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
|
}
|
|
})
|
|
creds := func(string) (string, string, error) {
|
|
return "", "somerefreshtoken", nil
|
|
}
|
|
|
|
runBasicTest(t, "testname", withTokenServer(th, creds))
|
|
}
|
|
|
|
func TestFetchRefreshToken(t *testing.T) {
|
|
f := func(t *testing.T, disablePOST bool) {
|
|
name := "testname"
|
|
if disablePOST {
|
|
name += "-disable-post"
|
|
}
|
|
var fetchedRefreshToken string
|
|
onFetchRefreshToken := func(ctx context.Context, refreshToken string, req *http.Request) {
|
|
fetchedRefreshToken = refreshToken
|
|
}
|
|
srv := newRefreshTokenServer(t, name, disablePOST, onFetchRefreshToken)
|
|
runBasicTest(t, name, srv.BasicTestFunc())
|
|
if fetchedRefreshToken != srv.RefreshToken {
|
|
t.Errorf("unexpected refresh token: got %q", fetchedRefreshToken)
|
|
}
|
|
}
|
|
|
|
t.Run("POST", func(t *testing.T) {
|
|
f(t, false)
|
|
})
|
|
t.Run("GET", func(t *testing.T) {
|
|
f(t, true)
|
|
})
|
|
}
|
|
|
|
func TestPostBasicAuthTokenResolver(t *testing.T) {
|
|
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
r.ParseForm()
|
|
if r.PostForm.Get("grant_type") != "password" || r.PostForm.Get("username") != "user1" || r.PostForm.Get("password") != "password1" {
|
|
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
|
} else {
|
|
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
|
}
|
|
})
|
|
creds := func(string) (string, string, error) {
|
|
return "user1", "password1", nil
|
|
}
|
|
|
|
runBasicTest(t, "testname", withTokenServer(th, creds))
|
|
}
|
|
|
|
func TestBadTokenResolver(t *testing.T) {
|
|
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
rw.WriteHeader(http.StatusOK)
|
|
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
|
})
|
|
creds := func(string) (string, string, error) {
|
|
return "", "somerefreshtoken", nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
h := newContent(ocispec.MediaTypeImageManifest, []byte("not anything parse-able"))
|
|
|
|
base, ro, close := withTokenServer(th, creds)(logHandler{t, h})
|
|
defer close()
|
|
|
|
resolver := NewResolver(ro)
|
|
image := fmt.Sprintf("%s/doesntmatter:sometatg", base)
|
|
|
|
_, _, err := resolver.Resolve(ctx, image)
|
|
if err == nil {
|
|
t.Fatal("Expected error getting token with inssufficient scope")
|
|
}
|
|
if !errors.Is(err, ErrInvalidAuthorization) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestHostFailureFallbackResolver(t *testing.T) {
|
|
sf := func(h http.Handler) (string, ResolverOptions, func()) {
|
|
s := httptest.NewServer(h)
|
|
base := s.URL[7:] // strip "http://"
|
|
|
|
options := ResolverOptions{}
|
|
createHost := func(host string) RegistryHost {
|
|
return RegistryHost{
|
|
Client: &http.Client{
|
|
// Set the timeout so we timeout waiting for the non-responsive HTTP server
|
|
Timeout: 500 * time.Millisecond,
|
|
},
|
|
Host: host,
|
|
Scheme: "http",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
|
}
|
|
}
|
|
|
|
// Create an unstarted HTTP server. We use this to generate a random port.
|
|
notRunning := httptest.NewUnstartedServer(nil)
|
|
notRunningBase := notRunning.Listener.Addr().String()
|
|
|
|
// Override hosts with two hosts
|
|
options.Hosts = func(host string) ([]RegistryHost, error) {
|
|
return []RegistryHost{
|
|
createHost(notRunningBase), // This host IS running, but with a non-responsive HTTP server
|
|
createHost(base), // This host IS running
|
|
}, nil
|
|
}
|
|
|
|
return base, options, s.Close
|
|
}
|
|
|
|
runBasicTest(t, "testname", sf)
|
|
}
|
|
|
|
func TestHostTLSFailureFallbackResolver(t *testing.T) {
|
|
sf := func(h http.Handler) (string, ResolverOptions, func()) {
|
|
// Start up two servers
|
|
server := httptest.NewServer(h)
|
|
httpBase := server.URL[7:] // strip "http://"
|
|
|
|
tlsServer := httptest.NewUnstartedServer(h)
|
|
tlsServer.StartTLS()
|
|
httpsBase := tlsServer.URL[8:] // strip "https://"
|
|
|
|
capool := x509.NewCertPool()
|
|
cert, _ := x509.ParseCertificate(tlsServer.TLS.Certificates[0].Certificate[0])
|
|
capool.AddCert(cert)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: capool,
|
|
},
|
|
},
|
|
}
|
|
|
|
options := ResolverOptions{}
|
|
createHost := func(host string) RegistryHost {
|
|
return RegistryHost{
|
|
Client: client,
|
|
Host: host,
|
|
Scheme: "https",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
|
}
|
|
}
|
|
|
|
// Override hosts with two hosts
|
|
options.Hosts = func(host string) ([]RegistryHost, error) {
|
|
return []RegistryHost{
|
|
createHost(httpBase), // This host is serving plain HTTP
|
|
createHost(httpsBase), // This host is serving TLS
|
|
}, nil
|
|
}
|
|
|
|
return httpBase, options, func() {
|
|
server.Close()
|
|
tlsServer.Close()
|
|
}
|
|
}
|
|
|
|
runBasicTest(t, "testname", sf)
|
|
}
|
|
|
|
func TestHTTPFallbackResolver(t *testing.T) {
|
|
sf := func(h http.Handler) (string, ResolverOptions, func()) {
|
|
s := httptest.NewServer(h)
|
|
u, err := url.Parse(s.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: HTTPFallback{http.DefaultTransport},
|
|
}
|
|
options := ResolverOptions{
|
|
Hosts: func(host string) ([]RegistryHost, error) {
|
|
return []RegistryHost{
|
|
{
|
|
Client: client,
|
|
Host: u.Host,
|
|
Scheme: "https",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
return u.Host, options, s.Close
|
|
}
|
|
|
|
runBasicTest(t, "testname", sf)
|
|
}
|
|
|
|
func TestResolveProxy(t *testing.T) {
|
|
var (
|
|
ctx = context.Background()
|
|
tag = "latest"
|
|
r = http.NewServeMux()
|
|
name = "testname"
|
|
ns = "upstream.example.com"
|
|
)
|
|
|
|
m := newManifest(
|
|
newContent(ocispec.MediaTypeImageConfig, []byte("1")),
|
|
newContent(ocispec.MediaTypeImageLayerGzip, []byte("2")),
|
|
)
|
|
mc := newContent(ocispec.MediaTypeImageManifest, m.OCIManifest())
|
|
m.RegisterHandler(r, name)
|
|
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc)
|
|
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc)
|
|
|
|
nr := namespaceRouter{
|
|
"upstream.example.com": r,
|
|
}
|
|
|
|
base, ro, close := tlsServer(logHandler{t, nr})
|
|
defer close()
|
|
|
|
ro.Hosts = func(host string) ([]RegistryHost, error) {
|
|
return []RegistryHost{{
|
|
Client: ro.Client,
|
|
Host: base,
|
|
Scheme: "https",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve,
|
|
}}, nil
|
|
}
|
|
|
|
resolver := NewResolver(ro)
|
|
image := fmt.Sprintf("%s/%s:%s", ns, name, tag)
|
|
|
|
_, d, err := resolver.Resolve(ctx, image)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := resolver.Fetcher(ctx, image)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
refs, err := testocimanifest(ctx, f, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(refs) != 2 {
|
|
t.Fatalf("Unexpected number of references: %d, expected 2", len(refs))
|
|
}
|
|
|
|
for _, ref := range refs {
|
|
if err := testFetch(ctx, f, ref); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveProxyFallback(t *testing.T) {
|
|
var (
|
|
ctx = context.Background()
|
|
tag = "latest"
|
|
r = http.NewServeMux()
|
|
name = "testname"
|
|
)
|
|
|
|
m := newManifest(
|
|
newContent(ocispec.MediaTypeImageConfig, []byte("1")),
|
|
newContent(ocispec.MediaTypeImageLayerGzip, []byte("2")),
|
|
)
|
|
mc := newContent(ocispec.MediaTypeImageManifest, m.OCIManifest())
|
|
m.RegisterHandler(r, name)
|
|
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc)
|
|
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc)
|
|
|
|
nr := namespaceRouter{
|
|
"": r,
|
|
}
|
|
s := httptest.NewServer(logHandler{t, nr})
|
|
defer s.Close()
|
|
|
|
base := s.URL[7:] // strip "http://"
|
|
|
|
ro := ResolverOptions{
|
|
Hosts: func(host string) ([]RegistryHost, error) {
|
|
return []RegistryHost{
|
|
{
|
|
Host: flipLocalhost(host),
|
|
Scheme: "http",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve,
|
|
},
|
|
{
|
|
Host: host,
|
|
Scheme: "http",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
resolver := NewResolver(ro)
|
|
image := fmt.Sprintf("%s/%s:%s", base, name, tag)
|
|
|
|
_, d, err := resolver.Resolve(ctx, image)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := resolver.Fetcher(ctx, image)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
refs, err := testocimanifest(ctx, f, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(refs) != 2 {
|
|
t.Fatalf("Unexpected number of references: %d, expected 2", len(refs))
|
|
}
|
|
|
|
for _, ref := range refs {
|
|
if err := testFetch(ctx, f, ref); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func flipLocalhost(host string) string {
|
|
if strings.HasPrefix(host, "127.0.0.1") {
|
|
return "localhost" + host[9:]
|
|
|
|
} else if strings.HasPrefix(host, "localhost") {
|
|
return "127.0.0.1" + host[9:]
|
|
}
|
|
return host
|
|
}
|
|
|
|
func withTokenServer(th http.Handler, creds func(string) (string, string, error)) func(h http.Handler) (string, ResolverOptions, func()) {
|
|
return func(h http.Handler) (string, ResolverOptions, func()) {
|
|
s := httptest.NewUnstartedServer(th)
|
|
s.StartTLS()
|
|
|
|
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
|
tokenBase := s.URL + "/token"
|
|
|
|
// Wrap with token auth
|
|
wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
auth := strings.ToLower(r.Header.Get("Authorization"))
|
|
if auth != "bearer perfectlyvalidopaquetoken" {
|
|
authHeader := fmt.Sprintf("Bearer realm=%q,service=registry,scope=\"repository:testname:pull,pull\"", tokenBase)
|
|
if strings.HasPrefix(auth, "bearer ") {
|
|
authHeader = authHeader + ",error=" + auth[7:]
|
|
}
|
|
rw.Header().Set("WWW-Authenticate", authHeader)
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
h.ServeHTTP(rw, r)
|
|
})
|
|
|
|
base, options, close := tlsServer(wrapped)
|
|
options.Hosts = ConfigureDefaultRegistries(
|
|
WithClient(options.Client),
|
|
WithAuthorizer(NewDockerAuthorizer(
|
|
WithAuthClient(options.Client),
|
|
WithAuthCreds(creds),
|
|
)),
|
|
)
|
|
options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
|
|
return base, options, func() {
|
|
s.Close()
|
|
close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func tlsServer(h http.Handler) (string, ResolverOptions, func()) {
|
|
s := httptest.NewUnstartedServer(h)
|
|
s.StartTLS()
|
|
|
|
capool := x509.NewCertPool()
|
|
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
|
capool.AddCert(cert)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: capool,
|
|
},
|
|
},
|
|
}
|
|
options := ResolverOptions{
|
|
Hosts: ConfigureDefaultRegistries(WithClient(client)),
|
|
// Set deprecated field for tests to use for configuration
|
|
Client: client,
|
|
}
|
|
base := s.URL[8:] // strip "https://"
|
|
return base, options, s.Close
|
|
}
|
|
|
|
type logHandler struct {
|
|
t *testing.T
|
|
handler http.Handler
|
|
}
|
|
|
|
func (h logHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
h.handler.ServeHTTP(rw, r)
|
|
}
|
|
|
|
type namespaceRouter map[string]http.Handler
|
|
|
|
func (nr namespaceRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
h, ok := nr[r.URL.Query().Get("ns")]
|
|
if !ok {
|
|
rw.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
h.ServeHTTP(rw, r)
|
|
}
|
|
|
|
func runBasicTest(t *testing.T, name string, sf func(h http.Handler) (string, ResolverOptions, func())) {
|
|
var (
|
|
ctx = context.Background()
|
|
tag = "latest"
|
|
r = http.NewServeMux()
|
|
)
|
|
|
|
m := newManifest(
|
|
newContent(ocispec.MediaTypeImageConfig, []byte("1")),
|
|
newContent(ocispec.MediaTypeImageLayerGzip, []byte("2")),
|
|
)
|
|
mc := newContent(ocispec.MediaTypeImageManifest, m.OCIManifest())
|
|
m.RegisterHandler(r, name)
|
|
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc)
|
|
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc)
|
|
|
|
base, ro, close := sf(logHandler{t, r})
|
|
defer close()
|
|
|
|
resolver := NewResolver(ro)
|
|
image := fmt.Sprintf("%s/%s:%s", base, name, tag)
|
|
|
|
_, d, err := resolver.Resolve(ctx, image)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := resolver.Fetcher(ctx, image)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
refs, err := testocimanifest(ctx, f, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(refs) != 2 {
|
|
t.Fatalf("Unexpected number of references: %d, expected 2", len(refs))
|
|
}
|
|
|
|
for _, ref := range refs {
|
|
if err := testFetch(ctx, f, ref); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testFetch(ctx context.Context, f remotes.Fetcher, desc ocispec.Descriptor) error {
|
|
r, err := f.Fetch(ctx, desc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dgstr := desc.Digest.Algorithm().Digester()
|
|
io.Copy(dgstr.Hash(), r)
|
|
if dgstr.Digest() != desc.Digest {
|
|
return fmt.Errorf("content mismatch: %s != %s", dgstr.Digest(), desc.Digest)
|
|
}
|
|
|
|
fByDigest, ok := f.(remotes.FetcherByDigest)
|
|
if !ok {
|
|
return fmt.Errorf("fetcher %T does not implement FetcherByDigest", f)
|
|
}
|
|
r2, desc2, err := fByDigest.FetchByDigest(ctx, desc.Digest)
|
|
if err != nil {
|
|
return fmt.Errorf("FetcherByDigest: faild to fetch %v: %w", desc.Digest, err)
|
|
}
|
|
if desc2.Size != desc.Size {
|
|
r2b, err := io.ReadAll(r2)
|
|
if err != nil {
|
|
return fmt.Errorf("FetcherByDigest: size mismatch: %d != %d (content: %v)", desc2.Size, desc.Size, err)
|
|
}
|
|
return fmt.Errorf("FetcherByDigest: size mismatch: %d != %d (content: %q)", desc2.Size, desc.Size, string(r2b))
|
|
}
|
|
dgstr2 := desc.Digest.Algorithm().Digester()
|
|
if _, err = io.Copy(dgstr2.Hash(), r2); err != nil {
|
|
return fmt.Errorf("FetcherByDigest: faild to copy: %w", err)
|
|
}
|
|
if dgstr2.Digest() != desc.Digest {
|
|
return fmt.Errorf("FetcherByDigest: content mismatch: %s != %s", dgstr2.Digest(), desc.Digest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func testocimanifest(ctx context.Context, f remotes.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
|
r, err := f.Fetch(ctx, desc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch %s: %w", desc.Digest, err)
|
|
}
|
|
p, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dgst := desc.Digest.Algorithm().FromBytes(p); dgst != desc.Digest {
|
|
return nil, fmt.Errorf("digest mismatch: %s != %s", dgst, desc.Digest)
|
|
}
|
|
|
|
var manifest ocispec.Manifest
|
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var descs []ocispec.Descriptor
|
|
|
|
descs = append(descs, manifest.Config)
|
|
descs = append(descs, manifest.Layers...)
|
|
|
|
return descs, nil
|
|
}
|
|
|
|
type testContent struct {
|
|
mediaType string
|
|
content []byte
|
|
}
|
|
|
|
func newContent(mediaType string, b []byte) testContent {
|
|
return testContent{
|
|
mediaType: mediaType,
|
|
content: b,
|
|
}
|
|
}
|
|
|
|
func (tc testContent) Descriptor() ocispec.Descriptor {
|
|
return ocispec.Descriptor{
|
|
MediaType: tc.mediaType,
|
|
Digest: digest.FromBytes(tc.content),
|
|
Size: int64(len(tc.content)),
|
|
}
|
|
}
|
|
|
|
func (tc testContent) Digest() digest.Digest {
|
|
return digest.FromBytes(tc.content)
|
|
}
|
|
|
|
func (tc testContent) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("Content-Type", tc.mediaType)
|
|
w.Header().Add("Content-Length", strconv.Itoa(len(tc.content)))
|
|
w.Header().Add("Docker-Content-Digest", tc.Digest().String())
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(tc.content)
|
|
}
|
|
|
|
type testManifest struct {
|
|
config testContent
|
|
references []testContent
|
|
}
|
|
|
|
func newManifest(config testContent, refs ...testContent) testManifest {
|
|
return testManifest{
|
|
config: config,
|
|
references: refs,
|
|
}
|
|
}
|
|
|
|
func (m testManifest) OCIManifest() []byte {
|
|
manifest := ocispec.Manifest{
|
|
Versioned: specs.Versioned{
|
|
SchemaVersion: 1,
|
|
},
|
|
Config: m.config.Descriptor(),
|
|
Layers: make([]ocispec.Descriptor, len(m.references)),
|
|
}
|
|
for i, c := range m.references {
|
|
manifest.Layers[i] = c.Descriptor()
|
|
}
|
|
b, _ := json.Marshal(manifest)
|
|
return b
|
|
}
|
|
|
|
func (m testManifest) RegisterHandler(r *http.ServeMux, name string) {
|
|
for _, c := range append(m.references, m.config) {
|
|
r.Handle(fmt.Sprintf("/v2/%s/blobs/%s", name, c.Digest()), c)
|
|
}
|
|
}
|
|
|
|
func newRefreshTokenServer(t testing.TB, name string, disablePOST bool, onFetchRefreshToken OnFetchRefreshToken) *refreshTokenServer {
|
|
return &refreshTokenServer{
|
|
T: t,
|
|
Name: name,
|
|
DisablePOST: disablePOST,
|
|
OnFetchRefreshToken: onFetchRefreshToken,
|
|
AccessToken: "testAccessToken-" + name,
|
|
RefreshToken: "testRefreshToken-" + name,
|
|
Username: "testUser-" + name,
|
|
Password: "testPassword-" + name,
|
|
}
|
|
}
|
|
|
|
type refreshTokenServer struct {
|
|
T testing.TB
|
|
Name string
|
|
DisablePOST bool
|
|
OnFetchRefreshToken OnFetchRefreshToken
|
|
AccessToken string
|
|
RefreshToken string
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
func (srv *refreshTokenServer) isValidAuthorizationHeader(s string) bool {
|
|
fields := strings.Fields(s)
|
|
return len(fields) == 2 && strings.ToLower(fields[0]) == "bearer" && (fields[1] == srv.RefreshToken || fields[1] == srv.AccessToken)
|
|
}
|
|
|
|
func (srv *refreshTokenServer) BasicTestFunc() func(h http.Handler) (string, ResolverOptions, func()) {
|
|
t := srv.T
|
|
return func(h http.Handler) (string, ResolverOptions, func()) {
|
|
wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/token" {
|
|
if !srv.isValidAuthorizationHeader(r.Header.Get("Authorization")) {
|
|
realm := fmt.Sprintf("https://%s/token", r.Host)
|
|
wwwAuthenticateHeader := fmt.Sprintf("Bearer realm=%q,service=registry,scope=\"repository:%s:pull\"", realm, srv.Name)
|
|
rw.Header().Set("WWW-Authenticate", wwwAuthenticateHeader)
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
h.ServeHTTP(rw, r)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet: // https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
|
|
u, p, ok := r.BasicAuth()
|
|
if !ok || u != srv.Username || p != srv.Password {
|
|
rw.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
var resp auth.FetchTokenResponse
|
|
resp.Token = srv.AccessToken
|
|
resp.AccessToken = srv.AccessToken // alias of Token
|
|
query := r.URL.Query()
|
|
switch query.Get("offline_token") {
|
|
case "true":
|
|
resp.RefreshToken = srv.RefreshToken
|
|
case "false", "":
|
|
default:
|
|
rw.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
b, err := json.Marshal(resp)
|
|
if err != nil {
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
t.Logf("GET mode: returning JSON %q, for query %+v", string(b), query)
|
|
rw.Write(b)
|
|
case http.MethodPost: // https://docs.docker.com/registry/spec/auth/oauth/#getting-a-token
|
|
if srv.DisablePOST {
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
r.ParseForm()
|
|
pf := r.PostForm
|
|
if pf.Get("grant_type") != "password" {
|
|
rw.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if pf.Get("username") != srv.Username || pf.Get("password") != srv.Password {
|
|
rw.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
var resp auth.OAuthTokenResponse
|
|
resp.AccessToken = srv.AccessToken
|
|
switch pf.Get("access_type") {
|
|
case "offline":
|
|
resp.RefreshToken = srv.RefreshToken
|
|
case "online", "":
|
|
default:
|
|
rw.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
b, err := json.Marshal(resp)
|
|
if err != nil {
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
t.Logf("POST mode: returning JSON %q, for form %+v", string(b), pf)
|
|
rw.Write(b)
|
|
default:
|
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
})
|
|
|
|
base, options, close := tlsServer(wrapped)
|
|
authorizer := NewDockerAuthorizer(
|
|
WithAuthClient(options.Client),
|
|
WithAuthCreds(func(string) (string, string, error) {
|
|
return srv.Username, srv.Password, nil
|
|
}),
|
|
WithFetchRefreshToken(srv.OnFetchRefreshToken),
|
|
)
|
|
options.Hosts = ConfigureDefaultRegistries(
|
|
WithClient(options.Client),
|
|
WithAuthorizer(authorizer),
|
|
)
|
|
return base, options, close
|
|
}
|
|
}
|