diff --git a/neofs/api/errors.go b/neofs/api/errors.go index 3cda50f..33a84cd 100644 --- a/neofs/api/errors.go +++ b/neofs/api/errors.go @@ -8,17 +8,9 @@ import ( "net/url" "strings" - "github.com/Azure/azure-storage-blob-go/azblob" - "github.com/minio/minio-go/v6" - "github.com/minio/minio-go/v6/pkg/tags" + "github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio/auth" "github.com/minio/minio/neofs/api/crypto" - "github.com/minio/minio/pkg/bucket/lifecycle" - objectlock "github.com/minio/minio/pkg/bucket/object/lock" - "github.com/minio/minio/pkg/bucket/policy" - "github.com/minio/minio/pkg/event" - "github.com/minio/minio/pkg/hash" - "google.golang.org/api/googleapi" ) type ( @@ -1629,7 +1621,7 @@ func (e errorCodeMap) ToAPIErr(errCode ErrorCode) Error { // toAPIErrorCode - Converts embedded errors. Convenience // function written to handle all cases where we have known types of // errors returned by underlying layers. -func toAPIErrorCode(ctx context.Context, err error) (apiErr ErrorCode) { +func toAPIErrorCode(_ context.Context, err error) (apiErr ErrorCode) { if err == nil { return ErrNone } @@ -1690,16 +1682,16 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr ErrorCode) { apiErr = ErrOperationTimedOut case errDiskNotFound: apiErr = ErrSlowDown - case objectlock.ErrInvalidRetentionDate: - apiErr = ErrInvalidRetentionDate - case objectlock.ErrPastObjectLockRetainDate: - apiErr = ErrPastObjectLockRetainDate - case objectlock.ErrUnknownWORMModeDirective: - apiErr = ErrUnknownWORMModeDirective - case objectlock.ErrObjectLockInvalidHeaders: - apiErr = ErrObjectLockInvalidHeaders - case objectlock.ErrMalformedXML: - apiErr = ErrMalformedXML + // case objectlock.ErrInvalidRetentionDate: + // apiErr = ErrInvalidRetentionDate + // case objectlock.ErrPastObjectLockRetainDate: + // apiErr = ErrPastObjectLockRetainDate + // case objectlock.ErrUnknownWORMModeDirective: + // apiErr = ErrUnknownWORMModeDirective + // case objectlock.ErrObjectLockInvalidHeaders: + // apiErr = ErrObjectLockInvalidHeaders + // case objectlock.ErrMalformedXML: + // apiErr = ErrMalformedXML } // Compression errors @@ -1722,8 +1714,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr ErrorCode) { switch err.(type) { case StorageFull: apiErr = ErrStorageFull - case hash.BadDigest: - apiErr = ErrBadDigest + // case hash.BadDigest: + // apiErr = ErrBadDigest case AllAccessDisabled: apiErr = ErrAllAccessDisabled case IncompleteBody: @@ -1774,8 +1766,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr ErrorCode) { apiErr = ErrEntityTooSmall case SignatureDoesNotMatch: apiErr = ErrSignatureDoesNotMatch - case hash.SHA256Mismatch: - apiErr = ErrContentSHA256Mismatch + // case hash.SHA256Mismatch: + // apiErr = ErrContentSHA256Mismatch case ObjectTooLarge: apiErr = ErrEntityTooLarge case ObjectTooSmall: @@ -1800,28 +1792,28 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr ErrorCode) { apiErr = ErrAdminNoSuchQuotaConfiguration case BucketQuotaExceeded: apiErr = ErrAdminBucketQuotaExceeded - case *event.ErrInvalidEventName: - apiErr = ErrEventNotification - case *event.ErrInvalidARN: - apiErr = ErrARNNotification - case *event.ErrARNNotFound: - apiErr = ErrARNNotification - case *event.ErrUnknownRegion: - apiErr = ErrRegionNotification - case *event.ErrInvalidFilterName: - apiErr = ErrFilterNameInvalid - case *event.ErrFilterNamePrefix: - apiErr = ErrFilterNamePrefix - case *event.ErrFilterNameSuffix: - apiErr = ErrFilterNameSuffix - case *event.ErrInvalidFilterValue: - apiErr = ErrFilterValueInvalid - case *event.ErrDuplicateEventName: - apiErr = ErrOverlappingConfigs - case *event.ErrDuplicateQueueConfiguration: - apiErr = ErrOverlappingFilterNotification - case *event.ErrUnsupportedConfiguration: - apiErr = ErrUnsupportedNotification + // case *event.ErrInvalidEventName: + // apiErr = ErrEventNotification + // case *event.ErrInvalidARN: + // apiErr = ErrARNNotification + // case *event.ErrARNNotFound: + // apiErr = ErrARNNotification + // case *event.ErrUnknownRegion: + // apiErr = ErrRegionNotification + // case *event.ErrInvalidFilterName: + // apiErr = ErrFilterNameInvalid + // case *event.ErrFilterNamePrefix: + // apiErr = ErrFilterNamePrefix + // case *event.ErrFilterNameSuffix: + // apiErr = ErrFilterNameSuffix + // case *event.ErrInvalidFilterValue: + // apiErr = ErrFilterValueInvalid + // case *event.ErrDuplicateEventName: + // apiErr = ErrOverlappingConfigs + // case *event.ErrDuplicateQueueConfiguration: + // apiErr = ErrOverlappingFilterNotification + // case *event.ErrUnsupportedConfiguration: + // apiErr = ErrUnsupportedNotification case OperationTimedOut: apiErr = ErrOperationTimedOut case BackendDown: @@ -1884,55 +1876,36 @@ func toAPIError(ctx context.Context, err error) Error { e.Error()), HTTPStatusCode: http.StatusBadRequest, } - case lifecycle.Error: - apiErr = Error{ - Code: "InvalidRequest", - Description: e.Error(), - HTTPStatusCode: http.StatusBadRequest, - } + // case lifecycle.Error: + // apiErr = Error{ + // Code: "InvalidRequest", + // Description: e.Error(), + // HTTPStatusCode: http.StatusBadRequest, + // } case tags.Error: apiErr = Error{ Code: e.Code(), Description: e.Error(), HTTPStatusCode: http.StatusBadRequest, } - case policy.Error: - apiErr = Error{ - Code: "MalformedPolicy", - Description: e.Error(), - HTTPStatusCode: http.StatusBadRequest, - } + // case policy.Error: + // apiErr = Error{ + // Code: "MalformedPolicy", + // Description: e.Error(), + // HTTPStatusCode: http.StatusBadRequest, + // } case crypto.Error: apiErr = Error{ Code: "XMinIOEncryptionError", Description: e.Error(), HTTPStatusCode: http.StatusBadRequest, } - case minio.ErrorResponse: + case ErrorResponse: apiErr = Error{ Code: e.Code, Description: e.Message, HTTPStatusCode: e.StatusCode, } - case *googleapi.Error: - apiErr = Error{ - Code: "XGCSInternalError", - Description: e.Message, - HTTPStatusCode: e.Code, - } - // GCS may send multiple errors, just pick the first one - // since S3 only sends one Error XML response. - if len(e.Errors) >= 1 { - apiErr.Code = e.Errors[0].Reason - - } - case azblob.StorageError: - apiErr = Error{ - Code: string(e.ServiceCode()), - Description: e.Error(), - HTTPStatusCode: e.Response().StatusCode, - } - // Add more Gateway SDKs here if any in future. } } diff --git a/neofs/api/handler/api.go b/neofs/api/handler/api.go index 3401108..d098b4f 100644 --- a/neofs/api/handler/api.go +++ b/neofs/api/handler/api.go @@ -1,131 +1,37 @@ package handler import ( - "context" - "crypto/ecdsa" - "math" + "errors" "github.com/minio/minio/neofs/api" - "github.com/minio/minio/neofs/pool" - "github.com/nspcc-dev/neofs-api-go/refs" - "github.com/nspcc-dev/neofs-api-go/service" - "github.com/nspcc-dev/neofs-api-go/session" - crypto "github.com/nspcc-dev/neofs-crypto" - "github.com/pkg/errors" + "github.com/minio/minio/neofs/layer" "go.uber.org/zap" ) type ( handler struct { log *zap.Logger - cli pool.Client - uid refs.OwnerID - tkn *service.Token - key *ecdsa.PrivateKey + obj layer.Client } Params struct { - Cli pool.Client Log *zap.Logger - Key *ecdsa.PrivateKey - } - - queryParams struct { - key *ecdsa.PrivateKey - addr refs.Address - verb service.Token_Info_Verb + Obj layer.Client } ) var _ api.Handler = (*handler)(nil) -func New(ctx context.Context, p Params) (api.Handler, error) { - var ( - err error - uid refs.OwnerID - tkn *service.Token - ) - +func New(log *zap.Logger, obj layer.Client) (api.Handler, error) { switch { - case p.Key == nil: - return nil, errors.New("empty private key") - case p.Cli == nil: - return nil, errors.New("empty gRPC client") - case p.Log == nil: + case obj == nil: + return nil, errors.New("empty NeoFS Object Layer") + case log == nil: return nil, errors.New("empty logger") } - if uid, err = refs.NewOwnerID(&p.Key.PublicKey); err != nil { - return nil, errors.Wrap(err, "could not fetch OwnerID") - } else if tkn, err = generateToken(ctx, p.Cli, p.Key); err != nil { - return nil, errors.Wrap(err, "could not prepare session token") - } - return &handler{ - uid: uid, - tkn: tkn, - key: p.Key, - log: p.Log, - cli: p.Cli, + log: log, + obj: obj, }, nil } - -func generateToken(ctx context.Context, cli pool.Client, key *ecdsa.PrivateKey) (*service.Token, error) { - owner, err := refs.NewOwnerID(&key.PublicKey) - if err != nil { - return nil, err - } - - token := new(service.Token) - token.SetOwnerID(owner) - token.SetExpirationEpoch(math.MaxUint64) - token.SetOwnerKey(crypto.MarshalPublicKey(&key.PublicKey)) - - conn, err := cli.GetConnection(ctx) - if err != nil { - return nil, err - } - - creator, err := session.NewGRPCCreator(conn, key) - if err != nil { - return nil, err - } - - res, err := creator.Create(ctx, token) - if err != nil { - return nil, err - } - - token.SetID(res.GetID()) - token.SetSessionKey(res.GetSessionKey()) - - return token, nil -} - -func prepareToken(t *service.Token, p queryParams) (*service.Token, error) { - sig := make([]byte, len(t.Signature)) - copy(sig, t.Signature) - - token := &service.Token{ - Token_Info: service.Token_Info{ - ID: t.ID, - OwnerID: t.OwnerID, - Verb: t.Verb, - Address: t.Address, - TokenLifetime: t.TokenLifetime, - SessionKey: t.SessionKey, - OwnerKey: t.OwnerKey, - }, - Signature: sig, - } - - token.SetAddress(p.addr) - token.SetVerb(p.verb) - - err := service.AddSignatureWithKey(p.key, service.NewSignedSessionToken(token)) - if err != nil { - return nil, err - } - - return token, nil -} diff --git a/neofs/api/handler/container.go b/neofs/api/handler/container.go deleted file mode 100644 index 2c8041b..0000000 --- a/neofs/api/handler/container.go +++ /dev/null @@ -1,171 +0,0 @@ -package handler - -import ( - "context" - "encoding/xml" - "net/http" - "time" - - "github.com/minio/minio/auth" - "github.com/minio/minio/neofs/api" - "github.com/nspcc-dev/neofs-api-go/container" - "github.com/nspcc-dev/neofs-api-go/refs" - "github.com/nspcc-dev/neofs-api-go/service" - "github.com/pkg/errors" - "go.uber.org/zap" - "google.golang.org/grpc" -) - -type ( - // Owner - bucket owner/principal - Owner struct { - ID string - DisplayName string - } - - // Bucket container for bucket metadata - Bucket struct { - CID refs.CID `xml:"-"` // ignored by response - Name string - CreationDate string // time string of format "2006-01-02T15:04:05.000Z" - } - - // ListBucketsResponse - format for list buckets response - ListBucketsResponse struct { - XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"` - - Owner Owner - - // Container for one or more buckets. - Buckets struct { - Buckets []*Bucket `xml:"Bucket"` - } // Buckets are nested - } - - cnrInfoParams struct { - cid refs.CID - tkn *service.BearerTokenMsg - } -) - -func (h *handler) getContainerInfo(ctx context.Context, p cnrInfoParams) (*Bucket, error) { - var ( - err error - con *grpc.ClientConn - res *container.GetResponse - ) - - req := new(container.GetRequest) - req.SetCID(p.cid) - req.SetTTL(service.SingleForwardingTTL) - req.SetBearer(p.tkn) - - if con, err = h.cli.GetConnection(ctx); err != nil { - return nil, errors.Wrap(err, "could not fetch connection") - } else if err = service.SignRequestData(h.key, req); err != nil { - return nil, errors.Wrap(err, "could not sign container info request") - } else if res, err = container.NewServiceClient(con).Get(ctx, req); err != nil { - return nil, errors.Wrap(err, "could not fetch container info") - } - - // TODO should extract nice name - // and datetime from container info: - _ = res - - return &Bucket{ - CID: p.cid, - Name: p.cid.String(), - CreationDate: new(time.Time).Format(time.RFC3339), - }, nil -} - -func (h *handler) getContainerList(ctx context.Context, tkn *service.BearerTokenMsg) ([]*Bucket, error) { - var ( - err error - inf *Bucket - con *grpc.ClientConn - res *container.ListResponse - ) - - req := new(container.ListRequest) - req.OwnerID = tkn.OwnerID - req.SetTTL(service.SingleForwardingTTL) - req.SetBearer(tkn) - - if con, err = h.cli.GetConnection(ctx); err != nil { - return nil, errors.Wrap(err, "could not fetch connection") - } else if err = service.SignRequestData(h.key, req); err != nil { - return nil, errors.Wrap(err, "could not sign request") - } else if res, err = container.NewServiceClient(con).List(ctx, req); err != nil { - return nil, errors.Wrap(err, "could not fetch list containers") - } - - params := cnrInfoParams{tkn: tkn} - result := make([]*Bucket, 0, len(res.CID)) - - for _, cid := range res.CID { - params.cid = cid - if inf, err = h.getContainerInfo(ctx, params); err != nil { - return nil, errors.Wrap(err, "could not fetch container info") - } - - result = append(result, inf) - } - - return result, nil -} - -func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { - var ( - err error - lst []*Bucket - tkn *service.BearerTokenMsg - ) - - // TODO think about deadlines - ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) - defer cancel() - - if tkn, err = auth.GetBearerToken(ctx); err != nil { - h.log.Error("could not fetch bearer token", - zap.Error(err)) - - e := api.GetAPIError(api.ErrInternalError) - - api.WriteErrorResponse(ctx, w, api.Error{ - Code: e.Code, - Description: err.Error(), - HTTPStatusCode: e.HTTPStatusCode, - }, r.URL) - - return - } else if lst, err = h.getContainerList(ctx, tkn); err != nil { - h.log.Error("could not fetch bearer token", - zap.Error(err)) - - // TODO check that error isn't gRPC error - - e := api.GetAPIError(api.ErrInternalError) - - api.WriteErrorResponse(ctx, w, api.Error{ - Code: e.Code, - Description: err.Error(), - HTTPStatusCode: e.HTTPStatusCode, - }, r.URL) - - return - } - - result := &ListBucketsResponse{Owner: Owner{ - ID: tkn.OwnerID.String(), - DisplayName: tkn.OwnerID.String(), - }} - - result.Buckets.Buckets = lst - - // Generate response. - encodedSuccessResponse := api.EncodeResponse(result) - - // Write response. - api.WriteSuccessResponseXML(w, encodedSuccessResponse) -} diff --git a/neofs/api/handler/unimplemented.go b/neofs/api/handler/unimplemented.go index ad04acc..0caa2e4 100644 --- a/neofs/api/handler/unimplemented.go +++ b/neofs/api/handler/unimplemented.go @@ -6,6 +6,14 @@ import ( "github.com/minio/minio/neofs/api" ) +func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { + api.WriteErrorResponse(r.Context(), w, api.Error{ + Code: "XNeoFSUnimplemented", + Description: "implement me ListBucketsHandler", + HTTPStatusCode: http.StatusNotImplemented, + }, r.URL) +} + func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { api.WriteErrorResponse(r.Context(), w, api.Error{ Code: "XNeoFSUnimplemented", diff --git a/neofs/api/response.go b/neofs/api/response.go index ba3cf71..546a6ae 100644 --- a/neofs/api/response.go +++ b/neofs/api/response.go @@ -14,7 +14,7 @@ import ( ) type ( - // APIErrorResponse - error response format + // ErrorResponse - error response format ErrorResponse struct { XMLName xml.Name `xml:"Error" json:"-"` Code string @@ -22,9 +22,18 @@ type ( Key string `xml:"Key,omitempty" json:"Key,omitempty"` BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"` Resource string - Region string `xml:"Region,omitempty" json:"Region,omitempty"` RequestID string `xml:"RequestId" json:"RequestId"` HostID string `xml:"HostId" json:"HostId"` + + // Region where the bucket is located. This header is returned + // only in HEAD bucket and ListObjects response. + Region string `xml:"Region,omitempty" json:"Region,omitempty"` + + // Captures the server string returned in response header. + Server string `xml:"-" json:"-"` + + // Underlying HTTP status code for the returned error + StatusCode int `xml:"-" json:"-"` } // APIError structure @@ -61,6 +70,49 @@ const ( var deploymentID, _ = uuid.NewRandom() +// Non exhaustive list of AWS S3 standard error responses - +// http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html +var s3ErrorResponseMap = map[string]string{ + "AccessDenied": "Access Denied.", + "BadDigest": "The Content-Md5 you specified did not match what we received.", + "EntityTooSmall": "Your proposed upload is smaller than the minimum allowed object size.", + "EntityTooLarge": "Your proposed upload exceeds the maximum allowed object size.", + "IncompleteBody": "You did not provide the number of bytes specified by the Content-Length HTTP header.", + "InternalError": "We encountered an internal error, please try again.", + "InvalidAccessKeyId": "The access key ID you provided does not exist in our records.", + "InvalidBucketName": "The specified bucket is not valid.", + "InvalidDigest": "The Content-Md5 you specified is not valid.", + "InvalidRange": "The requested range is not satisfiable", + "MalformedXML": "The XML you provided was not well-formed or did not validate against our published schema.", + "MissingContentLength": "You must provide the Content-Length HTTP header.", + "MissingContentMD5": "Missing required header for this request: Content-Md5.", + "MissingRequestBodyError": "Request body is empty.", + "NoSuchBucket": "The specified bucket does not exist.", + "NoSuchBucketPolicy": "The bucket policy does not exist", + "NoSuchKey": "The specified key does not exist.", + "NoSuchUpload": "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.", + "NotImplemented": "A header you provided implies functionality that is not implemented", + "PreconditionFailed": "At least one of the pre-conditions you specified did not hold", + "RequestTimeTooSkewed": "The difference between the request time and the server's time is too large.", + "SignatureDoesNotMatch": "The request signature we calculated does not match the signature you provided. Check your key and signing method.", + "MethodNotAllowed": "The specified method is not allowed against this resource.", + "InvalidPart": "One or more of the specified parts could not be found.", + "InvalidPartOrder": "The list of parts was not in ascending order. The parts list must be specified in order by part number.", + "InvalidObjectState": "The operation is not valid for the current state of the object.", + "AuthorizationHeaderMalformed": "The authorization header is malformed; the region is wrong.", + "MalformedPOSTRequest": "The body of your POST request is not well-formed multipart/form-data.", + "BucketNotEmpty": "The bucket you tried to delete is not empty", + "AllAccessDisabled": "All access to this bucket has been disabled.", + "MalformedPolicy": "Policy has invalid resource.", + "MissingFields": "Missing fields in request.", + "AuthorizationQueryParametersError": "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"/YYYYMMDD/REGION/SERVICE/aws4_request\".", + "MalformedDate": "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.", + "BucketAlreadyOwnedByYou": "Your previous request to create the named bucket succeeded and you already own it.", + "InvalidDuration": "Duration provided in the request is invalid.", + "XAmzContentSHA256Mismatch": "The provided 'x-amz-content-sha256' header does not match what was computed.", + // Add new API errors here. +} + // WriteErrorResponse writes error headers func WriteErrorResponse(ctx context.Context, w http.ResponseWriter, err Error, reqURL *url.URL) { switch err.Code { @@ -134,3 +186,15 @@ func EncodeResponse(response interface{}) []byte { func WriteSuccessResponseXML(w http.ResponseWriter, response []byte) { writeResponse(w, http.StatusOK, response, mimeXML) } + +// Error - Returns S3 error string. +func (e ErrorResponse) Error() string { + if e.Message == "" { + msg, ok := s3ErrorResponseMap[e.Code] + if !ok { + msg = fmt.Sprintf("Error response code %s.", e.Code) + } + return msg + } + return e.Message +}