forked from TrueCloudLab/distribution
cc23fdacff
Our registry client is not currently in a good place to be used as the reference OCI Distribution client implementation. But the registry proxy currently depends on it. Make the registry client internal to the distribution application to remove it from the API surface area (and any implied compatibility promises) of distribution/v3@v3.0.0 without breaking the proxy. Signed-off-by: Cory Snider <csnider@mirantis.com>
180 lines
5.4 KiB
Go
180 lines
5.4 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
|
|
"github.com/distribution/distribution/v3/internal/client/auth/challenge"
|
|
"github.com/distribution/distribution/v3/registry/api/errcode"
|
|
)
|
|
|
|
// ErrNoErrorsInBody is returned when an HTTP response body parses to an empty
|
|
// errcode.Errors slice.
|
|
var ErrNoErrorsInBody = errors.New("no error details found in HTTP response body")
|
|
|
|
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
|
|
// returned when making a registry api call.
|
|
type UnexpectedHTTPStatusError struct {
|
|
Status string
|
|
}
|
|
|
|
func (e *UnexpectedHTTPStatusError) Error() string {
|
|
return fmt.Sprintf("received unexpected HTTP status: %s", e.Status)
|
|
}
|
|
|
|
// UnexpectedHTTPResponseError is returned when an expected HTTP status code
|
|
// is returned, but the content was unexpected and failed to be parsed.
|
|
type UnexpectedHTTPResponseError struct {
|
|
ParseErr error
|
|
StatusCode int
|
|
Response []byte
|
|
}
|
|
|
|
func (e *UnexpectedHTTPResponseError) Error() string {
|
|
return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response))
|
|
}
|
|
|
|
func parseHTTPErrorResponse(resp *http.Response) error {
|
|
var errors errcode.Errors
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
statusCode := resp.StatusCode
|
|
ctHeader := resp.Header.Get("Content-Type")
|
|
|
|
if ctHeader == "" {
|
|
return makeError(statusCode, string(body))
|
|
}
|
|
|
|
contentType, _, err := mime.ParseMediaType(ctHeader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed parsing content-type: %w", err)
|
|
}
|
|
|
|
if contentType != "application/json" && contentType != "application/vnd.api+json" {
|
|
return makeError(statusCode, string(body))
|
|
}
|
|
|
|
// For backward compatibility, handle irregularly formatted
|
|
// messages that contain a "details" field.
|
|
var detailsErr struct {
|
|
Details string `json:"details"`
|
|
}
|
|
err = json.Unmarshal(body, &detailsErr)
|
|
if err == nil && detailsErr.Details != "" {
|
|
return makeError(statusCode, detailsErr.Details)
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &errors); err != nil {
|
|
return &UnexpectedHTTPResponseError{
|
|
ParseErr: err,
|
|
StatusCode: statusCode,
|
|
Response: body,
|
|
}
|
|
}
|
|
|
|
if len(errors) == 0 {
|
|
// If there was no error specified in the body, return
|
|
// UnexpectedHTTPResponseError.
|
|
return &UnexpectedHTTPResponseError{
|
|
ParseErr: ErrNoErrorsInBody,
|
|
StatusCode: statusCode,
|
|
Response: body,
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
func makeError(statusCode int, details string) error {
|
|
switch statusCode {
|
|
case http.StatusUnauthorized:
|
|
return errcode.ErrorCodeUnauthorized.WithMessage(details)
|
|
case http.StatusForbidden:
|
|
return errcode.ErrorCodeDenied.WithMessage(details)
|
|
case http.StatusTooManyRequests:
|
|
return errcode.ErrorCodeTooManyRequests.WithMessage(details)
|
|
default:
|
|
return errcode.ErrorCodeUnknown.WithMessage(details)
|
|
}
|
|
}
|
|
|
|
func makeErrorList(err error) []error {
|
|
if errL, ok := err.(errcode.Errors); ok {
|
|
return []error(errL)
|
|
}
|
|
return []error{err}
|
|
}
|
|
|
|
func mergeErrors(err1, err2 error) error {
|
|
return errcode.Errors(append(makeErrorList(err1), makeErrorList(err2)...))
|
|
}
|
|
|
|
// HandleHTTPResponseError returns error parsed from HTTP response, if any.
|
|
// It returns nil if no error occurred (HTTP status 200-399), or an error
|
|
// for unsuccessful HTTP response codes (in the range 400 - 499 inclusive).
|
|
// If possible, it returns a typed error, but an UnexpectedHTTPStatusError
|
|
// is returned for response code outside the expected range (HTTP status < 200
|
|
// and > 500).
|
|
func HandleHTTPResponseError(resp *http.Response) error {
|
|
if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
|
|
return nil
|
|
}
|
|
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
|
// Check for OAuth errors within the `WWW-Authenticate` header first
|
|
// See https://tools.ietf.org/html/rfc6750#section-3
|
|
for _, c := range challenge.ResponseChallenges(resp) {
|
|
if c.Scheme == "bearer" {
|
|
var err errcode.Error
|
|
// codes defined at https://tools.ietf.org/html/rfc6750#section-3.1
|
|
switch c.Parameters["error"] {
|
|
case "invalid_token":
|
|
err.Code = errcode.ErrorCodeUnauthorized
|
|
case "insufficient_scope":
|
|
err.Code = errcode.ErrorCodeDenied
|
|
default:
|
|
continue
|
|
}
|
|
if description := c.Parameters["error_description"]; description != "" {
|
|
err.Message = description
|
|
} else {
|
|
err.Message = err.Code.Message()
|
|
}
|
|
return mergeErrors(err, parseHTTPErrorResponse(resp))
|
|
}
|
|
}
|
|
err := parseHTTPErrorResponse(resp)
|
|
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 {
|
|
return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
|
|
}
|
|
return err
|
|
}
|
|
return &UnexpectedHTTPStatusError{Status: resp.Status}
|
|
}
|
|
|
|
// HandleErrorResponse returns error parsed from HTTP response for an
|
|
// unsuccessful HTTP response code (in the range 400 - 499 inclusive). An
|
|
// UnexpectedHTTPStatusError returned for response code outside of expected
|
|
// range.
|
|
//
|
|
// Deprecated: use [HandleHTTPResponseError] and check the error.
|
|
func HandleErrorResponse(resp *http.Response) error {
|
|
if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
|
|
return &UnexpectedHTTPStatusError{Status: resp.Status}
|
|
}
|
|
return HandleHTTPResponseError(resp)
|
|
}
|
|
|
|
// SuccessStatus returns true if the argument is a successful HTTP response
|
|
// code (in the range 200 - 399 inclusive).
|
|
//
|
|
// Deprecated: use [HandleHTTPResponseError] and check the error.
|
|
func SuccessStatus(status int) bool {
|
|
return status >= 200 && status <= 399
|
|
}
|