use new xxml for XML responses to support rare control characters (#15511)

use new xxml/XML responses to support rare control characters

fixes #15023
This commit is contained in:
Harshavardhana 2022-08-23 17:04:11 -07:00 committed by GitHub
parent a67116b5bc
commit 8902561f3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 203 additions and 47 deletions

View File

@ -120,7 +120,7 @@ function start_minio_16drive() {
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
echo "FAILED" echo "FAILED"
mkdir -p inspects mkdir -p inspects
(cd inspects; "${WORK_DIR}/mc" admin inspect minio/healing-shard-bucket/unaligned/**) (cd inspects; "${WORK_DIR}/mc" support inspect minio/healing-shard-bucket/unaligned/**)
"${WORK_DIR}/mc" mb play/inspects "${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects "${WORK_DIR}/mc" mirror inspects play/inspects
@ -140,7 +140,7 @@ function start_minio_16drive() {
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
echo "FAILED" echo "FAILED"
mkdir -p inspects mkdir -p inspects
(cd inspects; "${WORK_DIR}/mc" admin inspect minio/healing-shard-bucket/unaligned/**) (cd inspects; "${WORK_DIR}/mc" support inspect minio/healing-shard-bucket/unaligned/**)
"${WORK_DIR}/mc" mb play/inspects "${WORK_DIR}/mc" mb play/inspects
"${WORK_DIR}/mc" mirror inspects play/inspects "${WORK_DIR}/mc" mirror inspects play/inspects

View File

@ -20,7 +20,6 @@ package cmd
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -30,6 +29,8 @@ import (
"github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/crypto"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
xxml "github.com/minio/xxml"
) )
// Returns a hexadecimal representation of time at the // Returns a hexadecimal representation of time at the
@ -64,9 +65,13 @@ func setCommonHeaders(w http.ResponseWriter) {
// Encodes the response headers into XML format. // Encodes the response headers into XML format.
func encodeResponse(response interface{}) []byte { func encodeResponse(response interface{}) []byte {
var bytesBuffer bytes.Buffer var bytesBuffer bytes.Buffer
bytesBuffer.WriteString(xml.Header) bytesBuffer.WriteString(xxml.Header)
e := xml.NewEncoder(&bytesBuffer) buf, err := xxml.Marshal(response)
e.Encode(response) if err != nil {
logger.LogIf(GlobalContext, err)
return nil
}
bytesBuffer.Write(buf)
return bytesBuffer.Bytes() return bytesBuffer.Bytes()
} }

View File

@ -89,7 +89,8 @@ type ListVersionsResponse struct {
IsTruncated bool IsTruncated bool
CommonPrefixes []CommonPrefix CommonPrefixes []CommonPrefix
Versions []ObjectVersion DeleteMarkers []DeleteMarkerVersion `xml:"DeleteMarker,omitempty"`
Versions []ObjectVersion `xml:"Version,omitempty"`
// Encoding type used to encode object keys in the response. // Encoding type used to encode object keys in the response.
EncodingType string `xml:"EncodingType,omitempty"` EncodingType string `xml:"EncodingType,omitempty"`
@ -247,50 +248,80 @@ type ObjectVersion struct {
Object Object
IsLatest bool IsLatest bool
VersionID string `xml:"VersionId"` VersionID string `xml:"VersionId"`
isDeleteMarker bool
} }
// MarshalXML - marshal ObjectVersion // DeleteMarkerVersion container for delete marker metadata
func (o ObjectVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error { type DeleteMarkerVersion struct {
if o.isDeleteMarker { Key string
start.Name.Local = "DeleteMarker" LastModified string // time string of format "2006-01-02T15:04:05.000Z"
} else {
start.Name.Local = "Version" // Owner of the object.
Owner Owner
IsLatest bool
VersionID string `xml:"VersionId"`
}
// Metadata metadata items implemented to ensure XML marshaling works.
type Metadata struct {
Items []struct {
Key string
Value string
} }
type objectVersionWrapper ObjectVersion
return e.EncodeElement(objectVersionWrapper(o), start)
} }
// StringMap is a map[string]string // Set add items, duplicate items get replaced.
type StringMap map[string]string func (s *Metadata) Set(k, v string) {
for i, item := range s.Items {
if item.Key == k {
s.Items[i] = struct {
Key string
Value string
}{
Key: k,
Value: v,
}
return
}
}
s.Items = append(s.Items, struct {
Key string
Value string
}{
Key: k,
Value: v,
})
}
type xmlKeyEntry struct {
XMLName xml.Name
Value string `xml:",chardata"`
}
// MarshalXML - StringMap marshals into XML. // MarshalXML - StringMap marshals into XML.
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (s *Metadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
tokens := []xml.Token{start} if s == nil {
return nil
for key, value := range s {
t := xml.StartElement{}
t.Name = xml.Name{
Space: "",
Local: key,
}
tokens = append(tokens, t, xml.CharData(value), xml.EndElement{Name: t.Name})
} }
tokens = append(tokens, xml.EndElement{ if len(s.Items) == 0 {
Name: start.Name, return nil
}) }
for _, t := range tokens { if err := e.EncodeToken(start); err != nil {
if err := e.EncodeToken(t); err != nil { return err
}
for _, item := range s.Items {
if err := e.Encode(xmlKeyEntry{
XMLName: xml.Name{Local: item.Key},
Value: item.Value,
}); err != nil {
return err return err
} }
} }
// flush to ensure tokens are written return e.EncodeToken(start.End())
return e.Flush()
} }
// Object container for object metadata // Object container for object metadata
@ -307,7 +338,7 @@ type Object struct {
StorageClass string StorageClass string
// UserMetadata user-defined metadata // UserMetadata user-defined metadata
UserMetadata StringMap `xml:"UserMetadata,omitempty"` UserMetadata *Metadata `xml:"UserMetadata,omitempty"`
} }
// CopyObjectResponse container returns ETag and LastModified of the successfully copied object // CopyObjectResponse container returns ETag and LastModified of the successfully copied object
@ -438,6 +469,8 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
// generates an ListBucketVersions response for the said bucket with other enumerated options. // generates an ListBucketVersions response for the said bucket with other enumerated options.
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse { func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse {
versions := make([]ObjectVersion, 0, len(resp.Objects)) versions := make([]ObjectVersion, 0, len(resp.Objects))
deleteMarkers := make([]DeleteMarkerVersion, 0, len(resp.Objects))
owner := Owner{ owner := Owner{
ID: globalMinioDefaultOwnerID, ID: globalMinioDefaultOwnerID,
DisplayName: "minio", DisplayName: "minio",
@ -445,10 +478,25 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
data := ListVersionsResponse{} data := ListVersionsResponse{}
for _, object := range resp.Objects { for _, object := range resp.Objects {
content := ObjectVersion{}
if object.Name == "" { if object.Name == "" {
continue continue
} }
if object.DeleteMarker {
deleteMarker := DeleteMarkerVersion{
Key: s3EncodeName(object.Name, encodingType),
LastModified: object.ModTime.UTC().Format(iso8601TimeFormat),
Owner: owner,
VersionID: object.VersionID,
}
if deleteMarker.VersionID == "" {
deleteMarker.VersionID = nullVersionID
}
deleteMarker.IsLatest = object.IsLatest
deleteMarkers = append(deleteMarkers, deleteMarker)
continue
}
content := ObjectVersion{}
content.Key = s3EncodeName(object.Name, encodingType) content.Key = s3EncodeName(object.Name, encodingType)
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat) content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
if object.ETag != "" { if object.ETag != "" {
@ -466,12 +514,12 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
content.VersionID = nullVersionID content.VersionID = nullVersionID
} }
content.IsLatest = object.IsLatest content.IsLatest = object.IsLatest
content.isDeleteMarker = object.DeleteMarker
versions = append(versions, content) versions = append(versions, content)
} }
data.Name = bucket data.Name = bucket
data.Versions = versions data.Versions = versions
data.DeleteMarkers = deleteMarkers
data.EncodingType = encodingType data.EncodingType = encodingType
data.Prefix = s3EncodeName(prefix, encodingType) data.Prefix = s3EncodeName(prefix, encodingType)
data.KeyMarker = s3EncodeName(marker, encodingType) data.KeyMarker = s3EncodeName(marker, encodingType)
@ -569,14 +617,14 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
} }
content.Owner = owner content.Owner = owner
if metadata { if metadata {
content.UserMetadata = make(StringMap) content.UserMetadata = &Metadata{}
switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind { switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
case crypto.S3: case crypto.S3:
content.UserMetadata[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
case crypto.S3KMS: case crypto.S3KMS:
content.UserMetadata[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionKMS content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
case crypto.SSEC: case crypto.SSEC:
content.UserMetadata[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES content.UserMetadata.Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, xhttp.AmzEncryptionAES)
} }
for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) { for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) {
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) { if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
@ -588,7 +636,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) { if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
continue continue
} }
content.UserMetadata[k] = v content.UserMetadata.Set(k, v)
} }
} }
contents = append(contents, content) contents = append(contents, content)

View File

@ -20,6 +20,7 @@ package cmd
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -3066,6 +3067,16 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
}) })
} }
// ObjectTagSet key value tags
type ObjectTagSet struct {
Tags []tags.Tag `xml:"Tag"`
}
type objectTagging struct {
XMLName xml.Name `xml:"Tagging"`
TagSet *ObjectTagSet `xml:"TagSet"`
}
// GetObjectTaggingHandler - GET object tagging // GetObjectTaggingHandler - GET object tagging
func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetObjectTagging") ctx := newContext(r, w, "GetObjectTagging")
@ -3103,7 +3114,7 @@ func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *h
} }
// Get object tags // Get object tags
tags, err := objAPI.GetObjectTags(ctx, bucket, object, opts) ot, err := objAPI.GetObjectTags(ctx, bucket, object, opts)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
@ -3113,7 +3124,20 @@ func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *h
w.Header()[xhttp.AmzVersionID] = []string{opts.VersionID} w.Header()[xhttp.AmzVersionID] = []string{opts.VersionID}
} }
writeSuccessResponseXML(w, encodeResponse(tags)) otags := &objectTagging{
TagSet: &ObjectTagSet{},
}
var list []tags.Tag
for k, v := range ot.ToMap() {
list = append(list, tags.Tag{
Key: k,
Value: v,
})
}
otags.TagSet.Tags = list
writeSuccessResponseXML(w, encodeResponse(otags))
} }
// PutObjectTaggingHandler - PUT object tagging // PutObjectTaggingHandler - PUT object tagging

View File

@ -28,6 +28,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"runtime"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -1636,6 +1637,81 @@ func (s *TestSuiteCommon) TestListObjectsHandler(c *check) {
} }
} }
// TestListObjectsSpecialCharactersHandler - Setting valid parameters to List Objects
// and then asserting the response with the expected one.
func (s *TestSuiteCommon) TestListObjectsSpecialCharactersHandler(c *check) {
if runtime.GOOS == globalWindowsOSName {
c.Skip("skip special character test for windows")
}
// generate a random bucket name.
bucketName := getRandomBucketName()
// HTTP request to create the bucket.
request, err := newTestSignedRequest(http.MethodPut, getMakeBucketURL(s.endPoint, bucketName),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
// execute the HTTP request to create bucket.
response, err := s.client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
for _, objectName := range []string{"foo bar 1", "foo bar 2", "foo \x01 bar"} {
buffer := bytes.NewReader([]byte("Hello World"))
request, err = newTestSignedRequest(http.MethodPut, getPutObjectURL(s.endPoint, bucketName, objectName),
int64(buffer.Len()), buffer, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
response, err = s.client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
}
testCases := []struct {
getURL string
expectedStrings []string
}{
{getListObjectsV1URL(s.endPoint, bucketName, "", "1000", ""), []string{"<Key>foo bar 1</Key>", "<Key>foo bar 2</Key>", "<Key>foo &#x1; bar</Key>"}},
{getListObjectsV1URL(s.endPoint, bucketName, "", "1000", "url"), []string{"<Key>foo+bar+1</Key>", "<Key>foo+bar+2</Key>", "<Key>foo+%01+bar</Key>"}},
{
getListObjectsV2URL(s.endPoint, bucketName, "", "1000", "", ""),
[]string{
"<Key>foo bar 1</Key>",
"<Key>foo bar 2</Key>",
"<Key>foo &#x1; bar</Key>",
fmt.Sprintf("<Owner><ID>%s</ID><DisplayName>minio</DisplayName></Owner>", globalMinioDefaultOwnerID),
},
},
{
getListObjectsV2URL(s.endPoint, bucketName, "", "1000", "true", ""),
[]string{
"<Key>foo bar 1</Key>",
"<Key>foo bar 2</Key>",
"<Key>foo &#x1; bar</Key>",
fmt.Sprintf("<Owner><ID>%s</ID><DisplayName>minio</DisplayName></Owner>", globalMinioDefaultOwnerID),
},
},
{getListObjectsV2URL(s.endPoint, bucketName, "", "1000", "", "url"), []string{"<Key>foo+bar+1</Key>", "<Key>foo+bar+2</Key>", "<Key>foo+%01+bar</Key>"}},
}
for _, testCase := range testCases {
// create listObjectsV1 request with valid parameters
request, err = newTestSignedRequest(http.MethodGet, testCase.getURL, 0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
// execute the HTTP request.
response, err = s.client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
getContent, err := ioutil.ReadAll(response.Body)
c.Assert(err, nil)
for _, expectedStr := range testCase.expectedStrings {
c.Assert(strings.Contains(string(getContent), expectedStr), true)
}
}
}
// TestListObjectsHandlerErrors - Setting invalid parameters to List Objects // TestListObjectsHandlerErrors - Setting invalid parameters to List Objects
// and then asserting the error response with the expected one. // and then asserting the error response with the expected one.
func (s *TestSuiteCommon) TestListObjectsHandlerErrors(c *check) { func (s *TestSuiteCommon) TestListObjectsHandlerErrors(c *check) {

1
go.mod
View File

@ -55,6 +55,7 @@ require (
github.com/minio/sha256-simd v1.0.0 github.com/minio/sha256-simd v1.0.0
github.com/minio/simdjson-go v0.4.2 github.com/minio/simdjson-go v0.4.2
github.com/minio/sio v0.3.0 github.com/minio/sio v0.3.0
github.com/minio/xxml v0.0.3
github.com/minio/zipindex v0.2.1 github.com/minio/zipindex v0.2.1
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/nats-io/nats-server/v2 v2.7.4 github.com/nats-io/nats-server/v2 v2.7.4

2
go.sum
View File

@ -645,6 +645,8 @@ github.com/minio/simdjson-go v0.4.2 h1:+5rVznvslY/oLIGOO0M7y2g0D8DnFbFZyF2Nr+KT5
github.com/minio/simdjson-go v0.4.2/go.mod h1:w6ptSSCcUPtqAOOdDVywJkX+8CyYzCdjrTCir0l6BVg= github.com/minio/simdjson-go v0.4.2/go.mod h1:w6ptSSCcUPtqAOOdDVywJkX+8CyYzCdjrTCir0l6BVg=
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus= github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw= github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
github.com/minio/xxml v0.0.3 h1:ZIpPQpfyG5uZQnqqC0LZuWtPk/WT8G/qkxvO6jb7zMU=
github.com/minio/xxml v0.0.3/go.mod h1:wcXErosl6IezQIMEWSK/LYC2VS7LJ1dAkgvuyIN3aH4=
github.com/minio/zipindex v0.2.1 h1:A37vDQJ7Uyp4RHpQEEpintgiIxg0t3npH2CWjLT//u4= github.com/minio/zipindex v0.2.1 h1:A37vDQJ7Uyp4RHpQEEpintgiIxg0t3npH2CWjLT//u4=
github.com/minio/zipindex v0.2.1/go.mod h1:s+b/Qyw9JtSEnYfaM4ASOWNO2xGnXCfzQ+SWAzVkVZc= github.com/minio/zipindex v0.2.1/go.mod h1:s+b/Qyw9JtSEnYfaM4ASOWNO2xGnXCfzQ+SWAzVkVZc=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=