Merge pull request #783 from dmcgowan/resolver-authentication

Pull from all registries
This commit is contained in:
Michael Crosby 2017-05-03 14:25:52 -07:00 committed by GitHub
commit 0fe0d8feeb
14 changed files with 1113 additions and 81 deletions

95
cmd/dist/common.go vendored
View File

@ -1,9 +1,14 @@
package main
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"path/filepath"
"strings"
"time"
imagesapi "github.com/containerd/containerd/api/services/images"
@ -12,10 +17,31 @@ import (
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
imagesservice "github.com/containerd/containerd/services/images"
"github.com/crosbymichael/console"
"github.com/pkg/errors"
"github.com/urfave/cli"
"google.golang.org/grpc"
)
var registryFlags = []cli.Flag{
cli.BoolFlag{
Name: "skip-verify,k",
Usage: "Skip SSL certificate validation",
},
cli.BoolFlag{
Name: "plain-http",
Usage: "Allow connections using plain HTTP",
},
cli.StringFlag{
Name: "user,u",
Usage: "user[:password] Registry user and password",
},
cli.StringFlag{
Name: "refresh",
Usage: "Refresh token for authorization server",
},
}
func resolveContentStore(context *cli.Context) (*content.Store, error) {
root := filepath.Join(context.GlobalString("root"), "content")
if !filepath.IsAbs(root) {
@ -50,6 +76,71 @@ func connectGRPC(context *cli.Context) (*grpc.ClientConn, error) {
}
// getResolver prepares the resolver from the environment and options.
func getResolver(ctx context.Context) (remotes.Resolver, error) {
return docker.NewResolver(), nil
func getResolver(ctx context.Context, clicontext *cli.Context) (remotes.Resolver, error) {
username := clicontext.String("user")
var secret string
if i := strings.IndexByte(username, ':'); i > 0 {
secret = username[i+1:]
username = username[0:i]
}
options := docker.ResolverOptions{
PlainHTTP: clicontext.Bool("plain-http"),
}
if username != "" {
if secret == "" {
fmt.Printf("Password: ")
var err error
secret, err = passwordPrompt()
if err != nil {
return nil, err
}
fmt.Print("\n")
}
} else if rt := clicontext.String("refresh"); rt != "" {
secret = rt
}
options.Credentials = func(host string) (string, string, error) {
// Only one host
return username, secret, nil
}
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: clicontext.Bool("insecure"),
},
ExpectContinueTimeout: 5 * time.Second,
}
options.Client = &http.Client{
Transport: tr,
}
return docker.NewResolver(options), nil
}
func passwordPrompt() (string, error) {
c := console.Current()
defer c.Reset()
if err := c.DisableEcho(); err != nil {
return "", errors.Wrap(err, "failed to disable echo")
}
line, _, err := bufio.NewReader(c).ReadLine()
if err != nil {
return "", errors.Wrap(err, "failed to read line")
}
return string(line), nil
}

4
cmd/dist/fetch.go vendored
View File

@ -39,7 +39,7 @@ not use this implementation as a guide. The end goal should be having metadata,
content and snapshots ready for a direct use via the 'ctr run'.
Most of this is experimental and there are few leaps to make this work.`,
Flags: []cli.Flag{},
Flags: registryFlags,
Action: func(clicontext *cli.Context) error {
var (
ctx = background
@ -51,7 +51,7 @@ Most of this is experimental and there are few leaps to make this work.`,
return err
}
resolver, err := getResolver(ctx)
resolver, err := getResolver(ctx, clicontext)
if err != nil {
return err
}

View File

@ -18,13 +18,13 @@ var fetchObjectCommand = cli.Command{
Usage: "retrieve objects from a remote",
ArgsUsage: "[flags] <remote> <object> [<hint>, ...]",
Description: `Fetch objects by identifier from a remote.`,
Flags: []cli.Flag{
Flags: append([]cli.Flag{
cli.DurationFlag{
Name: "timeout",
Usage: "total timeout for fetch",
EnvVar: "CONTAINERD_FETCH_TIMEOUT",
},
},
}, registryFlags...),
Action: func(context *cli.Context) error {
var (
ctx = background
@ -38,7 +38,7 @@ var fetchObjectCommand = cli.Command{
defer cancel()
}
resolver, err := getResolver(ctx)
resolver, err := getResolver(ctx, context)
if err != nil {
return err
}

5
cmd/dist/pull.go vendored
View File

@ -35,7 +35,7 @@ command. As part of this process, we do the following:
2. Prepare the snapshot filesystem with the pulled resources.
3. Register metadata for the image.
`,
Flags: []cli.Flag{},
Flags: registryFlags,
Action: func(clicontext *cli.Context) error {
var (
ctx = background
@ -52,7 +52,7 @@ command. As part of this process, we do the following:
return err
}
resolver, err := getResolver(ctx)
resolver, err := getResolver(ctx, clicontext)
if err != nil {
return err
}
@ -75,6 +75,7 @@ command. As part of this process, we do the following:
ongoing.add(ref)
name, desc, fetcher, err := resolver.Resolve(ctx, ref)
if err != nil {
log.G(ctx).WithError(err).Error("failed to resolve")
return err
}
log.G(ctx).WithField("image", name).Debug("fetching")

182
remotes/docker/auth.go Normal file
View File

@ -0,0 +1,182 @@
package docker
import (
"net/http"
"sort"
"strings"
)
type authenticationScheme byte
const (
basicAuth authenticationScheme = 1 << iota // Defined in RFC 7617
digestAuth // Defined in RFC 7616
bearerAuth // Defined in RFC 6750
)
// challenge carries information from a WWW-Authenticate response header.
// See RFC 2617.
type challenge struct {
// scheme is the auth-scheme according to RFC 2617
scheme authenticationScheme
// parameters are the auth-params according to RFC 2617
parameters map[string]string
}
type byScheme []challenge
func (bs byScheme) Len() int { return len(bs) }
func (bs byScheme) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
// Sort in priority order: token > digest > basic
func (bs byScheme) Less(i, j int) bool { return bs[i].scheme > bs[j].scheme }
// Octet types from RFC 2616.
type octetType byte
var octetTypes [256]octetType
const (
isToken octetType = 1 << iota
isSpace
)
func init() {
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpace
}
if isChar && !isCtl && !isSeparator {
t |= isToken
}
octetTypes[c] = t
}
}
func parseAuthHeader(header http.Header) []challenge {
challenges := []challenge{}
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
v, p := parseValueAndParams(h)
var s authenticationScheme
switch v {
case "basic":
s = basicAuth
case "digest":
s = digestAuth
case "bearer":
s = bearerAuth
default:
continue
}
challenges = append(challenges, challenge{scheme: s, parameters: p})
}
sort.Stable(byScheme(challenges))
return challenges
}
func parseValueAndParams(header string) (value string, params map[string]string) {
params = make(map[string]string)
value, s := expectToken(header)
if value == "" {
return
}
value = strings.ToLower(value)
for {
var pkey string
pkey, s = expectToken(skipSpace(s))
if pkey == "" {
return
}
if !strings.HasPrefix(s, "=") {
return
}
var pvalue string
pvalue, s = expectTokenOrQuoted(s[1:])
if pvalue == "" {
return
}
pkey = strings.ToLower(pkey)
params[pkey] = pvalue
s = skipSpace(s)
if !strings.HasPrefix(s, ",") {
return
}
s = s[1:]
}
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpace == 0 {
break
}
}
return s[i:]
}
func expectToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isToken == 0 {
break
}
}
return s[:i], s[i:]
}
func expectTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return expectToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j++
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j++
}
}
return "", ""
}
}
return "", ""
}

View File

@ -7,10 +7,12 @@ import (
"io"
"io/ioutil"
"net/http"
"net/textproto"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/containerd/containerd/images"
@ -23,15 +25,43 @@ import (
"golang.org/x/net/context/ctxhttp"
)
// NOTE(stevvooe): Most of the code below this point is prototype code to
// demonstrate a very simplified docker.io fetcher. We have a lot of hard coded
// values but we leave many of the details down to the fetcher, creating a lot
// of room for ways to fetch content.
var (
// ErrNoToken is returned if a request is successful but the body does not
// contain an authorization token.
ErrNoToken = errors.New("authorization server did not include a token in the response")
type dockerResolver struct{}
// ErrInvalidAuthorization is used when credentials are passed to a server but
// those credentials are rejected.
ErrInvalidAuthorization = errors.New("authorization failed")
)
func NewResolver() remotes.Resolver {
return &dockerResolver{}
type dockerResolver struct {
credentials func(string) (string, string, error)
plainHTTP bool
client *http.Client
}
// ResolverOptions are used to configured a new Docker register resolver
type ResolverOptions struct {
// Credentials provides username and secret given a host.
// If username is empty but a secret is given, that secret
// is interpretted as a long lived token.
Credentials func(string) (string, string, error)
// PlainHTTP specifies to use plain http and not https
PlainHTTP bool
// Client is the http client to used when making registry requests
Client *http.Client
}
// NewResolver returns a new resolver to a Docker registry
func NewResolver(options ResolverOptions) remotes.Resolver {
return &dockerResolver{
credentials: options.Credentials,
plainHTTP: options.PlainHTTP,
client: options.Client,
}
}
var _ remotes.Resolver = &dockerResolver{}
@ -43,31 +73,38 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
}
var (
base url.URL
token string
base url.URL
username, secret string
)
switch refspec.Hostname() {
case "docker.io":
base.Scheme = "https"
host := refspec.Hostname()
base.Scheme = "https"
if host == "docker.io" {
base.Host = "registry-1.docker.io"
prefix := strings.TrimPrefix(refspec.Locator, "docker.io/")
base.Path = path.Join("/v2", prefix)
token, err = getToken(ctx, "repository:"+prefix+":pull")
} else {
base.Host = host
if r.plainHTTP || strings.HasPrefix(host, "localhost:") {
base.Scheme = "http"
}
}
if r.credentials != nil {
username, secret, err = r.credentials(base.Host)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
case "localhost:5000":
base.Scheme = "http"
base.Host = "localhost:5000"
base.Path = path.Join("/v2", strings.TrimPrefix(refspec.Locator, "localhost:5000/"))
default:
return "", ocispec.Descriptor{}, nil, errors.Errorf("unsupported locator: %q", refspec.Locator)
}
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
base.Path = path.Join("/v2", prefix)
fetcher := &dockerFetcher{
base: base,
token: token,
base: base,
client: r.client,
username: username,
secret: secret,
}
var (
@ -125,16 +162,13 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
if dgstHeader != "" {
if err := dgstHeader.Validate(); err != nil {
if err == nil {
return "", ocispec.Descriptor{}, nil, errors.Errorf("%q in header not a valid digest", dgstHeader)
}
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
}
dgst = dgstHeader
}
if dgst == "" {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "could not resolve digest for %v", ref)
return "", ocispec.Descriptor{}, nil, errors.Errorf("could not resolve digest for %v", ref)
}
var (
@ -143,8 +177,12 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
)
size, err = strconv.ParseInt(sizeHeader, 10, 64)
if err != nil || size < 0 {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid size", sizeHeader)
if err != nil {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "invalid size header: %q", sizeHeader)
}
if size < 0 {
return "", ocispec.Descriptor{}, nil, errors.Errorf("%q in header not a valid size", sizeHeader)
}
desc := ocispec.Descriptor{
@ -163,6 +201,11 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
type dockerFetcher struct {
base url.URL
token string
client *http.Client
useBasic bool
username string
secret string
}
func (r *dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
@ -213,13 +256,14 @@ func (r *dockerFetcher) url(ps ...string) string {
}
func (r *dockerFetcher) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
return r.doRequestWithRetries(ctx, req, nil)
}
func (r *dockerFetcher) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
log.G(ctx).WithField("request.headers", req.Header).Debug("fetch content")
if r.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
}
resp, err := ctxhttp.Do(ctx, http.DefaultClient, req)
r.authorize(req)
resp, err := ctxhttp.Do(ctx, r.client, req)
if err != nil {
return nil, err
}
@ -228,50 +272,119 @@ func (r *dockerFetcher) doRequest(ctx context.Context, req *http.Request) (*http
"response.headers": resp.Header,
}).Debug("fetch response received")
responses = append(responses, resp)
req, err = r.retryRequest(ctx, req, responses)
if err != nil {
return nil, err
}
if req != nil {
return r.doRequestWithRetries(ctx, req, responses)
}
return resp, err
}
func getToken(ctx context.Context, scopes ...string) (string, error) {
var (
u = url.URL{
Scheme: "https",
Host: "auth.docker.io",
Path: "/token",
func (r *dockerFetcher) authorize(req *http.Request) {
if r.useBasic {
req.SetBasicAuth(r.username, r.secret)
} else if r.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
}
}
func (r *dockerFetcher) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
if len(responses) > 5 {
return nil, nil
}
last := responses[len(responses)-1]
if last.StatusCode == http.StatusUnauthorized {
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
for _, c := range parseAuthHeader(last.Header) {
if c.scheme == bearerAuth {
if errStr := c.parameters["error"]; errStr != "" {
// TODO: handle expired case
return nil, errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
}
if err := r.setTokenAuth(ctx, c.parameters); err != nil {
return nil, err
}
return req, nil
} else if c.scheme == basicAuth {
if r.username != "" && r.secret != "" {
r.useBasic = true
}
return req, nil
}
}
q = url.Values{
"scope": scopes,
"service": []string{"registry.docker.io"}, // usually comes from auth challenge
return nil, nil
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
// Support registries which have not properly implemented the HEAD method for
// manifests endpoint
if strings.Contains(req.URL.Path, "/manifests/") {
// TODO: copy request?
req.Method = http.MethodGet
return req, nil
}
)
}
u.RawQuery = q.Encode()
// TODO: Handle 50x errors accounting for attempt history
return nil, nil
}
log.G(ctx).WithField("token.url", u.String()).Debug("requesting token")
resp, err := ctxhttp.Get(ctx, http.DefaultClient, u.String())
func isManifestAccept(h http.Header) bool {
for _, ah := range h[textproto.CanonicalMIMEHeaderKey("Accept")] {
switch ah {
case images.MediaTypeDockerSchema2Manifest:
fallthrough
case images.MediaTypeDockerSchema2ManifestList:
fallthrough
case ocispec.MediaTypeImageManifest:
fallthrough
case ocispec.MediaTypeImageIndex:
return true
}
}
return false
}
func (r *dockerFetcher) setTokenAuth(ctx context.Context, params map[string]string) error {
realm, ok := params["realm"]
if !ok {
return errors.New("no realm specified for token auth challenge")
}
realmURL, err := url.Parse(realm)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return "", errors.Errorf("unexpected status code: %v %v", resp.StatusCode, resp.Status)
return fmt.Errorf("invalid token auth challenge realm: %s", err)
}
p, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
to := tokenOptions{
realm: realmURL.String(),
service: params["service"],
}
var tokenResponse struct {
Token string `json:"token"`
scope, ok := params["scope"]
if !ok {
return errors.Errorf("no scope specified for token auth challenge")
}
if err := json.Unmarshal(p, &tokenResponse); err != nil {
return "", err
// TODO: Get added scopes from context
to.scopes = []string{scope}
if r.secret != "" {
// Credential information is provided, use oauth POST endpoint
r.token, err = r.fetchTokenWithOAuth(ctx, to)
if err != nil {
return errors.Wrap(err, "failed to fetch oauth token")
}
} else {
// Do request anonymously
r.token, err = r.getToken(ctx, to)
if err != nil {
return errors.Wrap(err, "failed to fetch anonymous token")
}
}
return tokenResponse.Token, nil
return nil
}
// getV2URLPaths generates the candidate urls paths for the object based on the
@ -291,3 +404,125 @@ func getV2URLPaths(desc ocispec.Descriptor) ([]string, error) {
return urls, nil
}
type tokenOptions struct {
realm string
service string
scopes []string
}
type postTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"`
Scope string `json:"scope"`
}
func (r *dockerFetcher) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
form := url.Values{}
form.Set("scope", strings.Join(to.scopes, " "))
form.Set("service", to.service)
// TODO: Allow setting client_id
form.Set("client_id", "containerd-dist-tool")
if r.username == "" {
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", r.secret)
} else {
form.Set("grant_type", "password")
form.Set("username", r.username)
form.Set("password", r.secret)
}
resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == 405 && r.username != "" {
// It would be nice if registries would implement the specifications
return r.getToken(ctx, to)
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
b, _ := ioutil.ReadAll(resp.Body)
log.G(ctx).WithFields(logrus.Fields{
"status": resp.Status,
"body": string(b),
}).Debugf("token request failed")
// TODO: handle error body and write debug output
return "", errors.Errorf("unexpected status: %s", resp.Status)
}
decoder := json.NewDecoder(resp.Body)
var tr postTokenResponse
if err = decoder.Decode(&tr); err != nil {
return "", fmt.Errorf("unable to decode token response: %s", err)
}
return tr.AccessToken, nil
}
type getTokenResponse struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"`
RefreshToken string `json:"refresh_token"`
}
// getToken fetches a token using a GET request
func (r *dockerFetcher) getToken(ctx context.Context, to tokenOptions) (string, error) {
req, err := http.NewRequest("GET", to.realm, nil)
if err != nil {
return "", err
}
reqParams := req.URL.Query()
if to.service != "" {
reqParams.Add("service", to.service)
}
for _, scope := range to.scopes {
reqParams.Add("scope", scope)
}
if r.secret != "" {
req.SetBasicAuth(r.username, r.secret)
}
req.URL.RawQuery = reqParams.Encode()
resp, err := ctxhttp.Do(ctx, r.client, req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
// TODO: handle error body and write debug output
return "", errors.Errorf("unexpected status: %s", resp.Status)
}
decoder := json.NewDecoder(resp.Body)
var tr getTokenResponse
if err = decoder.Decode(&tr); err != nil {
return "", fmt.Errorf("unable to decode token response: %s", err)
}
// `access_token` is equivalent to `token` and if both are specified
// the choice is undefined. Canonicalize `access_token` by sticking
// things in `token`.
if tr.AccessToken != "" {
tr.Token = tr.AccessToken
}
if tr.Token == "" {
return "", ErrNoToken
}
return tr.Token, nil
}

View File

@ -0,0 +1,388 @@
package docker
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/containerd/containerd/remotes"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func TestHTTPResolver(t *testing.T) {
s := func(h http.Handler) (string, ResolverOptions, func()) {
s := httptest.NewServer(h)
options := ResolverOptions{
PlainHTTP: true,
}
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 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)
options.Credentials = func(string) (string, string, error) {
return "user1", "password1", nil
}
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 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 := content(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.Cause(err) != ErrInvalidAuthorization {
t.Fatal(err)
}
}
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.Credentials = 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)
options := ResolverOptions{
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: capool,
},
},
},
}
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.t.Logf("%s %s", r.Method, r.URL.String())
h.handler.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(
content(ocispec.MediaTypeImageConfig, []byte("1")),
content(ocispec.MediaTypeImageLayerGzip, []byte("2")),
)
mc := content(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, f, err := resolver.Resolve(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 errors.Errorf("content mismatch: %s != %s", dgstr.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, errors.Wrapf(err, "failed to fetch %s", desc.Digest)
}
p, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
if dgst := desc.Digest.Algorithm().FromBytes(p); dgst != desc.Digest {
return nil, errors.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 content(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 append(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)
}
}

View File

@ -1,6 +1,6 @@
github.com/coreos/go-systemd 48702e0da86bd25e76cfef347e2adeb434a0d0a6
github.com/crosbymichael/go-runc 65847bfc51952703ca24b564d10de50d3f2db6e7
github.com/crosbymichael/console f13f890e20a94bdec6c328cdf9410b7158f0cfa4
github.com/crosbymichael/console 2a5cbd32a84cd1268c20c69bd090ec49e37009f8
github.com/crosbymichael/cgroups e950a27f3faf567abbf995bfbec90eaddc766d25
github.com/docker/go-metrics 8fd5772bf1584597834c6f7961a530f06cbfbb87
github.com/godbus/dbus c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f

View File

@ -20,6 +20,8 @@ type Console interface {
ResizeFrom(Console) error
// SetRaw sets the console in raw mode
SetRaw() error
// DisableEcho disables echo on the console
DisableEcho() error
// Reset restores the console to its orignal state
Reset() error
// Size returns the window size of the console

View File

@ -1,3 +1,5 @@
// +build darwin freebsd linux
package console
// #include <termios.h>
@ -34,8 +36,8 @@ func NewPty() (Console, string, error) {
}
type master struct {
f *os.File
termios *unix.Termios
f *os.File
original *unix.Termios
}
func (m *master) Read(b []byte) (int, error) {
@ -67,23 +69,42 @@ func (m *master) ResizeFrom(c Console) error {
}
func (m *master) Reset() error {
if m.termios == nil {
if m.original == nil {
return nil
}
return tcset(m.f.Fd(), m.termios)
return tcset(m.f.Fd(), m.original)
}
func (m *master) getCurrent() (unix.Termios, error) {
var termios unix.Termios
if err := tcget(m.f.Fd(), &termios); err != nil {
return unix.Termios{}, err
}
if m.original == nil {
m.original = &termios
}
return termios, nil
}
func (m *master) SetRaw() error {
m.termios = &unix.Termios{}
if err := tcget(m.f.Fd(), m.termios); err != nil {
rawState, err := m.getCurrent()
if err != nil {
return err
}
rawState := *m.termios
C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&rawState)))
rawState.Oflag = rawState.Oflag | C.OPOST
return tcset(m.f.Fd(), &rawState)
}
func (m *master) DisableEcho() error {
rawState, err := m.getCurrent()
if err != nil {
return err
}
rawState.Lflag = rawState.Lflag &^ unix.ECHO
return tcset(m.f.Fd(), &rawState)
}
func (m *master) Size() (WinSize, error) {
var ws WinSize
if err := ioctl(

View File

@ -126,6 +126,18 @@ func (m *master) ResizeFrom(c Console) error {
return ErrNotImplemented
}
func (m *master) DisableEcho() error {
mode := m.inMode &^ winterm.ENABLE_ECHO_INPUT
mode |= winterm.ENABLE_PROCESSED_INPUT
mode |= winterm.ENABLE_LINE_INPUT
if err := winterm.SetConsoleMode(m.in, mode); err != nil {
return errors.Wrap(err, "unable to set console to disable echo")
}
return nil
}
func (m *master) Close() error {
return nil
}

51
vendor/github.com/crosbymichael/console/tc_darwin.go generated vendored Normal file
View File

@ -0,0 +1,51 @@
package console
import (
"fmt"
"os"
"unsafe"
"golang.org/x/sys/unix"
)
func tcget(fd uintptr, p *unix.Termios) error {
return ioctl(fd, unix.TIOCGETA, uintptr(unsafe.Pointer(p)))
}
func tcset(fd uintptr, p *unix.Termios) error {
return ioctl(fd, unix.TIOCSETA, uintptr(unsafe.Pointer(p)))
}
func ioctl(fd, flag, data uintptr) error {
if _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, flag, data); err != 0 {
return err
}
return nil
}
// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
// unlockpt should be called before opening the slave side of a pty.
func unlockpt(f *os.File) error {
var u int32
return ioctl(f.Fd(), unix.TIOCPTYUNLK, uintptr(unsafe.Pointer(&u)))
}
// ptsname retrieves the name of the first available pts for the given master.
func ptsname(f *os.File) (string, error) {
var n int32
if err := ioctl(f.Fd(), unix.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n))); err != nil {
return "", err
}
return fmt.Sprintf("/dev/pts/%d", n), nil
}
func saneTerminal(f *os.File) error {
// Go doesn't have a wrapper for any of the termios ioctls.
var termios unix.Termios
if err := tcget(f.Fd(), &termios); err != nil {
return err
}
// Set -onlcr so we don't have to deal with \r.
termios.Oflag &^= unix.ONLCR
return tcset(f.Fd(), &termios)
}

51
vendor/github.com/crosbymichael/console/tc_freebsd.go generated vendored Normal file
View File

@ -0,0 +1,51 @@
package console
import (
"fmt"
"os"
"unsafe"
"golang.org/x/sys/unix"
)
func tcget(fd uintptr, p *unix.Termios) error {
return ioctl(fd, unix.TIOCGETA, uintptr(unsafe.Pointer(p)))
}
func tcset(fd uintptr, p *unix.Termios) error {
return ioctl(fd, unix.TIOCSETA, uintptr(unsafe.Pointer(p)))
}
func ioctl(fd, flag, data uintptr) error {
if _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, flag, data); err != 0 {
return err
}
return nil
}
// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
// unlockpt should be called before opening the slave side of a pty.
// This does not exist on FreeBSD, it does not allocate controlling terminals on open
func unlockpt(f *os.File) error {
return nil
}
// ptsname retrieves the name of the first available pts for the given master.
func ptsname(f *os.File) (string, error) {
var n int32
if err := ioctl(f.Fd(), unix.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
return "", err
}
return fmt.Sprintf("/dev/pts/%d", n), nil
}
func saneTerminal(f *os.File) error {
// Go doesn't have a wrapper for any of the termios ioctls.
var termios unix.Termios
if err := tcget(f.Fd(), &termios); err != nil {
return err
}
// Set -onlcr so we don't have to deal with \r.
termios.Oflag &^= unix.ONLCR
return tcset(f.Fd(), &termios)
}

View File

@ -1,5 +1,3 @@
// +build linux
package console
import (