Compare commits

...

22 Commits

Author SHA1 Message Date
Praveen raj Mani 78d116c487 Event persistence for MQTT (#7268)
- The events will be persisted in queueStore if `queueDir` is set.
- Else, if queueDir is not set events persist in memory.

The events are replayed back when the mqtt broker is back online.
2019-02-25 18:01:13 -08:00
dcharbonnier 2fc341394d fix json syntax (#7285) 2019-02-25 13:02:59 +05:30
Anis Elleuch 6584c7ea2b s3: Encode StartAfter when encoding type is passed (#7281)
In ObjectList V2, StartAfter needs to be encoded when encoding-type
is specified.
2019-02-24 18:50:28 -08:00
Harshavardhana 2520e535a0 Allow lazyQuotes for certain types of CSV (#7278)
Set lazyQuotes to true, to allow a quote to appear
in an unquote field and a non-doubled quote may
appear in a quoted field.
2019-02-24 06:51:02 -08:00
Anis Elleuch 5efbe8a1b3 s3: Add support of encodingType parameter (#7265)
This commit honors encoding-type parameter in object listing,
parts listing and multipart uploads listing.
2019-02-24 11:44:24 +05:30
Nitish Tiwari dab314900d Fix healthcheck script to wait for netstat command output (#7275)
Fixes #7272
2019-02-23 09:32:53 -08:00
Harshavardhana 7923b83953 Support multiple-domains in MINIO_DOMAIN (#7274)
Fixes #7173
2019-02-23 08:48:01 +05:30
Aditya Manthramurthy 80a351633f Update vendorized bcicen/jstream (#7257)
- Includes an error handling fix that is waiting to be merged upstream
- Uses order-preserving (un)marshalling for JSON objects.
2019-02-20 23:59:23 -08:00
Harshavardhana bedcb7442a Write xml.Header first instead of spaces to handle XML parsers (#7253)
Clients like AWS SDK Java and AWS cli XML parsers are
unable to handle on `\r\n` characters to avoid these
errors send XML header first and write white space characters
instead.

Also handle cases to avoid double WriteHeader calls
2019-02-21 11:50:15 +05:30
Harshavardhana 2232b0b55f Fix mac build failure for healthcheck binary (#7263) 2019-02-21 08:11:16 +05:30
Harshavardhana 91576d416d Fix GetLocalPeer usage in perf handlers (#7249)
GetLocalPeer usage should be fixed and used only
once per call for not all local endpoints.
2019-02-20 16:04:55 -08:00
Minio Trusted 3aabe45fd9 Update yaml files to latest version RELEASE.2019-02-20T22-44-29Z 2019-02-20 22:49:40 +00:00
Krishna Srinivas 6dd26b8231 Detect change in underlying mounted disks (#7229) 2019-02-20 13:32:29 -08:00
poornas e098852a80 Revert PR #7241 to fix vault renewal (#7259)
- Current implementation was spawning renewer goroutines
without waiting for the lease duration to end. Remove vault renewer
and call vault.RenewToken directly and manage reauthentication if
lease expired.
2019-02-20 12:23:59 -08:00
Nitish Tiwari 1e82c4a7c4 Implement Docker healthcheck script in Go (#7105)
Go script makes it easy to read/maintain. Also updated the timeout
in Dockerfiles from 5s to default 30s and test interval to 1m

Higher timeout makes sense as server may sometimes respond slowly
if under high load as reported in #6974

Fixes #6974
2019-02-20 21:42:03 +05:30
Krishna Srinivas ce960565b1 Validate and reject unusual requests (#7258) 2019-02-19 21:02:41 -08:00
poornas 755e675d5c Fix: send decrypted size to notification event (#7248) 2019-02-19 14:14:26 +05:30
Harshavardhana b6c00405ec Do not pro-actively return false in isObjectDir() (#7246)
We should change the logic for both isObject()
and isObjectDir() leaf detection to be done
with quorum, due to how our directory navigation
works - this allows for properly deleting all
the dangling directories or objects if any.
2019-02-15 16:21:19 -08:00
Harshavardhana 8f62935448 Add proper requestID for STS errors (#7245) 2019-02-14 17:54:33 -08:00
Harshavardhana 396d78352d Support HTTP/2.0 (#7204)
Fixes #6704
2019-02-14 17:53:46 -08:00
Aditya Manthramurthy 8a405cab2f COUNT() function in select should return an int (#7243) 2019-02-13 16:32:59 -08:00
Minio Trusted 6d778a883f Update yaml files to latest version RELEASE.2019-02-14T00-21-45Z 2019-02-14 00:27:52 +00:00
71 changed files with 1624 additions and 1252 deletions
+2 -1
View File
@@ -22,4 +22,5 @@ parts/
prime/
stage/
.sia_temp/
config.json
config.json
healthcheck
+5 -4
View File
@@ -11,7 +11,8 @@ RUN \
apk add --no-cache git && \
go get -v -d github.com/minio/minio && \
cd /go/src/github.com/minio/minio && \
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)"
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
go build -ldflags "-s -w" -o /usr/bin/healthcheck dockerscripts/healthcheck.go
FROM alpine:3.7
@@ -22,7 +23,8 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
EXPOSE 9000
COPY --from=0 /go/bin/minio /usr/bin/minio
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
COPY --from=0 /usr/bin/healthcheck /usr/bin/healthcheck
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
RUN \
apk add --no-cache ca-certificates 'curl>7.61.0' && \
@@ -32,7 +34,6 @@ ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=5s \
CMD /usr/bin/healthcheck.sh
HEALTHCHECK --interval=1m CMD healthcheck
CMD ["minio"]
+3 -4
View File
@@ -2,7 +2,7 @@ FROM alpine:3.7
LABEL maintainer="Minio Inc <dev@minio.io>"
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck /usr/bin/
COPY minio /usr/bin/
ENV MINIO_UPDATE off
@@ -14,7 +14,7 @@ RUN \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/healthcheck.sh
chmod +x /usr/bin/healthcheck
EXPOSE 9000
@@ -22,7 +22,6 @@ ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=5s \
CMD /usr/bin/healthcheck.sh
HEALTHCHECK --interval=1m CMD healthcheck
CMD ["minio"]
+17 -4
View File
@@ -1,8 +1,22 @@
FROM golang:1.11.4-alpine3.7
ENV GOPATH /go
ENV CGO_ENABLED 0
WORKDIR /go/src/github.com/minio/
RUN \
apk add --no-cache git && \
go get -v -d github.com/minio/minio && \
cd /go/src/github.com/minio/minio/dockerscripts && \
go build -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go
FROM alpine:3.7
LABEL maintainer="Minio Inc <dev@minio.io>"
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
COPY --from=0 /usr/bin/healthcheck /usr/bin/healthcheck
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \
@@ -14,7 +28,7 @@ RUN \
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/healthcheck.sh
chmod +x /usr/bin/healthcheck
EXPOSE 9000
@@ -22,7 +36,6 @@ ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=5s \
CMD /usr/bin/healthcheck.sh
HEALTHCHECK --interval=1m CMD healthcheck
CMD ["minio"]
+1
View File
@@ -66,6 +66,7 @@ coverage: build
build: checks
@echo "Building minio binary to './minio'"
@GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
@GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags="-s -w" -o $(PWD)/dockerscripts/healthcheck $(PWD)/dockerscripts/healthcheck.go
docker: build
@docker build -t $(TAG) . -f Dockerfile.dev
+2 -1
View File
@@ -122,7 +122,8 @@ var (
"password": "",
"reconnectInterval": 0,
"keepAliveInterval": 0,
"queueDir": ""
"queueDir": "",
"queueLimit": 0
}
},
"mysql": {
+6
View File
@@ -73,6 +73,7 @@ const (
ErrInvalidCopyPartRange
ErrInvalidCopyPartRangeSource
ErrInvalidMaxKeys
ErrInvalidEncodingMethod
ErrInvalidMaxUploads
ErrInvalidMaxParts
ErrInvalidPartNumberMarker
@@ -357,6 +358,11 @@ var errorCodes = errorCodeMap{
Description: "Argument maxKeys must be an integer between 0 and 2147483647",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidEncodingMethod: {
Code: "InvalidArgument",
Description: "Invalid Encoding Method specified in Request",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidMaxParts: {
Code: "InvalidArgument",
Description: "Argument max-parts must be an integer between 0 and 2147483647",
-60
View File
@@ -1,60 +0,0 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"context"
"net/http"
)
// Represents additional fields necessary for ErrPartTooSmall S3 error.
type completeMultipartAPIError struct {
// Proposed size represents uploaded size of the part.
ProposedSize int64
// Minimum size allowed epresents the minimum size allowed per
// part. Defaults to 5MB.
MinSizeAllowed int64
// Part number of the part which is incorrect.
PartNumber int
// ETag of the part which is incorrect.
PartETag string
// Other default XML error responses.
APIErrorResponse
}
// writeErrorResponsePartTooSmall - function is used specifically to
// construct a proper error response during CompleteMultipartUpload
// when one of the parts is < 5MB.
// The requirement comes due to the fact that generic ErrorResponse
// XML doesn't carry the additional fields required to send this
// error. So we construct a new type which lies well within the scope
// of this function.
func writePartSmallErrorResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, err PartTooSmall) {
apiError := toAPIError(ctx, err)
// Generate complete multipart error response.
errorResponse := getAPIErrorResponse(ctx, apiError, r.URL.Path,
w.Header().Get(responseRequestIDKey),
w.Header().Get(responseDeploymentIDKey))
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
encodedErrorResponse := encodeResponse(cmpErrResp)
// respond with 400 bad request.
w.WriteHeader(apiError.HTTPStatusCode)
// Write error body.
w.Write(encodedErrorResponse)
w.(http.Flusher).Flush()
}
+45 -29
View File
@@ -280,7 +280,7 @@ func getURLScheme(tls bool) string {
}
// getObjectLocation gets the fully qualified URL of an object.
func getObjectLocation(r *http.Request, domain, bucket, object string) string {
func getObjectLocation(r *http.Request, domains []string, bucket, object string) string {
// unit tests do not have host set.
if r.Host == "" {
return path.Clean(r.URL.Path)
@@ -295,15 +295,31 @@ func getObjectLocation(r *http.Request, domain, bucket, object string) string {
Scheme: proto,
}
// If domain is set then we need to use bucket DNS style.
if domain != "" {
for _, domain := range domains {
if strings.Contains(r.Host, domain) {
u.Host = bucket + "." + r.Host
u.Path = path.Join(slashSeparator, object)
break
}
}
return u.String()
}
// s3EncodeName encodes string in response when encodingType
// is specified in AWS S3 requests.
func s3EncodeName(name string, encodingType string) (result string) {
// Quick path to exit
if encodingType == "" {
return name
}
encodingType = strings.ToLower(encodingType)
switch encodingType {
case "url":
return url.QueryEscape(name)
}
return name
}
// generates ListBucketsResponse from array of BucketInfo which can be
// serialized to match XML and JSON API spec output.
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
@@ -326,7 +342,7 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
}
// generates an ListObjectsV1 response for the said bucket with other enumerated options.
func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
var contents []Object
var prefixes []CommonPrefix
var owner = Owner{}
@@ -338,7 +354,7 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
if object.Name == "" {
continue
}
content.Key = object.Name
content.Key = s3EncodeName(object.Name, encodingType)
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
if object.ETag != "" {
content.ETag = "\"" + object.ETag + "\""
@@ -348,20 +364,20 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
content.Owner = owner
contents = append(contents, content)
}
// TODO - support EncodingType in xml decoding
data.Name = bucket
data.Contents = contents
data.Prefix = prefix
data.Marker = marker
data.Delimiter = delimiter
data.EncodingType = encodingType
data.Prefix = s3EncodeName(prefix, encodingType)
data.Marker = s3EncodeName(marker, encodingType)
data.Delimiter = s3EncodeName(delimiter, encodingType)
data.MaxKeys = maxKeys
data.NextMarker = resp.NextMarker
data.NextMarker = s3EncodeName(resp.NextMarker, encodingType)
data.IsTruncated = resp.IsTruncated
for _, prefix := range resp.Prefixes {
var prefixItem = CommonPrefix{}
prefixItem.Prefix = prefix
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
prefixes = append(prefixes, prefixItem)
}
data.CommonPrefixes = prefixes
@@ -369,7 +385,7 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
}
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string) ListObjectsV2Response {
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string) ListObjectsV2Response {
var contents []Object
var commonPrefixes []CommonPrefix
var owner = Owner{}
@@ -384,7 +400,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
if object.Name == "" {
continue
}
content.Key = object.Name
content.Key = s3EncodeName(object.Name, encodingType)
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
if object.ETag != "" {
content.ETag = "\"" + object.ETag + "\""
@@ -394,20 +410,20 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
content.Owner = owner
contents = append(contents, content)
}
// TODO - support EncodingType in xml decoding
data.Name = bucket
data.Contents = contents
data.StartAfter = startAfter
data.Delimiter = delimiter
data.Prefix = prefix
data.EncodingType = encodingType
data.StartAfter = s3EncodeName(startAfter, encodingType)
data.Delimiter = s3EncodeName(delimiter, encodingType)
data.Prefix = s3EncodeName(prefix, encodingType)
data.MaxKeys = maxKeys
data.ContinuationToken = token
data.NextContinuationToken = nextToken
data.IsTruncated = isTruncated
for _, prefix := range prefixes {
var prefixItem = CommonPrefix{}
prefixItem.Prefix = prefix
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
commonPrefixes = append(commonPrefixes, prefixItem)
}
data.CommonPrefixes = commonPrefixes
@@ -451,11 +467,10 @@ func generateCompleteMultpartUploadResponse(bucket, key, location, etag string)
}
// generates ListPartsResponse from ListPartsInfo.
func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
// TODO - support EncodingType in xml decoding
func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) ListPartsResponse {
listPartsResponse := ListPartsResponse{}
listPartsResponse.Bucket = partsInfo.Bucket
listPartsResponse.Key = partsInfo.Object
listPartsResponse.Key = s3EncodeName(partsInfo.Object, encodingType)
listPartsResponse.UploadID = partsInfo.UploadID
listPartsResponse.StorageClass = globalMinioDefaultStorageClass
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
@@ -479,29 +494,29 @@ func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
}
// generates ListMultipartUploadsResponse for given bucket and ListMultipartsInfo.
func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMultipartsInfo) ListMultipartUploadsResponse {
func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMultipartsInfo, encodingType string) ListMultipartUploadsResponse {
listMultipartUploadsResponse := ListMultipartUploadsResponse{}
listMultipartUploadsResponse.Bucket = bucket
listMultipartUploadsResponse.Delimiter = multipartsInfo.Delimiter
listMultipartUploadsResponse.Delimiter = s3EncodeName(multipartsInfo.Delimiter, encodingType)
listMultipartUploadsResponse.IsTruncated = multipartsInfo.IsTruncated
listMultipartUploadsResponse.EncodingType = multipartsInfo.EncodingType
listMultipartUploadsResponse.Prefix = multipartsInfo.Prefix
listMultipartUploadsResponse.KeyMarker = multipartsInfo.KeyMarker
listMultipartUploadsResponse.NextKeyMarker = multipartsInfo.NextKeyMarker
listMultipartUploadsResponse.EncodingType = encodingType
listMultipartUploadsResponse.Prefix = s3EncodeName(multipartsInfo.Prefix, encodingType)
listMultipartUploadsResponse.KeyMarker = s3EncodeName(multipartsInfo.KeyMarker, encodingType)
listMultipartUploadsResponse.NextKeyMarker = s3EncodeName(multipartsInfo.NextKeyMarker, encodingType)
listMultipartUploadsResponse.MaxUploads = multipartsInfo.MaxUploads
listMultipartUploadsResponse.NextUploadIDMarker = multipartsInfo.NextUploadIDMarker
listMultipartUploadsResponse.UploadIDMarker = multipartsInfo.UploadIDMarker
listMultipartUploadsResponse.CommonPrefixes = make([]CommonPrefix, len(multipartsInfo.CommonPrefixes))
for index, commonPrefix := range multipartsInfo.CommonPrefixes {
listMultipartUploadsResponse.CommonPrefixes[index] = CommonPrefix{
Prefix: commonPrefix,
Prefix: s3EncodeName(commonPrefix, encodingType),
}
}
listMultipartUploadsResponse.Uploads = make([]Upload, len(multipartsInfo.Uploads))
for index, upload := range multipartsInfo.Uploads {
newUpload := Upload{}
newUpload.UploadID = upload.UploadID
newUpload.Key = upload.Object
newUpload.Key = s3EncodeName(upload.Object, encodingType)
newUpload.Initiated = upload.Initiated.UTC().Format(timeFormatAMZLong)
listMultipartUploadsResponse.Uploads[index] = newUpload
}
@@ -587,7 +602,8 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
}
// Generate error response.
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path, w.Header().Get(responseRequestIDKey), w.Header().Get(responseDeploymentIDKey))
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
w.Header().Get(responseRequestIDKey), w.Header().Get(responseDeploymentIDKey))
encodedErrorResponse := encodeResponse(errorResponse)
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
}
+4 -4
View File
@@ -26,7 +26,7 @@ func TestObjectLocation(t *testing.T) {
testCases := []struct {
request *http.Request
bucket, object string
domain string
domains []string
expectedLocation string
}{
// Server binding to localhost IP with https.
@@ -80,7 +80,7 @@ func TestObjectLocation(t *testing.T) {
Host: "mys3.bucket.org",
Header: map[string][]string{},
},
domain: "mys3.bucket.org",
domains: []string{"mys3.bucket.org"},
bucket: "mybucket",
object: "test/1.txt",
expectedLocation: "http://mybucket.mys3.bucket.org/test/1.txt",
@@ -92,14 +92,14 @@ func TestObjectLocation(t *testing.T) {
"X-Forwarded-Scheme": {httpsScheme},
},
},
domain: "mys3.bucket.org",
domains: []string{"mys3.bucket.org"},
bucket: "mybucket",
object: "test/1.txt",
expectedLocation: "https://mybucket.mys3.bucket.org/test/1.txt",
},
}
for i, testCase := range testCases {
gotLocation := getObjectLocation(testCase.request, testCase.domain, testCase.bucket, testCase.object)
gotLocation := getObjectLocation(testCase.request, testCase.domains, testCase.bucket, testCase.object)
if testCase.expectedLocation != gotLocation {
t.Errorf("Test %d: expected %s, got %s", i+1, testCase.expectedLocation, gotLocation)
}
+2 -2
View File
@@ -44,8 +44,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled bool) {
// API Router
apiRouter := router.PathPrefix("/").Subrouter()
var routers []*mux.Router
if globalDomainName != "" {
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter())
for _, domainName := range globalDomainNames {
routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter())
}
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
+15 -7
View File
@@ -18,6 +18,7 @@ package cmd
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/minio/minio/cmd/crypto"
@@ -32,12 +33,19 @@ import (
// - delimiter if set should be equal to '/', otherwise the request is rejected.
// - marker if set should have a common prefix with 'prefix' param, otherwise
// the request is rejected.
func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIErrorCode {
func validateListObjectsArgs(prefix, marker, delimiter, encodingType string, maxKeys int) APIErrorCode {
// Max keys cannot be negative.
if maxKeys < 0 {
return ErrInvalidMaxKeys
}
if encodingType != "" {
// Only url encoding type is supported
if strings.ToLower(encodingType) != "url" {
return ErrInvalidEncodingMethod
}
}
/// Minio special conditions for ListObjects.
// Verify if delimiter is anything other than '/', which we do not support.
@@ -78,7 +86,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
urlValues := r.URL.Query()
// Extract all the listObjectsV2 query params to their native values.
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _, errCode := getListObjectsV2Args(urlValues)
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(urlValues)
if errCode != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r))
return
@@ -86,7 +94,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
// Validate the query params before beginning to serve the request.
// fetch-owner is not validated since it is a boolean
if s3Error := validateListObjectsArgs(prefix, token, delimiter, maxKeys); s3Error != ErrNone {
if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
@@ -126,7 +134,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
}
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
delimiter, fetchOwner, listObjectsV2Info.IsTruncated, maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes)
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated, maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes)
// Write success response.
writeSuccessResponseXML(w, encodeResponse(response))
@@ -158,14 +166,14 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
}
// Extract all the litsObjectsV1 query params to their native values.
prefix, marker, delimiter, maxKeys, _, s3Error := getListObjectsV1Args(r.URL.Query())
prefix, marker, delimiter, maxKeys, encodingType, s3Error := getListObjectsV1Args(r.URL.Query())
if s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Validate all the query params before beginning to serve the request.
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, encodingType, maxKeys); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
@@ -204,7 +212,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
}
}
}
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
// Write success response.
writeSuccessResponseXML(w, encodeResponse(response))
+5 -5
View File
@@ -155,7 +155,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
return
}
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _, errCode := getBucketMultipartResources(r.URL.Query())
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, encodingType, errCode := getBucketMultipartResources(r.URL.Query())
if errCode != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r))
return
@@ -180,7 +180,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
return
}
// generate response
response := generateListMultipartUploadsResponse(bucket, listMultipartsInfo)
response := generateListMultipartUploadsResponse(bucket, listMultipartsInfo, encodingType)
encodedSuccessResponse := encodeResponse(response)
// write success response.
@@ -446,7 +446,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
}
// Make sure to add Location information here only for bucket
w.Header().Set("Location", getObjectLocation(r, globalDomainName, bucket, ""))
w.Header().Set("Location", getObjectLocation(r, globalDomainNames, bucket, ""))
writeSuccessResponseHeadersOnly(w)
return
@@ -505,7 +505,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
return
}
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return
@@ -677,7 +677,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return
}
location := getObjectLocation(r, globalDomainName, bucket, object)
location := getObjectLocation(r, globalDomainNames, bucket, object)
w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
w.Header().Set("Location", location)
+11 -7
View File
@@ -262,10 +262,14 @@ func handleCommonEnvVars() {
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
}
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
if globalDomainName != "" {
if _, ok = dns2.IsDomainName(globalDomainName); !ok {
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN value in environment variable")
v, ok := os.LookupEnv("MINIO_DOMAIN")
if ok {
for _, domainName := range strings.Split(v, ",") {
if _, ok = dns2.IsDomainName(domainName); !ok {
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName),
"Invalid MINIO_DOMAIN value in environment variable")
}
globalDomainNames = append(globalDomainNames, domainName)
}
}
@@ -294,10 +298,10 @@ func handleCommonEnvVars() {
updateDomainIPs(localIP4)
}
if globalDomainName != "" && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
if len(globalDomainNames) != 0 && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
var err error
globalDNSConfig, err = dns.NewCoreDNS(globalDomainName, globalDomainIPs, globalMinioPort, globalEtcdClient)
logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainName)
globalDNSConfig, err = dns.NewCoreDNS(globalDomainNames, globalDomainIPs, globalMinioPort, globalEtcdClient)
logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainNames)
}
if drives := os.Getenv("MINIO_CACHE_DRIVES"); drives != "" {
+3 -3
View File
@@ -130,8 +130,8 @@ func TestServerConfigWithEnvs(t *testing.T) {
}
// Check if serverConfig has the correct domain
if globalDomainName != "domain.com" {
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainName)
if globalDomainNames[0] != "domain.com" {
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainNames[0])
}
}
@@ -233,7 +233,7 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
// Test 27 - Test MQTT
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "username": "", "password": "", "queueDir": ""}}}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "username": "", "password": "", "queueDir": "", "queueLimit": 0}}}}`, false},
// Test 28 - Test NSQ
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nsq": { "1": { "enable": true, "nsqdAddress": "", "topic": ""} }}}`, false},
+1 -1
View File
@@ -884,7 +884,7 @@ type serverConfigV32 struct {
} `json:"policy"`
}
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir with MQTT.
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit with MQTT.
type serverConfigV33 struct {
quick.Config `json:"-"` // ignore interfaces
+55 -61
View File
@@ -16,7 +16,6 @@ package crypto
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
@@ -24,7 +23,6 @@ import (
"time"
vault "github.com/hashicorp/vault/api"
"github.com/minio/minio/cmd/logger"
)
var (
@@ -64,6 +62,7 @@ type VaultConfig struct {
type vaultService struct {
config *VaultConfig
client *vault.Client
secret *vault.Secret
leaseDuration time.Duration
}
@@ -123,86 +122,81 @@ func NewVault(config VaultConfig) (KMS, error) {
client.SetNamespace(config.Namespace)
}
v := &vaultService{client: client, config: &config}
if err := v.authenticate(); err != nil {
return nil, err
}
v.renewToken()
return v, nil
}
// renewSecret tries to renew the given secret. It blocks
// until it receives either the new secret or encounters an error.
func (v *vaultService) renewSecret(secret *vault.Secret) (*vault.Secret, error) {
renewer, err := v.client.NewRenewer(&vault.RenewerInput{
Secret: secret,
})
if err != nil {
logger.CriticalIf(context.Background(), fmt.Errorf("crypto: failed to create hashicorp vault renewer: %s", err))
}
go renewer.Renew()
defer renewer.Stop()
for {
select {
case err := <-renewer.DoneCh():
// renewToken starts a new go-routine which renews
// the vault authentication token periodically and re-authenticates
// if the token renewal fails
func (v *vaultService) renewToken() {
retryDelay := v.leaseDuration / 2
go func() {
for {
if v.secret == nil {
if err := v.authenticate(); err != nil {
time.Sleep(retryDelay)
continue
}
}
s, err := v.client.Auth().Token().RenewSelf(int(v.leaseDuration))
if err != nil || s == nil {
v.secret = nil
time.Sleep(retryDelay)
continue
}
if ok, err := s.TokenIsRenewable(); !ok || err != nil {
v.secret = nil
continue
}
ttl, err := s.TokenTTL()
if err != nil {
return nil, err
v.secret = nil
continue
}
case renew := <-renewer.RenewCh():
if renew.Secret == nil || renew.Secret.Auth == nil {
return nil, ErrKMSAuthLogin
}
return renew.Secret, nil
v.secret = s
retryDelay = ttl / 2
time.Sleep(retryDelay)
}
}
}()
}
// login tries to authenticate the minio server to
// the Vault KMS using the approle ID and secret.
func (v *vaultService) login() (*vault.Secret, error) {
// authenticate logs the app to vault, and starts the auto renewer
// before secret expires
func (v *vaultService) authenticate() (err error) {
payload := map[string]interface{}{
"role_id": v.config.Auth.AppRole.ID,
"secret_id": v.config.Auth.AppRole.Secret,
}
secret, err := v.client.Logical().Write("auth/approle/login", payload)
var tokenID string
var ttl time.Duration
var secret *vault.Secret
secret, err = v.client.Logical().Write("auth/approle/login", payload)
if err != nil {
return nil, err
return
}
if secret == nil || secret.Auth == nil {
return nil, ErrKMSAuthLogin
if secret == nil {
err = ErrKMSAuthLogin
return
}
return secret, nil
}
// authenticate tries to authenticate the minio server
// to the Vault KMS and starts a background job to renew
// the login.
func (v *vaultService) authenticate() error {
secret, err := v.login()
tokenID, err = secret.TokenID()
if err != nil {
return err
err = ErrKMSAuthLogin
return
}
v.client.SetToken(secret.Auth.ClientToken)
v.leaseDuration = time.Duration(secret.Auth.LeaseDuration)
// Start background job trying to renew the token
// or (if this fails) try to login again with app-ID and app-Secret.
go func(secret *vault.Secret) {
for {
newSecret, err := v.renewSecret(secret) // try to renew the secret (blocking)
if err != nil {
// Try to login again with app-ID and app-Secret
if newSecret, err = v.login(); err != nil { // failed -> try again
time.Sleep(1 * time.Minute) // retry delay
continue
}
}
secret = newSecret // Now newSecret contains a valid, non-nil *vault.Secret
v.client.SetToken(secret.Auth.ClientToken)
v.leaseDuration = time.Duration(secret.Auth.LeaseDuration)
}
}(secret)
return nil
ttl, err = secret.TokenTTL()
if err != nil {
err = ErrKMSAuthLogin
return
}
v.client.SetToken(tokenID)
v.secret = secret
v.leaseDuration = ttl
return
}
// GenerateKey returns a new plaintext key, generated by the KMS,
+8 -20
View File
@@ -205,7 +205,6 @@ func (endpoints EndpointList) GetString(i int) string {
func localEndpointsMemUsage(endpoints EndpointList) ServerMemUsageInfo {
var memUsages []mem.Usage
var historicUsages []mem.Usage
var addr string
scratchSpace := map[string]bool{}
for _, endpoint := range endpoints {
// Only proceed for local endpoints
@@ -213,16 +212,13 @@ func localEndpointsMemUsage(endpoints EndpointList) ServerMemUsageInfo {
if _, ok := scratchSpace[endpoint.Host]; ok {
continue
}
addr = GetLocalPeer(endpoints)
memUsage := mem.GetUsage()
memUsages = append(memUsages, memUsage)
historicUsage := mem.GetHistoricUsage()
historicUsages = append(historicUsages, historicUsage)
memUsages = append(memUsages, mem.GetUsage())
historicUsages = append(historicUsages, mem.GetHistoricUsage())
scratchSpace[endpoint.Host] = true
}
}
return ServerMemUsageInfo{
Addr: addr,
Addr: GetLocalPeer(endpoints),
Usage: memUsages,
HistoricUsage: historicUsages,
}
@@ -233,7 +229,6 @@ func localEndpointsMemUsage(endpoints EndpointList) ServerMemUsageInfo {
func localEndpointsCPULoad(endpoints EndpointList) ServerCPULoadInfo {
var cpuLoads []cpu.Load
var historicLoads []cpu.Load
var addr string
scratchSpace := map[string]bool{}
for _, endpoint := range endpoints {
// Only proceed for local endpoints
@@ -241,16 +236,13 @@ func localEndpointsCPULoad(endpoints EndpointList) ServerCPULoadInfo {
if _, ok := scratchSpace[endpoint.Host]; ok {
continue
}
addr = GetLocalPeer(endpoints)
cpuLoad := cpu.GetLoad()
cpuLoads = append(cpuLoads, cpuLoad)
historicLoad := cpu.GetHistoricLoad()
historicLoads = append(historicLoads, historicLoad)
cpuLoads = append(cpuLoads, cpu.GetLoad())
historicLoads = append(historicLoads, cpu.GetHistoricLoad())
scratchSpace[endpoint.Host] = true
}
}
return ServerCPULoadInfo{
Addr: addr,
Addr: GetLocalPeer(endpoints),
Load: cpuLoads,
HistoricLoad: historicLoads,
}
@@ -260,26 +252,22 @@ func localEndpointsCPULoad(endpoints EndpointList) ServerCPULoadInfo {
// local endpoints from given list of endpoints
func localEndpointsDrivePerf(endpoints EndpointList) ServerDrivesPerfInfo {
var dps []disk.Performance
var addr string
for _, endpoint := range endpoints {
// Only proceed for local endpoints
if endpoint.IsLocal {
addr = GetLocalPeer(endpoints)
if _, err := os.Stat(endpoint.Path); err != nil {
// Since this drive is not available, add relevant details and proceed
dps = append(dps, disk.Performance{Path: endpoint.Path, Error: err.Error()})
continue
}
tempObj := mustGetUUID()
fsPath := pathJoin(endpoint.Path, minioMetaTmpBucket, tempObj)
dp := disk.GetPerformance(fsPath)
dp := disk.GetPerformance(pathJoin(endpoint.Path, minioMetaTmpBucket, mustGetUUID()))
dp.Path = endpoint.Path
dps = append(dps, dp)
}
}
return ServerDrivesPerfInfo{
Addr: addr,
Addr: GetLocalPeer(endpoints),
Perf: dps,
}
}
+23 -8
View File
@@ -550,14 +550,14 @@ func (h httpStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
globalHTTPStats.updateStats(r, ww, durationSecs)
}
// pathValidityHandler validates all the incoming paths for
// any bad components and rejects them.
type pathValidityHandler struct {
// requestValidityHandler validates all the incoming paths for
// any malicious requests.
type requestValidityHandler struct {
handler http.Handler
}
func setPathValidityHandler(h http.Handler) http.Handler {
return pathValidityHandler{handler: h}
func setRequestValidityHandler(h http.Handler) http.Handler {
return requestValidityHandler{handler: h}
}
// Bad path components to be rejected by the path validity handler.
@@ -581,7 +581,18 @@ func hasBadPathComponent(path string) bool {
return false
}
func (h pathValidityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if client is sending a malicious request.
func hasMultipleAuth(r *http.Request) bool {
authTypeCount := 0
for _, hasValidAuth := range []func(*http.Request) bool{isRequestSignatureV2, isRequestPresignedSignatureV2, isRequestSignatureV4, isRequestPresignedSignatureV4, isRequestJWT, isRequestPostPolicySignatureV4} {
if hasValidAuth(r) {
authTypeCount++
}
}
return authTypeCount > 1
}
func (h requestValidityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check for bad components in URL path.
if hasBadPathComponent(r.URL.Path) {
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL, guessIsBrowserReq(r))
@@ -596,6 +607,10 @@ func (h pathValidityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
}
if hasMultipleAuth(r) {
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return
}
h.handler.ServeHTTP(w, r)
}
@@ -608,7 +623,7 @@ type bucketForwardingHandler struct {
}
func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if globalDNSConfig == nil || globalDomainName == "" ||
if globalDNSConfig == nil || len(globalDomainNames) == 0 ||
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
guessIsRPCReq(r) || isAdminReq(r) {
f.handler.ServeHTTP(w, r)
@@ -617,7 +632,7 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
// For browser requests, when federation is setup we need to
// specifically handle download and upload for browser requests.
if guessIsBrowserReq(r) && globalDNSConfig != nil && globalDomainName != "" {
if guessIsBrowserReq(r) && globalDNSConfig != nil && len(globalDomainNames) > 0 {
var bucket, _ string
switch r.Method {
case http.MethodPut:
+3 -4
View File
@@ -33,7 +33,7 @@ import (
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/certs"
"github.com/minio/minio/pkg/dns"
"github.com/minio/minio/pkg/iam/policy"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/iam/validator"
)
@@ -175,9 +175,8 @@ var (
globalActiveCred auth.Credentials
globalPublicCerts []*x509.Certificate
globalIsEnvDomainName bool
globalDomainName string // Root domain for virtual host style requests
globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed Minio deployment
globalDomainNames []string // Root domains for virtual host style requests
globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed Minio deployment
globalListingTimeout = newDynamicTimeout( /*30*/ 600*time.Second /*5*/, 600*time.Second) // timeout for listing related ops
globalObjectTimeout = newDynamicTimeout( /*1*/ 10*time.Minute /*10*/, 600*time.Second) // timeout for Object API related ops
+9 -6
View File
@@ -341,8 +341,8 @@ func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
}
// Returns "/bucketName/objectName" for path-style or virtual-host-style requests.
func getResource(path string, host string, domain string) (string, error) {
if domain == "" {
func getResource(path string, host string, domains []string) (string, error) {
if len(domains) == 0 {
return path, nil
}
// If virtual-host-style is enabled construct the "resource" properly.
@@ -357,11 +357,14 @@ func getResource(path string, host string, domain string) (string, error) {
return "", err
}
}
if !strings.HasSuffix(host, "."+domain) {
return path, nil
for _, domain := range domains {
if !strings.HasSuffix(host, "."+domain) {
continue
}
bucket := strings.TrimSuffix(host, "."+domain)
return slashSeparator + pathJoin(bucket, path), nil
}
bucket := strings.TrimSuffix(host, "."+domain)
return slashSeparator + pathJoin(bucket, path), nil
return path, nil
}
// If none of the http routes match respond with MethodNotAllowed, in JSON
+5 -5
View File
@@ -213,15 +213,15 @@ func TestGetResource(t *testing.T) {
testCases := []struct {
p string
host string
domain string
domains []string
expectedResource string
}{
{"/a/b/c", "test.mydomain.com", "mydomain.com", "/test/a/b/c"},
{"/a/b/c", "test.mydomain.com", "notmydomain.com", "/a/b/c"},
{"/a/b/c", "test.mydomain.com", "", "/a/b/c"},
{"/a/b/c", "test.mydomain.com", []string{"mydomain.com"}, "/test/a/b/c"},
{"/a/b/c", "test.mydomain.com", []string{"notmydomain.com"}, "/a/b/c"},
{"/a/b/c", "test.mydomain.com", nil, "/a/b/c"},
}
for i, test := range testCases {
gotResource, err := getResource(test.p, test.host, test.domain)
gotResource, err := getResource(test.p, test.host, test.domains)
if err != nil {
t.Fatal(err)
}
+6 -36
View File
@@ -19,58 +19,28 @@ package cmd
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/atomic"
)
func getRequestResource(r *http.Request) string {
if r == nil {
// http.Request is nil when non-HTTP data (like TLS record) is read/written.
return ""
}
if globalDomainName != "" {
host := r.Header.Get("Host")
if strings.HasSuffix(host, "."+globalDomainName) {
return "/" + strings.TrimSuffix(host, "."+globalDomainName) + r.URL.Path
}
}
return r.URL.Path
}
// ConnStats - Network statistics
// Count total input/output transferred bytes during
// the server's life.
type ConnStats struct {
totalInputBytes atomic.Uint64
totalOutputBytes atomic.Uint64
totalRPCInputBytes atomic.Uint64
totalRPCOutputBytes atomic.Uint64
totalInputBytes atomic.Uint64
totalOutputBytes atomic.Uint64
}
// Increase total input bytes
func (s *ConnStats) incInputBytes(r *http.Request, n int) {
resource := getRequestResource(r)
if resource == minioReservedBucketPath || strings.HasPrefix(resource, minioReservedBucketPath+"/") {
s.totalRPCInputBytes.Add(uint64(n))
} else {
s.totalInputBytes.Add(uint64(n))
}
func (s *ConnStats) incInputBytes(n int) {
s.totalInputBytes.Add(uint64(n))
}
// Increase total output bytes
func (s *ConnStats) incOutputBytes(r *http.Request, n int) {
resource := getRequestResource(r)
if resource == minioReservedBucketPath || strings.HasPrefix(resource, minioReservedBucketPath+"/") {
s.totalRPCOutputBytes.Add(uint64(n))
} else {
s.totalOutputBytes.Add(uint64(n))
}
func (s *ConnStats) incOutputBytes(n int) {
s.totalOutputBytes.Add(uint64(n))
}
// Return total input bytes
+14 -23
View File
@@ -19,28 +19,17 @@ package http
import (
"bufio"
"net"
"net/http"
"time"
)
// BufConn - is a generic stream-oriented network connection supporting buffered reader and read/write timeout.
type BufConn struct {
QuirkConn
bufReader *bufio.Reader // buffered reader wraps reader in net.Conn.
readTimeout time.Duration // sets the read timeout in the connection.
writeTimeout time.Duration // sets the write timeout in the connection.
request *http.Request // HTTP request information.
updateBytesReadFunc func(*http.Request, int) // function to be called to update bytes read.
updateBytesWrittenFunc func(*http.Request, int) // function to be called to update bytes written.
}
func (c *BufConn) setRequest(request *http.Request) {
c.request = request
}
func (c *BufConn) setUpdateFuncs(updateBytesReadFunc, updateBytesWrittenFunc func(*http.Request, int)) {
c.updateBytesReadFunc = updateBytesReadFunc
c.updateBytesWrittenFunc = updateBytesWrittenFunc
bufReader *bufio.Reader // buffered reader wraps reader in net.Conn.
readTimeout time.Duration // sets the read timeout in the connection.
writeTimeout time.Duration // sets the write timeout in the connection.
updateBytesReadFunc func(int) // function to be called to update bytes read.
updateBytesWrittenFunc func(int) // function to be called to update bytes written.
}
// Sets read timeout
@@ -81,7 +70,7 @@ func (c *BufConn) Read(b []byte) (n int, err error) {
c.setReadTimeout()
n, err = c.bufReader.Read(b)
if err == nil && c.updateBytesReadFunc != nil {
c.updateBytesReadFunc(c.request, n)
c.updateBytesReadFunc(n)
}
return n, err
@@ -92,18 +81,20 @@ func (c *BufConn) Write(b []byte) (n int, err error) {
c.setWriteTimeout()
n, err = c.Conn.Write(b)
if err == nil && c.updateBytesWrittenFunc != nil {
c.updateBytesWrittenFunc(c.request, n)
c.updateBytesWrittenFunc(n)
}
return n, err
}
// newBufConn - creates a new connection object wrapping net.Conn.
func newBufConn(c net.Conn, readTimeout, writeTimeout time.Duration, maxHeaderBytes int) *BufConn {
func newBufConn(c net.Conn, readTimeout, writeTimeout time.Duration, maxHeaderBytes int, updateBytesReadFunc, updateBytesWrittenFunc func(int)) *BufConn {
return &BufConn{
QuirkConn: QuirkConn{Conn: c},
bufReader: bufio.NewReaderSize(c, maxHeaderBytes),
readTimeout: readTimeout,
writeTimeout: writeTimeout,
QuirkConn: QuirkConn{Conn: c},
bufReader: bufio.NewReaderSize(c, maxHeaderBytes),
readTimeout: readTimeout,
writeTimeout: writeTimeout,
updateBytesReadFunc: updateBytesReadFunc,
updateBytesWrittenFunc: updateBytesWrittenFunc,
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ func TestBuffConnReadTimeout(t *testing.T) {
t.Errorf("failed to accept new connection. %v", terr)
return
}
bufconn := newBufConn(tcpConn, 1*time.Second, 1*time.Second, 4096)
bufconn := newBufConn(tcpConn, 1*time.Second, 1*time.Second, 4096, nil, nil)
defer bufconn.Close()
// Read a line
+7 -138
View File
@@ -23,9 +23,7 @@ import (
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"syscall"
"time"
@@ -33,9 +31,7 @@ import (
"github.com/minio/minio/cmd/logger"
)
var sslRequiredErrMsg = []byte("HTTP/1.0 403 Forbidden\r\n\r\nSSL required")
var badRequestMsg = []byte("HTTP/1.0 400 Bad Request\r\n\r\n")
var sslRequiredErrMsg = []byte("HTTP/1.1 403 Forbidden\r\n\r\nSSL required")
// HTTP methods.
var methods = []string{
@@ -51,30 +47,6 @@ var methods = []string{
"PRI", // HTTP 2 method
}
// maximum length of above methods + one space.
var methodMaxLen = getMethodMaxLen() + 1
func getMethodMaxLen() int {
maxLen := 0
for _, method := range methods {
if len(method) > maxLen {
maxLen = len(method)
}
}
return maxLen
}
func isHTTPMethod(s string) bool {
for _, method := range methods {
if s == method {
return true
}
}
return false
}
func getPlainText(bufConn *BufConn) (bool, error) {
defer bufConn.setReadTimeout()
@@ -95,66 +67,6 @@ func getPlainText(bufConn *BufConn) (bool, error) {
return false, nil
}
func getMethodResourceHost(bufConn *BufConn, maxHeaderBytes int) (method string, resource string, host string, err error) {
defer bufConn.setReadTimeout()
var data []byte
for dataLen := 1; dataLen < maxHeaderBytes; dataLen++ {
if bufConn.canSetReadDeadline() {
// Set deadline such that we close the connection quickly
// of no data was received from the Peek()
bufConn.SetReadDeadline(time.Now().UTC().Add(time.Second * 3))
}
data, err = bufConn.bufReader.Peek(dataLen)
if err != nil {
return "", "", "", err
}
tokens := strings.Split(string(data), "\n")
if len(tokens) < 2 {
continue
}
if method == "" && resource == "" {
if i := strings.IndexByte(tokens[0], ' '); i == -1 {
return "", "", "", fmt.Errorf("malformed HTTP request from '%s'", bufConn.LocalAddr())
}
httpTokens := strings.SplitN(tokens[0], " ", 3)
if len(httpTokens) < 3 {
return "", "", "", fmt.Errorf("malformed HTTP request from '%s'", bufConn.LocalAddr())
}
if !isHTTPMethod(httpTokens[0]) {
return "", "", "", fmt.Errorf("malformed HTTP request, invalid HTTP method '%s' from '%s'",
httpTokens[0], bufConn.LocalAddr())
}
method = httpTokens[0]
resource = httpTokens[1]
}
for _, token := range tokens[1:] {
if token == "" || !strings.HasSuffix(token, "\r") {
continue
}
// HTTP headers are case insensitive, so we should simply convert
// each tokens to their lower case form to match 'host' header.
token = strings.ToLower(token)
if strings.HasPrefix(token, "host:") {
host = strings.TrimPrefix(strings.TrimSuffix(token, "\r"), "host:")
return method, resource, host, nil
}
}
if tokens[len(tokens)-1] == "\r" {
break
}
}
return "", "", "", fmt.Errorf("malformed HTTP request from %s", bufConn.LocalAddr())
}
type acceptResult struct {
conn net.Conn
err error
@@ -171,8 +83,8 @@ type httpListener struct {
readTimeout time.Duration
writeTimeout time.Duration
maxHeaderBytes int
updateBytesReadFunc func(*http.Request, int) // function to be called to update bytes read in BufConn.
updateBytesWrittenFunc func(*http.Request, int) // function to be called to update bytes written in BufConn.
updateBytesReadFunc func(int) // function to be called to update bytes read in BufConn.
updateBytesWrittenFunc func(int) // function to be called to update bytes written in BufConn.
}
// isRoutineNetErr returns true if error is due to a network timeout,
@@ -222,7 +134,8 @@ func (listener *httpListener) start() {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(listener.tcpKeepAliveTimeout)
bufconn := newBufConn(tcpConn, listener.readTimeout, listener.writeTimeout, listener.maxHeaderBytes)
bufconn := newBufConn(tcpConn, listener.readTimeout, listener.writeTimeout, listener.maxHeaderBytes,
listener.updateBytesReadFunc, listener.updateBytesWrittenFunc)
if listener.tlsConfig != nil {
ok, err := getPlainText(bufconn)
if err != nil {
@@ -248,52 +161,8 @@ func (listener *httpListener) start() {
bufconn.Close()
return
}
// As the listener is configured with TLS, try to do TLS handshake, drop the connection if it fails.
tlsConn := tls.Server(bufconn, listener.tlsConfig)
if err := tlsConn.Handshake(); err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
bufconn.Close()
return
}
bufconn = newBufConn(tlsConn, listener.readTimeout, listener.writeTimeout, listener.maxHeaderBytes)
}
method, resource, host, err := getMethodResourceHost(bufconn, listener.maxHeaderBytes)
if err != nil {
// Peek could fail legitimately when clients abruptly close
// connection. E.g. Chrome browser opens connections speculatively to
// speed up loading of a web page. Peek may also fail due to network
// saturation on a transport with read timeout set. All other kind of
// errors should be logged for further investigation. Thanks @brendanashworth.
if !isRoutineNetErr(err) {
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
bufconn.Write(badRequestMsg)
}
bufconn.Close()
return
}
header := make(http.Header)
if host != "" {
header.Add("Host", host)
}
bufconn.setRequest(&http.Request{
Method: method,
URL: &url.URL{Path: resource},
Host: bufconn.LocalAddr().String(),
Header: header,
})
bufconn.setUpdateFuncs(listener.updateBytesReadFunc, listener.updateBytesWrittenFunc)
send(acceptResult{bufconn, nil}, doneCh)
}
@@ -380,8 +249,8 @@ func newHTTPListener(serverAddrs []string,
readTimeout time.Duration,
writeTimeout time.Duration,
maxHeaderBytes int,
updateBytesReadFunc func(*http.Request, int),
updateBytesWrittenFunc func(*http.Request, int)) (listener *httpListener, err error) {
updateBytesReadFunc func(int),
updateBytesWrittenFunc func(int)) (listener *httpListener, err error) {
var tcpListeners []*net.TCPListener
+6 -387
View File
@@ -17,17 +17,14 @@
package http
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -156,43 +153,6 @@ func getNonLoopBackIP(t *testing.T) string {
return nonLoopBackIP
}
// Test getMethodMaxLen()
func TestGetMethodMaxLen(t *testing.T) {
l := getMethodMaxLen()
if l != (methodMaxLen - 1) {
t.Fatalf("expected: %v, got: %v", (methodMaxLen - 1), l)
}
}
// Test isHTTPMethod()
func TestIsHTTPMethod(t *testing.T) {
testCases := []struct {
method string
expectedResult bool
}{
{"", false},
{"get", false},
{"put", false},
{"UPLOAD", false},
{"OPTIONS", true},
{"GET", true},
{"HEAD", true},
{"POST", true},
{"PUT", true},
{"DELETE", true},
{"TRACE", true},
{"CONNECT", true},
{"PRI", true},
}
for _, testCase := range testCases {
result := isHTTPMethod(testCase.method)
if result != testCase.expectedResult {
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, result)
}
}
}
func TestNewHTTPListener(t *testing.T) {
tlsConfig := getTLSConfig(t)
@@ -202,8 +162,8 @@ func TestNewHTTPListener(t *testing.T) {
tcpKeepAliveTimeout time.Duration
readTimeout time.Duration
writeTimeout time.Duration
updateBytesReadFunc func(*http.Request, int)
updateBytesWrittenFunc func(*http.Request, int)
updateBytesReadFunc func(int)
updateBytesWrittenFunc func(int)
expectedErr bool
}{
{[]string{"93.184.216.34:65432"}, nil, time.Duration(0), time.Duration(0), time.Duration(0), nil, nil, true},
@@ -266,8 +226,7 @@ func TestHTTPListenerStartClose(t *testing.T) {
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
nil, nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
@@ -314,8 +273,7 @@ func TestHTTPListenerAddr(t *testing.T) {
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
nil, nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
@@ -359,8 +317,7 @@ func TestHTTPListenerAddrs(t *testing.T) {
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
nil, nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
@@ -380,149 +337,6 @@ func TestHTTPListenerAddrs(t *testing.T) {
}
}
func TestHTTPListenerAccept(t *testing.T) {
tlsConfig := getTLSConfig(t)
nonLoopBackIP := getNonLoopBackIP(t)
testCases := []struct {
serverAddrs []string
tlsConfig *tls.Config
request string
reply string
expectedRequestLine string
}{
{[]string{"localhost:0"}, nil, "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "GET / HTTP/1.0\r\n"},
{[]string{nonLoopBackIP + ":0"}, nil, "POST / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "POST / HTTP/1.0\r\n"},
{[]string{nonLoopBackIP + ":0"}, nil, "HEAD / HTTP/1.0\r\nhost: example.org\r\n\r\n", "200 OK\r\n", "HEAD / HTTP/1.0\r\n"},
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, nil, "CONNECT / HTTP/1.0\r\nHost: www.example.org\r\n\r\n", "200 OK\r\n", "CONNECT / HTTP/1.0\r\n"},
{[]string{"localhost:0"}, tlsConfig, "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "GET / HTTP/1.0\r\n"},
{[]string{nonLoopBackIP + ":0"}, tlsConfig, "POST / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "POST / HTTP/1.0\r\n"},
{[]string{nonLoopBackIP + ":0"}, tlsConfig, "HEAD / HTTP/1.0\r\nhost: example.org\r\n\r\n", "200 OK\r\n", "HEAD / HTTP/1.0\r\n"},
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, "CONNECT / HTTP/1.0\r\nHost: www.example.org\r\n\r\n", "200 OK\r\n", "CONNECT / HTTP/1.0\r\n"},
}
for i, testCase := range testCases {
listener, err := newHTTPListener(
testCase.serverAddrs,
testCase.tlsConfig,
time.Duration(0),
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
for _, serverAddr := range listener.Addrs() {
var conn net.Conn
var err error
if testCase.tlsConfig == nil {
conn, err = net.Dial("tcp", serverAddr.String())
} else {
conn, err = tls.Dial("tcp", serverAddr.String(), &tls.Config{InsecureSkipVerify: true})
}
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
if _, err = io.WriteString(conn, testCase.request); err != nil {
t.Fatalf("Test %d: request send: expected = <nil>, got = %v", i+1, err)
}
serverConn, err := listener.Accept()
if err != nil {
t.Fatalf("Test %d: accept: expected = <nil>, got = %v", i+1, err)
}
requestLine, err := bufio.NewReader(serverConn).ReadString('\n')
if err != nil {
t.Fatalf("Test %d: request read: expected = <nil>, got = %v", i+1, err)
}
if requestLine != testCase.expectedRequestLine {
t.Fatalf("Test %d: request: expected = %v, got = %v", i+1, testCase.expectedRequestLine, requestLine)
}
if _, err = io.WriteString(serverConn, testCase.reply); err != nil {
t.Fatalf("Test %d: reply send: expected = <nil>, got = %v", i+1, err)
}
reply, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
t.Fatalf("Test %d: reply read: expected = <nil>, got = %v", i+1, err)
}
if testCase.reply != reply {
t.Fatalf("Test %d: reply: expected = %v, got = %v", i+1, testCase.reply, reply)
}
serverConn.Close()
conn.Close()
}
listener.Close()
}
}
func TestHTTPListenerAcceptPeekError(t *testing.T) {
tlsConfig := getTLSConfig(t)
nonLoopBackIP := getNonLoopBackIP(t)
testCases := []struct {
serverAddrs []string
tlsConfig *tls.Config
request string
}{
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, nil, "CONN"},
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, "CONN"},
}
for i, testCase := range testCases {
listener, err := newHTTPListener(
testCase.serverAddrs,
testCase.tlsConfig,
time.Duration(0),
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
go func() {
serverConn, aerr := listener.Accept()
if aerr == nil {
fail(t, "Test %d: accept: expected = <error>, got = <nil>", i+1)
}
if serverConn != nil {
fail(t, "Test %d: accept: server expected = <nil>, got = %v", i+1, serverConn)
}
}()
for _, serverAddr := range listener.Addrs() {
conn, err := net.Dial("tcp", serverAddr.String())
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
if _, err = io.WriteString(conn, testCase.request); err != nil {
t.Fatalf("Test %d: request send: expected = <nil>, got = %v", i+1, err)
}
conn.Close()
}
listener.Close()
}
}
func TestHTTPListenerAcceptTLSError(t *testing.T) {
tlsConfig := getTLSConfig(t)
nonLoopBackIP := getNonLoopBackIP(t)
@@ -543,8 +357,7 @@ func TestHTTPListenerAcceptTLSError(t *testing.T) {
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
nil, nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
@@ -587,200 +400,6 @@ func TestHTTPListenerAcceptTLSError(t *testing.T) {
}
}
func TestHTTPListenerAcceptError(t *testing.T) {
tlsConfig := getTLSConfig(t)
nonLoopBackIP := getNonLoopBackIP(t)
testCases := []struct {
serverAddrs []string
tlsConfig *tls.Config
secureClient bool
request string
}{
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, nil, false, "CONNECTION"},
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, false, "CONNECTION"},
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, true, "CONNECTION"},
}
for i, testCase := range testCases {
listener, err := newHTTPListener(
testCase.serverAddrs,
testCase.tlsConfig,
time.Duration(0),
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
for _, serverAddr := range listener.Addrs() {
var conn net.Conn
var err error
if testCase.secureClient {
conn, err = tls.Dial("tcp", serverAddr.String(), &tls.Config{InsecureSkipVerify: true})
} else {
conn, err = net.Dial("tcp", serverAddr.String())
}
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
if _, err = io.WriteString(conn, testCase.request); err != nil {
t.Fatalf("Test %d: request send: expected = <nil>, got = %v", i+1, err)
}
go func() {
serverConn, aerr := listener.Accept()
if aerr == nil {
fail(t, "Test %d: accept: expected = <error>, got = <nil>", i+1)
}
if serverConn != nil {
fail(t, "Test %d: accept: server expected = <nil>, got = %v", i+1, serverConn)
}
}()
if !testCase.secureClient && testCase.tlsConfig != nil {
buf := make([]byte, len(sslRequiredErrMsg))
var n int
n, err = io.ReadFull(conn, buf)
if err != nil {
t.Fatalf("Test %d: reply read: expected = <nil> got = %v", i+1, err)
} else if n != len(buf) {
t.Fatalf("Test %d: reply length: expected = %v got = %v", i+1, len(buf), n)
} else if !bytes.Equal(buf, sslRequiredErrMsg) {
t.Fatalf("Test %d: reply: expected = %v got = %v", i+1, string(sslRequiredErrMsg), string(buf))
}
continue
}
if _, err = bufio.NewReader(conn).ReadString('\n'); err != io.EOF {
t.Errorf("Test %d: reply read: expected = io.EOF, got = %s", i+1, err)
}
conn.Close()
}
listener.Close()
}
}
func TestHTTPListenerAcceptParallel(t *testing.T) {
tlsConfig := getTLSConfig(t)
nonLoopBackIP := getNonLoopBackIP(t)
testCases := []struct {
serverAddrs []string
tlsConfig *tls.Config
reply string
}{
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, nil, "200 OK\n"},
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, "200 OK\n"},
}
connect := func(i int, serverAddr string, secure bool, delay bool, request, reply string) {
var conn net.Conn
var err error
if secure {
conn, err = tls.Dial("tcp", serverAddr, &tls.Config{InsecureSkipVerify: true})
} else {
conn, err = net.Dial("tcp", serverAddr)
}
if err != nil {
fail(t, "Test %d: error: expected = <nil>, got = %v", i+1, err)
}
if delay {
if _, err = io.WriteString(conn, request[:3]); err != nil {
fail(t, "Test %d: request send: expected = <nil>, got = %v", i+1, err)
}
time.Sleep(1 * time.Second)
if _, err = io.WriteString(conn, request[3:]); err != nil {
fail(t, "Test %d: request send: expected = <nil>, got = %v", i+1, err)
}
} else {
if _, err = io.WriteString(conn, request); err != nil {
fail(t, "Test %d: request send: expected = <nil>, got = %v", i+1, err)
}
}
received, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
fail(t, "Test %d: reply read: expected = <nil>, got = %v", i+1, err)
}
if received != reply {
fail(t, "Test %d: reply: expected = %v, got = %v", i+1, reply, received)
}
conn.Close()
}
handleConnection := func(i int, wg *sync.WaitGroup, serverConn net.Conn, request, reply string) {
defer wg.Done()
received, err := bufio.NewReader(serverConn).ReadString('\n')
if err != nil {
fail(t, "Test %d: request read: expected = <nil>, got = %v", i+1, err)
}
if received != request {
fail(t, "Test %d: request: expected = %v, got = %v", i+1, request, received)
}
if _, err := io.WriteString(serverConn, reply); err != nil {
fail(t, "Test %d: reply send: expected = <nil>, got = %v", i+1, err)
}
serverConn.Close()
}
for i, testCase := range testCases {
listener, err := newHTTPListener(
testCase.serverAddrs,
testCase.tlsConfig,
time.Duration(0),
time.Duration(0),
time.Duration(0),
DefaultMaxHeaderBytes,
nil,
nil,
)
if err != nil {
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
}
for _, serverAddr := range listener.Addrs() {
go connect(i, serverAddr.String(), testCase.tlsConfig != nil, true, "GET /1 HTTP/1.0\r\nHost: example.org\r\nr\n", testCase.reply)
go connect(i, serverAddr.String(), testCase.tlsConfig != nil, false, "GET /2 HTTP/1.0\r\nHost: example.org\r\n\r\n", testCase.reply)
var wg sync.WaitGroup
serverConn, err := listener.Accept()
if err != nil {
t.Fatalf("Test %d: accept: expected = <nil>, got = %v", i+1, err)
}
wg.Add(1)
go handleConnection(i, &wg, serverConn, "GET /2 HTTP/1.0\r\n", testCase.reply)
serverConn, err = listener.Accept()
if err != nil {
t.Fatalf("Test %d: accept: expected = <nil>, got = %v", i+1, err)
}
wg.Add(1)
go handleConnection(i, &wg, serverConn, "GET /1 HTTP/1.0\r\n", testCase.reply)
wg.Wait()
}
listener.Close()
}
}
type myTimeoutErr struct {
timeout bool
}
+15 -14
View File
@@ -51,15 +51,15 @@ const (
// Server - extended http.Server supports multiple addresses to serve and enhanced connection handling.
type Server struct {
http.Server
Addrs []string // addresses on which the server listens for new connection.
ShutdownTimeout time.Duration // timeout used for graceful server shutdown.
TCPKeepAliveTimeout time.Duration // timeout used for underneath TCP connection.
UpdateBytesReadFunc func(*http.Request, int) // function to be called to update bytes read in bufConn.
UpdateBytesWrittenFunc func(*http.Request, int) // function to be called to update bytes written in bufConn.
listenerMutex *sync.Mutex // to guard 'listener' field.
listener *httpListener // HTTP listener for all 'Addrs' field.
inShutdown uint32 // indicates whether the server is in shutdown or not
requestCount int32 // counter holds no. of request in progress.
Addrs []string // addresses on which the server listens for new connection.
ShutdownTimeout time.Duration // timeout used for graceful server shutdown.
TCPKeepAliveTimeout time.Duration // timeout used for underneath TCP connection.
UpdateBytesReadFunc func(int) // function to be called to update bytes read in bufConn.
UpdateBytesWrittenFunc func(int) // function to be called to update bytes written in bufConn.
listenerMutex *sync.Mutex // to guard 'listener' field.
listener *httpListener // HTTP listener for all 'Addrs' field.
inShutdown uint32 // indicates whether the server is in shutdown or not
requestCount int32 // counter holds no. of request in progress.
}
// GetRequestCount - returns number of request in progress.
@@ -80,8 +80,6 @@ func (srv *Server) Start() (err error) {
addrs := set.CreateStringSet(srv.Addrs...).ToSlice() // copy and remove duplicates
tcpKeepAliveTimeout := srv.TCPKeepAliveTimeout
updateBytesReadFunc := srv.UpdateBytesReadFunc
updateBytesWrittenFunc := srv.UpdateBytesWrittenFunc
// Create new HTTP listener.
var listener *httpListener
@@ -92,8 +90,8 @@ func (srv *Server) Start() (err error) {
readTimeout,
writeTimeout,
srv.MaxHeaderBytes,
updateBytesReadFunc,
updateBytesWrittenFunc,
srv.UpdateBytesReadFunc,
srv.UpdateBytesWrittenFunc,
)
if err != nil {
return err
@@ -121,6 +119,9 @@ func (srv *Server) Start() (err error) {
srv.listenerMutex.Unlock()
// Start servicing with listener.
if tlsConfig != nil {
return srv.Server.Serve(tls.NewListener(listener, tlsConfig))
}
return srv.Server.Serve(listener)
}
@@ -192,7 +193,7 @@ func NewServer(addrs []string, handler http.Handler, getCert certs.GetCertificat
CipherSuites: defaultCipherSuites,
CurvePreferences: secureCurves,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1"},
NextProtos: []string{"h2", "http/1.1"},
}
tlsConfig.GetCertificate = getCert
}
+1 -1
View File
@@ -100,7 +100,7 @@ func newStorageAPI(endpoint Endpoint) (storage StorageAPI, err error) {
return newPosix(endpoint.Path)
}
return newStorageRESTClient(endpoint), nil
return newStorageRESTClient(endpoint)
}
// Cleanup a directory recursively.
+2 -2
View File
@@ -239,8 +239,8 @@ func testPutObjectPartDiskNotFound(obj ObjectLayer, instanceType string, disks [
if err == nil {
t.Fatalf("Test %s: expected to fail but passed instead", instanceType)
}
// as majority of xl.json are not available, we expect InsufficientReadQuorum while trying to fetch the object quorum
expectedErr := InsufficientReadQuorum{}
// as majority of xl.json are not available, we expect uploadID to be not available.
expectedErr := InvalidUploadID{UploadID: testCase.uploadID}
if err.Error() != expectedErr.Error() {
t.Fatalf("Test %s: expected error %s, got %s instead.", instanceType, expectedErr, err)
}
+82 -28
View File
@@ -1312,6 +1312,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
if objectAPI.IsEncryptionSupported() {
if crypto.IsEncrypted(objInfo.UserDefined) {
objInfo.Size, _ = objInfo.DecryptedSize()
switch {
case crypto.S3.IsEncrypted(objInfo.UserDefined):
w.Header().Set(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
@@ -2093,7 +2094,7 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
return
}
uploadID, partNumberMarker, maxParts, _, s3Error := getObjectResources(r.URL.Query())
uploadID, partNumberMarker, maxParts, encodingType, s3Error := getObjectResources(r.URL.Query())
if s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
@@ -2144,34 +2145,59 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
}
}
response := generateListPartsResponse(listPartsInfo)
response := generateListPartsResponse(listPartsInfo, encodingType)
encodedSuccessResponse := encodeResponse(response)
// Write success response.
writeSuccessResponseXML(w, encodedSuccessResponse)
}
// Send white spaces every 10 seconds to the client till completeMultiPartUpload() is done so that the client does not time out.
// Downside is we might send 200OK and then send error XML. But accoording to S3 spec
// the client is supposed to check for error XML even if it received 200OK. But for erasure this is not a
// problem as completeMultiPartUpload() is quick. Even For FS, it would not be an issue
// as we do background append as and when the parts arrive and completeMultiPartUpload is quick.
// Only in a rare case where parts would be out of order will FS' completeMultiPartUpload() take a longer time.
func sendWhiteSpace(ctx context.Context, w http.ResponseWriter) <-chan struct{} {
doneCh := make(chan struct{})
type whiteSpaceWriter struct {
http.ResponseWriter
http.Flusher
written bool
}
func (w *whiteSpaceWriter) Write(b []byte) (n int, err error) {
n, err = w.ResponseWriter.Write(b)
w.written = true
return
}
func (w *whiteSpaceWriter) WriteHeader(statusCode int) {
if !w.written {
w.ResponseWriter.WriteHeader(statusCode)
}
}
// Send empty whitespaces every 10 seconds to the client till completeMultiPartUpload() is
// done so that the client does not time out. Downside is we might send 200 OK and
// then send error XML. But accoording to S3 spec the client is supposed to check
// for error XML even if it received 200 OK. But for erasure this is not a problem
// as completeMultiPartUpload() is quick. Even For FS, it would not be an issue as
// we do background append as and when the parts arrive and completeMultiPartUpload
// is quick. Only in a rare case where parts would be out of order will
// FS:completeMultiPartUpload() take a longer time.
func sendWhiteSpace(ctx context.Context, w http.ResponseWriter) <-chan bool {
doneCh := make(chan bool)
go func() {
ticker := time.NewTicker(time.Second * 10)
headerWritten := false
for {
select {
case <-ticker.C:
// Write a white space char to the client to prevent client timeouts
// when the server takes a long time to completeMultiPartUpload()
if _, err := w.Write([]byte("\n\r")); err != nil {
logger.LogIf(ctx, err)
return
// Write header if not written yet.
if !headerWritten {
w.Write([]byte(xml.Header))
headerWritten = true
}
// Once header is written keep writing empty spaces
// which are ignored by client SDK XML parsers.
// This occurs when server takes long time to completeMultiPartUpload()
w.Write([]byte(" "))
w.(http.Flusher).Flush()
case doneCh <- struct{}{}:
case doneCh <- headerWritten:
ticker.Stop()
return
}
@@ -2308,31 +2334,54 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
if api.CacheAPI() != nil {
completeMultiPartUpload = api.CacheAPI().CompleteMultipartUpload
}
// This code is specifically to handle the requirements for slow
// complete multipart upload operations on FS mode.
writeErrorResponseWithoutXMLHeader := func(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
switch err.Code {
case "SlowDown", "XMinioServerNotInitialized", "XMinioReadQuorum", "XMinioWriteQuorum":
// Set retry-after header to indicate user-agents to retry request after 120secs.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
w.Header().Set("Retry-After", "120")
}
// Generate error response.
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
w.Header().Get(responseRequestIDKey), w.Header().Get(responseDeploymentIDKey))
encodedErrorResponse, _ := xml.Marshal(errorResponse)
setCommonHeaders(w)
w.Header().Set("Content-Type", string(mimeXML))
w.Write(encodedErrorResponse)
w.(http.Flusher).Flush()
}
w = &whiteSpaceWriter{ResponseWriter: w, Flusher: w.(http.Flusher)}
completeDoneCh := sendWhiteSpace(ctx, w)
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, completeParts, opts)
// Stop writing white spaces to the client. Note that close(doneCh) style is not used as it
// can cause white space to be written after we send XML response in a race condition.
<-completeDoneCh
headerWritten := <-completeDoneCh
if err != nil {
switch oErr := err.(type) {
case PartTooSmall:
// Write part too small error.
writePartSmallErrorResponse(ctx, w, r, oErr)
default:
// Handle all other generic issues.
if headerWritten {
writeErrorResponseWithoutXMLHeader(ctx, w, toAPIError(ctx, err), r.URL)
} else {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
}
return
}
// Get object location.
location := getObjectLocation(r, globalDomainName, bucket, object)
location := getObjectLocation(r, globalDomainNames, bucket, object)
// Generate complete multipart response.
response := generateCompleteMultpartUploadResponse(bucket, object, location, objInfo.ETag)
encodedSuccessResponse := encodeResponse(response)
if err != nil {
writeErrorResponse(ctx, w, toAdminAPIErr(ctx, err), r.URL, guessIsBrowserReq(r))
return
var encodedSuccessResponse []byte
if !headerWritten {
encodedSuccessResponse = encodeResponse(response)
} else {
encodedSuccessResponse, err = xml.Marshal(response)
if err != nil {
writeErrorResponseWithoutXMLHeader(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
// Set etag.
@@ -2347,6 +2396,11 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
host, port = "", ""
}
if objectAPI.IsEncryptionSupported() {
if crypto.IsEncrypted(objInfo.UserDefined) {
objInfo.Size, _ = objInfo.DecryptedSize()
}
}
// Notify object created event.
sendEvent(eventArgs{
EventName: event.ObjectCreatedCompleteMultipartUpload,
+3 -3
View File
@@ -2740,9 +2740,9 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
accessKey: credentials.AccessKey,
secretKey: credentials.SecretKey,
expectedContent: encodeResponse(completeMultipartAPIError{int64(4), int64(5242880), 1, "e2fc714c4727ee9395f324cd2e7f331f",
getAPIErrorResponse(ctx, toAPIError(ctx, PartTooSmall{PartNumber: 1}),
getGetObjectURL("", bucketName, objectName), "", "")}),
expectedContent: encodeResponse(getAPIErrorResponse(ctx,
toAPIError(ctx, PartTooSmall{PartNumber: 1}),
getGetObjectURL("", bucketName, objectName), "", "")),
expectedRespStatus: http.StatusBadRequest,
},
// Test case - 5.
+13 -4
View File
@@ -73,6 +73,7 @@ type posix struct {
diskMount bool // indicates if the path is an actual mount.
driveSync bool // indicates if the backend is synchronous.
diskFileInfo os.FileInfo
// Disk usage metrics
stopUsageCh chan struct{}
}
@@ -177,7 +178,10 @@ func newPosix(path string) (*posix, error) {
if path, err = getValidPath(path); err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
p := &posix{
connected: true,
diskPath: path,
@@ -188,8 +192,9 @@ func newPosix(path string) (*posix, error) {
return &b
},
},
stopUsageCh: make(chan struct{}),
diskMount: mountinfo.IsLikelyMountPoint(path),
stopUsageCh: make(chan struct{}),
diskFileInfo: fi,
diskMount: mountinfo.IsLikelyMountPoint(path),
}
var pf BoolFlag
@@ -351,7 +356,7 @@ func (s *posix) checkDiskFound() (err error) {
if !s.IsOnline() {
return errDiskNotFound
}
_, err = os.Stat(s.diskPath)
fi, err := os.Stat(s.diskPath)
if err != nil {
switch {
case os.IsNotExist(err):
@@ -364,6 +369,10 @@ func (s *posix) checkDiskFound() (err error) {
return err
}
}
if !os.SameFile(s.diskFileInfo, fi) {
s.connected = false
return errDiskNotFound
}
return nil
}
+12 -6
View File
@@ -28,6 +28,7 @@ import (
"time"
xhttp "github.com/minio/minio/cmd/http"
"golang.org/x/net/http2"
)
// DefaultRESTTimeout - default RPC timeout is one minute.
@@ -95,7 +96,7 @@ func newCustomDialContext(timeout time.Duration) func(ctx context.Context, netwo
}
// NewClient - returns new REST client.
func NewClient(url *url.URL, tlsConfig *tls.Config, timeout time.Duration, newAuthToken func() string) *Client {
func NewClient(url *url.URL, tlsConfig *tls.Config, timeout time.Duration, newAuthToken func() string) (*Client, error) {
// Transport is exactly same as Go default in https://golang.org/pkg/net/http/#RoundTripper
// except custom DialContext and TLSClientConfig.
tr := &http.Transport{
@@ -103,17 +104,22 @@ func NewClient(url *url.URL, tlsConfig *tls.Config, timeout time.Duration, newAu
DialContext: newCustomDialContext(timeout),
MaxIdleConnsPerHost: 4096,
MaxIdleConns: 4096,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 120 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
TLSClientConfig: tlsConfig,
DisableCompression: true,
}
if tlsConfig != nil {
// If TLS is enabled configure http2
if err := http2.ConfigureTransport(tr); err != nil {
return nil, err
}
}
return &Client{
httpClient: &http.Client{Transport: tr},
httpIdleConnsCloser: tr.CloseIdleConnections,
url: url,
newAuthToken: newAuthToken,
}
}, nil
}
+2 -2
View File
@@ -55,8 +55,8 @@ var globalHandlers = []HandlerFunc{
setBucketForwardingHandler,
// Ratelimit the incoming requests using a token bucket algorithm
setRateLimitHandler,
// Validate all the incoming paths.
setPathValidityHandler,
// Validate all the incoming requests.
setRequestValidityHandler,
// Network statistics
setHTTPStatsHandler,
// Limits all requests size to a maximum fixed limit
+6 -1
View File
@@ -268,9 +268,14 @@ func NewRPCClient(args RPCClientArgs) (*RPCClient, error) {
return nil, err
}
rpcClient, err := xrpc.NewClient(args.ServiceURL, args.TLSConfig, defaultRPCTimeout)
if err != nil {
return nil, err
}
return &RPCClient{
args: args,
authToken: args.NewAuthTokenFunc(),
rpcClient: xrpc.NewClient(args.ServiceURL, args.TLSConfig, defaultRPCTimeout),
rpcClient: rpcClient,
}, nil
}
+32 -21
View File
@@ -29,6 +29,7 @@ import (
xhttp "github.com/minio/minio/cmd/http"
xnet "github.com/minio/minio/pkg/net"
"golang.org/x/net/http2"
)
// DefaultRPCTimeout - default RPC timeout is one minute.
@@ -36,8 +37,9 @@ const DefaultRPCTimeout = 1 * time.Minute
// Client - http based RPC client.
type Client struct {
httpClient *http.Client
serviceURL *xnet.URL
httpClient *http.Client
httpIdleConnsCloser func()
serviceURL *xnet.URL
}
// Call - calls service method on RPC server.
@@ -87,8 +89,11 @@ func (client *Client) Call(serviceMethod string, args, reply interface{}) error
return gobDecode(callResponse.ReplyBytes, reply)
}
// Close - does nothing and presents for interface compatibility.
// Close closes all idle connections of the underlying http client
func (client *Client) Close() error {
if client.httpIdleConnsCloser != nil {
client.httpIdleConnsCloser()
}
return nil
}
@@ -110,23 +115,29 @@ func newCustomDialContext(timeout time.Duration) func(ctx context.Context, netwo
}
// NewClient - returns new RPC client.
func NewClient(serviceURL *xnet.URL, tlsConfig *tls.Config, timeout time.Duration) *Client {
return &Client{
httpClient: &http.Client{
// Transport is exactly same as Go default in https://golang.org/pkg/net/http/#RoundTripper
// except custom DialContext and TLSClientConfig.
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: newCustomDialContext(timeout),
MaxIdleConnsPerHost: 4096,
MaxIdleConns: 4096,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: tlsConfig,
DisableCompression: true,
},
},
serviceURL: serviceURL,
func NewClient(serviceURL *xnet.URL, tlsConfig *tls.Config, timeout time.Duration) (*Client, error) {
// Transport is exactly same as Go default in https://golang.org/pkg/net/http/#RoundTripper
// except custom DialContext and TLSClientConfig.
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: newCustomDialContext(timeout),
MaxIdleConnsPerHost: 4096,
MaxIdleConns: 4096,
IdleConnTimeout: 120 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
TLSClientConfig: tlsConfig,
DisableCompression: true,
}
if tlsConfig != nil {
// If TLS is enabled configure http2
if err := http2.ConfigureTransport(tr); err != nil {
return nil, err
}
}
return &Client{
httpClient: &http.Client{Transport: tr},
httpIdleConnsCloser: tr.CloseIdleConnections,
serviceURL: serviceURL,
}, nil
}
+4 -1
View File
@@ -39,7 +39,10 @@ func TestClientCall(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error %v", err)
}
rpcClient := NewClient(url, nil, DefaultRPCTimeout)
rpcClient, err := NewClient(url, nil, DefaultRPCTimeout)
if err != nil {
t.Fatalf("NewClient initialization error %v", err)
}
var reply int
var boolReply bool
+50 -53
View File
@@ -1658,62 +1658,59 @@ func (s *TestSuiteCommon) TestListObjectsHandler(c *check) {
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
buffer1 := bytes.NewReader([]byte("Hello World"))
request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, "bar"),
int64(buffer1.Len()), buffer1, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
for _, objectName := range []string{"foo bar 1", "foo bar 2"} {
buffer := bytes.NewReader([]byte("Hello World"))
request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, objectName),
int64(buffer.Len()), buffer, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
client = http.Client{Transport: s.transport}
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
}
// create listObjectsV1 request with valid parameters
request, err = newTestSignedRequest("GET", getListObjectsV1URL(s.endPoint, bucketName, "1000"),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
// execute the HTTP request.
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
var testCases = []struct {
getURL string
expectedStrings []string
}{
{getListObjectsV1URL(s.endPoint, bucketName, "", "1000", ""), []string{"<Key>foo bar 1</Key>", "<Key>foo bar 2</Key>"}},
{getListObjectsV1URL(s.endPoint, bucketName, "", "1000", "url"), []string{"<Key>foo+bar+1</Key>", "<Key>foo+bar+2</Key>"}},
{getListObjectsV2URL(s.endPoint, bucketName, "", "1000", "", ""),
[]string{
"<Key>foo bar 1</Key>",
"<Key>foo bar 2</Key>",
"<Owner><ID></ID><DisplayName></DisplayName></Owner>",
},
},
{getListObjectsV2URL(s.endPoint, bucketName, "", "1000", "true", ""),
[]string{
"<Key>foo bar 1</Key>",
"<Key>foo bar 2</Key>",
fmt.Sprintf("<Owner><ID>%s</ID><DisplayName></DisplayName></Owner>", globalMinioDefaultOwnerID),
},
},
{getListObjectsV2URL(s.endPoint, bucketName, "", "1000", "", "url"), []string{"<Key>foo+bar+1</Key>", "<Key>foo+bar+2</Key>"}},
}
getContent, err := ioutil.ReadAll(response.Body)
c.Assert(err, nil)
c.Assert(strings.Contains(string(getContent), "<Key>bar</Key>"), true)
for i, testCase := range testCases {
// create listObjectsV1 request with valid parameters
request, err = newTestSignedRequest("GET", testCase.getURL, 0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
// execute the HTTP request.
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
// create listObjectsV2 request with valid parameters
request, err = newTestSignedRequest("GET", getListObjectsV2URL(s.endPoint, bucketName, "1000", ""),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
// execute the HTTP request.
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
getContent, err = ioutil.ReadAll(response.Body)
c.Assert(err, nil)
c.Assert(strings.Contains(string(getContent), "<Key>bar</Key>"), true)
c.Assert(strings.Contains(string(getContent), "<Owner><ID></ID><DisplayName></DisplayName></Owner>"), true)
// create listObjectsV2 request with valid parameters and fetch-owner activated
request, err = newTestSignedRequest("GET", getListObjectsV2URL(s.endPoint, bucketName, "1000", "true"),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
// execute the HTTP request.
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
getContent, err = ioutil.ReadAll(response.Body)
c.Assert(err, nil)
c.Assert(strings.Contains(string(getContent), "<Key>bar</Key>"), true)
c.Assert(strings.Contains(string(getContent), fmt.Sprintf("<Owner><ID>%s</ID><DisplayName></DisplayName></Owner>",
globalMinioDefaultOwnerID)), true)
getContent, err := ioutil.ReadAll(response.Body)
c.Assert(err, nil)
fmt.Printf("Test %d: %+v vs %+v\n", i+1, string(getContent), testCase.expectedStrings)
for _, expectedStr := range testCase.expectedStrings {
c.Assert(strings.Contains(string(getContent), expectedStr), true)
}
}
}
// TestListObjectsHandlerErrors - Setting invalid parameters to List Objects
@@ -1733,7 +1730,7 @@ func (s *TestSuiteCommon) TestListObjectsHandlerErrors(c *check) {
c.Assert(response.StatusCode, http.StatusOK)
// create listObjectsV1 request with invalid value of max-keys parameter. max-keys is set to -2.
request, err = newTestSignedRequest("GET", getListObjectsV1URL(s.endPoint, bucketName, "-2"),
request, err = newTestSignedRequest("GET", getListObjectsV1URL(s.endPoint, bucketName, "", "-2", ""),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
@@ -1744,7 +1741,7 @@ func (s *TestSuiteCommon) TestListObjectsHandlerErrors(c *check) {
verifyError(c, response, "InvalidArgument", "Argument maxKeys must be an integer between 0 and 2147483647", http.StatusBadRequest)
// create listObjectsV2 request with invalid value of max-keys parameter. max-keys is set to -2.
request, err = newTestSignedRequest("GET", getListObjectsV2URL(s.endPoint, bucketName, "-2", ""),
request, err = newTestSignedRequest("GET", getListObjectsV2URL(s.endPoint, bucketName, "", "-2", "", ""),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
client = http.Client{Transport: s.transport}
+2 -2
View File
@@ -163,7 +163,7 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
return ErrExpiredPresignRequest
}
encodedResource, err = getResource(encodedResource, r.Host, globalDomainName)
encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames)
if err != nil {
return ErrInvalidRequest
}
@@ -257,7 +257,7 @@ func doesSignV2Match(r *http.Request) APIErrorCode {
return ErrInvalidQueryParams
}
encodedResource, err = getResource(encodedResource, r.Host, globalDomainName)
encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames)
if err != nil {
return ErrInvalidRequest
}
+9 -5
View File
@@ -34,7 +34,6 @@ import (
"strings"
"github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/cmd/rest"
xnet "github.com/minio/minio/pkg/net"
)
@@ -381,9 +380,11 @@ func (client *storageRESTClient) Close() error {
}
// Returns a storage rest client.
func newStorageRESTClient(endpoint Endpoint) *storageRESTClient {
func newStorageRESTClient(endpoint Endpoint) (*storageRESTClient, error) {
host, err := xnet.ParseHost(endpoint.Host)
logger.FatalIf(err, "Unable to parse storage Host")
if err != nil {
return nil, err
}
scheme := "http"
if globalIsSSL {
@@ -404,8 +405,11 @@ func newStorageRESTClient(endpoint Endpoint) *storageRESTClient {
}
}
restClient := rest.NewClient(serverURL, tlsConfig, storageRESTTimeout, newAuthToken)
restClient, err := rest.NewClient(serverURL, tlsConfig, storageRESTTimeout, newAuthToken)
if err != nil {
return nil, err
}
client := &storageRESTClient{endpoint: endpoint, restClient: restClient, connected: true}
client.connected = client.getInstanceID() == nil
return client
return client, nil
}
+4 -1
View File
@@ -509,7 +509,10 @@ func newStorageRESTHTTPServerClient(t *testing.T) (*httptest.Server, *storageRES
}
registerStorageRESTHandlers(router, EndpointList{endpoint})
restClient := newStorageRESTClient(endpoint)
restClient, err := newStorageRESTClient(endpoint)
if err != nil {
t.Fatalf("newStorageRESTClient failed for %v, with error %s", endpoint, err)
}
prevGlobalServerConfig := globalServerConfig
globalServerConfig = newServerConfig()
+17 -13
View File
@@ -22,12 +22,11 @@ import (
)
// writeSTSErrorRespone writes error headers
func writeSTSErrorResponse(w http.ResponseWriter, errorCode STSErrorCode) {
stsError := getSTSError(errorCode)
func writeSTSErrorResponse(w http.ResponseWriter, err STSError) {
// Generate error response.
stsErrorResponse := getSTSErrorResponse(stsError)
stsErrorResponse := getSTSErrorResponse(err, w.Header().Get(responseRequestIDKey))
encodedErrorResponse := encodeResponse(stsErrorResponse)
writeResponse(w, stsError.HTTPStatusCode, encodedErrorResponse, mimeXML)
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
}
// STSError structure
@@ -64,9 +63,19 @@ const (
ErrSTSInternalError
)
type stsErrorCodeMap map[STSErrorCode]STSError
func (e stsErrorCodeMap) ToSTSErr(errCode STSErrorCode) STSError {
apiErr, ok := e[errCode]
if !ok {
return e[ErrSTSInternalError]
}
return apiErr
}
// error code to STSError structure, these fields carry respective
// descriptions for all the error responses.
var stsErrCodeResponse = map[STSErrorCode]STSError{
var stsErrCodes = stsErrorCodeMap{
ErrSTSMissingParameter: {
Code: "MissingParameter",
Description: "A required parameter for the specified action is not supplied.",
@@ -109,17 +118,12 @@ var stsErrCodeResponse = map[STSErrorCode]STSError{
},
}
// getSTSError provides STS Error for input STS error code.
func getSTSError(code STSErrorCode) STSError {
return stsErrCodeResponse[code]
}
// getErrorResponse gets in standard error and resource value and
// getSTSErrorResponse gets in standard error and
// provides a encodable populated response values
func getSTSErrorResponse(err STSError) STSErrorResponse {
func getSTSErrorResponse(err STSError, requestID string) STSErrorResponse {
errRsp := STSErrorResponse{}
errRsp.Error.Code = err.Code
errRsp.Error.Message = err.Description
errRsp.RequestID = "3L137"
errRsp.RequestID = requestID
return errRsp
}
+11 -11
View File
@@ -70,13 +70,13 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
// Parse the incoming form data.
if err := r.ParseForm(); err != nil {
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
return
}
if r.Form.Get("Version") != stsAPIVersion {
logger.LogIf(ctx, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion))
writeSTSErrorResponse(w, ErrSTSMissingParameter)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSMissingParameter))
return
}
@@ -85,7 +85,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
case clientGrants, webIdentity:
default:
logger.LogIf(ctx, fmt.Errorf("Unsupported action %s", action))
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
return
}
@@ -93,14 +93,14 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
defer logger.AuditLog(w, r, action, nil)
if globalIAMValidators == nil {
writeSTSErrorResponse(w, ErrSTSNotInitialized)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSNotInitialized))
return
}
v, err := globalIAMValidators.Get("jwt")
if err != nil {
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
return
}
@@ -115,17 +115,17 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
case validator.ErrTokenExpired:
switch action {
case clientGrants:
writeSTSErrorResponse(w, ErrSTSClientGrantsExpiredToken)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSClientGrantsExpiredToken))
case webIdentity:
writeSTSErrorResponse(w, ErrSTSWebIdentityExpiredToken)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSWebIdentityExpiredToken))
}
return
case validator.ErrInvalidDuration:
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
return
}
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
return
}
@@ -133,7 +133,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
if err != nil {
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, ErrSTSInternalError)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInternalError))
return
}
@@ -154,7 +154,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
// Set the newly generated credentials.
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, ErrSTSInternalError)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInternalError))
return
}
+10 -4
View File
@@ -1458,16 +1458,19 @@ func getBucketLocationURL(endPoint, bucketName string) string {
}
// return URL for listing objects in the bucket with V1 legacy API.
func getListObjectsV1URL(endPoint, bucketName string, maxKeys string) string {
func getListObjectsV1URL(endPoint, bucketName, prefix, maxKeys, encodingType string) string {
queryValue := url.Values{}
if maxKeys != "" {
queryValue.Set("max-keys", maxKeys)
}
return makeTestTargetURL(endPoint, bucketName, "", queryValue)
if encodingType != "" {
queryValue.Set("encoding-type", encodingType)
}
return makeTestTargetURL(endPoint, bucketName, prefix, queryValue)
}
// return URL for listing objects in the bucket with V2 API.
func getListObjectsV2URL(endPoint, bucketName string, maxKeys string, fetchOwner string) string {
func getListObjectsV2URL(endPoint, bucketName, prefix, maxKeys, fetchOwner, encodingType string) string {
queryValue := url.Values{}
queryValue.Set("list-type", "2") // Enables list objects V2 URL.
if maxKeys != "" {
@@ -1476,7 +1479,10 @@ func getListObjectsV2URL(endPoint, bucketName string, maxKeys string, fetchOwner
if fetchOwner != "" {
queryValue.Set("fetch-owner", fetchOwner)
}
return makeTestTargetURL(endPoint, bucketName, "", queryValue)
if encodingType != "" {
queryValue.Set("encoding-type", encodingType)
}
return makeTestTargetURL(endPoint, bucketName, prefix, queryValue)
}
// return URL for a new multipart upload.
+2 -78
View File
@@ -1323,82 +1323,6 @@ func (s *xlSets) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error) {
return listBuckets, nil
}
// Returns function "listDir" of the type listDirFunc.
// isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry.
// disks - used for doing disk.ListDir(). Sets passes set of disks.
func listDirSetsHealFactory(isLeaf isLeafFunc, sets ...[]StorageAPI) listDirFunc {
listDirInternal := func(bucket, prefixDir, prefixEntry string, disks []StorageAPI) (mergedEntries []string) {
for _, disk := range disks {
if disk == nil {
continue
}
var entries []string
var newEntries []string
var err error
entries, err = disk.ListDir(bucket, prefixDir, -1)
if err != nil {
continue
}
// Filter entries that have the prefix prefixEntry.
entries = filterMatchingPrefix(entries, prefixEntry)
// isLeaf() check has to happen here so that
// trailing "/" for objects can be removed.
for i, entry := range entries {
if isLeaf(bucket, pathJoin(prefixDir, entry)) {
entries[i] = strings.TrimSuffix(entry, slashSeparator)
}
}
// Find elements in entries which are not in mergedEntries
for _, entry := range entries {
idx := sort.SearchStrings(mergedEntries, entry)
// if entry is already present in mergedEntries don't add.
if idx < len(mergedEntries) && mergedEntries[idx] == entry {
continue
}
newEntries = append(newEntries, entry)
}
if len(newEntries) > 0 {
// Merge the entries and sort it.
mergedEntries = append(mergedEntries, newEntries...)
sort.Strings(mergedEntries)
}
}
return mergedEntries
}
// listDir - lists all the entries at a given prefix and given entry in the prefix.
listDir := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool) {
for _, disks := range sets {
entries := listDirInternal(bucket, prefixDir, prefixEntry, disks)
var newEntries []string
// Find elements in entries which are not in mergedEntries
for _, entry := range entries {
idx := sort.SearchStrings(mergedEntries, entry)
// if entry is already present in mergedEntries don't add.
if idx < len(mergedEntries) && mergedEntries[idx] == entry {
continue
}
newEntries = append(newEntries, entry)
}
if len(newEntries) > 0 {
// Merge the entries and sort it.
mergedEntries = append(mergedEntries, newEntries...)
sort.Strings(mergedEntries)
}
}
return mergedEntries, false
}
return listDir
}
// listObjectsHeal - wrapper function implemented over file tree walk.
func (s *xlSets) listObjectsHeal(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
// Default is recursive, if delimiter is set then list non recursive.
@@ -1434,8 +1358,8 @@ func (s *xlSets) listObjectsHeal(ctx context.Context, bucket, prefix, marker, de
setDisks = append(setDisks, set.getLoadBalancedDisks())
}
listDir := listDirSetsHealFactory(isLeaf, setDisks...)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, nil, isLeafDir, endWalkCh)
listDir := listDirSetsFactory(ctx, isLeaf, isLeafDir, setDisks...)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
}
var objInfos []ObjectInfo
+33 -36
View File
@@ -20,8 +20,6 @@ import (
"context"
"path"
"sync"
"github.com/minio/minio/cmd/logger"
)
// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
@@ -52,31 +50,44 @@ func (xl xlObjects) parentDirIsObject(ctx context.Context, bucket, parent string
return isParentDirObject(parent)
}
var xlTreeWalkIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errVolumeNotFound, errFileNotFound)
// isObject - returns `true` if the prefix is an object i.e if
// `xl.json` exists at the leaf, false otherwise.
func (xl xlObjects) isObject(bucket, prefix string) (ok bool) {
for _, disk := range xl.getLoadBalancedDisks() {
var errs = make([]error, len(xl.getDisks()))
var wg sync.WaitGroup
for index, disk := range xl.getDisks() {
if disk == nil {
continue
}
// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
_, err := disk.StatFile(bucket, path.Join(prefix, xlMetaJSONFile))
if err == nil {
return true
}
// Ignore for file not found, disk not found or faulty disk.
if IsErrIgnored(err, xlTreeWalkIgnoredErrs...) {
continue
}
} // Exhausted all disks - return false.
return false
wg.Add(1)
go func(index int, disk StorageAPI) {
defer wg.Done()
// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
fi, err := disk.StatFile(bucket, path.Join(prefix, xlMetaJSONFile))
if err != nil {
errs[index] = err
return
}
if fi.Size == 0 {
errs[index] = errCorruptedFormat
return
}
}(index, disk)
}
wg.Wait()
// NOTE: Observe we are not trying to read `xl.json` and figure out the actual
// quorum intentionally, but rely on the default case scenario. Actual quorum
// verification will happen by top layer by using getObjectInfo() and will be
// ignored if necessary.
readQuorum := len(xl.getDisks()) / 2
return reduceReadQuorumErrs(context.Background(), errs, objectOpIgnoredErrs, readQuorum) == nil
}
// isObjectDir returns if the specified path represents an empty directory.
func (xl xlObjects) isObjectDir(bucket, prefix string) (ok bool) {
var objectDirs = make([]bool, len(xl.getDisks()))
var errs = make([]error, len(xl.getDisks()))
var wg sync.WaitGroup
for index, disk := range xl.getDisks() {
@@ -92,7 +103,10 @@ func (xl xlObjects) isObjectDir(bucket, prefix string) (ok bool) {
errs[index] = err
return
}
objectDirs[index] = len(entries) == 0
if len(entries) > 0 {
errs[index] = errVolumeNotEmpty
return
}
}(index, disk)
}
@@ -100,22 +114,5 @@ func (xl xlObjects) isObjectDir(bucket, prefix string) (ok bool) {
readQuorum := len(xl.getDisks()) / 2
err := reduceReadQuorumErrs(context.Background(), errs, xlTreeWalkIgnoredErrs, readQuorum)
if err != nil {
reqInfo := &logger.ReqInfo{BucketName: bucket}
reqInfo.AppendTags("prefix", prefix)
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
return false
}
for _, ok := range objectDirs {
if !ok {
// We perhaps found some entries in a prefix.
return false
}
}
// Return true if and only if the all directories are empty.
return true
return reduceReadQuorumErrs(context.Background(), errs, objectOpIgnoredErrs, readQuorum) == nil
}
+175
View File
@@ -0,0 +1,175 @@
/*
* Minio Cloud Storage, (C) 2019 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bufio"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"time"
xhttp "github.com/minio/minio/cmd/http"
)
const (
initGraceTime = 300
healthPath = "/minio/health/live"
timeout = time.Duration(30 * time.Second)
minioProcess = "minio"
)
// returns container boot time by finding
// modtime of /proc/1 directory
func getStartTime() time.Time {
di, err := os.Stat("/proc/1")
if err != nil {
// Cant stat proc dir successfully, exit with error
log.Fatal(err.Error())
}
return di.ModTime()
}
// Returns the ip:port of the Minio process
// running in the container
func findEndpoint() string {
cmd := exec.Command("netstat", "-ntlp")
stdout, err := cmd.StdoutPipe()
if err != nil {
// error getting stdout pipe
log.Fatal(err.Error())
}
if err = cmd.Start(); err != nil {
// error starting the command
log.Fatal(err.Error())
}
// split netstat output in rows
scanner := bufio.NewScanner(stdout)
scanner.Split(bufio.ScanLines)
// loop over the rows to find Minio process
for scanner.Scan() {
if strings.Contains(scanner.Text(), minioProcess) {
line := scanner.Text()
newLine := strings.Replace(line, ":::", "127.0.0.1:", 1)
fields := strings.Fields(newLine)
// index 3 in the row has the Local address
// find the last index of ":" - address will
// have port number after this index
i := strings.LastIndex(fields[3], ":")
// split address and port
addr := fields[3][:i]
port := fields[3][i+1:]
// add surrounding [] for ip6 address
if strings.Count(addr, ":") > 0 {
addr = strings.Join([]string{"[", addr, "]"}, "")
}
// wait for cmd to complete before return
if err = cmd.Wait(); err != nil {
// command failed to run
log.Fatal(err.Error())
}
// return joint address and port
return strings.Join([]string{addr, port}, ":")
}
}
if err = cmd.Wait(); err != nil {
// command failed to run
log.Fatal(err.Error())
}
// minio process not found, exit with error
os.Exit(1)
return ""
}
func main() {
startTime := getStartTime()
// In distributed environment like Swarm, traffic is routed
// to a container only when it reports a `healthy` status. So, we exit
// with 0 to ensure healthy status till distributed Minio starts (120s).
// Refer: https://github.com/moby/moby/pull/28938#issuecomment-301753272
if (time.Now().Sub(startTime) / time.Second) < initGraceTime {
os.Exit(0)
} else {
endPoint := findEndpoint()
u, err := url.Parse(fmt.Sprintf("http://%s%s", endPoint, healthPath))
if err != nil {
// Could not parse URL successfully
log.Fatal(err.Error())
}
// Minio server may be using self-signed or CA certificates. To avoid
// making Docker setup complicated, we skip verifying certificates here.
// This is because, following request tests for health status within
// containerized environment, i.e. requests are always made to the Minio
// server running on the same host.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr, Timeout: timeout}
resp, err := client.Get(u.String())
if err != nil {
// GET failed exit
log.Fatal(err.Error())
}
if resp.StatusCode == http.StatusOK {
// Drain any response.
xhttp.DrainBody(resp.Body)
// exit with success
os.Exit(0)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
// Drain any response.
xhttp.DrainBody(resp.Body)
// GET failed exit
log.Fatal(err.Error())
}
bodyString := string(bodyBytes)
// Drain any response.
xhttp.DrainBody(resp.Body)
// This means sever is configured with https
if resp.StatusCode == http.StatusForbidden && bodyString == "SSL required" {
// Try with https
u.Scheme = "https"
resp, err = client.Get(u.String())
if err != nil {
// GET failed exit
log.Fatal(err.Error())
}
if resp.StatusCode == http.StatusOK {
// Drain any response.
xhttp.DrainBody(resp.Body)
// exit with success
os.Exit(0)
}
// Drain any response.
xhttp.DrainBody(resp.Body)
}
}
// Execution reaching here means none of
// the success cases were satisfied
os.Exit(1)
}
-56
View File
@@ -1,56 +0,0 @@
#!/bin/sh
#
# Minio Cloud Storage, (C) 2017 Minio, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -x
_init () {
scheme="http://"
address="$(netstat -nplt 2>/dev/null | awk ' /(.*\/minio)/ { gsub(":::","127.0.0.1:",$4); print $4}')"
resource="/minio/health/live"
start=$(stat -c "%Y" /proc/1)
}
healthcheck_main () {
# In distributed environment like Swarm, traffic is routed
# to a container only when it reports a `healthy` status. So, we exit
# with 0 to ensure healthy status till distributed Minio starts (120s).
#
# Refer: https://github.com/moby/moby/pull/28938#issuecomment-301753272
if [ $(( $(date +%s) - start )) -lt 120 ]; then
exit 0
else
# Get the http response code
http_response=$(curl -s -k -o /dev/null -w "%{http_code}" ${scheme}${address}${resource})
# Get the http response body
http_response_body=$(curl -k -s ${scheme}${address}${resource})
# server returns response 403 and body "SSL required" if non-TLS
# connection is attempted on a TLS-configured server. Change
# the scheme and try again
if [ "$http_response" = "403" ] && \
[ "$http_response_body" = "SSL required" ]; then
scheme="https://"
http_response=$(curl -s -k -o /dev/null -w "%{http_code}" ${scheme}${address}${resource})
fi
# If http_response is 200 - server is up.
[ "$http_response" = "200" ]
fi
}
_init && healthcheck_main
+5 -4
View File
@@ -163,6 +163,7 @@ The Minio server configuration file is stored on the backend in json format. The
| `username` | _string_ | Username to connect to the MQTT server (if required) |
| `password` | _string_ | Password to connect to the MQTT server (if required) |
| `queueDir` | _string_ | Persistent store for events when MQTT broker is offline |
| `queueLimit` | _int_ | Set the maximum event limit for the persistent store. The default limit is 10000 |
An example configuration for MQTT is shown below:
@@ -175,12 +176,12 @@ An example configuration for MQTT is shown below:
"qos": 1,
"username": "",
"password": "",
"queueDir": ""
"queueDir": "",
"queueLimit": 0
}
}
```
MQTT supports persistent event store in the client-side. The persistent store will backup events when the MQTT broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field in the mqtt config. For eg, the `queueDir` can be `/home/events`.
Minio supports persistent event store. The persistent store will backup events when the MQTT broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
```sh
@@ -549,7 +550,7 @@ Minio server also supports [NATS Streaming mode](http://nats.io/documentation/st
"password": "yoursecret",
"token": "",
"secure": false,
"pingInterval": 0
"pingInterval": 0,
"streaming": {
"enable": true,
"clusterID": "test-cluster",
+6
View File
@@ -156,6 +156,12 @@ export MINIO_DOMAIN=mydomain.com
minio server /data
```
For advanced use cases `MINIO_DOMAIN` environment variable supports multiple-domains with comma separated values.
```sh
export MINIO_DOMAIN=sub1.mydomain.com,sub2.mydomain.com
minio server /data
```
### Drive Sync
By default, Minio writes to disk in synchronous mode for all metadata operations. Set `MINIO_DRIVE_SYNC` environment variable to enable synchronous mode for all data operations as well.
+2 -1
View File
@@ -86,7 +86,8 @@
"password": "",
"reconnectInterval": 0,
"keepAliveInterval": 0,
"queueDir": ""
"queueDir": "",
"queueLimit": 0
}
},
"mysql": {
@@ -5,7 +5,7 @@ version: '2'
# 9001 through 9004.
services:
minio1:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
volumes:
- data1:/data
ports:
@@ -15,7 +15,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio2:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
volumes:
- data2:/data
ports:
@@ -25,7 +25,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio3:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
volumes:
- data3:/data
ports:
@@ -35,7 +35,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio4:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
volumes:
- data4:/data
ports:
@@ -2,7 +2,7 @@ version: '3.1'
services:
minio1:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio1
volumes:
- minio1-data:/export
@@ -24,7 +24,7 @@ services:
- access_key
minio2:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio2
volumes:
- minio2-data:/export
@@ -46,7 +46,7 @@ services:
- access_key
minio3:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio3
volumes:
- minio3-data:/export
@@ -68,7 +68,7 @@ services:
- access_key
minio4:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio4
volumes:
- minio4-data:/export
@@ -2,7 +2,7 @@ version: '3'
services:
minio1:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio1
volumes:
- minio1-data:/export
@@ -24,7 +24,7 @@ services:
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
minio2:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio2
volumes:
- minio2-data:/export
@@ -46,7 +46,7 @@ services:
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
minio3:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio3
volumes:
- minio3-data:/export
@@ -68,7 +68,7 @@ services:
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
minio4:
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
hostname: minio4
volumes:
- minio4-data:/export
@@ -30,7 +30,7 @@ spec:
value: "minio"
- name: MINIO_SECRET_KEY
value: "minio123"
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
# Unfortunately you must manually define each server. Perhaps autodiscovery via DNS can be implemented in the future.
args:
- server
@@ -21,7 +21,7 @@ spec:
value: "minio"
- name: MINIO_SECRET_KEY
value: "minio123"
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
args:
- server
- http://minio-0.minio.default.svc.cluster.local/data
@@ -21,7 +21,7 @@ spec:
containers:
- name: minio
# Pulls the default Minio image from Docker Hub
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
args:
- gateway
- gcs
@@ -29,7 +29,7 @@ spec:
- name: data
mountPath: "/data"
# Pulls the lastest Minio image from Docker Hub
image: minio/minio:RELEASE.2019-02-12T21-58-47Z
image: minio/minio:RELEASE.2019-02-20T22-44-29Z
args:
- server
- /data
+60 -38
View File
@@ -47,14 +47,30 @@ func newCoreDNSMsg(bucket, ip string, port int, ttl uint32) ([]byte, error) {
// Retrieves list of DNS entries for the domain.
func (c *coreDNS) List() ([]SrvRecord, error) {
key := msg.Path(fmt.Sprintf("%s.", c.domainName), defaultPrefixPath)
return c.list(key)
var srvRecords []SrvRecord
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.", domainName), defaultPrefixPath)
records, err := c.list(key)
if err != nil {
return nil, err
}
srvRecords = append(srvRecords, records...)
}
return srvRecords, nil
}
// Retrieves DNS records for a bucket.
func (c *coreDNS) Get(bucket string) ([]SrvRecord, error) {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath)
return c.list(key)
var srvRecords []SrvRecord
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), defaultPrefixPath)
records, err := c.list(key)
if err != nil {
return nil, err
}
srvRecords = append(srvRecords, records...)
}
return srvRecords, nil
}
// Retrieves list of entries under the key passed.
@@ -92,12 +108,14 @@ func (c *coreDNS) list(key string) ([]SrvRecord, error) {
//
// In all other situations when we want to list all DNS records,
// which is handled in the else clause.
if key != msg.Path(fmt.Sprintf(".%s.", c.domainName), defaultPrefixPath) {
if srvRecord.Key == "/" {
for _, domainName := range c.domainNames {
if key != msg.Path(fmt.Sprintf(".%s.", domainName), defaultPrefixPath) {
if srvRecord.Key == "/" {
srvRecords = append(srvRecords, srvRecord)
}
} else {
srvRecords = append(srvRecords, srvRecord)
}
} else {
srvRecords = append(srvRecords, srvRecord)
}
}
@@ -117,16 +135,18 @@ func (c *coreDNS) Put(bucket string) error {
if err != nil {
return err
}
key := msg.Path(fmt.Sprintf("%s.%s", bucket, c.domainName), defaultPrefixPath)
key = key + "/" + ip
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
_, err = c.etcdClient.Put(ctx, key, string(bucketMsg))
defer cancel()
if err != nil {
ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
c.etcdClient.Delete(ctx, key)
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s", bucket, domainName), defaultPrefixPath)
key = key + "/" + ip
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
_, err = c.etcdClient.Put(ctx, key, string(bucketMsg))
defer cancel()
return err
if err != nil {
ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
c.etcdClient.Delete(ctx, key)
defer cancel()
return err
}
}
}
return nil
@@ -134,33 +154,35 @@ func (c *coreDNS) Put(bucket string) error {
// Removes DNS entries added in Put().
func (c *coreDNS) Delete(bucket string) error {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath)
srvRecords, err := c.list(key)
if err != nil {
return err
}
for _, record := range srvRecords {
dctx, dcancel := context.WithTimeout(context.Background(), defaultContextTimeout)
if _, err = c.etcdClient.Delete(dctx, key+"/"+record.Host); err != nil {
dcancel()
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), defaultPrefixPath)
srvRecords, err := c.list(key)
if err != nil {
return err
}
dcancel()
for _, record := range srvRecords {
dctx, dcancel := context.WithTimeout(context.Background(), defaultContextTimeout)
if _, err = c.etcdClient.Delete(dctx, key+"/"+record.Host); err != nil {
dcancel()
return err
}
dcancel()
}
}
return err
return nil
}
// CoreDNS - represents dns config for coredns server.
type coreDNS struct {
domainName string
domainIPs set.StringSet
domainPort int
etcdClient *etcd.Client
domainNames []string
domainIPs set.StringSet
domainPort int
etcdClient *etcd.Client
}
// NewCoreDNS - initialize a new coreDNS set/unset values.
func NewCoreDNS(domainName string, domainIPs set.StringSet, domainPort string, etcdClient *etcd.Client) (Config, error) {
if domainName == "" || domainIPs.IsEmpty() {
func NewCoreDNS(domainNames []string, domainIPs set.StringSet, domainPort string, etcdClient *etcd.Client) (Config, error) {
if len(domainNames) == 0 || domainIPs.IsEmpty() {
return nil, errors.New("invalid argument")
}
@@ -170,9 +192,9 @@ func NewCoreDNS(domainName string, domainIPs set.StringSet, domainPort string, e
}
return &coreDNS{
domainName: domainName,
domainIPs: domainIPs,
domainPort: port,
etcdClient: etcdClient,
domainNames: domainNames,
domainIPs: domainIPs,
domainPort: port,
etcdClient: etcdClient,
}, nil
}
+104
View File
@@ -0,0 +1,104 @@
/*
* Minio Cloud Storage, (C) 2019 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package target
import (
"sync"
"github.com/minio/minio/pkg/event"
)
const (
maxStoreLimit = 10000
)
// MemoryStore persists events in memory.
type MemoryStore struct {
sync.RWMutex
events map[string]event.Event
eC uint16
limit uint16
}
// NewMemoryStore creates a memory store instance.
func NewMemoryStore(limit uint16) *MemoryStore {
if limit == 0 || limit > maxStoreLimit {
limit = maxStoreLimit
}
memoryStore := &MemoryStore{
events: make(map[string]event.Event),
limit: limit,
}
return memoryStore
}
// Open is in-effective here.
// Implemented for interface compatibility.
func (store *MemoryStore) Open() error {
return nil
}
// Put - puts the event in store.
func (store *MemoryStore) Put(e event.Event) error {
store.Lock()
defer store.Unlock()
if store.eC == store.limit {
return ErrLimitExceeded
}
key, kErr := getNewUUID()
if kErr != nil {
return kErr
}
store.events[key] = e
store.eC++
return nil
}
// Get - retrieves the event from store.
func (store *MemoryStore) Get(key string) (event.Event, error) {
store.RLock()
defer store.RUnlock()
if event, exist := store.events[key]; exist {
return event, nil
}
return event.Event{}, ErrNoSuchKey
}
// Del - deletes the event from store.
func (store *MemoryStore) Del(key string) {
store.Lock()
defer store.Unlock()
delete(store.events, key)
store.eC--
}
// ListAll - lists all the keys in the store.
func (store *MemoryStore) ListAll() []string {
store.RLock()
defer store.RUnlock()
keys := []string{}
for k := range store.events {
keys = append(keys, k)
}
return keys
}
+106
View File
@@ -0,0 +1,106 @@
/*
* Minio Cloud Storage, (C) 2019 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package target
import (
"reflect"
"testing"
)
// TestMemoryStorePut - Tests for store.Put
func TestMemoryStorePut(t *testing.T) {
store := NewMemoryStore(1000)
defer func() {
store = nil
}()
for i := 0; i < 100; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
if len(store.ListAll()) != 100 {
t.Fatalf("ListAll() Expected: 100, got %d", len(store.ListAll()))
}
}
// TestMemoryStoreGet - Tests for store.Get.
func TestMemoryStoreGet(t *testing.T) {
store := NewMemoryStore(1000)
defer func() {
store = nil
}()
for i := 0; i < 10; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
eventKeys := store.ListAll()
if len(eventKeys) == 10 {
for _, key := range eventKeys {
event, eErr := store.Get(key)
if eErr != nil {
t.Fatal("Failed to Get the event from the queue store ", eErr)
}
if !reflect.DeepEqual(testEvent, event) {
t.Fatalf("Failed to read the event: error: expected = %v, got = %v", testEvent, event)
}
}
} else {
t.Fatalf("ListAll() Expected: 10, got %d", len(eventKeys))
}
}
// TestMemoryStoreDel - Tests for store.Del.
func TestMemoryStoreDel(t *testing.T) {
store := NewMemoryStore(1000)
defer func() {
store = nil
}()
for i := 0; i < 20; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
eventKeys := store.ListAll()
if len(eventKeys) == 20 {
for _, key := range eventKeys {
store.Del(key)
}
} else {
t.Fatalf("ListAll() Expected: 20, got %d", len(eventKeys))
}
if len(store.ListAll()) != 0 {
t.Fatalf("ListAll() Expected: 0, got %d", len(store.ListAll()))
}
}
// TestMemoryStoreLimit - tests for store limit.
func TestMemoryStoreLimit(t *testing.T) {
store := NewMemoryStore(5)
defer func() {
store = nil
}()
for i := 0; i < 5; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
if err := store.Put(testEvent); err == nil {
t.Fatalf("Expected to fail with %s, but passes", ErrLimitExceeded)
}
}
+87 -21
View File
@@ -30,6 +30,11 @@ import (
xnet "github.com/minio/minio/pkg/net"
)
const (
retryInterval = 3 // In Seconds
reconnectInterval = 5 // In Seconds
)
// MQTTArgs - MQTT target arguments.
type MQTTArgs struct {
Enable bool `json:"enable"`
@@ -42,6 +47,7 @@ type MQTTArgs struct {
KeepAlive time.Duration `json:"keepAliveInterval"`
RootCAs *x509.CertPool `json:"-"`
QueueDir string `json:"queueDir"`
QueueLimit uint16 `json:"queueLimit"`
}
// Validate MQTTArgs fields
@@ -75,6 +81,8 @@ type MQTTTarget struct {
id event.TargetID
args MQTTArgs
client mqtt.Client
store Store
reconn bool
}
// ID - returns target ID.
@@ -82,16 +90,33 @@ func (target *MQTTTarget) ID() event.TargetID {
return target.id
}
// Send - sends event to MQTT.
func (target *MQTTTarget) Send(eventData event.Event) error {
if !target.client.IsConnected() {
token := target.client.Connect()
if token.Wait() {
if err := token.Error(); err != nil {
return err
// Reads persisted events from the store and re-plays.
func (target *MQTTTarget) retry() {
target.reconn = true
events := target.store.ListAll()
for len(events) != 0 {
for _, key := range events {
event, eErr := target.store.Get(key)
if eErr != nil {
continue
}
for !target.client.IsConnectionOpen() {
time.Sleep(retryInterval * time.Second)
}
// The connection is open.
if err := target.send(event); err != nil {
continue
}
// Delete after a successful publish.
target.store.Del(key)
}
events = target.store.ListAll()
}
// Release the reconn state.
target.reconn = false
}
func (target *MQTTTarget) send(eventData event.Event) error {
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
if err != nil {
@@ -105,16 +130,27 @@ func (target *MQTTTarget) Send(eventData event.Event) error {
}
token := target.client.Publish(target.args.Topic, target.args.QoS, false, string(data))
token.Wait()
return token.Error()
// In-case if the queueDir is configured OR mqtt broker is offline, the token.Wait() waits indefinitely
if target.args.QueueDir == "" {
token.Wait()
return token.Error()
}
// Send - sends event to MQTT when the connection is active.
func (target *MQTTTarget) Send(eventData event.Event) error {
// Persist the events if the connection is not active.
if !target.client.IsConnectionOpen() {
if err := target.store.Put(eventData); err != nil {
return err
}
// Ignore if retry is triggered already.
if !target.reconn {
go target.retry()
}
return nil
}
// Need a fix from the paho.mqtt.golang library - https://github.com/eclipse/paho.mqtt.golang/issues/274
// Right now the server panics on IO File store errors, returning nil for now.
return nil
// Publishes to the broker as the connection is active.
return target.send(eventData)
}
// Close - does nothing and available for interface compatibility.
@@ -134,19 +170,49 @@ func NewMQTTTarget(id string, args MQTTArgs) (*MQTTTarget, error) {
SetTLSConfig(&tls.Config{RootCAs: args.RootCAs}).
AddBroker(args.Broker.String())
var store Store
if args.QueueDir != "" {
options = options.SetStore(mqtt.NewFileStore(args.QueueDir))
store = NewQueueStore(args.QueueDir, args.QueueLimit)
if oErr := store.Open(); oErr != nil {
return nil, oErr
}
} else {
store = NewMemoryStore(args.QueueLimit)
}
client := mqtt.NewClient(options)
token := client.Connect()
if token.Wait() && token.Error() != nil {
return nil, token.Error()
}
return &MQTTTarget{
// The client should establish a first time connection.
// Connect() should be successful atleast once to publish events.
token := client.Connect()
go func() {
// Repeat the pings until the client registers the clientId and receives a token.
for {
if token.Wait() && token.Error() == nil {
// Connected
break
}
// Reconnecting
time.Sleep(reconnectInterval * time.Second)
token = client.Connect()
}
}()
target := &MQTTTarget{
id: event.TargetID{ID: id, Name: "mqtt"},
args: args,
client: client,
}, nil
store: store,
reconn: false,
}
// Replay any previously persisted events in the store.
if len(target.store.ListAll()) != 0 {
go target.retry()
}
return target, nil
}
+178
View File
@@ -0,0 +1,178 @@
/*
* Minio Cloud Storage, (C) 2019 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package target
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"github.com/minio/minio/pkg/event"
)
const (
maxLimit = 10000 // Max store limit.
eventExt = ".event"
)
// QueueStore - Filestore for persisting events.
type QueueStore struct {
sync.RWMutex
directory string
eC uint16
limit uint16
}
// NewQueueStore - Creates an instance for QueueStore.
func NewQueueStore(directory string, limit uint16) *QueueStore {
if limit == 0 {
limit = maxLimit
}
queueStore := &QueueStore{
directory: directory,
limit: limit,
}
return queueStore
}
// Open - Creates the directory if not present.
func (store *QueueStore) Open() error {
store.Lock()
defer store.Unlock()
if terr := os.MkdirAll(store.directory, os.FileMode(0770)); terr != nil {
return terr
}
eCount := uint16(len(store.listAll()))
if eCount >= store.limit {
return ErrLimitExceeded
}
store.eC = eCount
return nil
}
// write - writes event to the directory.
func (store *QueueStore) write(directory string, key string, e event.Event) error {
// Marshalls the event.
eventData, err := json.Marshal(e)
if err != nil {
return err
}
path := filepath.Join(store.directory, key+eventExt)
if err := ioutil.WriteFile(path, eventData, os.FileMode(0770)); err != nil {
return err
}
// Increment the event count.
store.eC++
return nil
}
// Put - puts a event to the store.
func (store *QueueStore) Put(e event.Event) error {
store.Lock()
defer store.Unlock()
if store.eC >= store.limit {
return ErrLimitExceeded
}
key, kErr := getNewUUID()
if kErr != nil {
return kErr
}
return store.write(store.directory, key, e)
}
// Get - gets a event from the store.
func (store *QueueStore) Get(key string) (event.Event, error) {
store.RLock()
defer store.RUnlock()
var event event.Event
filepath := filepath.Join(store.directory, key+eventExt)
eventData, rerr := ioutil.ReadFile(filepath)
if rerr != nil {
store.del(key)
return event, rerr
}
if len(eventData) == 0 {
store.del(key)
}
uerr := json.Unmarshal(eventData, &event)
if uerr != nil {
store.del(key)
return event, uerr
}
return event, nil
}
// Del - Deletes an entry from the store.
func (store *QueueStore) Del(key string) {
store.Lock()
defer store.Unlock()
store.del(key)
}
// lockless call
func (store *QueueStore) del(key string) {
p := filepath.Join(store.directory, key+eventExt)
rerr := os.Remove(p)
if rerr != nil {
return
}
// Decrement the event count.
store.eC--
}
// ListAll - lists all the keys in the directory.
func (store *QueueStore) ListAll() []string {
store.RLock()
defer store.RUnlock()
return store.listAll()
}
// lockless call.
func (store *QueueStore) listAll() []string {
var err error
var keys []string
var files []os.FileInfo
files, err = ioutil.ReadDir(store.directory)
if err != nil {
return nil
}
for _, f := range files {
keys = append(keys, strings.TrimSuffix(f.Name(), eventExt))
}
return keys
}
+162
View File
@@ -0,0 +1,162 @@
/*
* Minio Cloud Storage, (C) 2019 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package target
import (
"os"
"path/filepath"
"reflect"
"testing"
"github.com/minio/minio/pkg/event"
)
// TestDir
var queueDir = filepath.Join(os.TempDir(), "minio_test")
// Sample test event.
var testEvent = event.Event{EventVersion: "1.0", EventSource: "test_source", AwsRegion: "test_region", EventTime: "test_time", EventName: event.ObjectAccessedGet}
// Initialize the store.
func setUpStore(directory string, limit uint16) (Store, error) {
store := NewQueueStore(queueDir, limit)
if oErr := store.Open(); oErr != nil {
return nil, oErr
}
return store, nil
}
// Tear down store
func tearDownStore() error {
if err := os.RemoveAll(queueDir); err != nil {
return err
}
return nil
}
// TestQueueStorePut - tests for store.Put
func TestQueueStorePut(t *testing.T) {
defer func() {
if err := tearDownStore(); err != nil {
t.Fatal("Failed to tear down store ", err)
}
}()
store, err := setUpStore(queueDir, 10000)
if err != nil {
t.Fatal("Failed to create a queue store ", err)
}
// Put 100 events.
for i := 0; i < 100; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
// Count the events.
if len(store.ListAll()) != 100 {
t.Fatalf("ListAll() Expected: 100, got %d", len(store.ListAll()))
}
}
// TestQueueStoreGet - tests for store.Get
func TestQueueStoreGet(t *testing.T) {
defer func() {
if err := tearDownStore(); err != nil {
t.Fatal("Failed to tear down store ", err)
}
}()
store, err := setUpStore(queueDir, 10000)
if err != nil {
t.Fatal("Failed to create a queue store ", err)
}
// Put 10 events
for i := 0; i < 10; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
eventKeys := store.ListAll()
// Get 10 events.
if len(eventKeys) == 10 {
for _, key := range eventKeys {
event, eErr := store.Get(key)
if eErr != nil {
t.Fatal("Failed to Get the event from the queue store ", eErr)
}
if !reflect.DeepEqual(testEvent, event) {
t.Fatalf("Failed to read the event: error: expected = %v, got = %v", testEvent, event)
}
}
} else {
t.Fatalf("ListAll() Expected: 10, got %d", len(eventKeys))
}
}
// TestQueueStoreDel - tests for store.Del
func TestQueueStoreDel(t *testing.T) {
defer func() {
if err := tearDownStore(); err != nil {
t.Fatal("Failed to tear down store ", err)
}
}()
store, err := setUpStore(queueDir, 10000)
if err != nil {
t.Fatal("Failed to create a queue store ", err)
}
// Put 20 events.
for i := 0; i < 20; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
eventKeys := store.ListAll()
// Remove all the events.
if len(eventKeys) == 20 {
for _, key := range eventKeys {
store.Del(key)
}
} else {
t.Fatalf("ListAll() Expected: 20, got %d", len(eventKeys))
}
if len(store.ListAll()) != 0 {
t.Fatalf("ListAll() Expected: 0, got %d", len(store.ListAll()))
}
}
// TestQueueStoreLimit - tests the event limit for the store.
func TestQueueStoreLimit(t *testing.T) {
defer func() {
if err := tearDownStore(); err != nil {
t.Fatal("Failed to tear down store ", err)
}
}()
// The max limit is set to 5.
store, err := setUpStore(queueDir, 5)
if err != nil {
t.Fatal("Failed to create a queue store ", err)
}
for i := 0; i < 5; i++ {
if err := store.Put(testEvent); err != nil {
t.Fatal("Failed to put to queue store ", err)
}
}
// Should not allow 6th Put.
if err := store.Put(testEvent); err == nil {
t.Fatalf("Expected to fail with %s, but passes", ErrLimitExceeded)
}
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Minio Cloud Storage, (C) 2019 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package target
import (
"errors"
"github.com/minio/minio/pkg/event"
)
// ErrLimitExceeded error is sent when the maximum limit is reached.
var ErrLimitExceeded = errors.New("[Store] The maximum limit reached")
// ErrNoSuchKey error is sent in Get when the key is not found.
var ErrNoSuchKey = errors.New("[Store] No such key found")
// Store - To persist the events.
type Store interface {
Put(event event.Event) error
Get(key string) (event.Event, error)
ListAll() []string
Del(key string)
Open() error
}
+6
View File
@@ -138,6 +138,12 @@ func NewReader(readCloser io.ReadCloser, args *ReaderArgs) (*Reader, error) {
csvReader.Comma = []rune(args.FieldDelimiter)[0]
csvReader.Comment = []rune(args.CommentCharacter)[0]
csvReader.FieldsPerRecord = -1
// If LazyQuotes is true, a quote may appear in an unquoted field and a
// non-doubled quote may appear in a quoted field.
csvReader.LazyQuotes = true
// If TrimLeadingSpace is true, leading white space in a field is ignored.
// This is done even if the field delimiter, Comma, is white space.
csvReader.TrimLeadingSpace = true
r := &Reader{
args: args,
+1 -1
View File
@@ -71,7 +71,7 @@ func (r *Reader) Close() error {
// NewReader - creates new JSON reader using readCloser.
func NewReader(readCloser io.ReadCloser, args *ReaderArgs) *Reader {
d := jstream.NewDecoder(readCloser, 0)
d := jstream.NewDecoder(readCloser, 0).ObjectAsKVS()
return &Reader{
args: args,
decoder: d,
+1 -1
View File
@@ -277,7 +277,7 @@ func (e *FuncExpr) aggregateRow(r Record) error {
func (e *FuncExpr) getAggregate() (*Value, error) {
switch e.getFunctionName() {
case aggFnCount:
return FromFloat(float64(e.aggregate.runningCount)), nil
return FromInt(e.aggregate.runningCount), nil
case aggFnAvg:
if e.aggregate.runningCount == 0 {
+106 -4
View File
@@ -42,8 +42,8 @@ type KV struct {
// KVS - represents key values in an JSON object
type KVS []KV
// MarshalJSON - implements converting slice of KVS into a JSON object
// with multiple keys and values.
// MarshalJSON - implements converting a KVS datastructure into a JSON
// object with multiple keys and values.
func (kvs KVS) MarshalJSON() ([]byte, error) {
b := new(bytes.Buffer)
b.Write([]byte("{"))
@@ -69,6 +69,7 @@ type Decoder struct {
emitDepth int
emitKV bool
emitRecursive bool
objectAsKVS bool
depth int
scratch *scratch
@@ -97,6 +98,15 @@ func NewDecoder(r io.Reader, emitDepth int) *Decoder {
return d
}
// ObjectAsKVS - by default JSON returns map[string]interface{} this
// is usually fine in most cases, but when you need to preserve the
// input order its not a right data structure. To preserve input
// order please use this option.
func (d *Decoder) ObjectAsKVS() *Decoder {
d.objectAsKVS = true
return d
}
// EmitKV enables emitting a jstream.KV struct when the items(s) parsed
// at configured emit depth are within a JSON object. By default, only
// the object values are emitted.
@@ -217,7 +227,13 @@ func (d *Decoder) any() (interface{}, ValueType, error) {
i, err := d.array()
return i, Array, err
case '{':
i, err := d.object()
var i interface{}
var err error
if d.objectAsKVS {
i, err = d.objectOrdered()
} else {
i, err = d.object()
}
return i, Object, err
default:
return nil, Unknown, d.mkError(ErrSyntax, "looking for beginning of value")
@@ -442,7 +458,92 @@ out:
}
// object accept valid JSON array value
func (d *Decoder) object() (KVS, error) {
func (d *Decoder) object() (map[string]interface{}, error) {
d.depth++
var (
c byte
k string
v interface{}
t ValueType
err error
obj map[string]interface{}
)
// skip allocating map if it will not be emitted
if d.depth > d.emitDepth {
obj = make(map[string]interface{})
}
// if the object has no keys
if c = d.skipSpaces(); c == '}' {
goto out
}
scan:
for {
offset := d.pos - 1
// read string key
if c != '"' {
err = d.mkError(ErrSyntax, "looking for beginning of object key string")
break
}
if k, err = d.string(); err != nil {
break
}
// read colon before value
if c = d.skipSpaces(); c != ':' {
err = d.mkError(ErrSyntax, "after object key")
break
}
// read value
d.skipSpaces()
if d.emitKV {
if v, t, err = d.any(); err != nil {
break
}
if d.willEmit() {
d.metaCh <- &MetaValue{
Offset: int(offset),
Length: int(d.pos - offset),
Depth: d.depth,
Value: KV{k, v},
ValueType: t,
}
}
} else {
if v, err = d.emitAny(); err != nil {
break
}
}
if obj != nil {
obj[k] = v
}
// next token must be ',' or '}'
switch c = d.skipSpaces(); c {
case '}':
goto out
case ',':
c = d.skipSpaces()
goto scan
default:
err = d.mkError(ErrSyntax, "after object key:value pair")
goto out
}
}
out:
d.depth--
return obj, err
}
// object (ordered) accept valid JSON array value
func (d *Decoder) objectOrdered() (KVS, error) {
d.depth++
var (
@@ -517,6 +618,7 @@ scan:
goto scan
default:
err = d.mkError(ErrSyntax, "after object key:value pair")
goto out
}
}
+3 -3
View File
@@ -116,10 +116,10 @@
"revisionTime": "2017-09-25T03:23:15Z"
},
{
"checksumSHA1": "mSo9Sti7F6+laUs4NLx/p23rHD0=",
"checksumSHA1": "YPGDETCyuMkceQvAXafFnj6ndkY=",
"path": "github.com/bcicen/jstream",
"revision": "f306cd3e1fa602a1b513114521a0d6a5a9d5c919",
"revisionTime": "2019-02-06T02:23:53Z"
"revision": "16c1f8af81c2a9967b30d365a29472126274f998",
"revisionTime": "2019-02-20T04:59:26Z"
},
{
"checksumSHA1": "0rido7hYHQtfq3UJzVT5LClLAWc=",