2014-12-10 05:25:54 +00:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
2015-04-17 20:32:51 +00:00
|
|
|
"encoding/json"
|
2016-03-14 17:06:30 +00:00
|
|
|
"errors"
|
2014-12-10 05:25:54 +00:00
|
|
|
"fmt"
|
2015-05-20 02:18:30 +00:00
|
|
|
"io"
|
2023-08-24 07:08:04 +00:00
|
|
|
"mime"
|
2015-04-17 20:32:51 +00:00
|
|
|
"net/http"
|
2014-12-10 05:25:54 +00:00
|
|
|
|
2020-08-24 11:18:39 +00:00
|
|
|
"github.com/distribution/distribution/v3/registry/api/errcode"
|
|
|
|
"github.com/distribution/distribution/v3/registry/client/auth/challenge"
|
2014-12-10 05:25:54 +00:00
|
|
|
)
|
|
|
|
|
2016-06-02 05:31:13 +00:00
|
|
|
// ErrNoErrorsInBody is returned when an HTTP response body parses to an empty
|
2016-03-14 17:06:30 +00:00
|
|
|
// errcode.Errors slice.
|
|
|
|
var ErrNoErrorsInBody = errors.New("no error details found in HTTP response body")
|
|
|
|
|
2014-12-10 05:25:54 +00:00
|
|
|
// 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 {
|
2016-03-14 17:06:30 +00:00
|
|
|
return fmt.Sprintf("received unexpected HTTP status: %s", e.Status)
|
2014-12-10 05:25:54 +00:00
|
|
|
}
|
2015-04-17 20:32:51 +00:00
|
|
|
|
|
|
|
// UnexpectedHTTPResponseError is returned when an expected HTTP status code
|
|
|
|
// is returned, but the content was unexpected and failed to be parsed.
|
|
|
|
type UnexpectedHTTPResponseError struct {
|
2016-03-15 16:03:56 +00:00
|
|
|
ParseErr error
|
|
|
|
StatusCode int
|
|
|
|
Response []byte
|
2015-04-17 20:32:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *UnexpectedHTTPResponseError) Error() string {
|
2016-03-15 16:03:56 +00:00
|
|
|
return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response))
|
2015-04-17 20:32:51 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 07:08:04 +00:00
|
|
|
func parseHTTPErrorResponse(resp *http.Response) error {
|
2015-05-15 01:21:39 +00:00
|
|
|
var errors errcode.Errors
|
2023-08-24 07:08:04 +00:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2015-04-17 20:32:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-05-07 23:11:04 +00:00
|
|
|
|
2023-08-24 07:08:04 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2016-01-20 22:45:08 +00:00
|
|
|
// 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 != "" {
|
2023-08-24 07:08:04 +00:00
|
|
|
return makeError(statusCode, detailsErr.Details)
|
2016-01-20 22:45:08 +00:00
|
|
|
}
|
|
|
|
|
2015-05-07 23:11:04 +00:00
|
|
|
if err := json.Unmarshal(body, &errors); err != nil {
|
2015-04-17 20:32:51 +00:00
|
|
|
return &UnexpectedHTTPResponseError{
|
2016-03-15 16:03:56 +00:00
|
|
|
ParseErr: err,
|
|
|
|
StatusCode: statusCode,
|
|
|
|
Response: body,
|
2015-04-17 20:32:51 +00:00
|
|
|
}
|
|
|
|
}
|
2016-03-14 17:06:30 +00:00
|
|
|
|
|
|
|
if len(errors) == 0 {
|
|
|
|
// If there was no error specified in the body, return
|
|
|
|
// UnexpectedHTTPResponseError.
|
|
|
|
return &UnexpectedHTTPResponseError{
|
2016-03-15 16:03:56 +00:00
|
|
|
ParseErr: ErrNoErrorsInBody,
|
|
|
|
StatusCode: statusCode,
|
|
|
|
Response: body,
|
2016-03-14 17:06:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-27 00:18:32 +00:00
|
|
|
return errors
|
2015-04-17 20:32:51 +00:00
|
|
|
}
|
2015-05-09 00:40:30 +00:00
|
|
|
|
2023-08-24 07:08:04 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-10 03:29:18 +00:00
|
|
|
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)...))
|
|
|
|
}
|
|
|
|
|
2023-09-08 12:33:46 +00:00
|
|
|
// 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
|
|
|
|
}
|
2016-11-09 22:57:53 +00:00
|
|
|
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
|
|
|
// Check for OAuth errors within the `WWW-Authenticate` header first
|
2016-11-10 03:29:18 +00:00
|
|
|
// See https://tools.ietf.org/html/rfc6750#section-3
|
2016-11-09 22:57:53 +00:00
|
|
|
for _, c := range challenge.ResponseChallenges(resp) {
|
|
|
|
if c.Scheme == "bearer" {
|
2016-11-10 03:29:18 +00:00
|
|
|
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()
|
2016-11-09 22:57:53 +00:00
|
|
|
}
|
2023-08-24 07:08:04 +00:00
|
|
|
return mergeErrors(err, parseHTTPErrorResponse(resp))
|
2016-11-09 22:57:53 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-24 07:08:04 +00:00
|
|
|
err := parseHTTPErrorResponse(resp)
|
2016-11-09 22:57:53 +00:00
|
|
|
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 {
|
2015-08-06 23:25:08 +00:00
|
|
|
return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
|
2015-05-20 17:05:44 +00:00
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
2015-05-09 00:40:30 +00:00
|
|
|
return &UnexpectedHTTPStatusError{Status: resp.Status}
|
|
|
|
}
|
2015-07-24 23:14:04 +00:00
|
|
|
|
2023-09-08 12:33:46 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2015-07-24 23:14:04 +00:00
|
|
|
// SuccessStatus returns true if the argument is a successful HTTP response
|
|
|
|
// code (in the range 200 - 399 inclusive).
|
2023-09-08 12:33:46 +00:00
|
|
|
//
|
|
|
|
// Deprecated: use [HandleHTTPResponseError] and check the error.
|
2015-07-24 23:14:04 +00:00
|
|
|
func SuccessStatus(status int) bool {
|
|
|
|
return status >= 200 && status <= 399
|
|
|
|
}
|