mirror of
https://github.com/pgsty/minio.git
synced 2026-11-08 08:41:50 +03:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed1275a063 | |||
| a7d407fa42 | |||
| 4e6e05f8e0 | |||
| b0deea27df | |||
| e98d89274f | |||
| c59206bcd3 | |||
| 7f2d439baa | |||
| 5a80cbec2a | |||
| 2d19011a1d | |||
| e82dcd195c | |||
| fcb56d864c | |||
| 75cd4201b0 | |||
| f24c017e9a | |||
| b5280ba243 | |||
| 2a0e4b6f58 | |||
| 1898961ce3 | |||
| 236796ebd6 | |||
| 4e4f855b30 | |||
| 2db22deb93 | |||
| fb8d0d7cf7 | |||
| a536cf5dc0 | |||
| b9b68e9331 | |||
| 632022971b | |||
| def04f01cf | |||
| bc67410548 | |||
| 7881791a91 | |||
| d2f8f8c7ee | |||
| 8c32311b80 | |||
| 9bb88e610e | |||
| d1e41695fe | |||
| 2aeb3fbe86 | |||
| 99b843a64e | |||
| 4f31a9a33b | |||
| 65ddff8899 | |||
| e7c902bbbc | |||
| 5a5895203b | |||
| 7da0336ac8 | |||
| 3be616de3f | |||
| c5bf22fd90 | |||
| 7c9f934875 | |||
| b6f9b24b30 | |||
| 13cb814a0e | |||
| d264d2c899 | |||
| bebaff269c | |||
| 52b159b1db | |||
| 324834e4da | |||
| c2ed1347d9 | |||
| 50f6f9fe58 | |||
| 48cb0ea34b | |||
| 6f7c99a333 | |||
| 3498f5b0ec | |||
| 21d8c0fd13 | |||
| 79b9a9ce46 | |||
| b9b353db4b | |||
| 11a9b317a3 | |||
| 9af7d627ac | |||
| 76d9d54603 | |||
| 4c7c571875 | |||
| 313ba74b09 | |||
| 3e124315c8 | |||
| 78a0fd951e |
+11
-4
@@ -16,24 +16,31 @@ matrix:
|
||||
sudo: required
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
go: 1.10.4
|
||||
- CGO_ENABLED=0
|
||||
go: 1.11.4
|
||||
script:
|
||||
- make
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
- make test GOFLAGS="-timeout 15m -race -v"
|
||||
- for d in $(go list ./... | grep -v browser); do CGO_ENABLED=1 go test -v -race --timeout 15m "$d"; done
|
||||
- make verify
|
||||
- make coverage
|
||||
- cd browser && yarn && yarn test && cd ..
|
||||
- os: windows
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
go: 1.10.4
|
||||
- CGO_ENABLED=0
|
||||
go: 1.11.4
|
||||
script:
|
||||
- go build --ldflags="$(go run buildscripts/gen-ldflags.go)" -o %GOPATH%\bin\minio.exe
|
||||
- for d in $(go list ./... | grep -v browser); do go test -v -race --timeout 20m "$d"; done
|
||||
- for d in $(go list ./... | grep -v browser); do CGO_ENABLED=1 go test -v -race --timeout 20m "$d"; done
|
||||
- bash buildscripts/go-coverage.sh
|
||||
|
||||
before_script:
|
||||
# Add an IPv6 config - see the corresponding Travis issue
|
||||
# https://github.com/travis-ci/travis-ci/issues/8361
|
||||
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then nvm install stable ; fi
|
||||
|
||||
|
||||
+15
-11
@@ -1,28 +1,32 @@
|
||||
FROM golang:1.10.1-alpine3.7
|
||||
FROM golang:1.11.4-alpine3.7
|
||||
|
||||
LABEL maintainer="Minio Inc <dev@minio.io>"
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH $PATH:$GOPATH/bin
|
||||
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 && \
|
||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)"
|
||||
|
||||
FROM alpine:3.7
|
||||
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key
|
||||
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
EXPOSE 9000
|
||||
|
||||
COPY --from=0 /go/bin/minio /usr/bin/minio
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
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)" && \
|
||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||
|
||||
EXPOSE 9000
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ spelling:
|
||||
check: test
|
||||
test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@go test $(GOFLAGS) -tags kqueue ./...
|
||||
@CGO_ENABLED=0 go test -tags kqueue ./...
|
||||
|
||||
verify: build
|
||||
@echo "Verifying build"
|
||||
@@ -73,7 +73,7 @@ coverage: build
|
||||
# Builds minio locally.
|
||||
build: checks
|
||||
@echo "Building minio binary to './minio'"
|
||||
@CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
@GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
|
||||
docker: build
|
||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||
|
||||
+10
-9
@@ -13,8 +13,7 @@
|
||||
"setupTestFrameworkScriptFile": "./app/js/jest/setup.js",
|
||||
"testURL": "https://localhost:8080",
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/app/js/jest/__mocks__/fileMock.js",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/app/js/jest/__mocks__/fileMock.js",
|
||||
"\\.(css|scss)$": "identity-obj-proxy"
|
||||
}
|
||||
},
|
||||
@@ -40,29 +39,31 @@
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"babel-register": "^6.14.0",
|
||||
"copy-webpack-plugin": "^0.3.3",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^0.23.1",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"esformatter": "^0.10.0",
|
||||
"esformatter-jsx": "^7.4.1",
|
||||
"esformatter-jsx-ignore": "^1.0.6",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^22.1.4",
|
||||
"jest-enzyme": "^4.0.2",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"purifycss-webpack-plugin": "^2.0.3",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^4.1.0",
|
||||
"purgecss-webpack-plugin": "^1.4.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack-dev-server": "^2.11.1"
|
||||
"webpack-cli": "^3.2.0",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.3.6",
|
||||
"classnames": "^2.2.3",
|
||||
"expect": "^1.20.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob-all": "^3.1.0",
|
||||
"history": "^4.7.2",
|
||||
"humanize": "0.0.9",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
@@ -89,6 +90,6 @@
|
||||
"reselect": "^3.0.1",
|
||||
"superagent": "^3.8.2",
|
||||
"superagent-es6-promise": "^1.0.0",
|
||||
"webpack": "^3.10.0"
|
||||
"webpack": "^4.28.3"
|
||||
}
|
||||
}
|
||||
|
||||
+42
-34
File diff suppressed because one or more lines are too long
@@ -16,11 +16,13 @@
|
||||
|
||||
var webpack = require('webpack')
|
||||
var path = require('path')
|
||||
var glob = require('glob-all')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var purify = require("purifycss-webpack-plugin")
|
||||
var PurgecssPlugin = require('purgecss-webpack-plugin')
|
||||
|
||||
var exports = {
|
||||
context: __dirname,
|
||||
mode: 'development',
|
||||
entry: [
|
||||
path.resolve(__dirname, 'app/index.js')
|
||||
],
|
||||
@@ -99,12 +101,11 @@ var exports = {
|
||||
{from: 'app/index.html'}
|
||||
]),
|
||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||
new purify({
|
||||
basePath: __dirname,
|
||||
paths: [
|
||||
"app/index.html",
|
||||
"app/js/*.js"
|
||||
]
|
||||
new PurgecssPlugin({
|
||||
paths: glob.sync([
|
||||
path.join(__dirname, 'app/index.html'),
|
||||
path.join(__dirname, 'app/js/*.js')
|
||||
])
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
var webpack = require('webpack')
|
||||
var path = require('path')
|
||||
var glob = require('glob-all')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var purify = require("purifycss-webpack-plugin")
|
||||
var PurgecssPlugin = require('purgecss-webpack-plugin')
|
||||
|
||||
var exports = {
|
||||
context: __dirname,
|
||||
mode: 'production',
|
||||
entry: [
|
||||
path.resolve(__dirname, 'app/index.js')
|
||||
],
|
||||
@@ -74,16 +76,12 @@ var exports = {
|
||||
{from: 'app/img/logo.svg'},
|
||||
{from: 'app/index.html'}
|
||||
]),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}),
|
||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||
new purify({
|
||||
basePath: __dirname,
|
||||
paths: [
|
||||
"app/index.html",
|
||||
"app/js/*.js"
|
||||
]
|
||||
new PurgecssPlugin({
|
||||
paths: glob.sync([
|
||||
path.join(__dirname, 'app/index.html'),
|
||||
path.join(__dirname, 'app/js/*.js')
|
||||
])
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
+2532
-1059
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v browser); do
|
||||
go test -v -coverprofile=profile.out -covermode=atomic "$d"
|
||||
CGO_ENABLED=0 go test -v -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
|
||||
@@ -68,6 +68,36 @@ function start_minio_erasure_sets()
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_dist_erasure_sets_ipv6()
|
||||
{
|
||||
declare -a minio_pids
|
||||
export MINIO_ACCESS_KEY=$ACCESS_KEY
|
||||
export MINIO_SECRET_KEY=$SECRET_KEY
|
||||
"${MINIO[@]}" server --address="[::1]:9000" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9000.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9001" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9001.log" 2>&1 &
|
||||
minio_pids[1]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9002" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9002.log" 2>&1 &
|
||||
minio_pids[2]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9003" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9003.log" 2>&1 &
|
||||
minio_pids[3]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9004" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9004.log" 2>&1 &
|
||||
minio_pids[4]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9005" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9005.log" 2>&1 &
|
||||
minio_pids[5]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9006" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9006.log" 2>&1 &
|
||||
minio_pids[6]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9007" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9007.log" 2>&1 &
|
||||
minio_pids[7]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9008" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9008.log" 2>&1 &
|
||||
minio_pids[8]=$!
|
||||
"${MINIO[@]}" server --address="[::1]:9009" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9009.log" 2>&1 &
|
||||
minio_pids[9]=$!
|
||||
|
||||
sleep 35
|
||||
echo "${minio_pids[@]}"
|
||||
}
|
||||
|
||||
function start_minio_dist_erasure_sets()
|
||||
{
|
||||
declare -a minio_pids
|
||||
@@ -161,6 +191,34 @@ function run_test_erasure_sets() {
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist_erasure_sets_ipv6()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist_erasure_sets_ipv6) )
|
||||
|
||||
export SERVER_ENDPOINT="[::1]:9000"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
for i in $(seq 0 9); do
|
||||
echo "server$i log:"
|
||||
cat "$WORK_DIR/dist-minio-v6-900$i.log"
|
||||
done
|
||||
fi
|
||||
|
||||
for i in $(seq 0 9); do
|
||||
rm -f "$WORK_DIR/dist-minio-v6-900$i.log"
|
||||
done
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist_erasure_sets()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist_erasure_sets) )
|
||||
@@ -237,6 +295,7 @@ function run_test_gateway_s3()
|
||||
{
|
||||
minio_pid="$(start_minio_gateway_s3)"
|
||||
|
||||
export SERVER_ENDPOINT="127.0.0.1:9000"
|
||||
export ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||
export SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
@@ -279,6 +338,7 @@ function __init__()
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i 's|-sS|-sSg|g' "$FUNCTIONAL_TESTS"
|
||||
chmod a+x "$FUNCTIONAL_TESTS"
|
||||
}
|
||||
|
||||
@@ -319,6 +379,13 @@ function main()
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distributed Erasure setup as sets with ipv6"
|
||||
if ! run_test_dist_erasure_sets_ipv6; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Gateway S3 setup"
|
||||
if ! run_test_gateway_s3; then
|
||||
echo "FAILED"
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -284,6 +285,57 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// ServerDrivesPerfInfo holds informantion about address, performance
|
||||
// of all drives on one server. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
type ServerDrivesPerfInfo struct {
|
||||
Addr string `json:"addr"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Perf []disk.Performance `json:"perf"`
|
||||
}
|
||||
|
||||
// PerfInfoHandler - GET /minio/admin/v1/performance?perfType={perfType}
|
||||
// ----------
|
||||
// Get all performance information based on input type
|
||||
// Supported types = drive
|
||||
func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PerfInfo")
|
||||
|
||||
// Authenticate request
|
||||
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
perfType := vars["perfType"]
|
||||
|
||||
if perfType == "drive" {
|
||||
// Get drive performance details from local server's drive(s)
|
||||
dp := localEndpointsPerf(globalEndpoints)
|
||||
|
||||
// Notify all other Minio peers to report drive performance numbers
|
||||
dps := globalNotificationSys.DrivePerfInfo()
|
||||
dps = append(dps, dp)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(dps)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with performance information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else {
|
||||
writeErrorResponseJSON(w, ErrNotImplemented, r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StartProfilingResult contains the status of the starting
|
||||
// profiling action in a given server
|
||||
type StartProfilingResult struct {
|
||||
@@ -867,6 +919,14 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddUser - PUT /minio/admin/v1/add-user?accessKey=<access_key>
|
||||
@@ -927,6 +987,14 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListCannedPolicies - GET /minio/admin/v1/list-canned-policies
|
||||
@@ -992,6 +1060,14 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddCannedPolicy - PUT /minio/admin/v1/add-canned-policy?name=<policy_name>
|
||||
@@ -1049,6 +1125,14 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserPolicy - PUT /minio/admin/v1/set-user-policy?accessKey=<access_key>&name=<policy_name>
|
||||
@@ -1088,6 +1172,14 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
|
||||
if err := globalIAMSys.SetUserPolicy(accessKey, policyName); err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigHandler - PUT /minio/admin/v1/config
|
||||
|
||||
@@ -277,7 +277,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
|
||||
// Setup admin mgmt REST API handlers.
|
||||
adminRouter := mux.NewRouter()
|
||||
registerAdminRouter(adminRouter)
|
||||
registerAdminRouter(adminRouter, true)
|
||||
|
||||
return &adminXLTestBed{
|
||||
xlDirs: xlDirs,
|
||||
|
||||
@@ -133,7 +133,7 @@ func (ahs *allHealState) periodicHealSeqsClean() {
|
||||
}
|
||||
}
|
||||
ahs.Unlock()
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
return
|
||||
@@ -641,7 +641,12 @@ func (h *healSequence) healDiskFormat() error {
|
||||
// Healing succeeded notify the peers to reload format and re-initialize disks.
|
||||
// We will not notify peers only if healing succeeded.
|
||||
if err == nil {
|
||||
peersReInitFormat(globalAdminPeers, h.settings.DryRun)
|
||||
for host, rerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
|
||||
if rerr != nil {
|
||||
logger.GetReqInfo(h.ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(h.ctx, rerr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push format heal result
|
||||
|
||||
+37
-30
@@ -31,7 +31,7 @@ type adminAPIHandlers struct {
|
||||
}
|
||||
|
||||
// registerAdminRouter - Add handler functions for each service REST API routes.
|
||||
func registerAdminRouter(router *mux.Router) {
|
||||
func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
|
||||
adminAPI := adminAPIHandlers{}
|
||||
// Admin router
|
||||
@@ -60,6 +60,11 @@ func registerAdminRouter(router *mux.Router) {
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||
|
||||
/// Health operations
|
||||
|
||||
// Performance command - return performance details based on input type
|
||||
adminV1Router.Methods(http.MethodGet).Path("/performance").HandlerFunc(httpTraceAll(adminAPI.PerfInfoHandler)).Queries("perfType", "{perfType:.*}")
|
||||
}
|
||||
|
||||
// Profiling operations
|
||||
@@ -69,42 +74,44 @@ func registerAdminRouter(router *mux.Router) {
|
||||
|
||||
/// Config operations
|
||||
|
||||
// Update credentials
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
|
||||
// Get config
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||
// Set config
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||
if enableIAM {
|
||||
// Update credentials
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
|
||||
// Get config
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||
// Set config
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||
|
||||
// Get config keys/values
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||
// Set config keys/values
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||
// Get config keys/values
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||
// Set config keys/values
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||
|
||||
// -- IAM APIs --
|
||||
// -- IAM APIs --
|
||||
|
||||
// Add policy IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||
// Add policy IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||
|
||||
// Add user IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)).
|
||||
Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}")
|
||||
adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
|
||||
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||
// Add user IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)).
|
||||
Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}")
|
||||
adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
|
||||
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||
|
||||
// Remove policy IAM
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
||||
// Remove policy IAM
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
||||
|
||||
// Remove user IAM
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
// Remove user IAM
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// List users
|
||||
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
||||
// List users
|
||||
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
||||
|
||||
// List policies
|
||||
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
||||
// List policies
|
||||
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
||||
}
|
||||
|
||||
// If none of the routes match.
|
||||
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandler))
|
||||
// If none of the routes match, return error.
|
||||
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON))
|
||||
}
|
||||
|
||||
+5
-39
@@ -45,29 +45,12 @@ func (rpcClient *AdminRPCClient) SignalService(signal serviceSignal) (err error)
|
||||
return rpcClient.Call(adminServiceName+".SignalService", &args, &reply)
|
||||
}
|
||||
|
||||
// ReInitFormat - re-initialize disk format, remotely.
|
||||
func (rpcClient *AdminRPCClient) ReInitFormat(dryRun bool) error {
|
||||
args := ReInitFormatArgs{DryRun: dryRun}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
||||
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
||||
return sid, err
|
||||
}
|
||||
|
||||
// GetConfig - returns config.json of the remote server.
|
||||
func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
|
||||
args := AuthArgs{}
|
||||
var reply []byte
|
||||
|
||||
err := rpcClient.Call(adminServiceName+".GetConfig", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling in the remote server.
|
||||
func (rpcClient *AdminRPCClient) StartProfiling(profiler string) error {
|
||||
args := StartProfilingArgs{Profiler: profiler}
|
||||
@@ -125,9 +108,7 @@ func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||
// commands like service stop and service restart.
|
||||
type adminCmdRunner interface {
|
||||
SignalService(s serviceSignal) error
|
||||
ReInitFormat(dryRun bool) error
|
||||
ServerInfo() (ServerInfoData, error)
|
||||
GetConfig() ([]byte, error)
|
||||
StartProfiling(string) error
|
||||
DownloadProfilingData() ([]byte, error)
|
||||
}
|
||||
@@ -149,6 +130,11 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
// Use first IPv4 instead of loopback address.
|
||||
localAddr = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
|
||||
}
|
||||
if strings.HasPrefix(localAddr, "[::1]:") {
|
||||
// Use first IPv4 instead of loopback address.
|
||||
localAddr = net.JoinHostPort(localIP6.ToSlice()[0], globalMinioPort)
|
||||
}
|
||||
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: localAddr,
|
||||
cmdRunner: localAdminClient{},
|
||||
@@ -169,26 +155,6 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
return adminPeerList
|
||||
}
|
||||
|
||||
// peersReInitFormat - reinitialize remote object layers to new format.
|
||||
func peersReInitFormat(peers adminPeers, dryRun bool) error {
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Send ReInitFormat RPC call to all nodes.
|
||||
// for local adminPeer this is a no-op.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
if !peer.isLocal {
|
||||
errs[idx] = peer.cmdRunner.ReInitFormat(dryRun)
|
||||
}
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize global adminPeer collection.
|
||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
||||
globalAdminPeers = makeAdminPeers(endpoints)
|
||||
|
||||
@@ -68,23 +68,6 @@ func (receiver *adminRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[
|
||||
return
|
||||
}
|
||||
|
||||
// GetConfig - returns the config.json of this server.
|
||||
func (receiver *adminRPCReceiver) GetConfig(args *AuthArgs, reply *[]byte) (err error) {
|
||||
*reply, err = receiver.local.GetConfig()
|
||||
return err
|
||||
}
|
||||
|
||||
// ReInitFormatArgs - provides dry-run information to re-initialize format.json
|
||||
type ReInitFormatArgs struct {
|
||||
AuthArgs
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// ReInitFormat - re-init 'format.json'
|
||||
func (receiver *adminRPCReceiver) ReInitFormat(args *ReInitFormatArgs, reply *VoidReply) error {
|
||||
return receiver.local.ReInitFormat(args.DryRun)
|
||||
}
|
||||
|
||||
// NewAdminRPCServer - returns new admin RPC server.
|
||||
func NewAdminRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
|
||||
+1
-79
@@ -40,11 +40,8 @@ import (
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalServiceSignalCh := globalServiceSignalCh
|
||||
defer func(sigChan chan serviceSignal) { globalServiceSignalCh = sigChan }(globalServiceSignalCh)
|
||||
globalServiceSignalCh = make(chan serviceSignal, 10)
|
||||
defer func() {
|
||||
globalServiceSignalCh = tmpGlobalServiceSignalCh
|
||||
}()
|
||||
|
||||
testCases := []struct {
|
||||
signal serviceSignal
|
||||
@@ -66,34 +63,6 @@ func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerReInitFormat(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalObjectAPI := globalObjectAPI
|
||||
defer func() {
|
||||
globalObjectAPI = tmpGlobalObjectAPI
|
||||
}()
|
||||
|
||||
testCases := []struct {
|
||||
objectAPI ObjectLayer
|
||||
dryRun bool
|
||||
expectErr bool
|
||||
}{
|
||||
{&DummyObjectLayer{}, true, false},
|
||||
{&DummyObjectLayer{}, false, false},
|
||||
{nil, true, true},
|
||||
{nil, false, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
globalObjectAPI = testCase.objectAPI
|
||||
err := client.ReInitFormat(testCase.dryRun)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalBootTime := globalBootTime
|
||||
tmpGlobalObjectAPI := globalObjectAPI
|
||||
@@ -140,33 +109,6 @@ func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerGetConfig(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
|
||||
config := newServerConfig()
|
||||
|
||||
testCases := []struct {
|
||||
config *serverConfig
|
||||
expectErr bool
|
||||
}{
|
||||
{globalServerConfig, false},
|
||||
{config, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
globalServerConfig = testCase.config
|
||||
_, err := client.GetConfig()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
if err != nil {
|
||||
@@ -208,16 +150,6 @@ func TestAdminRPCClientSignalService(t *testing.T) {
|
||||
testAdminCmdRunnerSignalService(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientReInitFormat(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerReInitFormat(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
@@ -227,13 +159,3 @@ func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||
|
||||
testAdminCmdRunnerServerInfo(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientGetConfig(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerGetConfig(t, rpcClient)
|
||||
}
|
||||
|
||||
+6
-164
@@ -28,8 +28,6 @@ import (
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/s3select"
|
||||
"github.com/minio/minio/pkg/s3select/format"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -87,6 +85,7 @@ const (
|
||||
ErrNoSuchBucketPolicy
|
||||
ErrNoSuchKey
|
||||
ErrNoSuchUpload
|
||||
ErrNoSuchVersion
|
||||
ErrNotImplemented
|
||||
ErrPreconditionFailed
|
||||
ErrRequestTimeTooSkewed
|
||||
@@ -447,6 +446,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNoSuchVersion: {
|
||||
Code: "NoSuchVersion",
|
||||
Description: "Indicates that the version ID specified in the request does not match an existing version.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNotImplemented: {
|
||||
Code: "NotImplemented",
|
||||
Description: "A header you provided implies functionality that is not implemented",
|
||||
@@ -1506,168 +1510,6 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
}
|
||||
switch err {
|
||||
case s3select.ErrBusy:
|
||||
apiErr = ErrBusy
|
||||
case s3select.ErrUnauthorizedAccess:
|
||||
apiErr = ErrUnauthorizedAccess
|
||||
case s3select.ErrExpressionTooLong:
|
||||
apiErr = ErrExpressionTooLong
|
||||
case s3select.ErrIllegalSQLFunctionArgument:
|
||||
apiErr = ErrIllegalSQLFunctionArgument
|
||||
case s3select.ErrInvalidKeyPath:
|
||||
apiErr = ErrInvalidKeyPath
|
||||
case s3select.ErrInvalidCompressionFormat:
|
||||
apiErr = ErrInvalidCompressionFormat
|
||||
case s3select.ErrInvalidFileHeaderInfo:
|
||||
apiErr = ErrInvalidFileHeaderInfo
|
||||
case s3select.ErrInvalidJSONType:
|
||||
apiErr = ErrInvalidJSONType
|
||||
case s3select.ErrInvalidQuoteFields:
|
||||
apiErr = ErrInvalidQuoteFields
|
||||
case s3select.ErrInvalidRequestParameter:
|
||||
apiErr = ErrInvalidRequestParameter
|
||||
case s3select.ErrInvalidDataType:
|
||||
apiErr = ErrInvalidDataType
|
||||
case s3select.ErrInvalidTextEncoding:
|
||||
apiErr = ErrInvalidTextEncoding
|
||||
case s3select.ErrInvalidTableAlias:
|
||||
apiErr = ErrInvalidTableAlias
|
||||
case s3select.ErrMissingRequiredParameter:
|
||||
apiErr = ErrMissingRequiredParameter
|
||||
case s3select.ErrObjectSerializationConflict:
|
||||
apiErr = ErrObjectSerializationConflict
|
||||
case s3select.ErrUnsupportedSQLOperation:
|
||||
apiErr = ErrUnsupportedSQLOperation
|
||||
case s3select.ErrUnsupportedSQLStructure:
|
||||
apiErr = ErrUnsupportedSQLStructure
|
||||
case s3select.ErrUnsupportedSyntax:
|
||||
apiErr = ErrUnsupportedSyntax
|
||||
case s3select.ErrUnsupportedRangeHeader:
|
||||
apiErr = ErrUnsupportedRangeHeader
|
||||
case s3select.ErrLexerInvalidChar:
|
||||
apiErr = ErrLexerInvalidChar
|
||||
case s3select.ErrLexerInvalidOperator:
|
||||
apiErr = ErrLexerInvalidOperator
|
||||
case s3select.ErrLexerInvalidLiteral:
|
||||
apiErr = ErrLexerInvalidLiteral
|
||||
case s3select.ErrLexerInvalidIONLiteral:
|
||||
apiErr = ErrLexerInvalidIONLiteral
|
||||
case s3select.ErrParseExpectedDatePart:
|
||||
apiErr = ErrParseExpectedDatePart
|
||||
case s3select.ErrParseExpectedKeyword:
|
||||
apiErr = ErrParseExpectedKeyword
|
||||
case s3select.ErrParseExpectedTokenType:
|
||||
apiErr = ErrParseExpectedTokenType
|
||||
case s3select.ErrParseExpected2TokenTypes:
|
||||
apiErr = ErrParseExpected2TokenTypes
|
||||
case s3select.ErrParseExpectedNumber:
|
||||
apiErr = ErrParseExpectedNumber
|
||||
case s3select.ErrParseExpectedRightParenBuiltinFunctionCall:
|
||||
apiErr = ErrParseExpectedRightParenBuiltinFunctionCall
|
||||
case s3select.ErrParseExpectedTypeName:
|
||||
apiErr = ErrParseExpectedTypeName
|
||||
case s3select.ErrParseExpectedWhenClause:
|
||||
apiErr = ErrParseExpectedWhenClause
|
||||
case s3select.ErrParseUnsupportedToken:
|
||||
apiErr = ErrParseUnsupportedToken
|
||||
case s3select.ErrParseUnsupportedLiteralsGroupBy:
|
||||
apiErr = ErrParseUnsupportedLiteralsGroupBy
|
||||
case s3select.ErrParseExpectedMember:
|
||||
apiErr = ErrParseExpectedMember
|
||||
case s3select.ErrParseUnsupportedSelect:
|
||||
apiErr = ErrParseUnsupportedSelect
|
||||
case s3select.ErrParseUnsupportedCase:
|
||||
apiErr = ErrParseUnsupportedCase
|
||||
case s3select.ErrParseUnsupportedCaseClause:
|
||||
apiErr = ErrParseUnsupportedCaseClause
|
||||
case s3select.ErrParseUnsupportedAlias:
|
||||
apiErr = ErrParseUnsupportedAlias
|
||||
case s3select.ErrParseUnsupportedSyntax:
|
||||
apiErr = ErrParseUnsupportedSyntax
|
||||
case s3select.ErrParseUnknownOperator:
|
||||
apiErr = ErrParseUnknownOperator
|
||||
case s3select.ErrParseMissingIdentAfterAt:
|
||||
apiErr = ErrParseMissingIdentAfterAt
|
||||
case s3select.ErrParseUnexpectedOperator:
|
||||
apiErr = ErrParseUnexpectedOperator
|
||||
case s3select.ErrParseUnexpectedTerm:
|
||||
apiErr = ErrParseUnexpectedTerm
|
||||
case s3select.ErrParseUnexpectedToken:
|
||||
apiErr = ErrParseUnexpectedToken
|
||||
case s3select.ErrParseUnexpectedKeyword:
|
||||
apiErr = ErrParseUnexpectedKeyword
|
||||
case s3select.ErrParseExpectedExpression:
|
||||
apiErr = ErrParseExpectedExpression
|
||||
case s3select.ErrParseExpectedLeftParenAfterCast:
|
||||
apiErr = ErrParseExpectedLeftParenAfterCast
|
||||
case s3select.ErrParseExpectedLeftParenValueConstructor:
|
||||
apiErr = ErrParseExpectedLeftParenValueConstructor
|
||||
case s3select.ErrParseExpectedLeftParenBuiltinFunctionCall:
|
||||
apiErr = ErrParseExpectedLeftParenBuiltinFunctionCall
|
||||
case s3select.ErrParseExpectedArgumentDelimiter:
|
||||
apiErr = ErrParseExpectedArgumentDelimiter
|
||||
case s3select.ErrParseCastArity:
|
||||
apiErr = ErrParseCastArity
|
||||
case s3select.ErrParseInvalidTypeParam:
|
||||
apiErr = ErrParseInvalidTypeParam
|
||||
case s3select.ErrParseEmptySelect:
|
||||
apiErr = ErrParseEmptySelect
|
||||
case s3select.ErrParseSelectMissingFrom:
|
||||
apiErr = ErrParseSelectMissingFrom
|
||||
case s3select.ErrParseExpectedIdentForGroupName:
|
||||
apiErr = ErrParseExpectedIdentForGroupName
|
||||
case s3select.ErrParseExpectedIdentForAlias:
|
||||
apiErr = ErrParseExpectedIdentForAlias
|
||||
case s3select.ErrParseUnsupportedCallWithStar:
|
||||
apiErr = ErrParseUnsupportedCallWithStar
|
||||
case s3select.ErrParseNonUnaryAgregateFunctionCall:
|
||||
apiErr = ErrParseNonUnaryAgregateFunctionCall
|
||||
case s3select.ErrParseMalformedJoin:
|
||||
apiErr = ErrParseMalformedJoin
|
||||
case s3select.ErrParseExpectedIdentForAt:
|
||||
apiErr = ErrParseExpectedIdentForAt
|
||||
case s3select.ErrParseAsteriskIsNotAloneInSelectList:
|
||||
apiErr = ErrParseAsteriskIsNotAloneInSelectList
|
||||
case s3select.ErrParseCannotMixSqbAndWildcardInSelectList:
|
||||
apiErr = ErrParseCannotMixSqbAndWildcardInSelectList
|
||||
case s3select.ErrParseInvalidContextForWildcardInSelectList:
|
||||
apiErr = ErrParseInvalidContextForWildcardInSelectList
|
||||
case s3select.ErrIncorrectSQLFunctionArgumentType:
|
||||
apiErr = ErrIncorrectSQLFunctionArgumentType
|
||||
case s3select.ErrValueParseFailure:
|
||||
apiErr = ErrValueParseFailure
|
||||
case s3select.ErrIntegerOverflow:
|
||||
apiErr = ErrIntegerOverflow
|
||||
case s3select.ErrLikeInvalidInputs:
|
||||
apiErr = ErrLikeInvalidInputs
|
||||
case s3select.ErrCastFailed:
|
||||
apiErr = ErrCastFailed
|
||||
case s3select.ErrInvalidCast:
|
||||
apiErr = ErrInvalidCast
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPattern:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPattern
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
|
||||
case s3select.ErrEvaluatorTimestampFormatPatternDuplicateFields:
|
||||
apiErr = ErrEvaluatorTimestampFormatPatternDuplicateFields
|
||||
case s3select.ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch:
|
||||
apiErr = ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
|
||||
case s3select.ErrEvaluatorUnterminatedTimestampFormatPatternToken:
|
||||
apiErr = ErrEvaluatorUnterminatedTimestampFormatPatternToken
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternToken:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||
case s3select.ErrEvaluatorBindingDoesNotExist:
|
||||
apiErr = ErrEvaluatorBindingDoesNotExist
|
||||
case s3select.ErrMissingHeaders:
|
||||
apiErr = ErrMissingHeaders
|
||||
case format.ErrParseInvalidPathComponent:
|
||||
apiErr = ErrMissingHeaders
|
||||
case format.ErrInvalidColumnIndex:
|
||||
apiErr = ErrInvalidColumnIndex
|
||||
}
|
||||
|
||||
// Compression errors
|
||||
switch err {
|
||||
|
||||
+6
-1
@@ -26,14 +26,19 @@ import (
|
||||
type objectAPIHandlers struct {
|
||||
ObjectAPI func() ObjectLayer
|
||||
CacheAPI func() CacheObjectLayer
|
||||
// Returns true of handlers should interpret encryption.
|
||||
EncryptionEnabled func() bool
|
||||
}
|
||||
|
||||
// registerAPIRouter - registers S3 compatible APIs.
|
||||
func registerAPIRouter(router *mux.Router) {
|
||||
func registerAPIRouter(router *mux.Router, encryptionEnabled bool) {
|
||||
// Initialize API.
|
||||
api := objectAPIHandlers{
|
||||
ObjectAPI: newObjectLayerFn,
|
||||
CacheAPI: newCacheObjectsFn,
|
||||
EncryptionEnabled: func() bool {
|
||||
return encryptionEnabled
|
||||
},
|
||||
}
|
||||
|
||||
// API Router
|
||||
|
||||
+21
-2
@@ -482,7 +482,14 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.S3KMS.IsRequested(r.Header) { // SSE-KMS is not supported
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
|
||||
// Require Content-Length to be set in the request
|
||||
@@ -619,6 +626,18 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
var objectEncryptionKey []byte
|
||||
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
|
||||
var reader io.Reader
|
||||
@@ -645,7 +664,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, metadata, ObjectOptions{})
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, metadata, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
|
||||
+72
-40
@@ -30,7 +30,6 @@ import (
|
||||
dns2 "github.com/miekg/dns"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/target/console"
|
||||
"github.com/minio/minio/cmd/logger/target/http"
|
||||
@@ -79,38 +78,75 @@ func loadLoggers() {
|
||||
|
||||
}
|
||||
|
||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
|
||||
var configDir string
|
||||
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) *ConfigDir {
|
||||
var dir string
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("config-dir"):
|
||||
configDir = ctx.String("config-dir")
|
||||
case ctx.GlobalIsSet("config-dir"):
|
||||
configDir = ctx.GlobalString("config-dir")
|
||||
// cli package does not expose parent's "config-dir" option. Below code is workaround.
|
||||
if configDir == "" || configDir == getConfigDir() {
|
||||
if ctx.Parent().GlobalIsSet("config-dir") {
|
||||
configDir = ctx.Parent().GlobalString("config-dir")
|
||||
case ctx.IsSet(option):
|
||||
dir = ctx.String(option)
|
||||
case ctx.GlobalIsSet(option):
|
||||
dir = ctx.GlobalString(option)
|
||||
// cli package does not expose parent's option option. Below code is workaround.
|
||||
if dir == "" || dir == getDefaultDir() {
|
||||
if ctx.Parent().GlobalIsSet(option) {
|
||||
dir = ctx.Parent().GlobalString(option)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Neither local nor global config-dir option is provided. In this case, try to use
|
||||
// default config directory.
|
||||
configDir = getConfigDir()
|
||||
if configDir == "" {
|
||||
logger.FatalIf(errors.New("missing option"), "config-dir option must be provided")
|
||||
// Neither local nor global option is provided. In this case, try to use
|
||||
// default directory.
|
||||
dir = getDefaultDir()
|
||||
if dir == "" {
|
||||
logger.FatalIf(errInvalidArgument, "%s option must be provided", option)
|
||||
}
|
||||
}
|
||||
|
||||
if configDir == "" {
|
||||
logger.FatalIf(errors.New("empty directory"), "Configuration directory cannot be empty")
|
||||
if dir == "" {
|
||||
logger.FatalIf(errors.New("empty directory"), "%s directory cannot be empty", option)
|
||||
}
|
||||
|
||||
// Disallow relative paths, figure out absolute paths.
|
||||
configDirAbs, err := filepath.Abs(configDir)
|
||||
logger.FatalIf(err, "Unable to fetch absolute path for config directory %s", configDir)
|
||||
setConfigDir(configDirAbs)
|
||||
dirAbs, err := filepath.Abs(dir)
|
||||
logger.FatalIf(err, "Unable to fetch absolute path for %s=%s", option, dir)
|
||||
|
||||
logger.FatalIf(mkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)
|
||||
|
||||
return &ConfigDir{path: dirAbs}
|
||||
}
|
||||
|
||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
|
||||
// Get "json" flag from command line argument and
|
||||
// enable json and quite modes if jason flag is turned on.
|
||||
globalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
|
||||
if globalCLIContext.JSON {
|
||||
logger.EnableJSON()
|
||||
}
|
||||
|
||||
// Get quiet flag from command line argument.
|
||||
globalCLIContext.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
|
||||
if globalCLIContext.Quiet {
|
||||
logger.EnableQuiet()
|
||||
}
|
||||
|
||||
// Get anonymous flag from command line argument.
|
||||
globalCLIContext.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
|
||||
if globalCLIContext.Anonymous {
|
||||
logger.EnableAnonymous()
|
||||
}
|
||||
|
||||
// Fetch address option
|
||||
globalCLIContext.Addr = ctx.GlobalString("address")
|
||||
if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+globalMinioDefaultPort {
|
||||
globalCLIContext.Addr = ctx.String("address")
|
||||
}
|
||||
|
||||
// Set all config, certs and CAs directories.
|
||||
globalConfigDir = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
||||
globalCertsDir = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)
|
||||
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
||||
|
||||
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
||||
}
|
||||
|
||||
// Parses the given compression exclude list `extensions` or `content-types`.
|
||||
@@ -221,12 +257,22 @@ func handleCommonEnvVars() {
|
||||
minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
||||
if ok {
|
||||
minioEndpoints := strings.Split(minioEndpointsEnv, ",")
|
||||
for i, ip := range minioEndpoints {
|
||||
if net.ParseIP(ip) == nil {
|
||||
logger.FatalIf(errInvalidArgument, "Unable to initialize Minio server with invalid MINIO_PUBLIC_IPS[%d]: %s", i, ip)
|
||||
var domainIPs = set.NewStringSet()
|
||||
for _, endpoint := range minioEndpoints {
|
||||
if net.ParseIP(endpoint) == nil {
|
||||
// Checking if the IP is a DNS entry.
|
||||
addrs, err := net.LookupHost(endpoint)
|
||||
if err != nil {
|
||||
logger.FatalIf(err, "Unable to initialize Minio server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
domainIPs.Add(addr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
domainIPs.Add(endpoint)
|
||||
}
|
||||
updateDomainIPs(set.CreateStringSet(minioEndpoints...))
|
||||
updateDomainIPs(domainIPs)
|
||||
} else {
|
||||
// Add found interfaces IP address to global domain IPS,
|
||||
// loopback addresses will be naturally dropped.
|
||||
@@ -322,20 +368,6 @@ func handleCommonEnvVars() {
|
||||
globalWORMEnabled = bool(wormFlag)
|
||||
}
|
||||
|
||||
kmsConf, err := crypto.NewVaultConfig()
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to initialize hashicorp vault")
|
||||
}
|
||||
if kmsConf.Vault.Endpoint != "" {
|
||||
kms, err := crypto.NewVault(kmsConf)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to initialize KMS")
|
||||
}
|
||||
globalKMS = kms
|
||||
globalKMSKeyID = kmsConf.Vault.Key.Name
|
||||
globalKMSConfig = kmsConf
|
||||
}
|
||||
|
||||
if compress := os.Getenv("MINIO_COMPRESS"); compress != "" {
|
||||
globalIsCompressionEnabled = strings.EqualFold(compress, "true")
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@ func (s *serverConfig) loadFromEnvs() {
|
||||
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||
}
|
||||
|
||||
if globalKMS != nil {
|
||||
s.KMS = globalKMSConfig
|
||||
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
|
||||
logger.FatalIf(err, "Unable to setup the KMS")
|
||||
}
|
||||
|
||||
if globalIsEnvCompression {
|
||||
@@ -534,12 +534,8 @@ func (s *serverConfig) loadToCachedConfigs() {
|
||||
globalCacheExpiry = cacheConf.Expiry
|
||||
globalCacheMaxUse = cacheConf.MaxUse
|
||||
}
|
||||
if globalKMS == nil {
|
||||
globalKMSConfig = s.KMS
|
||||
if kms, err := crypto.NewVault(globalKMSConfig); err == nil {
|
||||
globalKMS = kms
|
||||
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
||||
}
|
||||
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
|
||||
logger.FatalIf(err, "Unable to setup the KMS")
|
||||
}
|
||||
|
||||
if !globalIsCompressionEnabled {
|
||||
|
||||
+37
-70
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
)
|
||||
@@ -41,63 +40,9 @@ const (
|
||||
privateKeyFile = "private.key"
|
||||
)
|
||||
|
||||
// ConfigDir - configuration directory with locking.
|
||||
// ConfigDir - points to a user set directory.
|
||||
type ConfigDir struct {
|
||||
sync.Mutex
|
||||
dir string
|
||||
}
|
||||
|
||||
// Set - saves given directory as configuration directory.
|
||||
func (config *ConfigDir) Set(dir string) {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
config.dir = dir
|
||||
}
|
||||
|
||||
// Get - returns current configuration directory.
|
||||
func (config *ConfigDir) Get() string {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
return config.dir
|
||||
}
|
||||
|
||||
func (config *ConfigDir) getCertsDir() string {
|
||||
return filepath.Join(config.Get(), certsDir)
|
||||
}
|
||||
|
||||
// GetCADir - returns certificate CA directory.
|
||||
func (config *ConfigDir) GetCADir() string {
|
||||
return filepath.Join(config.getCertsDir(), certsCADir)
|
||||
}
|
||||
|
||||
// Create - creates configuration directory tree.
|
||||
func (config *ConfigDir) Create() error {
|
||||
err := os.MkdirAll(config.GetCADir(), 0700)
|
||||
// It is possible in kubernetes like deployments this directory
|
||||
// is already mounted and is not writable, ignore any write errors.
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMinioConfigFile - returns absolute path of config.json file.
|
||||
func (config *ConfigDir) GetMinioConfigFile() string {
|
||||
return filepath.Join(config.Get(), minioConfigFile)
|
||||
}
|
||||
|
||||
// GetPublicCertFile - returns absolute path of public.crt file.
|
||||
func (config *ConfigDir) GetPublicCertFile() string {
|
||||
return filepath.Join(config.getCertsDir(), publicCertFile)
|
||||
}
|
||||
|
||||
// GetPrivateKeyFile - returns absolute path of private.key file.
|
||||
func (config *ConfigDir) GetPrivateKeyFile() string {
|
||||
return filepath.Join(config.getCertsDir(), privateKeyFile)
|
||||
path string
|
||||
}
|
||||
|
||||
func getDefaultConfigDir() string {
|
||||
@@ -109,32 +54,54 @@ func getDefaultConfigDir() string {
|
||||
return filepath.Join(homeDir, defaultMinioConfigDir)
|
||||
}
|
||||
|
||||
var configDir = &ConfigDir{dir: getDefaultConfigDir()}
|
||||
|
||||
func setConfigDir(dir string) {
|
||||
configDir.Set(dir)
|
||||
func getDefaultCertsDir() string {
|
||||
return filepath.Join(getDefaultConfigDir(), certsDir)
|
||||
}
|
||||
|
||||
func getConfigDir() string {
|
||||
return configDir.Get()
|
||||
func getDefaultCertsCADir() string {
|
||||
return filepath.Join(getDefaultCertsDir(), certsCADir)
|
||||
}
|
||||
|
||||
func getCADir() string {
|
||||
return configDir.GetCADir()
|
||||
var (
|
||||
// Default config, certs and CA directories.
|
||||
defaultConfigDir = &ConfigDir{path: getDefaultConfigDir()}
|
||||
defaultCertsDir = &ConfigDir{path: getDefaultCertsDir()}
|
||||
defaultCertsCADir = &ConfigDir{path: getDefaultCertsCADir()}
|
||||
|
||||
// Points to current configuration directory -- deprecated, to be removed in future.
|
||||
globalConfigDir = defaultConfigDir
|
||||
// Points to current certs directory set by user with --certs-dir
|
||||
globalCertsDir = defaultCertsDir
|
||||
// Points to relative path to certs directory and is <value-of-certs-dir>/CAs
|
||||
globalCertsCADir = defaultCertsCADir
|
||||
)
|
||||
|
||||
// Get - returns current directory.
|
||||
func (dir *ConfigDir) Get() string {
|
||||
return dir.path
|
||||
}
|
||||
|
||||
func createConfigDir() error {
|
||||
return configDir.Create()
|
||||
// Attempts to create all directories, ignores any permission denied errors.
|
||||
func mkdirAllIgnorePerm(path string) error {
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
// It is possible in kubernetes like deployments this directory
|
||||
// is already mounted and is not writable, ignore any write errors.
|
||||
if os.IsPermission(err) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getConfigFile() string {
|
||||
return configDir.GetMinioConfigFile()
|
||||
return filepath.Join(globalConfigDir.Get(), minioConfigFile)
|
||||
}
|
||||
|
||||
func getPublicCertFile() string {
|
||||
return configDir.GetPublicCertFile()
|
||||
return filepath.Join(globalCertsDir.Get(), publicCertFile)
|
||||
}
|
||||
|
||||
func getPrivateKeyFile() string {
|
||||
return configDir.GetPrivateKeyFile()
|
||||
return filepath.Join(globalCertsDir.Get(), privateKeyFile)
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ func migrateConfig() error {
|
||||
|
||||
// Version '1' is not supported anymore and deprecated, safe to delete.
|
||||
func purgeV1() error {
|
||||
configFile := filepath.Join(getConfigDir(), "fsUsers.json")
|
||||
configFile := filepath.Join(globalConfigDir.Get(), "fsUsers.json")
|
||||
|
||||
cv1 := &configV1{}
|
||||
_, err := Load(configFile, cv1)
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = objLayer
|
||||
@@ -77,7 +77,7 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
if err := migrateV2ToV3(); err != nil {
|
||||
t.Fatal("migrate v2 to v3 should succeed when no config file is found")
|
||||
@@ -166,7 +166,8 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
@@ -235,7 +236,8 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
// Create a corrupted config file
|
||||
@@ -331,7 +333,8 @@ func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
for i := 3; i <= 17; i++ {
|
||||
|
||||
@@ -16,5 +16,6 @@ package crypto
|
||||
|
||||
// KMSConfig has the KMS config for hashicorp vault
|
||||
type KMSConfig struct {
|
||||
Vault VaultConfig `json:"vault"`
|
||||
AutoEncryption bool `json:"-"`
|
||||
Vault VaultConfig `json:"vault"`
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ var ssecIsRequestedTests = []struct {
|
||||
Header http.Header
|
||||
Expected bool
|
||||
}{
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||
@@ -137,7 +137,7 @@ var ssecCopyIsRequestedTests = []struct {
|
||||
Header http.Header
|
||||
Expected bool
|
||||
}{
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||
|
||||
@@ -40,6 +40,14 @@ func RemoveSensitiveEntries(metadata map[string]string) { // The functions is te
|
||||
delete(metadata, SSECopyKey)
|
||||
}
|
||||
|
||||
// RemoveSSEHeaders removes all crypto-specific SSE
|
||||
// header entries from the metadata map.
|
||||
func RemoveSSEHeaders(metadata map[string]string) {
|
||||
delete(metadata, SSEHeader)
|
||||
delete(metadata, SSECKeyMD5)
|
||||
delete(metadata, SSECAlgorithm)
|
||||
}
|
||||
|
||||
// RemoveInternalEntries removes all crypto-specific internal
|
||||
// metadata entries from the metadata map.
|
||||
func RemoveInternalEntries(metadata map[string]string) {
|
||||
|
||||
@@ -370,9 +370,9 @@ var isETagSealedTests = []struct {
|
||||
ETag string
|
||||
IsSealed bool
|
||||
}{
|
||||
{ETag: "", IsSealed: false}, // 0
|
||||
{ETag: "90682b8e8cc7609c4671e1d64c73fc30", IsSealed: false}, // 1
|
||||
{ETag: "f201040c9dc593e39ea004dc1323699bcd", IsSealed: true}, // 2 not valid ciphertext but looks like sealed ETag
|
||||
{ETag: "", IsSealed: false}, // 0
|
||||
{ETag: "90682b8e8cc7609c4671e1d64c73fc30", IsSealed: false}, // 1
|
||||
{ETag: "f201040c9dc593e39ea004dc1323699bcd", IsSealed: true}, // 2 not valid ciphertext but looks like sealed ETag
|
||||
{ETag: "20000f00fba2ee2ae4845f725964eeb9e092edfabc7ab9f9239e8344341f769a51ce99b4801b0699b92b16a72fa94972", IsSealed: true}, // 3
|
||||
}
|
||||
|
||||
|
||||
+123
-184
@@ -19,200 +19,133 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
vault "github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
const (
|
||||
// vaultEndpointEnv Vault endpoint environment variable
|
||||
vaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
|
||||
// vaultAuthTypeEnv type of vault auth to be used
|
||||
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
|
||||
// vaultAppRoleIDEnv Vault AppRole ID environment variable
|
||||
vaultAppRoleIDEnv = "MINIO_SSE_VAULT_APPROLE_ID"
|
||||
// vaultAppSecretIDEnv Vault AppRole Secret environment variable
|
||||
vaultAppSecretIDEnv = "MINIO_SSE_VAULT_APPROLE_SECRET"
|
||||
// vaultKeyVersionEnv Vault Key Version environment variable
|
||||
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
|
||||
// vaultKeyNameEnv Vault Encryption Key Name environment variable
|
||||
vaultKeyNameEnv = "MINIO_SSE_VAULT_KEY_NAME"
|
||||
|
||||
// vaultCAPath is the path to a directory of PEM-encoded CA
|
||||
// cert files to verify the Vault server SSL certificate.
|
||||
vaultCAPath = "MINIO_SSE_VAULT_CAPATH"
|
||||
)
|
||||
|
||||
var (
|
||||
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
|
||||
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
|
||||
)
|
||||
|
||||
// VaultKey represents vault encryption key-ring.
|
||||
type VaultKey struct {
|
||||
Name string `json:"name"` // The name of the encryption key-ring
|
||||
Version int `json:"version"` // The key version
|
||||
}
|
||||
|
||||
// VaultAuth represents vault authentication type.
|
||||
// Currently the only supported authentication type is AppRole.
|
||||
type VaultAuth struct {
|
||||
Type string `json:"type"` // The authentication type
|
||||
AppRole VaultAppRole `json:"approle"` // The AppRole authentication credentials
|
||||
}
|
||||
|
||||
// VaultAppRole represents vault AppRole authentication credentials
|
||||
type VaultAppRole struct {
|
||||
ID string `json:"id"` // The AppRole access ID
|
||||
Secret string `json:"secret"` // The AppRole secret
|
||||
}
|
||||
|
||||
// VaultConfig represents vault configuration.
|
||||
type VaultConfig struct {
|
||||
Endpoint string `json:"endpoint"` // The vault API endpoint as URL
|
||||
CAPath string `json:"-"` // The path to PEM-encoded certificate files used for mTLS. Currently not used in config file.
|
||||
Auth VaultAuth `json:"auth"` // The vault authentication configuration
|
||||
Key VaultKey `json:"key-id"` // The named key used for key-generation / decryption.
|
||||
Namespace string `json:"-"` // The vault namespace of enterprise vault instances
|
||||
}
|
||||
|
||||
// vaultService represents a connection to a vault KMS.
|
||||
type vaultService struct {
|
||||
config *VaultConfig
|
||||
client *vault.Client
|
||||
leaseDuration time.Duration
|
||||
}
|
||||
|
||||
// return transit secret engine's path for generate data key operation
|
||||
func (v *vaultService) genDataKeyEndpoint(key string) string {
|
||||
return "/transit/datakey/plaintext/" + key
|
||||
var _ KMS = (*vaultService)(nil) // compiler check that *vaultService implements KMS
|
||||
|
||||
// empty/default vault configuration used to check whether a particular is empty.
|
||||
var emptyVaultConfig = VaultConfig{}
|
||||
|
||||
// IsEmpty returns true if the vault config struct is an
|
||||
// empty configuration.
|
||||
func (v *VaultConfig) IsEmpty() bool { return *v == emptyVaultConfig }
|
||||
|
||||
// Verify returns a nil error if the vault configuration
|
||||
// is valid. A valid configuration is either empty or
|
||||
// contains valid non-default values.
|
||||
func (v *VaultConfig) Verify() (err error) {
|
||||
if v.IsEmpty() {
|
||||
return // an empty configuration is valid
|
||||
}
|
||||
switch {
|
||||
case v.Endpoint == "":
|
||||
err = errors.New("crypto: missing hashicorp vault endpoint")
|
||||
case strings.ToLower(v.Auth.Type) != "approle":
|
||||
err = fmt.Errorf("crypto: invalid hashicorp vault authentication type: %s is not supported", v.Auth.Type)
|
||||
case v.Auth.AppRole.ID == "":
|
||||
err = errors.New("crypto: missing hashicorp vault AppRole ID")
|
||||
case v.Auth.AppRole.Secret == "":
|
||||
err = errors.New("crypto: missing hashicorp vault AppSecret ID")
|
||||
case v.Key.Name == "":
|
||||
err = errors.New("crypto: missing hashicorp vault key name")
|
||||
case v.Key.Version < 0:
|
||||
err = errors.New("crypto: invalid hashicorp vault key version: The key version must not be negative")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// return transit secret engine's path for decrypt operation
|
||||
func (v *vaultService) decryptEndpoint(key string) string {
|
||||
return "/transit/decrypt/" + key
|
||||
}
|
||||
// NewVault initializes Hashicorp Vault KMS by authenticating
|
||||
// to Vault with the credentials in config and gets a client
|
||||
// token for future api calls.
|
||||
func NewVault(config VaultConfig) (KMS, error) {
|
||||
if config.IsEmpty() {
|
||||
return nil, errors.New("crypto: the hashicorp vault configuration must not be empty")
|
||||
}
|
||||
if err := config.Verify(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// VaultKey represents vault encryption key-id name & version
|
||||
type VaultKey struct {
|
||||
Name string `json:"name"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
vaultCfg := vault.Config{Address: config.Endpoint}
|
||||
if err := vaultCfg.ConfigureTLS(&vault.TLSConfig{CAPath: config.CAPath}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := vault.NewClient(&vaultCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Namespace != "" {
|
||||
client.SetNamespace(config.Namespace)
|
||||
}
|
||||
|
||||
// VaultAuth represents vault auth type to use. For now, AppRole is the only supported
|
||||
// auth type.
|
||||
type VaultAuth struct {
|
||||
Type string `json:"type"`
|
||||
AppRole VaultAppRole `json:"approle"`
|
||||
}
|
||||
|
||||
// VaultAppRole represents vault approle credentials
|
||||
type VaultAppRole struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// VaultConfig holds config required to start vault service
|
||||
type VaultConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Auth VaultAuth `json:"auth"`
|
||||
Key VaultKey `json:"key-id"`
|
||||
}
|
||||
|
||||
// validate whether all required env variables needed to start vault service have
|
||||
// been set
|
||||
func validateVaultConfig(c *VaultConfig) error {
|
||||
if c.Endpoint == "" {
|
||||
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", vaultEndpointEnv)
|
||||
payload := map[string]interface{}{
|
||||
"role_id": config.Auth.AppRole.ID,
|
||||
"secret_id": config.Auth.AppRole.Secret,
|
||||
}
|
||||
if strings.ToLower(c.Auth.Type) != "approle" {
|
||||
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
|
||||
}
|
||||
if c.Auth.AppRole.ID == "" {
|
||||
return fmt.Errorf("Missing hashicorp vault AppRole ID - %s is empty", vaultAppRoleIDEnv)
|
||||
}
|
||||
if c.Auth.AppRole.Secret == "" {
|
||||
return fmt.Errorf("Missing hashicorp vault AppSecret ID - %s is empty", vaultAppSecretIDEnv)
|
||||
}
|
||||
if c.Key.Name == "" {
|
||||
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyNameEnv)
|
||||
}
|
||||
if c.Key.Version < 0 {
|
||||
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authenticate to vault with app role id and app role secret, and get a client access token, lease duration
|
||||
func getVaultAccessToken(client *vault.Client, appRoleID, appSecret string) (token string, duration int, err error) {
|
||||
data := map[string]interface{}{
|
||||
"role_id": appRoleID,
|
||||
"secret_id": appSecret,
|
||||
}
|
||||
resp, e := client.Logical().Write("auth/approle/login", data)
|
||||
if e != nil {
|
||||
return token, duration, e
|
||||
resp, err := client.Logical().Write("auth/approle/login", payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Auth == nil {
|
||||
return token, duration, ErrKMSAuthLogin
|
||||
return nil, ErrKMSAuthLogin
|
||||
}
|
||||
return resp.Auth.ClientToken, resp.Auth.LeaseDuration, nil
|
||||
|
||||
client.SetToken(resp.Auth.ClientToken)
|
||||
v := &vaultService{client: client, config: &config, leaseDuration: time.Duration(resp.Auth.LeaseDuration)}
|
||||
v.renewToken()
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// NewVaultConfig sets KMSConfig from environment
|
||||
// variables and performs validations.
|
||||
func NewVaultConfig() (KMSConfig, error) {
|
||||
kc := KMSConfig{}
|
||||
endpoint := os.Getenv(vaultEndpointEnv)
|
||||
roleID := os.Getenv(vaultAppRoleIDEnv)
|
||||
roleSecret := os.Getenv(vaultAppSecretIDEnv)
|
||||
keyName := os.Getenv(vaultKeyNameEnv)
|
||||
keyVersion := 0
|
||||
authType := "approle"
|
||||
if versionStr := os.Getenv(vaultKeyVersionEnv); versionStr != "" {
|
||||
version, err := strconv.Atoi(versionStr)
|
||||
if err != nil {
|
||||
return kc, fmt.Errorf("Unable to parse %s value (`%s`)", vaultKeyVersionEnv, versionStr)
|
||||
}
|
||||
keyVersion = version
|
||||
}
|
||||
// return if none of the vault env variables are configured
|
||||
if (endpoint == "") && (roleID == "") && (roleSecret == "") && (keyName == "") && (keyVersion == 0) {
|
||||
return kc, nil
|
||||
}
|
||||
c := VaultConfig{
|
||||
Endpoint: endpoint,
|
||||
Auth: VaultAuth{
|
||||
Type: authType,
|
||||
AppRole: VaultAppRole{
|
||||
ID: roleID,
|
||||
Secret: roleSecret,
|
||||
},
|
||||
},
|
||||
Key: VaultKey{
|
||||
Version: keyVersion,
|
||||
Name: keyName,
|
||||
},
|
||||
}
|
||||
if err := validateVaultConfig(&c); err != nil {
|
||||
return kc, err
|
||||
}
|
||||
kc.Vault = c
|
||||
return kc, nil
|
||||
}
|
||||
|
||||
// NewVault initializes Hashicorp Vault KMS by
|
||||
// authenticating to Vault with the credentials in KMSConfig,
|
||||
// and gets a client token for future api calls.
|
||||
func NewVault(kmsConf KMSConfig) (KMS, error) {
|
||||
config := kmsConf.Vault
|
||||
vconfig := &vault.Config{
|
||||
Address: config.Endpoint,
|
||||
}
|
||||
if err := vconfig.ConfigureTLS(&vault.TLSConfig{
|
||||
CAPath: os.Getenv(vaultCAPath),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := vault.NewClient(vconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ns, ok := os.LookupEnv("MINIO_SSE_VAULT_NAMESPACE"); ok {
|
||||
c.SetNamespace(ns)
|
||||
}
|
||||
|
||||
accessToken, leaseDuration, err := getVaultAccessToken(c, config.Auth.AppRole.ID, config.Auth.AppRole.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// authenticate and get the access token
|
||||
c.SetToken(accessToken)
|
||||
v := vaultService{client: c, config: &config, leaseDuration: time.Duration(leaseDuration)}
|
||||
v.renewToken(c)
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func (v *vaultService) renewToken(c *vault.Client) {
|
||||
// renewToken starts a new go-routine which renews
|
||||
// the vault authentication token periodically.
|
||||
func (v *vaultService) renewToken() {
|
||||
retryDelay := 1 * time.Minute
|
||||
go func() {
|
||||
for {
|
||||
s, err := c.Auth().Token().RenewSelf(int(v.leaseDuration))
|
||||
s, err := v.client.Auth().Token().RenewSelf(int(v.leaseDuration))
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
@@ -223,48 +156,54 @@ func (v *vaultService) renewToken(c *vault.Client) {
|
||||
}()
|
||||
}
|
||||
|
||||
// Generates a random plain text key, sealed plain text key from
|
||||
// Vault. It returns the plaintext key and sealed plaintext key on success
|
||||
// GenerateKey returns a new plaintext key, generated by the KMS,
|
||||
// and a sealed version of this plaintext key encrypted using the
|
||||
// named key referenced by keyID. It also binds the generated key
|
||||
// cryptographically to the provided context.
|
||||
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
||||
contextStream := new(bytes.Buffer)
|
||||
ctx.WriteTo(contextStream)
|
||||
var contextStream bytes.Buffer
|
||||
ctx.WriteTo(&contextStream)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||
}
|
||||
s, err1 := v.client.Logical().Write(v.genDataKeyEndpoint(keyID), payload)
|
||||
|
||||
if err1 != nil {
|
||||
return key, sealedKey, err1
|
||||
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/datakey/plaintext/%s", keyID), payload)
|
||||
if err != nil {
|
||||
return key, sealedKey, err
|
||||
}
|
||||
sealKey := s.Data["ciphertext"].(string)
|
||||
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
|
||||
if err != nil {
|
||||
return key, sealedKey, err1
|
||||
return key, sealedKey, err
|
||||
}
|
||||
copy(key[:], []byte(plainKey))
|
||||
return key, []byte(sealKey), nil
|
||||
}
|
||||
|
||||
// unsealKMSKey unseals the sealedKey using the Vault master key
|
||||
// referenced by the keyID. The plain text key is returned on success.
|
||||
// UnsealKey returns the decrypted sealedKey as plaintext key.
|
||||
// Therefore it sends the sealedKey to the KMS which decrypts
|
||||
// it using the named key referenced by keyID and responses with
|
||||
// the plaintext key.
|
||||
//
|
||||
// The context must be same context as the one provided while
|
||||
// generating the plaintext key / sealedKey.
|
||||
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
||||
contextStream := new(bytes.Buffer)
|
||||
ctx.WriteTo(contextStream)
|
||||
var contextStream bytes.Buffer
|
||||
ctx.WriteTo(&contextStream)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"ciphertext": string(sealedKey),
|
||||
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||
}
|
||||
s, err1 := v.client.Logical().Write(v.decryptEndpoint(keyID), payload)
|
||||
if err1 != nil {
|
||||
return key, err1
|
||||
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/decrypt/%s", keyID), payload)
|
||||
if err != nil {
|
||||
return key, err
|
||||
}
|
||||
base64Key := s.Data["plaintext"].(string)
|
||||
plainKey, err1 := base64.StdEncoding.DecodeString(base64Key)
|
||||
if err1 != nil {
|
||||
return key, err1
|
||||
plainKey, err := base64.StdEncoding.DecodeString(base64Key)
|
||||
if err != nil {
|
||||
return key, err
|
||||
}
|
||||
copy(key[:], []byte(plainKey))
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func newCacheFSObjects(dir string, expiry int, maxDiskUsagePct int) (*cacheFSObj
|
||||
appendFileMap: make(map[string]*fsAppendFile),
|
||||
}
|
||||
|
||||
go fsObjects.cleanupStaleMultipartUploads(context.Background(), globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
|
||||
go fsObjects.cleanupStaleMultipartUploads(context.Background(), GlobalMultipartCleanupInterval, GlobalMultipartExpiry, GlobalServiceDoneCh)
|
||||
|
||||
cacheFS := cacheFSObjects{
|
||||
FSObjects: fsObjects,
|
||||
@@ -158,7 +158,7 @@ func (cfs *cacheFSObjects) purgeTrash() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
trashPath := path.Join(cfs.fsPath, minioMetaBucket, cacheTrashDir)
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestCacheExclusion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cobj := cobjects.(*cacheObjects)
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
objectName string
|
||||
|
||||
@@ -99,7 +99,7 @@ func (api *DummyObjectLayer) PutObjectPart(ctx context.Context, bucket, object,
|
||||
return
|
||||
}
|
||||
|
||||
func (api *DummyObjectLayer) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result ListPartsInfo, err error) {
|
||||
func (api *DummyObjectLayer) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+137
-26
@@ -29,6 +29,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
@@ -55,11 +56,11 @@ const (
|
||||
// SSEIVSize is the size of the IV data
|
||||
SSEIVSize = 32 // 32 bytes
|
||||
|
||||
// SSE dare package block size.
|
||||
sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
||||
// SSEDAREPackageBlockSize - SSE dare package block size.
|
||||
SSEDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
||||
|
||||
// SSE dare package meta padding bytes.
|
||||
sseDAREPackageMetaSize = 32 // 32 bytes
|
||||
// SSEDAREPackageMetaSize - SSE dare package meta padding bytes.
|
||||
SSEDAREPackageMetaSize = 32 // 32 bytes
|
||||
|
||||
)
|
||||
|
||||
@@ -158,14 +159,14 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return nil
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if globalKMS == nil {
|
||||
if GlobalKMS == nil {
|
||||
return errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
oldKey, err := GlobalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -174,7 +175,7 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
newKey, encKey, err := GlobalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -187,10 +188,10 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
if sseS3 {
|
||||
if globalKMS == nil {
|
||||
if GlobalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
key, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
key, encKey, err := GlobalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -279,7 +280,7 @@ func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]st
|
||||
default:
|
||||
return nil, errObjectTampered
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if globalKMS == nil {
|
||||
if GlobalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
@@ -287,7 +288,7 @@ func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
extKey, err := GlobalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -395,7 +396,6 @@ func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||
io.Reader, error) {
|
||||
|
||||
bucket, object := oi.Bucket, oi.Name
|
||||
|
||||
// Single part case
|
||||
if !isEncryptedMultipart(oi) {
|
||||
var reader io.Reader
|
||||
@@ -411,8 +411,8 @@ func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
partDecRelOffset := int64(seqNumber) * sseDAREPackageBlockSize
|
||||
partEncRelOffset := int64(seqNumber) * (sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
partDecRelOffset := int64(seqNumber) * SSEDAREPackageBlockSize
|
||||
partEncRelOffset := int64(seqNumber) * (SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
|
||||
w := &DecryptBlocksReader{
|
||||
reader: inputReader,
|
||||
@@ -477,7 +477,7 @@ type DecryptBlocksReader struct {
|
||||
// Current part index
|
||||
partIndex int
|
||||
// Parts information
|
||||
parts []objectPartInfo
|
||||
parts []ObjectPartInfo
|
||||
header http.Header
|
||||
bucket, object string
|
||||
metadata map[string]string
|
||||
@@ -592,7 +592,7 @@ type DecryptBlocksWriter struct {
|
||||
// Current part index
|
||||
partIndex int
|
||||
// Parts information
|
||||
parts []objectPartInfo
|
||||
parts []ObjectPartInfo
|
||||
req *http.Request
|
||||
bucket, object string
|
||||
metadata map[string]string
|
||||
@@ -743,6 +743,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
}
|
||||
|
||||
seqNumber, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
|
||||
|
||||
var partStartIndex int
|
||||
var partStartOffset = startOffset
|
||||
// Skip parts until final offset maps to a particular part offset.
|
||||
@@ -765,8 +766,8 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
partStartOffset -= int64(decryptedSize)
|
||||
}
|
||||
|
||||
startSeqNum := partStartOffset / sseDAREPackageBlockSize
|
||||
partEncRelOffset := int64(startSeqNum) * (sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
startSeqNum := partStartOffset / SSEDAREPackageBlockSize
|
||||
partEncRelOffset := int64(startSeqNum) * (SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
|
||||
w := &DecryptBlocksWriter{
|
||||
writer: client,
|
||||
@@ -857,9 +858,9 @@ func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (u
|
||||
|
||||
// getEncryptedSinglePartOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||
func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo) (seqNumber uint32, encOffset int64, encLength int64) {
|
||||
onePkgSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
onePkgSize := int64(SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
|
||||
seqNumber = uint32(offset / sseDAREPackageBlockSize)
|
||||
seqNumber = uint32(offset / SSEDAREPackageBlockSize)
|
||||
encOffset = int64(seqNumber) * onePkgSize
|
||||
// The math to compute the encrypted length is always
|
||||
// originalLength i.e (offset+length-1) to be divided under
|
||||
@@ -867,10 +868,10 @@ func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo
|
||||
// block. This is then multiplied by final package size which
|
||||
// is basically 64KiB + 32. Finally negate the encrypted offset
|
||||
// to get the final encrypted length on disk.
|
||||
encLength = ((offset+length)/sseDAREPackageBlockSize)*onePkgSize - encOffset
|
||||
encLength = ((offset+length)/SSEDAREPackageBlockSize)*onePkgSize - encOffset
|
||||
|
||||
// Check for the remainder, to figure if we need one extract package to read from.
|
||||
if (offset+length)%sseDAREPackageBlockSize > 0 {
|
||||
if (offset+length)%SSEDAREPackageBlockSize > 0 {
|
||||
encLength += onePkgSize
|
||||
}
|
||||
|
||||
@@ -1041,11 +1042,11 @@ func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, sk
|
||||
// partStart is always found in the loop above,
|
||||
// because off is validated.
|
||||
|
||||
sseDAREEncPackageBlockSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
startPkgNum := (off - cumulativeSum) / sseDAREPackageBlockSize
|
||||
sseDAREEncPackageBlockSize := int64(SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
startPkgNum := (off - cumulativeSum) / SSEDAREPackageBlockSize
|
||||
|
||||
// Now we can calculate the number of bytes to skip
|
||||
skipLen = (off - cumulativeSum) % sseDAREPackageBlockSize
|
||||
skipLen = (off - cumulativeSum) % SSEDAREPackageBlockSize
|
||||
|
||||
encOff = encCumulativeSum + startPkgNum*sseDAREEncPackageBlockSize
|
||||
// Locate the part containing the end of the required range
|
||||
@@ -1062,7 +1063,7 @@ func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, sk
|
||||
}
|
||||
// partEnd is always found in the loop above, because off and
|
||||
// length are validated.
|
||||
endPkgNum := (endOffset - cumulativeSum) / sseDAREPackageBlockSize
|
||||
endPkgNum := (endOffset - cumulativeSum) / SSEDAREPackageBlockSize
|
||||
// Compute endEncOffset with one additional DARE package (so
|
||||
// we read the package containing the last desired byte).
|
||||
endEncOffset := encCumulativeSum + (endPkgNum+1)*sseDAREEncPackageBlockSize
|
||||
@@ -1157,3 +1158,113 @@ func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (encrypted bool, e
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The customer key in the header is used by the gateway for encryption in the case of
|
||||
// s3 gateway double encryption. A new client key is derived from the customer provided
|
||||
// key to be sent to the s3 backend for encryption at the backend.
|
||||
func deriveClientKey(clientKey [32]byte, bucket, object string) [32]byte {
|
||||
var key [32]byte
|
||||
mac := hmac.New(sha256.New, clientKey[:])
|
||||
mac.Write([]byte(crypto.SSEC.String()))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(key[:0])
|
||||
return key
|
||||
}
|
||||
|
||||
// extract encryption options for pass through to backend in the case of gateway
|
||||
func extractEncryptionOption(header http.Header, copySource bool, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||
var clientKey [32]byte
|
||||
var sse encrypt.ServerSide
|
||||
|
||||
if copySource {
|
||||
if crypto.SSECopy.IsRequested(header) {
|
||||
clientKey, err = crypto.SSECopy.ParseHTTP(header)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
||||
return
|
||||
}
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.SSECopy(sse)}, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.SSEC.IsRequested(header) {
|
||||
clientKey, err = crypto.SSEC.ParseHTTP(header)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
||||
return
|
||||
}
|
||||
return ObjectOptions{ServerSideEncryption: sse}, nil
|
||||
}
|
||||
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.NewSSE()}, nil
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// get ObjectOptions for GET calls from encryption headers
|
||||
func getEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
||||
var (
|
||||
encryption encrypt.ServerSide
|
||||
opts ObjectOptions
|
||||
)
|
||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||
key, err := crypto.SSEC.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
derivedKey := deriveClientKey(key, bucket, object)
|
||||
encryption, err = encrypt.NewSSEC(derivedKey[:])
|
||||
logger.CriticalIf(ctx, err)
|
||||
return ObjectOptions{ServerSideEncryption: encryption}, nil
|
||||
}
|
||||
// default case of passing encryption headers to backend
|
||||
return extractEncryptionOption(r.Header, false, nil)
|
||||
}
|
||||
|
||||
// get ObjectOptions for PUT calls from encryption headers
|
||||
func putEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||
// In the case of multipart custom format, the metadata needs to be checked in addition to header to see if it
|
||||
// is SSE-S3 encrypted, primarily because S3 protocol does not require SSE-S3 headers in PutObjectPart calls
|
||||
if GlobalGatewaySSE.SSES3() && (crypto.S3.IsRequested(r.Header) || crypto.S3.IsEncrypted(metadata)) {
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.NewSSE()}, nil
|
||||
}
|
||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||
return getEncryptionOpts(ctx, r, bucket, object)
|
||||
}
|
||||
// default case of passing encryption headers to backend
|
||||
return extractEncryptionOption(r.Header, false, metadata)
|
||||
}
|
||||
|
||||
// get ObjectOptions for Copy calls for encryption headers provided on the target side
|
||||
func copyDstEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||
return putEncryptionOpts(ctx, r, bucket, object, metadata)
|
||||
}
|
||||
|
||||
// get ObjectOptions for Copy calls for encryption headers provided on the source side
|
||||
func copySrcEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
||||
var (
|
||||
ssec encrypt.ServerSide
|
||||
opts ObjectOptions
|
||||
)
|
||||
|
||||
if GlobalGatewaySSE.SSEC() && crypto.SSECopy.IsRequested(r.Header) {
|
||||
key, err := crypto.SSECopy.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
derivedKey := deriveClientKey(key, bucket, object)
|
||||
ssec, err = encrypt.NewSSEC(derivedKey[:])
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.SSECopy(ssec)}, nil
|
||||
}
|
||||
|
||||
// default case of passing encryption headers to backend
|
||||
return extractEncryptionOption(r.Header, true, nil)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
@@ -355,7 +357,7 @@ func TestGetDecryptedRange_Issue50(t *testing.T) {
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "166b1545b4c1535294ee0686678bea8c-2",
|
||||
},
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Number: 1,
|
||||
Name: "part.1",
|
||||
@@ -503,7 +505,7 @@ func TestGetDecryptedRange(t *testing.T) {
|
||||
var (
|
||||
// make a multipart object-info given part sizes
|
||||
mkMPObj = func(sizes []int64) ObjectInfo {
|
||||
r := make([]objectPartInfo, len(sizes))
|
||||
r := make([]ObjectPartInfo, len(sizes))
|
||||
sum := int64(0)
|
||||
for i, s := range sizes {
|
||||
r[i].Number = i
|
||||
@@ -675,3 +677,84 @@ func TestGetDecryptedRange(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var extractEncryptionOptionTests = []struct {
|
||||
headers http.Header
|
||||
copySource bool
|
||||
metadata map[string]string
|
||||
encryptionType encrypt.Type
|
||||
err error
|
||||
}{
|
||||
{headers: http.Header{crypto.SSECAlgorithm: []string{"AES256"},
|
||||
crypto.SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: encrypt.SSEC,
|
||||
err: nil}, // 0
|
||||
{headers: http.Header{crypto.SSECAlgorithm: []string{"AES256"},
|
||||
crypto.SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: true,
|
||||
metadata: nil,
|
||||
encryptionType: "",
|
||||
err: nil}, // 1
|
||||
{headers: http.Header{crypto.SSECAlgorithm: []string{"AES256"},
|
||||
crypto.SSECKey: []string{"Mz"},
|
||||
crypto.SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: "",
|
||||
err: crypto.ErrInvalidCustomerKey}, // 2
|
||||
{headers: http.Header{crypto.SSEHeader: []string{"AES256"}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: encrypt.S3,
|
||||
err: nil}, // 3
|
||||
{headers: http.Header{},
|
||||
copySource: false,
|
||||
metadata: map[string]string{crypto.S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)),
|
||||
crypto.S3KMSKeyID: "kms-key",
|
||||
crypto.S3KMSSealedKey: "m-key"},
|
||||
encryptionType: encrypt.S3,
|
||||
err: nil}, // 4
|
||||
{headers: http.Header{},
|
||||
copySource: true,
|
||||
metadata: map[string]string{crypto.S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)),
|
||||
crypto.S3KMSKeyID: "kms-key",
|
||||
crypto.S3KMSSealedKey: "m-key"},
|
||||
encryptionType: "",
|
||||
err: nil}, // 5
|
||||
{headers: http.Header{crypto.SSECopyAlgorithm: []string{"AES256"},
|
||||
crypto.SSECopyKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: true,
|
||||
metadata: nil,
|
||||
encryptionType: encrypt.SSEC,
|
||||
err: nil}, // 6
|
||||
{headers: http.Header{crypto.SSECopyAlgorithm: []string{"AES256"},
|
||||
crypto.SSECopyKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: "",
|
||||
err: nil}, // 7
|
||||
}
|
||||
|
||||
func TestExtractEncryptionOptions(t *testing.T) {
|
||||
for i, test := range extractEncryptionOptionTests {
|
||||
opts, err := extractEncryptionOption(test.headers, test.copySource, test.metadata)
|
||||
if test.err != err {
|
||||
t.Errorf("Case %d: expected err: %v , actual err: %v", i, test.err, err)
|
||||
}
|
||||
if err == nil {
|
||||
if opts.ServerSideEncryption == nil && test.encryptionType != "" {
|
||||
t.Errorf("Case %d: expected opts to be of %v encryption type", i, test.encryptionType)
|
||||
|
||||
}
|
||||
if opts.ServerSideEncryption != nil && test.encryptionType != opts.ServerSideEncryption.Type() {
|
||||
t.Errorf("Case %d: expected opts to have encryption type %v but was %v ", i, test.encryptionType, opts.ServerSideEncryption.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,17 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func getHexSequences(start int, number int, paddinglen int) (seq []string) {
|
||||
for i := start; i <= number; i++ {
|
||||
if paddinglen == 0 {
|
||||
seq = append(seq, fmt.Sprintf("%x", i))
|
||||
} else {
|
||||
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", paddinglen), i))
|
||||
}
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
func getSequences(start int, number int, paddinglen int) (seq []string) {
|
||||
for i := start; i <= number; i++ {
|
||||
if paddinglen == 0 {
|
||||
@@ -472,6 +483,52 @@ func TestParseEndpointSet(t *testing.T) {
|
||||
},
|
||||
true,
|
||||
},
|
||||
// IPv6 ellipses with hexadecimal expansion
|
||||
{
|
||||
"http://[2001:3984:3989::{1...a}]/disk{1...10}",
|
||||
endpointSet{
|
||||
[]ellipses.ArgPattern{
|
||||
[]ellipses.Pattern{
|
||||
{
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
Seq: getSequences(1, 10, 0),
|
||||
},
|
||||
{
|
||||
Prefix: "http://[2001:3984:3989::",
|
||||
Suffix: "]/disk",
|
||||
Seq: getHexSequences(1, 10, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
|
||||
},
|
||||
true,
|
||||
},
|
||||
// IPv6 ellipses with hexadecimal expansion with 3 position numerics.
|
||||
{
|
||||
"http://[2001:3984:3989::{001...00a}]/disk{1...10}",
|
||||
endpointSet{
|
||||
[]ellipses.ArgPattern{
|
||||
[]ellipses.Pattern{
|
||||
{
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
Seq: getSequences(1, 10, 0),
|
||||
},
|
||||
{
|
||||
Prefix: "http://[2001:3984:3989::",
|
||||
Suffix: "]/disk",
|
||||
Seq: getHexSequences(1, 10, 3),
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
|
||||
+41
-10
@@ -29,6 +29,7 @@ import (
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
)
|
||||
|
||||
@@ -112,6 +113,9 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
|
||||
}
|
||||
}
|
||||
if i := strings.Index(host, "%"); i > -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
|
||||
@@ -152,7 +156,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||
// /mnt/export1. So we go ahead and start the minio server in FS modes in these cases.
|
||||
if isHostIPv4(arg) {
|
||||
if isHostIP(arg) {
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")
|
||||
}
|
||||
u = &url.URL{Path: path.Clean(arg)}
|
||||
@@ -194,6 +198,34 @@ func (endpoints EndpointList) GetString(i int) string {
|
||||
return endpoints[i].String()
|
||||
}
|
||||
|
||||
// localEndpointsPerf - returns ServerDrivesPerfInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsPerf(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.Path = endpoint.Path
|
||||
dps = append(dps, dp)
|
||||
}
|
||||
}
|
||||
|
||||
return ServerDrivesPerfInfo{
|
||||
Addr: addr,
|
||||
Perf: dps,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEndpointList - returns new endpoint list based on input args.
|
||||
func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
||||
var endpointType EndpointType
|
||||
@@ -224,7 +256,6 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
||||
uniqueArgs.Add(arg)
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
@@ -341,7 +372,7 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
||||
if err != nil {
|
||||
host = endpoint.Host
|
||||
}
|
||||
hostIPSet, _ := getHostIP4(host)
|
||||
hostIPSet, _ := getHostIP(host)
|
||||
if IPSet, ok := pathIPMap[endpoint.Path]; ok {
|
||||
if !IPSet.Intersection(hostIPSet).IsEmpty() {
|
||||
return serverAddr, endpoints, setupType,
|
||||
@@ -411,12 +442,12 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
||||
host = localServerAddr
|
||||
}
|
||||
|
||||
ipList, err := getHostIP4(host)
|
||||
ipList, err := getHostIP(host)
|
||||
logger.FatalIf(err, "unexpected error when resolving host '%s'", host)
|
||||
|
||||
// Filter ipList by IPs those start with '127.'.
|
||||
// Filter ipList by IPs those start with '127.' or '::1'
|
||||
loopBackIPs := ipList.FuncMatch(func(ip string, matchString string) bool {
|
||||
return strings.HasPrefix(ip, "127.")
|
||||
return strings.HasPrefix(ip, "127.") || strings.HasPrefix(ip, "::1")
|
||||
}, "")
|
||||
|
||||
// If loop back IP is found and ipList contains only loop back IPs, then error out.
|
||||
@@ -485,10 +516,10 @@ func GetLocalPeer(endpoints EndpointList) (localPeer string) {
|
||||
// Local peer can be empty in FS or Erasure coded mode.
|
||||
// If so, return globalMinioHost + globalMinioPort value.
|
||||
if globalMinioHost != "" {
|
||||
return globalMinioHost + ":" + globalMinioPort
|
||||
return net.JoinHostPort(globalMinioHost, globalMinioPort)
|
||||
}
|
||||
|
||||
return "127.0.0.1:" + globalMinioPort
|
||||
return net.JoinHostPort("127.0.0.1", globalMinioPort)
|
||||
}
|
||||
return peerSet.ToSlice()[0]
|
||||
}
|
||||
@@ -525,10 +556,10 @@ func updateDomainIPs(endPoints set.StringSet) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
IPs, _ := getHostIP4(host)
|
||||
IPs, _ := getHostIP(host)
|
||||
ipList = ipList.Union(IPs)
|
||||
}
|
||||
globalDomainIPs = ipList.FuncMatch(func(ip string, matchString string) bool {
|
||||
return !strings.HasPrefix(ip, "127.")
|
||||
return !strings.HasPrefix(ip, "127.") || strings.HasPrefix(ip, "::1")
|
||||
}, "")
|
||||
}
|
||||
|
||||
@@ -304,27 +304,27 @@ func TestCreateEndpoints(t *testing.T) {
|
||||
}, DistXLSetupType, nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
for i, testCase := range testCases {
|
||||
serverAddr, endpoints, setupType, err := CreateEndpoints(testCase.serverAddr, testCase.args...)
|
||||
|
||||
if err == nil {
|
||||
if testCase.expectedErr != nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
t.Fatalf("Test (%d) error: expected = %v, got = <nil>", i+1, testCase.expectedErr)
|
||||
} else {
|
||||
if serverAddr != testCase.expectedServerAddr {
|
||||
t.Fatalf("serverAddr: expected = %v, got = %v", testCase.expectedServerAddr, serverAddr)
|
||||
t.Fatalf("Test (%d) serverAddr: expected = %v, got = %v", i+1, testCase.expectedServerAddr, serverAddr)
|
||||
}
|
||||
if !reflect.DeepEqual(endpoints, testCase.expectedEndpoints) {
|
||||
t.Fatalf("endpoints: expected = %v, got = %v", testCase.expectedEndpoints, endpoints)
|
||||
t.Fatalf("Test (%d) endpoints: expected = %v, got = %v", i+1, testCase.expectedEndpoints, endpoints)
|
||||
}
|
||||
if setupType != testCase.expectedSetupType {
|
||||
t.Fatalf("setupType: expected = %v, got = %v", testCase.expectedSetupType, setupType)
|
||||
t.Fatalf("Test (%d) setupType: expected = %v, got = %v", i+1, testCase.expectedSetupType, setupType)
|
||||
}
|
||||
}
|
||||
} else if testCase.expectedErr == nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
t.Fatalf("Test (%d) error: expected = <nil>, got = %v", i+1, err)
|
||||
} else if err.Error() != testCase.expectedErr.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
t.Fatalf("Test (%d) error: expected = %v, got = %v", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// Minio Cloud Storage, (C) 2016, 2017, 2018 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 (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvKMSMasterKey is the environment variable used to specify
|
||||
// a KMS master key used to protect SSE-S3 per-object keys.
|
||||
// Valid values must be of the from: "KEY_ID:32_BYTE_HEX_VALUE".
|
||||
EnvKMSMasterKey = "MINIO_SSE_MASTER_KEY"
|
||||
|
||||
// EnvAutoEncryption is the environment variable used to en/disable
|
||||
// SSE-S3 auto-encryption. SSE-S3 auto-encryption, if enabled,
|
||||
// requires a valid KMS configuration and turns any non-SSE-C
|
||||
// request into an SSE-S3 request.
|
||||
// If present EnvAutoEncryption must be either "on" or "off".
|
||||
EnvAutoEncryption = "MINIO_SSE_AUTO_ENCRYPTION"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvVaultEndpoint is the environment variable used to specify
|
||||
// the vault HTTPS endpoint.
|
||||
EnvVaultEndpoint = "MINIO_SSE_VAULT_ENDPOINT"
|
||||
|
||||
// EnvVaultAuthType is the environment variable used to specify
|
||||
// the authentication type for vault.
|
||||
EnvVaultAuthType = "MINIO_SSE_VAULT_AUTH_TYPE"
|
||||
|
||||
// EnvVaultAppRoleID is the environment variable used to specify
|
||||
// the vault AppRole ID.
|
||||
EnvVaultAppRoleID = "MINIO_SSE_VAULT_APPROLE_ID"
|
||||
|
||||
// EnvVaultAppSecretID is the environment variable used to specify
|
||||
// the vault AppRole secret corresponding to the AppRole ID.
|
||||
EnvVaultAppSecretID = "MINIO_SSE_VAULT_APPROLE_SECRET"
|
||||
|
||||
// EnvVaultKeyVersion is the environment variable used to specify
|
||||
// the vault key version.
|
||||
EnvVaultKeyVersion = "MINIO_SSE_VAULT_KEY_VERSION"
|
||||
|
||||
// EnvVaultKeyName is the environment variable used to specify
|
||||
// the vault named key-ring. In the S3 context it's referred as
|
||||
// customer master key ID (CMK-ID).
|
||||
EnvVaultKeyName = "MINIO_SSE_VAULT_KEY_NAME"
|
||||
|
||||
// EnvVaultCAPath is the environment variable used to specify the
|
||||
// path to a directory of PEM-encoded CA cert files. These CA cert
|
||||
// files are used to authenticate Minio to Vault over mTLS.
|
||||
EnvVaultCAPath = "MINIO_SSE_VAULT_CAPATH"
|
||||
|
||||
// EnvVaultNamespace is the environment variable used to specify
|
||||
// vault namespace. The vault namespace is used if the enterprise
|
||||
// version of Hashicorp Vault is used.
|
||||
EnvVaultNamespace = "MINIO_SSE_VAULT_NAMESPACE"
|
||||
)
|
||||
|
||||
// Environment provides functions for accessing environment
|
||||
// variables.
|
||||
var Environment = environment{}
|
||||
|
||||
type environment struct{}
|
||||
|
||||
// Get retrieves the value of the environment variable named
|
||||
// by the key. If the variable is present in the environment the
|
||||
// value (which may be empty) is returned. Otherwise it returns
|
||||
// the specified default value.
|
||||
func (environment) Get(key, defaultValue string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Lookup retrieves the value of the environment variable named
|
||||
// by the key. If the variable is present in the environment the
|
||||
// value (which may be empty) is returned and the boolean is true.
|
||||
// Otherwise the returned value will be empty and the boolean will
|
||||
// be false.
|
||||
func (environment) Lookup(key string) (string, bool) { return os.LookupEnv(key) }
|
||||
|
||||
// LookupKMSConfig extracts the KMS configuration provided by environment
|
||||
// variables and merge them with the provided KMS configuration. The
|
||||
// merging follows the following rules:
|
||||
//
|
||||
// 1. A valid value provided as environment variable is higher prioritized
|
||||
// than the provided configuration and overwrites the value from the
|
||||
// configuration file.
|
||||
//
|
||||
// 2. A value specified as environment variable never changes the configuration
|
||||
// file. So it is never made a persistent setting.
|
||||
//
|
||||
// It sets the global KMS configuration according to the merged configuration
|
||||
// on success.
|
||||
func (env environment) LookupKMSConfig(config crypto.KMSConfig) (err error) {
|
||||
// Lookup Hashicorp-Vault configuration & overwrite config entry if ENV var is present
|
||||
config.Vault.Endpoint = env.Get(EnvVaultEndpoint, config.Vault.Endpoint)
|
||||
config.Vault.CAPath = env.Get(EnvVaultCAPath, config.Vault.CAPath)
|
||||
config.Vault.Auth.Type = env.Get(EnvVaultAuthType, config.Vault.Auth.Type)
|
||||
config.Vault.Auth.AppRole.ID = env.Get(EnvVaultAppRoleID, config.Vault.Auth.AppRole.ID)
|
||||
config.Vault.Auth.AppRole.Secret = env.Get(EnvVaultAppSecretID, config.Vault.Auth.AppRole.Secret)
|
||||
config.Vault.Key.Name = env.Get(EnvVaultKeyName, config.Vault.Key.Name)
|
||||
config.Vault.Namespace = env.Get(EnvVaultNamespace, config.Vault.Namespace)
|
||||
keyVersion := env.Get(EnvVaultKeyVersion, strconv.Itoa(config.Vault.Key.Version))
|
||||
config.Vault.Key.Version, err = strconv.Atoi(keyVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid ENV variable: Unable to parse %s value (`%s`)", EnvVaultKeyVersion, keyVersion)
|
||||
}
|
||||
if err = config.Vault.Verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lookup KMS master keys - only available through ENV.
|
||||
if masterKey, ok := env.Lookup(EnvKMSMasterKey); ok {
|
||||
if !config.Vault.IsEmpty() { // Vault and KMS master key provided
|
||||
return errors.New("Ambiguous KMS configuration: vault configuration and a master key are provided at the same time")
|
||||
}
|
||||
globalKMSKeyID, GlobalKMS, err = parseKMSMasterKey(masterKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !config.Vault.IsEmpty() {
|
||||
GlobalKMS, err = crypto.NewVault(config.Vault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalKMSKeyID = config.Vault.Key.Name
|
||||
}
|
||||
|
||||
autoEncryption, err := ParseBoolFlag(env.Get(EnvAutoEncryption, "off"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalAutoEncryption = bool(autoEncryption)
|
||||
if globalAutoEncryption && GlobalKMS == nil { // auto-encryption enabled but no KMS
|
||||
return errors.New("Invalid KMS configuration: auto-encryption is enabled but no valid KMS configuration is present")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseKMSMasterKey parses the value of the environment variable
|
||||
// `EnvKMSMasterKey` and returns a key-ID and a master-key KMS on success.
|
||||
func parseKMSMasterKey(envArg string) (string, crypto.KMS, error) {
|
||||
values := strings.SplitN(envArg, ":", 2)
|
||||
if len(values) != 2 {
|
||||
return "", nil, fmt.Errorf("Invalid KMS master key: %s does not contain a ':'", envArg)
|
||||
}
|
||||
var (
|
||||
keyID = values[0]
|
||||
hexKey = values[1]
|
||||
)
|
||||
if len(hexKey) != 64 { // 2 hex bytes = 1 byte
|
||||
return "", nil, fmt.Errorf("Invalid KMS master key: %s not a 32 bytes long HEX value", hexKey)
|
||||
}
|
||||
var masterKey [32]byte
|
||||
if _, err := hex.Decode(masterKey[:], []byte(hexKey)); err != nil {
|
||||
return "", nil, fmt.Errorf("Invalid KMS master key: %s not a 32 bytes long HEX value", hexKey)
|
||||
}
|
||||
return keyID, crypto.NewKMS(masterKey), nil
|
||||
}
|
||||
+34
-19
@@ -24,6 +24,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
)
|
||||
|
||||
@@ -332,29 +333,43 @@ func formatFSFixDeploymentID(fsFormatPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
formatStartTime := time.Now().Round(time.Second)
|
||||
getElapsedTime := func() string {
|
||||
return time.Now().Round(time.Second).Sub(formatStartTime).String()
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
||||
if err == lock.ErrAlreadyLocked {
|
||||
// Lock already present, sleep and attempt again.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wlk.Close()
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
|
||||
err = jsonLoad(wlk, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
||||
if err == lock.ErrAlreadyLocked {
|
||||
// Lock already present, sleep and attempt again
|
||||
|
||||
// Check if it needs to be updated
|
||||
if format.ID != "" {
|
||||
return nil
|
||||
logger.Info("Another minio process(es) might be holding a lock to the file %s. Please kill that minio process(es) (elapsed %s)\n", fsFormatPath, getElapsedTime())
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wlk.Close()
|
||||
|
||||
err = jsonLoad(wlk, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it needs to be updated
|
||||
if format.ID != "" {
|
||||
return nil
|
||||
}
|
||||
format.ID = mustGetUUID()
|
||||
return jsonSave(wlk, format)
|
||||
}
|
||||
format.ID = mustGetUUID()
|
||||
return jsonSave(wlk, format)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ type fsMetaV1 struct {
|
||||
// Metadata map for current object.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// parts info for current object - used in encryption.
|
||||
Parts []objectPartInfo `json:"parts,omitempty"`
|
||||
Parts []ObjectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid - tells if the format is sane by validating the version
|
||||
@@ -207,9 +207,9 @@ func parseFSMetaMap(fsMetaBuf []byte) map[string]string {
|
||||
return metaMap
|
||||
}
|
||||
|
||||
func parseFSPartsArray(fsMetaBuf []byte) []objectPartInfo {
|
||||
func parseFSPartsArray(fsMetaBuf []byte) []ObjectPartInfo {
|
||||
// Get xlMetaV1.Parts array
|
||||
var partsArray []objectPartInfo
|
||||
var partsArray []ObjectPartInfo
|
||||
|
||||
partsArrayResult := gjson.GetBytes(fsMetaBuf, "parts")
|
||||
partsArrayResult.ForEach(func(key, part gjson.Result) bool {
|
||||
@@ -219,7 +219,7 @@ func parseFSPartsArray(fsMetaBuf []byte) []objectPartInfo {
|
||||
etag := gjson.Get(partJSON, "etag").String()
|
||||
size := gjson.Get(partJSON, "size").Int()
|
||||
actualSize := gjson.Get(partJSON, "actualSize").Int()
|
||||
partsArray = append(partsArray, objectPartInfo{
|
||||
partsArray = append(partsArray, ObjectPartInfo{
|
||||
Number: int(number),
|
||||
Name: name,
|
||||
ETag: etag,
|
||||
|
||||
@@ -353,7 +353,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
// Implements S3 compatible ListObjectParts API. The resulting
|
||||
// ListPartsInfo structure is unmarshalled directly into XML and
|
||||
// replied back to the client.
|
||||
func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int) (result ListPartsInfo, e error) {
|
||||
func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, e error) {
|
||||
if err := checkListPartsArgs(ctx, bucket, object, fs); err != nil {
|
||||
return result, toObjectErr(err)
|
||||
}
|
||||
@@ -515,7 +515,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
fsMeta := fsMetaV1{}
|
||||
|
||||
// Allocate parts similar to incoming slice.
|
||||
fsMeta.Parts = make([]objectPartInfo, len(parts))
|
||||
fsMeta.Parts = make([]ObjectPartInfo, len(parts))
|
||||
|
||||
entries, err := readDir(uploadIDDir)
|
||||
if err != nil {
|
||||
@@ -560,7 +560,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
partSize = actualSize
|
||||
}
|
||||
|
||||
fsMeta.Parts[i] = objectPartInfo{
|
||||
fsMeta.Parts[i] = ObjectPartInfo{
|
||||
Number: part.PartNumber,
|
||||
ETag: part.ETag,
|
||||
Size: fi.Size(),
|
||||
@@ -682,6 +682,8 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
fsRemoveAll(ctx, uploadIDDir)
|
||||
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
|
||||
fsRemoveDir(ctx, fs.getMultipartSHADir(bucket, object))
|
||||
fi, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object))
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
@@ -769,6 +771,9 @@ func (fs *FSObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupIn
|
||||
}
|
||||
if now.Sub(fi.ModTime()) > expiry {
|
||||
fsRemoveAll(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry, uploadID))
|
||||
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
|
||||
fsRemoveDir(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
|
||||
// Close the go-routine, we are going to
|
||||
// manually start it and test in this test case.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
@@ -47,14 +47,14 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, globalServiceDoneCh)
|
||||
go fs.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, GlobalServiceDoneCh)
|
||||
|
||||
// Wait for 40ms such that - we have given enough time for
|
||||
// cleanup routine to kick in.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Close the routine we do not need it anymore.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Check if upload id was already purged.
|
||||
if err = obj.AbortMultipartUpload(context.Background(), bucketName, objectName, uploadID); err != nil {
|
||||
|
||||
+11
-5
@@ -152,10 +152,10 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
fs.fsFormatRlk = rlk
|
||||
|
||||
if !fs.diskMount {
|
||||
go fs.diskUsage(globalServiceDoneCh)
|
||||
go fs.diskUsage(GlobalServiceDoneCh)
|
||||
}
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(ctx, globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
|
||||
go fs.cleanupStaleMultipartUploads(ctx, GlobalMultipartCleanupInterval, GlobalMultipartExpiry, GlobalServiceDoneCh)
|
||||
|
||||
// Return successfully initialized object layer.
|
||||
return fs, nil
|
||||
@@ -1212,9 +1212,8 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
|
||||
fs.listPool.Set(params, walkResultCh, endWalkCh)
|
||||
}
|
||||
|
||||
result := ListObjectsInfo{IsTruncated: !eof}
|
||||
result := ListObjectsInfo{}
|
||||
for _, objInfo := range objInfos {
|
||||
result.NextMarker = objInfo.Name
|
||||
if objInfo.IsDir && delimiter == slashSeparator {
|
||||
result.Prefixes = append(result.Prefixes, objInfo.Name)
|
||||
continue
|
||||
@@ -1222,6 +1221,13 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
|
||||
result.Objects = append(result.Objects, objInfo)
|
||||
}
|
||||
|
||||
if !eof {
|
||||
result.IsTruncated = true
|
||||
if len(objInfos) > 0 {
|
||||
result.NextMarker = objInfos[len(objInfos)-1].Name
|
||||
}
|
||||
}
|
||||
|
||||
// Success.
|
||||
return result, nil
|
||||
}
|
||||
@@ -1311,7 +1317,7 @@ func (fs *FSObjects) IsListenBucketSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (fs *FSObjects) IsEncryptionSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
+1
-1
@@ -354,7 +354,7 @@ func TestFSListBuckets(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Create a bucket with invalid name
|
||||
if err := os.MkdirAll(pathJoin(fs.fsPath, "vo^"), 0777); err != nil {
|
||||
|
||||
@@ -18,7 +18,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
|
||||
minio "github.com/minio/minio-go"
|
||||
@@ -30,8 +33,27 @@ var (
|
||||
|
||||
// MustGetUUID function alias.
|
||||
MustGetUUID = mustGetUUID
|
||||
|
||||
// IsMinAllowedPartSize function alias.
|
||||
IsMinAllowedPartSize = isMinAllowedPartSize
|
||||
|
||||
// GetCompleteMultipartMD5 functon alias.
|
||||
GetCompleteMultipartMD5 = getCompleteMultipartMD5
|
||||
|
||||
// Contains function alias.
|
||||
Contains = contains
|
||||
|
||||
// ExtractETag provides extractETag function alias.
|
||||
ExtractETag = extractETag
|
||||
// CleanMetadataKeys provides cleanMetadataKeys function alias.
|
||||
CleanMetadataKeys = cleanMetadataKeys
|
||||
)
|
||||
|
||||
// StatInfo - alias for statInfo
|
||||
type StatInfo struct {
|
||||
statInfo
|
||||
}
|
||||
|
||||
// AnonErrToObjectErr - converts standard http codes into meaningful object layer errors.
|
||||
func AnonErrToObjectErr(statusCode int, params ...string) error {
|
||||
bucket := ""
|
||||
@@ -321,3 +343,30 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// parse gateway sse env variable
|
||||
func parseGatewaySSE(s string) (gatewaySSE, error) {
|
||||
l := strings.Split(s, ";")
|
||||
var gwSlice = make([]string, 0)
|
||||
for _, val := range l {
|
||||
v := strings.ToUpper(val)
|
||||
if v == gatewaySSES3 || v == gatewaySSEC {
|
||||
gwSlice = append(gwSlice, v)
|
||||
continue
|
||||
}
|
||||
return nil, uiErrInvalidGWSSEValue(nil).Msg("gateway SSE cannot be (%s) ", v)
|
||||
}
|
||||
return gatewaySSE(gwSlice), nil
|
||||
}
|
||||
|
||||
// handle gateway env vars
|
||||
func handleGatewayEnvVars() {
|
||||
gwsseVal, ok := os.LookupEnv("MINIO_GATEWAY_SSE")
|
||||
if ok {
|
||||
var err error
|
||||
GlobalGatewaySSE, err = parseGatewaySSE(gwsseVal)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to parse MINIO_GATEWAY_SSE value (`%s`)", gwsseVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests cache exclude parsing.
|
||||
func TestParseGatewaySSE(t *testing.T) {
|
||||
testCases := []struct {
|
||||
gwSSEStr string
|
||||
expected gatewaySSE
|
||||
success bool
|
||||
}{
|
||||
// valid input
|
||||
{"c;S3", []string{"C", "S3"}, true},
|
||||
{"S3", []string{"S3"}, true},
|
||||
{"c,S3", []string{}, false},
|
||||
{"c;S3;KMS", []string{}, false},
|
||||
{"C;s3", []string{"C", "S3"}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
gwSSE, err := parseGatewaySSE(testCase.gwSSEStr)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Test %d: Expected success but failed instead %s", i+1, err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Errorf("Test %d: Expected failure but passed instead", i+1)
|
||||
}
|
||||
if err == nil {
|
||||
if !reflect.DeepEqual(gwSSE, testCase.expected) {
|
||||
t.Errorf("Test %d: Expected %v, got %v", i+1, testCase.expected, gwSSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,37 +14,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package format
|
||||
package cmd
|
||||
|
||||
import "strconv"
|
||||
type gatewaySSE []string
|
||||
|
||||
// IsInt - returns a true or false, whether a string can
|
||||
// be represented as an int.
|
||||
func IsInt(s string) bool {
|
||||
_, err := strconv.Atoi(s)
|
||||
return err == nil
|
||||
}
|
||||
const (
|
||||
// GatewaySSES3 is set when SSE-S3 encryption needed on both gateway and backend
|
||||
gatewaySSES3 = "S3"
|
||||
// GatewaySSEC is set when SSE-C encryption needed on both gateway and backend
|
||||
gatewaySSEC = "C"
|
||||
)
|
||||
|
||||
// StringInSlice - this function finds whether a string is in a list
|
||||
func StringInSlice(x string, list []string) bool {
|
||||
for _, y := range list {
|
||||
if x == y {
|
||||
func (sse gatewaySSE) SSES3() bool {
|
||||
for _, v := range sse {
|
||||
if v == gatewaySSES3 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ProcessSize - this function processes size so that we can calculate bytes BytesProcessed.
|
||||
func ProcessSize(myrecord []string) int64 {
|
||||
if len(myrecord) > 0 {
|
||||
var size int64
|
||||
size = int64(len(myrecord)-1) + 1
|
||||
for i := range myrecord {
|
||||
size += int64(len(myrecord[i]))
|
||||
func (sse gatewaySSE) SSEC() bool {
|
||||
for _, v := range sse {
|
||||
if v == gatewaySSEC {
|
||||
return true
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
return 0
|
||||
return false
|
||||
}
|
||||
|
||||
func (sse gatewaySSE) IsSet() bool {
|
||||
return sse.SSES3() || sse.SSEC()
|
||||
}
|
||||
+54
-53
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -112,34 +111,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
cli.ShowCommandHelpAndExit(ctx, gatewayName, 1)
|
||||
}
|
||||
|
||||
// Get "json" flag from command line argument and
|
||||
// enable json and quite modes if jason flag is turned on.
|
||||
jsonFlag := ctx.IsSet("json") || ctx.GlobalIsSet("json")
|
||||
if jsonFlag {
|
||||
logger.EnableJSON()
|
||||
}
|
||||
|
||||
// Get quiet flag from command line argument.
|
||||
quietFlag := ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
|
||||
if quietFlag {
|
||||
logger.EnableQuiet()
|
||||
}
|
||||
|
||||
// Fetch address option
|
||||
gatewayAddr := ctx.GlobalString("address")
|
||||
if gatewayAddr == ":"+globalMinioPort {
|
||||
gatewayAddr = ctx.String("address")
|
||||
}
|
||||
|
||||
// Handle common command args.
|
||||
handleCommonCmdArgs(ctx)
|
||||
|
||||
// Get port to listen on from gateway address
|
||||
var pErr error
|
||||
_, globalMinioPort, pErr = net.SplitHostPort(gatewayAddr)
|
||||
if pErr != nil {
|
||||
logger.FatalIf(pErr, "Unable to start gateway")
|
||||
}
|
||||
globalMinioHost, globalMinioPort = mustSplitHostPort(globalCLIContext.Addr)
|
||||
|
||||
// On macOS, if a process already listens on LOCALIPADDR:PORT, net.Listen() falls back
|
||||
// to IPv6 address ie minio will start listening on IPv6 address whereas another
|
||||
@@ -147,21 +123,21 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// To avoid this error situation we check for port availability.
|
||||
logger.FatalIf(checkPortAvailability(globalMinioPort), "Unable to start the gateway")
|
||||
|
||||
// Create certs path.
|
||||
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
||||
|
||||
// Check and load TLS certificates.
|
||||
var err error
|
||||
globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig()
|
||||
logger.FatalIf(err, "Invalid TLS certificate file")
|
||||
|
||||
// Check and load Root CAs.
|
||||
globalRootCAs, err = getRootCAs(getCADir())
|
||||
globalRootCAs, err = getRootCAs(globalCertsCADir.Get())
|
||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||
|
||||
// Handle common env vars.
|
||||
handleCommonEnvVars()
|
||||
|
||||
// Handle gateway specific env
|
||||
handleGatewayEnvVars()
|
||||
|
||||
// Validate if we have access, secret set through environment.
|
||||
if !globalIsEnvCreds {
|
||||
logger.Fatal(uiErrEnvCredentialsMissingGateway(nil), "Unable to start gateway")
|
||||
@@ -177,11 +153,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
if globalEtcdClient != nil {
|
||||
// Enable STS router if etcd is enabled.
|
||||
registerSTSRouter(router)
|
||||
|
||||
// Enable admin router if etcd is enabled.
|
||||
registerAdminRouter(router)
|
||||
}
|
||||
|
||||
// Enable IAM admin APIs if etcd is enabled, if not just enable basic
|
||||
// operations such as profiling, server info etc.
|
||||
registerAdminRouter(router, globalEtcdClient != nil)
|
||||
|
||||
// Add healthcheck router
|
||||
registerHealthCheckRouter(router)
|
||||
|
||||
@@ -193,8 +170,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(registerWebRouter(router), "Unable to configure web browser")
|
||||
}
|
||||
|
||||
// Currently only NAS and S3 gateway support encryption headers.
|
||||
encryptionEnabled := gatewayName == "s3" || gatewayName == "nas"
|
||||
|
||||
// Add API router.
|
||||
registerAPIRouter(router)
|
||||
registerAPIRouter(router, encryptionEnabled)
|
||||
|
||||
// Dummy endpoint representing gateway instance.
|
||||
globalEndpoints = []Endpoint{{
|
||||
@@ -210,7 +190,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
getCert = globalTLSCerts.GetCertificate
|
||||
}
|
||||
|
||||
globalHTTPServer = xhttp.NewServer([]string{gatewayAddr}, criticalErrorHandler{registerHandlers(router, globalHandlers...)}, getCert)
|
||||
globalHTTPServer = xhttp.NewServer([]string{globalCLIContext.Addr}, criticalErrorHandler{registerHandlers(router, globalHandlers...)}, getCert)
|
||||
globalHTTPServer.UpdateBytesReadFunc = globalConnStats.incInputBytes
|
||||
globalHTTPServer.UpdateBytesWrittenFunc = globalConnStats.incOutputBytes
|
||||
go func() {
|
||||
@@ -219,6 +199,22 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
|
||||
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// !!! Do not move this block !!!
|
||||
// For all gateways, the config needs to be loaded from env
|
||||
// prior to initializing the gateway layer
|
||||
{
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
}
|
||||
|
||||
newObject, err := gw.NewGatewayLayer(globalServerConfig.GetCredential())
|
||||
if err != nil {
|
||||
// Stop watching for any certificate changes.
|
||||
@@ -228,25 +224,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
|
||||
// Create a new config system.
|
||||
globalConfigSys = NewConfigSys()
|
||||
if globalEtcdClient != nil && gatewayName == "nas" {
|
||||
// Initialize server config.
|
||||
// Create a new config system.
|
||||
globalConfigSys = NewConfigSys()
|
||||
|
||||
// Load globalServerConfig from etcd
|
||||
_ = globalConfigSys.Init(newObject)
|
||||
} else {
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// Load values to cached global values.
|
||||
srvCfg.loadToCachedConfigs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
}
|
||||
|
||||
// Load logger subsystem
|
||||
@@ -284,13 +267,28 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
if globalEtcdClient != nil && newObject.IsNotificationSupported() {
|
||||
_ = globalNotificationSys.Init(newObject)
|
||||
}
|
||||
|
||||
// Encryption support checks in gateway mode.
|
||||
{
|
||||
|
||||
if (globalAutoEncryption || GlobalKMS != nil) && !newObject.IsEncryptionSupported() {
|
||||
logger.Fatal(errInvalidArgument,
|
||||
"Encryption support is requested but (%s) gateway does not support encryption", gw.Name())
|
||||
}
|
||||
|
||||
if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
|
||||
logger.Fatal(uiErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured"),
|
||||
"Unable to start gateway with SSE")
|
||||
}
|
||||
}
|
||||
|
||||
// Once endpoints are finalized, initialize the new object api.
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = newObject
|
||||
globalObjLayerMutex.Unlock()
|
||||
|
||||
// Prints the formatted startup message once object layer is initialized.
|
||||
if !quietFlag {
|
||||
if !globalCLIContext.Quiet {
|
||||
mode := globalMinioModeGatewayPrefix + gatewayName
|
||||
// Check update mode.
|
||||
checkUpdate(mode)
|
||||
@@ -301,8 +299,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
}
|
||||
|
||||
// Print gateway startup message.
|
||||
printGatewayStartupMessage(getAPIEndpoints(gatewayAddr), gatewayName)
|
||||
printGatewayStartupMessage(getAPIEndpoints(), gatewayName)
|
||||
}
|
||||
|
||||
// Set uptime time after object layer has initialized.
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
handleSignals()
|
||||
}
|
||||
|
||||
@@ -55,9 +55,10 @@ func printGatewayCommonMsg(apiEndpoints []string) {
|
||||
cred := globalServerConfig.GetCredential()
|
||||
|
||||
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
||||
|
||||
// Colorize the message and print.
|
||||
logger.StartupMessage(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
||||
if isTerminal() {
|
||||
logger.StartupMessage(colorBlue("Endpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
||||
if isTerminal() && !globalCLIContext.Anonymous {
|
||||
logger.StartupMessage(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
||||
logger.StartupMessage(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (a GatewayUnsupported) PutObjectPart(ctx context.Context, bucket string, ob
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (a GatewayUnsupported) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi ListPartsInfo, err error) {
|
||||
func (a GatewayUnsupported) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (lpi ListPartsInfo, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return lpi, NotImplemented{}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (a GatewayUnsupported) IsListenBucketSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (a GatewayUnsupported) IsEncryptionSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -981,7 +981,7 @@ func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, upload
|
||||
}
|
||||
|
||||
// ListObjectParts - Use Azure equivalent GetBlockList.
|
||||
func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result minio.ListPartsInfo, err error) {
|
||||
func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (result minio.ListPartsInfo, err error) {
|
||||
if err = a.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ func (l *b2Objects) PutObjectPart(ctx context.Context, bucket string, object str
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi minio.ListPartsInfo, err error) {
|
||||
func (l *b2Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return lpi, err
|
||||
|
||||
@@ -1033,8 +1033,7 @@ func (l *gcsGateway) ListMultipartUploads(ctx context.Context, bucket string, pr
|
||||
UploadID: components[3],
|
||||
Initiated: attrs.Created,
|
||||
}
|
||||
uploads = []minio.MultipartInfo{upload}
|
||||
break
|
||||
uploads = append(uploads, upload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,7 +1120,7 @@ func gcsGetPartInfo(ctx context.Context, attrs *storage.ObjectAttrs) (minio.Part
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *gcsGateway) ListObjectParts(ctx context.Context, bucket string, key string, uploadID string, partNumberMarker int, maxParts int) (minio.ListPartsInfo, error) {
|
||||
func (l *gcsGateway) ListObjectParts(ctx context.Context, bucket string, key string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (minio.ListPartsInfo, error) {
|
||||
it := l.client.Bucket(bucket).Objects(ctx, &storage.Query{
|
||||
Prefix: path.Join(gcsMinioMultipartPathV1, uploadID),
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ func (g *NAS) Production() bool {
|
||||
}
|
||||
|
||||
// IsListenBucketSupported returns whether listen bucket notification is applicable for this gateway.
|
||||
func (g *NAS) IsListenBucketSupported() bool {
|
||||
func (n *nasObjects) IsListenBucketSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -886,7 +886,7 @@ func (l *ossObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *ossObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int) (lpi minio.ListPartsInfo, err error) {
|
||||
func (l *ossObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, err error) {
|
||||
lupr, err := ossListObjectParts(l.Client, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 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 s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var (
|
||||
errGWMetaNotFound = errors.New("dare.meta file not found")
|
||||
errGWMetaInvalidFormat = errors.New("dare.meta format is invalid")
|
||||
)
|
||||
|
||||
// A gwMetaV1 represents `gw.json` metadata header.
|
||||
type gwMetaV1 struct {
|
||||
Version string `json:"version"` // Version of the current `gw.json`.
|
||||
Format string `json:"format"` // Format of the current `gw.json`.
|
||||
Stat minio.StatInfo `json:"stat"` // Stat of the current object `gw.json`.
|
||||
ETag string `json:"etag"` // ETag of the current object
|
||||
|
||||
// Metadata map for current object `gw.json`.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// Captures all the individual object `gw.json`.
|
||||
Parts []minio.ObjectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// Gateway metadata constants.
|
||||
const (
|
||||
// Gateway meta version.
|
||||
gwMetaVersion = "1.0.0"
|
||||
|
||||
// Gateway meta version.
|
||||
gwMetaVersion100 = "1.0.0"
|
||||
|
||||
// Gateway meta format string.
|
||||
gwMetaFormat = "gw"
|
||||
|
||||
// Add new constants here.
|
||||
)
|
||||
|
||||
// newGWMetaV1 - initializes new gwMetaV1, adds version.
|
||||
func newGWMetaV1() (gwMeta gwMetaV1) {
|
||||
gwMeta = gwMetaV1{}
|
||||
gwMeta.Version = gwMetaVersion
|
||||
gwMeta.Format = gwMetaFormat
|
||||
return gwMeta
|
||||
}
|
||||
|
||||
// IsValid - tells if the format is sane by validating the version
|
||||
// string, format fields.
|
||||
func (m gwMetaV1) IsValid() bool {
|
||||
return ((m.Version == gwMetaVersion || m.Version == gwMetaVersion100) &&
|
||||
m.Format == gwMetaFormat)
|
||||
}
|
||||
|
||||
// Converts metadata to object info.
|
||||
func (m gwMetaV1) ToObjectInfo(bucket, object string) minio.ObjectInfo {
|
||||
filterKeys := append([]string{
|
||||
"ETag",
|
||||
"Content-Length",
|
||||
"Last-Modified",
|
||||
"Content-Type",
|
||||
}, defaultFilterKeys...)
|
||||
objInfo := minio.ObjectInfo{
|
||||
IsDir: false,
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
Size: m.Stat.Size,
|
||||
ModTime: m.Stat.ModTime,
|
||||
ContentType: m.Meta["content-type"],
|
||||
ContentEncoding: m.Meta["content-encoding"],
|
||||
ETag: minio.CanonicalizeETag(m.ETag),
|
||||
UserDefined: minio.CleanMetadataKeys(m.Meta, filterKeys...),
|
||||
Parts: m.Parts,
|
||||
}
|
||||
|
||||
if sc, ok := m.Meta["x-amz-storage-class"]; ok {
|
||||
objInfo.StorageClass = sc
|
||||
}
|
||||
// Success.
|
||||
return objInfo
|
||||
}
|
||||
|
||||
// ObjectToPartOffset - translate offset of an object to offset of its individual part.
|
||||
func (m gwMetaV1) ObjectToPartOffset(ctx context.Context, offset int64) (partIndex int, partOffset int64, err error) {
|
||||
if offset == 0 {
|
||||
// Special case - if offset is 0, then partIndex and partOffset are always 0.
|
||||
return 0, 0, nil
|
||||
}
|
||||
partOffset = offset
|
||||
// Seek until object offset maps to a particular part offset.
|
||||
for i, part := range m.Parts {
|
||||
partIndex = i
|
||||
// Offset is smaller than size we have reached the proper part offset.
|
||||
if partOffset < part.Size {
|
||||
return partIndex, partOffset, nil
|
||||
}
|
||||
// Continue to towards the next part.
|
||||
partOffset -= part.Size
|
||||
}
|
||||
logger.LogIf(ctx, minio.InvalidRange{})
|
||||
// Offset beyond the size of the object return InvalidRange.
|
||||
return 0, 0, minio.InvalidRange{}
|
||||
}
|
||||
|
||||
// parses gateway metadata stat info from metadata json
|
||||
func parseGWStat(gwMetaBuf []byte) (si minio.StatInfo, e error) {
|
||||
// obtain stat info.
|
||||
stat := minio.StatInfo{}
|
||||
// fetching modTime.
|
||||
modTime, err := time.Parse(time.RFC3339, gjson.GetBytes(gwMetaBuf, "stat.modTime").String())
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
stat.ModTime = modTime
|
||||
// obtain Stat.Size .
|
||||
stat.Size = gjson.GetBytes(gwMetaBuf, "stat.size").Int()
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// parses gateway metadata version from metadata json
|
||||
func parseGWVersion(gwMetaBuf []byte) string {
|
||||
return gjson.GetBytes(gwMetaBuf, "version").String()
|
||||
}
|
||||
|
||||
// parses gateway ETag from metadata json
|
||||
func parseGWETag(gwMetaBuf []byte) string {
|
||||
return gjson.GetBytes(gwMetaBuf, "etag").String()
|
||||
}
|
||||
|
||||
// parses gateway metadata format from metadata json
|
||||
func parseGWFormat(gwMetaBuf []byte) string {
|
||||
return gjson.GetBytes(gwMetaBuf, "format").String()
|
||||
}
|
||||
|
||||
// parses gateway metadata json to get list of ObjectPartInfo
|
||||
func parseGWParts(gwMetaBuf []byte) []minio.ObjectPartInfo {
|
||||
// Parse the GW Parts.
|
||||
partsResult := gjson.GetBytes(gwMetaBuf, "parts").Array()
|
||||
partInfo := make([]minio.ObjectPartInfo, len(partsResult))
|
||||
for i, p := range partsResult {
|
||||
info := minio.ObjectPartInfo{}
|
||||
info.Number = int(p.Get("number").Int())
|
||||
info.Name = p.Get("name").String()
|
||||
info.ETag = p.Get("etag").String()
|
||||
info.Size = p.Get("size").Int()
|
||||
partInfo[i] = info
|
||||
}
|
||||
return partInfo
|
||||
}
|
||||
|
||||
// parses gateway metadata json to get the metadata map
|
||||
func parseGWMetaMap(gwMetaBuf []byte) map[string]string {
|
||||
// Get gwMetaV1.Meta map.
|
||||
metaMapResult := gjson.GetBytes(gwMetaBuf, "meta").Map()
|
||||
metaMap := make(map[string]string)
|
||||
for key, valResult := range metaMapResult {
|
||||
metaMap[key] = valResult.String()
|
||||
}
|
||||
return metaMap
|
||||
}
|
||||
|
||||
// Constructs GWMetaV1 using `gjson` lib to retrieve each field.
|
||||
func gwMetaUnmarshalJSON(ctx context.Context, gwMetaBuf []byte) (gwMeta gwMetaV1, e error) {
|
||||
// obtain version.
|
||||
gwMeta.Version = parseGWVersion(gwMetaBuf)
|
||||
// obtain format.
|
||||
gwMeta.Format = parseGWFormat(gwMetaBuf)
|
||||
// Parse gwMetaV1.Stat .
|
||||
stat, err := parseGWStat(gwMetaBuf)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return gwMeta, err
|
||||
}
|
||||
gwMeta.ETag = parseGWETag(gwMetaBuf)
|
||||
gwMeta.Stat = stat
|
||||
|
||||
// Parse the GW Parts.
|
||||
gwMeta.Parts = parseGWParts(gwMetaBuf)
|
||||
// parse gwMetaV1.
|
||||
gwMeta.Meta = parseGWMetaMap(gwMetaBuf)
|
||||
|
||||
return gwMeta, nil
|
||||
}
|
||||
|
||||
// readGWMeta reads `dare.meta` and returns back GW metadata structure.
|
||||
func readGWMetadata(ctx context.Context, buf bytes.Buffer) (gwMeta gwMetaV1, err error) {
|
||||
if buf.Len() == 0 {
|
||||
return gwMetaV1{}, errGWMetaNotFound
|
||||
}
|
||||
gwMeta, err = gwMetaUnmarshalJSON(ctx, buf.Bytes())
|
||||
if err != nil {
|
||||
return gwMetaV1{}, err
|
||||
}
|
||||
if !gwMeta.IsValid() {
|
||||
return gwMetaV1{}, errGWMetaInvalidFormat
|
||||
}
|
||||
// Return structured `dare.meta`.
|
||||
return gwMeta, nil
|
||||
}
|
||||
|
||||
// getGWMetadata - unmarshals dare.meta into a *minio.PutObjReader
|
||||
func getGWMetadata(ctx context.Context, bucket, prefix string, gwMeta gwMetaV1) (*minio.PutObjReader, error) {
|
||||
// Marshal json.
|
||||
metadataBytes, err := json.Marshal(&gwMeta)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(metadataBytes), int64(len(metadataBytes)), "", "", int64(len(metadataBytes)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return minio.NewPutObjReader(hashReader, nil, nil), nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 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 s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests for GW metadata format validity.
|
||||
func TestGWMetaFormatValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name int
|
||||
version string
|
||||
format string
|
||||
want bool
|
||||
}{
|
||||
{1, "123", "fs", false},
|
||||
{2, "123", gwMetaFormat, false},
|
||||
{3, gwMetaVersion, "test", false},
|
||||
{4, gwMetaVersion100, "hello", false},
|
||||
{5, gwMetaVersion, gwMetaFormat, true},
|
||||
{6, gwMetaVersion100, gwMetaFormat, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := newGWMetaV1()
|
||||
m.Version = tt.version
|
||||
m.Format = tt.format
|
||||
if got := m.IsValid(); got != tt.want {
|
||||
t.Errorf("Test %d: Expected %v but received %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for reading GW metadata info.
|
||||
func TestReadGWMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
metaStr string
|
||||
pass bool
|
||||
}{
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "132", "modTime": "2018-08-31T22:25:39.23626461Z" }}}`, true},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "132", "modTime": "0000-00-00T00:00:00.00000000Z" }}}`, false},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "5242880", "modTime": "2018-08-31T22:25:39.23626461Z" }},"meta":{"content-type":"application/octet-stream","etag":"57c743902b2fc8eea6ba3bb4fc58c8e8"},"parts":[{"number":1,"name":"part.1","etag":"","size":5242880}]}}`, true},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "68190720", "modTime": "2018-08-31T22:25:39.23626461Z" }},"meta":{"X-Minio-Internal-Encrypted-Multipart":"","X-Minio-Internal-Server-Side-Encryption-Iv":"kdbOcKdXD3Sew8tOiHe5eI9xkX1oQ2W9JURz0oslCZA=","X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm":"DAREv2-HMAC-SHA256","X-Minio-Internal-Server-Side-Encryption-Sealed-Key":"IAAfAMfqKrxMXC9LuiI7ENP+p0xArepzAiIeB/MftFp7Xmq2OzDkKlmNbj5RKI89RrjiAbOVLSSEMvqQsrIrTQ==","content-type":"text/plain; charset=utf-8","etag":"2b137fa4ab80126af54623b010c98de6-2"},"parts":[{"number":1,"name":"part.1","etag":"c5cac075eefdab801a5198812f51b36e","size":67141632},{"number":2,"name":"part.2","etag":"ccdf4b774bc3be8eef9a8987309e8171","size":1049088}]}`, true},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "68190720", "modTime": "2018-08-31T22:25:39.23626461Z" }},"meta":{"X-Minio-Internal-Encrypted-Multipart":"","X-Minio-Internal-Server-Side-Encryption-Iv":"kdbOcKdXD3Sew8tOiHe5eI9xkX1oQ2W9JURz0oslCZA=","X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm":"DAREv2-HMAC-SHA256","X-Minio-Internal-Server-Side-Encryption-Sealed-Key":"IAAfAMfqKrxMXC9LuiI7ENP+p0xArepzAiIeB/MftFp7Xmq2OzDkKlmNbj5RKI89RrjiAbOVLSSEMvqQsrIrTQ==","content-type":"text/plain; charset=utf-8","etag":"2b137fa4ab80126af54623b010c98de6-2"},"parts":"123"}`, true},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
buf := bytes.NewBufferString(tt.metaStr)
|
||||
m, err := readGWMetadata(context.Background(), *buf)
|
||||
if err != nil && tt.pass {
|
||||
t.Errorf("Test %d: Expected parse gw metadata to succeed, but failed", i)
|
||||
}
|
||||
if err == nil && !tt.pass {
|
||||
t.Errorf("Test %d: Expected parse gw metadata to succeed, but failed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if m.Version != gwMetaVersion {
|
||||
t.Errorf("Test %d: Expected version %s, but failed with %s", i, gwMetaVersion, m.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 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 s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// name of custom multipart metadata file for s3 backend.
|
||||
gwdareMetaJSON string = "dare.meta"
|
||||
|
||||
// name of temporary per part metadata file
|
||||
gwpartMetaJSON string = "part.meta"
|
||||
// custom multipart files are stored under the defaultMinioGWPrefix
|
||||
defaultMinioGWPrefix = ".minio"
|
||||
defaultGWContentFileName = "data"
|
||||
slashSeparator = "/"
|
||||
)
|
||||
|
||||
// s3EncObjects is a wrapper around s3Objects and implements gateway calls for
|
||||
// custom large objects encrypted at the gateway
|
||||
type s3EncObjects struct {
|
||||
s3Objects
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
Custom gateway encrypted objects are stored on backend as follows:
|
||||
obj/.minio/data <= encrypted content
|
||||
obj/.minio/dare.meta <= metadata
|
||||
|
||||
When a multipart upload operation is in progress, the metadata set during
|
||||
NewMultipartUpload is stored in obj/.minio/uploadID/dare.meta and each
|
||||
UploadPart operation saves additional state of the part's encrypted ETag and
|
||||
encrypted size in obj/.minio/uploadID/part1/part.meta
|
||||
|
||||
All the part metadata and temp dare.meta are cleaned up when upload completes
|
||||
*/
|
||||
|
||||
// ListObjects lists all blobs in S3 bucket filtered by prefix
|
||||
func (l *s3EncObjects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) {
|
||||
var continuationToken, startAfter string
|
||||
res, err := l.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, maxKeys, false, startAfter)
|
||||
if err != nil {
|
||||
return loi, err
|
||||
}
|
||||
loi.IsTruncated = res.IsTruncated
|
||||
loi.NextMarker = res.NextContinuationToken
|
||||
loi.Objects = res.Objects
|
||||
loi.Prefixes = res.Prefixes
|
||||
return loi, nil
|
||||
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
||||
func (l *s3EncObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
||||
|
||||
var objects []minio.ObjectInfo
|
||||
var prefixes []string
|
||||
var isTruncated bool
|
||||
|
||||
// filter out objects that contain a .minio prefix, but is not a dare.meta metadata file.
|
||||
for {
|
||||
loi, e = l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, fetchOwner, startAfter)
|
||||
if e != nil {
|
||||
return loi, minio.ErrorRespToObjectError(e, bucket)
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
startAfter = obj.Name
|
||||
continuationToken = loi.NextContinuationToken
|
||||
isTruncated = loi.IsTruncated
|
||||
|
||||
if !isGWObject(obj.Name) {
|
||||
continue
|
||||
}
|
||||
// get objectname and ObjectInfo from the custom metadata file
|
||||
if strings.HasSuffix(obj.Name, gwdareMetaJSON) {
|
||||
objSlice := strings.Split(obj.Name, slashSeparator+defaultMinioGWPrefix)
|
||||
gwMeta, e := l.getGWMetadata(ctx, bucket, getDareMetaPath(objSlice[0]))
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
oInfo := gwMeta.ToObjectInfo(bucket, objSlice[0])
|
||||
objects = append(objects, oInfo)
|
||||
} else {
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
if len(objects) > maxKeys {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, p := range loi.Prefixes {
|
||||
objName := strings.TrimSuffix(p, slashSeparator)
|
||||
gm, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(objName))
|
||||
// if prefix is actually a custom multi-part object, append it to objects
|
||||
if err == nil {
|
||||
objects = append(objects, gm.ToObjectInfo(bucket, objName))
|
||||
continue
|
||||
}
|
||||
isPrefix := l.isPrefix(ctx, bucket, p, fetchOwner, startAfter)
|
||||
if isPrefix {
|
||||
prefixes = append(prefixes, p)
|
||||
}
|
||||
}
|
||||
if (len(objects) > maxKeys) || !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loi.IsTruncated = isTruncated
|
||||
loi.ContinuationToken = continuationToken
|
||||
loi.Objects = make([]minio.ObjectInfo, 0)
|
||||
loi.Prefixes = make([]string, 0)
|
||||
|
||||
for _, obj := range objects {
|
||||
loi.NextContinuationToken = obj.Name
|
||||
loi.Objects = append(loi.Objects, obj)
|
||||
}
|
||||
for _, pfx := range prefixes {
|
||||
if pfx != prefix {
|
||||
loi.Prefixes = append(loi.Prefixes, pfx)
|
||||
}
|
||||
}
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// isGWObject returns true if it is a custom object
|
||||
func isGWObject(objName string) bool {
|
||||
isEncrypted := strings.Contains(objName, defaultMinioGWPrefix)
|
||||
if !isEncrypted {
|
||||
return true
|
||||
}
|
||||
// ignore temp part.meta files
|
||||
if strings.Contains(objName, gwpartMetaJSON) {
|
||||
return false
|
||||
}
|
||||
|
||||
pfxSlice := strings.Split(objName, slashSeparator)
|
||||
var i1, i2 int
|
||||
for i := len(pfxSlice) - 1; i >= 0; i-- {
|
||||
p := pfxSlice[i]
|
||||
if p == defaultMinioGWPrefix {
|
||||
i1 = i
|
||||
}
|
||||
if p == gwdareMetaJSON {
|
||||
i2 = i
|
||||
}
|
||||
if i1 > 0 && i2 > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// incomplete uploads would have a uploadID between defaultMinioGWPrefix and gwdareMetaJSON
|
||||
return i2 > 0 && i1 > 0 && i2-i1 == 1
|
||||
}
|
||||
|
||||
// isPrefix returns true if prefix exists and is not an incomplete multipart upload entry
|
||||
func (l *s3EncObjects) isPrefix(ctx context.Context, bucket, prefix string, fetchOwner bool, startAfter string) bool {
|
||||
var continuationToken, delimiter string
|
||||
|
||||
for {
|
||||
loi, e := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, fetchOwner, startAfter)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
if isGWObject(obj.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetObject reads an object from S3. Supports additional
|
||||
// parameters like offset and length which are synonymous with
|
||||
// HTTP Range requests.
|
||||
func (l *s3EncObjects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
return l.getObject(ctx, bucket, key, startOffset, length, writer, etag, opts)
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) isGWEncrypted(ctx context.Context, bucket, object string) bool {
|
||||
_, err := l.s3Objects.GetObjectInfo(ctx, bucket, getDareMetaPath(object), minio.ObjectOptions{})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// getDaremetadata fetches dare.meta from s3 backend and marshals into a structured format.
|
||||
func (l *s3EncObjects) getGWMetadata(ctx context.Context, bucket, metaFileName string) (m gwMetaV1, err error) {
|
||||
oi, err1 := l.s3Objects.GetObjectInfo(ctx, bucket, metaFileName, minio.ObjectOptions{})
|
||||
if err1 != nil {
|
||||
return m, err1
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = l.s3Objects.GetObject(ctx, bucket, metaFileName, 0, oi.Size, &buffer, oi.ETag, minio.ObjectOptions{})
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
return readGWMetadata(ctx, buffer)
|
||||
}
|
||||
|
||||
// writes dare metadata to the s3 backend
|
||||
func (l *s3EncObjects) writeGWMetadata(ctx context.Context, bucket, metaFileName string, m gwMetaV1, o minio.ObjectOptions) error {
|
||||
reader, err := getGWMetadata(ctx, bucket, metaFileName, m)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
_, err = l.s3Objects.PutObject(ctx, bucket, metaFileName, reader, map[string]string{}, o)
|
||||
return err
|
||||
}
|
||||
|
||||
// returns path of temporary metadata json file for the upload
|
||||
func getTmpDareMetaPath(object, uploadID string) string {
|
||||
return path.Join(getGWMetaPath(object), uploadID, gwdareMetaJSON)
|
||||
}
|
||||
|
||||
// returns path of metadata json file for encrypted objects
|
||||
func getDareMetaPath(object string) string {
|
||||
return path.Join(getGWMetaPath(object), gwdareMetaJSON)
|
||||
}
|
||||
|
||||
// returns path of temporary part metadata file for multipart uploads
|
||||
func getPartMetaPath(object, uploadID string, partID int) string {
|
||||
return path.Join(object, defaultMinioGWPrefix, uploadID, strconv.Itoa(partID), gwpartMetaJSON)
|
||||
}
|
||||
|
||||
// deletes the custom dare metadata file saved at the backend
|
||||
func (l *s3EncObjects) deleteGWMetadata(ctx context.Context, bucket, metaFileName string) error {
|
||||
return l.s3Objects.DeleteObject(ctx, bucket, metaFileName)
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) getObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
var o minio.ObjectOptions
|
||||
if minio.GlobalGatewaySSE.SSEC() {
|
||||
o = opts
|
||||
}
|
||||
dmeta, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(key))
|
||||
if err != nil {
|
||||
// unencrypted content
|
||||
return l.s3Objects.GetObject(ctx, bucket, key, startOffset, length, writer, etag, o)
|
||||
}
|
||||
if startOffset < 0 {
|
||||
logger.LogIf(ctx, minio.InvalidRange{})
|
||||
}
|
||||
|
||||
// For negative length read everything.
|
||||
if length < 0 {
|
||||
length = dmeta.Stat.Size - startOffset
|
||||
}
|
||||
// Reply back invalid range if the input offset and length fall out of range.
|
||||
if startOffset > dmeta.Stat.Size || startOffset+length > dmeta.Stat.Size {
|
||||
logger.LogIf(ctx, minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size})
|
||||
return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}
|
||||
}
|
||||
// Get start part index and offset.
|
||||
_, partOffset, err := dmeta.ObjectToPartOffset(ctx, startOffset)
|
||||
if err != nil {
|
||||
return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}
|
||||
}
|
||||
|
||||
// Calculate endOffset according to length
|
||||
endOffset := startOffset
|
||||
if length > 0 {
|
||||
endOffset += length - 1
|
||||
}
|
||||
|
||||
// Get last part index to read given length.
|
||||
if _, _, err := dmeta.ObjectToPartOffset(ctx, endOffset); err != nil {
|
||||
return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}
|
||||
}
|
||||
return l.s3Objects.GetObject(ctx, bucket, key, partOffset, endOffset, writer, dmeta.ETag, o)
|
||||
}
|
||||
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
func (l *s3EncObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, o minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||
var opts minio.ObjectOptions
|
||||
if minio.GlobalGatewaySSE.SSEC() {
|
||||
opts = o
|
||||
}
|
||||
objInfo, err := l.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return l.s3Objects.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
objInfo.UserDefined = minio.CleanMinioInternalMetadataKeys(objInfo.UserDefined)
|
||||
fn, off, length, err := minio.NewGetObjectReader(rs, objInfo)
|
||||
if err != nil {
|
||||
return nil, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
if l.isGWEncrypted(ctx, bucket, object) {
|
||||
object = getGWContentPath(object)
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err := l.getObject(ctx, bucket, object, off, length, pw, objInfo.ETag, opts)
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
|
||||
// Setup cleanup function to cause the above go-routine to
|
||||
// exit in case of partial read
|
||||
pipeCloser := func() { pr.Close() }
|
||||
return fn(pr, h, pipeCloser)
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
// For custom gateway encrypted large objects, the ObjectInfo is retrieved from the dare.meta file.
|
||||
func (l *s3EncObjects) GetObjectInfo(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
var opts minio.ObjectOptions
|
||||
if minio.GlobalGatewaySSE.SSEC() {
|
||||
opts = o
|
||||
}
|
||||
|
||||
gwMeta, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
if err != nil {
|
||||
return l.s3Objects.GetObjectInfo(ctx, bucket, object, opts)
|
||||
}
|
||||
return gwMeta.ToObjectInfo(bucket, object), nil
|
||||
}
|
||||
|
||||
// CopyObject copies an object from source bucket to a destination bucket.
|
||||
func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, s, d minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
cpSrcDstSame := strings.EqualFold(path.Join(srcBucket, srcObject), path.Join(dstBucket, dstObject))
|
||||
if cpSrcDstSame {
|
||||
var gwMeta gwMetaV1
|
||||
if s.ServerSideEncryption != nil && d.ServerSideEncryption != nil &&
|
||||
((s.ServerSideEncryption.Type() == encrypt.SSEC && d.ServerSideEncryption.Type() == encrypt.SSEC) ||
|
||||
(s.ServerSideEncryption.Type() == encrypt.S3 && d.ServerSideEncryption.Type() == encrypt.S3)) {
|
||||
gwMeta, err = l.getGWMetadata(ctx, srcBucket, getDareMetaPath(srcObject))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
header := make(http.Header)
|
||||
if d.ServerSideEncryption != nil {
|
||||
d.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
}
|
||||
gwMeta.Meta = srcInfo.UserDefined
|
||||
if err = l.writeGWMetadata(ctx, dstBucket, getDareMetaPath(dstObject), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return gwMeta.ToObjectInfo(dstBucket, dstObject), nil
|
||||
}
|
||||
}
|
||||
return l.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, srcInfo.UserDefined, d)
|
||||
}
|
||||
|
||||
// DeleteObject deletes a blob in bucket
|
||||
// For custom gateway encrypted large objects, cleans up encrypted content and metadata files
|
||||
// from the backend.
|
||||
func (l *s3EncObjects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
||||
|
||||
// Get dare meta json
|
||||
if _, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(object)); err != nil {
|
||||
return l.s3Objects.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
// delete encrypted object
|
||||
l.s3Objects.DeleteObject(ctx, bucket, getGWContentPath(object))
|
||||
return l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
}
|
||||
|
||||
// ListMultipartUploads lists all multipart uploads.
|
||||
func (l *s3EncObjects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, e error) {
|
||||
|
||||
lmi, e = l.s3Objects.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
lmi.KeyMarker = strings.TrimSuffix(lmi.KeyMarker, getGWContentPath("/"))
|
||||
lmi.NextKeyMarker = strings.TrimSuffix(lmi.NextKeyMarker, getGWContentPath("/"))
|
||||
for i := range lmi.Uploads {
|
||||
lmi.Uploads[i].Object = strings.TrimSuffix(lmi.Uploads[i].Object, getGWContentPath("/"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewMultipartUpload uploads object in multiple parts
|
||||
func (l *s3EncObjects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
var opts minio.ObjectOptions
|
||||
if o.ServerSideEncryption != nil &&
|
||||
((minio.GlobalGatewaySSE.SSEC() && o.ServerSideEncryption.Type() == encrypt.SSEC) ||
|
||||
(minio.GlobalGatewaySSE.SSES3() && o.ServerSideEncryption.Type() == encrypt.S3)) {
|
||||
opts = o
|
||||
}
|
||||
if o.ServerSideEncryption == nil {
|
||||
return l.s3Objects.NewMultipartUpload(ctx, bucket, object, metadata, opts)
|
||||
}
|
||||
uploadID, err = l.s3Objects.NewMultipartUpload(ctx, bucket, getGWContentPath(object), map[string]string{}, opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Create uploadID and write a temporary dare.meta object under object/uploadID prefix
|
||||
gwmeta := newGWMetaV1()
|
||||
gwmeta.Meta = metadata
|
||||
gwmeta.Stat.ModTime = time.Now().UTC()
|
||||
err = l.writeGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID), gwmeta, minio.ObjectOptions{})
|
||||
if err != nil {
|
||||
return uploadID, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return uploadID, nil
|
||||
}
|
||||
|
||||
// PutObject creates a new object with the incoming data,
|
||||
func (l *s3EncObjects) PutObject(ctx context.Context, bucket string, object string, data *minio.PutObjReader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
var s3Opts minio.ObjectOptions
|
||||
if opts.ServerSideEncryption != nil &&
|
||||
((minio.GlobalGatewaySSE.SSEC() && opts.ServerSideEncryption.Type() == encrypt.SSEC) ||
|
||||
(minio.GlobalGatewaySSE.SSES3() && opts.ServerSideEncryption.Type() == encrypt.S3)) {
|
||||
s3Opts = opts
|
||||
}
|
||||
if opts.ServerSideEncryption == nil {
|
||||
defer l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
defer l.DeleteObject(ctx, bucket, getGWContentPath(object))
|
||||
return l.s3Objects.PutObject(ctx, bucket, object, data, metadata, s3Opts)
|
||||
}
|
||||
|
||||
oi, err := l.s3Objects.PutObject(ctx, bucket, getGWContentPath(object), data, map[string]string{}, s3Opts)
|
||||
if err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
|
||||
gwMeta := newGWMetaV1()
|
||||
gwMeta.Meta = make(map[string]string)
|
||||
for k, v := range oi.UserDefined {
|
||||
gwMeta.Meta[k] = v
|
||||
}
|
||||
for k, v := range metadata {
|
||||
gwMeta.Meta[k] = v
|
||||
}
|
||||
encMD5 := data.MD5CurrentHexString()
|
||||
|
||||
gwMeta.ETag = encMD5
|
||||
gwMeta.Stat.Size = oi.Size
|
||||
gwMeta.Stat.ModTime = time.Now().UTC()
|
||||
if err = l.writeGWMetadata(ctx, bucket, getDareMetaPath(object), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
objInfo = gwMeta.ToObjectInfo(bucket, object)
|
||||
// delete any unencrypted content of the same name created previously
|
||||
l.s3Objects.DeleteObject(ctx, bucket, object)
|
||||
return objInfo, nil
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket
|
||||
func (l *s3EncObjects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *minio.PutObjReader, opts minio.ObjectOptions) (pi minio.PartInfo, e error) {
|
||||
|
||||
if opts.ServerSideEncryption == nil {
|
||||
return l.s3Objects.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
var s3Opts minio.ObjectOptions
|
||||
// for sse-s3 encryption options should not be passed to backend
|
||||
if opts.ServerSideEncryption != nil && opts.ServerSideEncryption.Type() == encrypt.SSEC && minio.GlobalGatewaySSE.SSEC() {
|
||||
s3Opts = opts
|
||||
}
|
||||
|
||||
uploadPath := getTmpGWMetaPath(object, uploadID)
|
||||
tmpDareMeta := path.Join(uploadPath, gwdareMetaJSON)
|
||||
_, err := l.s3Objects.GetObjectInfo(ctx, bucket, tmpDareMeta, minio.ObjectOptions{})
|
||||
if err != nil {
|
||||
return pi, minio.InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
pi, e = l.s3Objects.PutObjectPart(ctx, bucket, getGWContentPath(object), uploadID, partID, data, s3Opts)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
gwMeta := newGWMetaV1()
|
||||
gwMeta.Parts = make([]minio.ObjectPartInfo, 1)
|
||||
// Add incoming part.
|
||||
gwMeta.Parts[0] = minio.ObjectPartInfo{
|
||||
Number: partID,
|
||||
ETag: pi.ETag,
|
||||
Size: pi.Size,
|
||||
Name: strconv.Itoa(partID),
|
||||
}
|
||||
gwMeta.ETag = data.MD5CurrentHexString() // encrypted ETag
|
||||
gwMeta.Stat.Size = pi.Size
|
||||
gwMeta.Stat.ModTime = pi.LastModified
|
||||
|
||||
if err = l.writeGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, partID), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return pi, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return minio.PartInfo{
|
||||
Size: gwMeta.Stat.Size,
|
||||
ETag: minio.CanonicalizeETag(gwMeta.ETag),
|
||||
LastModified: gwMeta.Stat.ModTime,
|
||||
PartNumber: partID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CopyObjectPart creates a part in a multipart upload by copying
|
||||
// existing object or a part of it.
|
||||
func (l *s3EncObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
||||
return l.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, srcInfo.PutObjReader, dstOpts)
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *s3EncObjects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, e error) {
|
||||
// We do not store parts uploaded so far in the dare.meta. Only CompleteMultipartUpload finalizes the parts under upload prefix.Otherwise,
|
||||
// there could be situations of dare.meta getting corrupted by competing upload parts.
|
||||
dm, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID))
|
||||
if err != nil {
|
||||
return l.s3Objects.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
}
|
||||
|
||||
lpi, err = l.s3Objects.ListObjectParts(ctx, bucket, getGWContentPath(object), uploadID, partNumberMarker, maxParts, opts)
|
||||
if err != nil {
|
||||
return lpi, err
|
||||
}
|
||||
for i, part := range lpi.Parts {
|
||||
partMeta, err := l.getGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, part.PartNumber))
|
||||
if err != nil || len(partMeta.Parts) == 0 {
|
||||
return lpi, minio.InvalidPart{}
|
||||
}
|
||||
lpi.Parts[i].ETag = partMeta.ETag
|
||||
}
|
||||
lpi.UserDefined = dm.Meta
|
||||
lpi.Object = object
|
||||
return lpi, nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload aborts a ongoing multipart upload
|
||||
func (l *s3EncObjects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
|
||||
if _, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID)); err != nil {
|
||||
return l.s3Objects.AbortMultipartUpload(ctx, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
if err := l.s3Objects.AbortMultipartUpload(ctx, bucket, getGWContentPath(object), uploadID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadPrefix := getTmpGWMetaPath(object, uploadID)
|
||||
var continuationToken, startAfter, delimiter string
|
||||
for {
|
||||
loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, uploadPrefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if err != nil {
|
||||
return minio.InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
if err := l.s3Objects.DeleteObject(ctx, bucket, obj.Name); err != nil {
|
||||
return minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
startAfter = obj.Name
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
||||
func (l *s3EncObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []minio.CompletePart, opts minio.ObjectOptions) (oi minio.ObjectInfo, e error) {
|
||||
|
||||
tmpMeta, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID))
|
||||
if err != nil {
|
||||
oi, e = l.s3Objects.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||
if e == nil {
|
||||
// delete any encrypted version of object that might exist
|
||||
defer l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
defer l.DeleteObject(ctx, bucket, getGWContentPath(object))
|
||||
}
|
||||
return oi, e
|
||||
}
|
||||
gwMeta := newGWMetaV1()
|
||||
gwMeta.Meta = make(map[string]string)
|
||||
for k, v := range tmpMeta.Meta {
|
||||
gwMeta.Meta[k] = v
|
||||
}
|
||||
// Allocate parts similar to incoming slice.
|
||||
gwMeta.Parts = make([]minio.ObjectPartInfo, len(uploadedParts))
|
||||
|
||||
bkUploadedParts := make([]minio.CompletePart, len(uploadedParts))
|
||||
// Calculate full object size.
|
||||
var objectSize int64
|
||||
|
||||
// Validate each part and then commit to disk.
|
||||
for i, part := range uploadedParts {
|
||||
partMeta, err := l.getGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, part.PartNumber))
|
||||
if err != nil || len(partMeta.Parts) == 0 {
|
||||
return oi, minio.InvalidPart{}
|
||||
}
|
||||
bkUploadedParts[i] = minio.CompletePart{PartNumber: part.PartNumber, ETag: partMeta.Parts[0].ETag}
|
||||
gwMeta.Parts[i] = partMeta.Parts[0]
|
||||
objectSize += partMeta.Parts[0].Size
|
||||
}
|
||||
oi, e = l.s3Objects.CompleteMultipartUpload(ctx, bucket, getGWContentPath(object), uploadID, bkUploadedParts, opts)
|
||||
if e != nil {
|
||||
return oi, e
|
||||
}
|
||||
|
||||
//delete any unencrypted version of object that might be on the backend
|
||||
defer l.s3Objects.DeleteObject(ctx, bucket, object)
|
||||
|
||||
// Save the final object size and modtime.
|
||||
gwMeta.Stat.Size = objectSize
|
||||
gwMeta.Stat.ModTime = time.Now().UTC()
|
||||
gwMeta.ETag = oi.ETag
|
||||
|
||||
if err = l.writeGWMetadata(ctx, bucket, getDareMetaPath(object), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return oi, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
// Clean up any uploaded parts that are not being committed by this CompleteMultipart operation
|
||||
var continuationToken, startAfter, delimiter string
|
||||
uploadPrefix := getTmpGWMetaPath(object, uploadID)
|
||||
done := false
|
||||
for {
|
||||
loi, lerr := l.s3Objects.ListObjectsV2(ctx, bucket, uploadPrefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if lerr != nil {
|
||||
done = true
|
||||
break
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
if !strings.HasPrefix(obj.Name, uploadPrefix) {
|
||||
done = true
|
||||
break
|
||||
}
|
||||
startAfter = obj.Name
|
||||
l.s3Objects.DeleteObject(ctx, bucket, obj.Name)
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated || done {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return gwMeta.ToObjectInfo(bucket, object), nil
|
||||
}
|
||||
|
||||
// getTmpGWMetaPath returns the prefix under which uploads in progress are stored on backend
|
||||
func getTmpGWMetaPath(object, uploadID string) string {
|
||||
return path.Join(object, defaultMinioGWPrefix, uploadID)
|
||||
}
|
||||
|
||||
// getGWMetaPath returns the prefix under which custom object metadata and object are stored on backend after upload completes
|
||||
func getGWMetaPath(object string) string {
|
||||
return path.Join(object, defaultMinioGWPrefix)
|
||||
}
|
||||
|
||||
// getGWContentPath returns the prefix under which custom object is stored on backend after upload completes
|
||||
func getGWContentPath(object string) string {
|
||||
return path.Join(object, defaultMinioGWPrefix, defaultGWContentFileName)
|
||||
}
|
||||
|
||||
// Clean-up the stale incomplete encrypted multipart uploads. Should be run in a Go routine.
|
||||
func (l *s3EncObjects) cleanupStaleEncMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration, doneCh chan struct{}) {
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
l.cleanupStaleEncMultipartUploadsOnGW(ctx, expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupStaleMultipartUploads removes old custom encryption multipart uploads on backend
|
||||
func (l *s3EncObjects) cleanupStaleEncMultipartUploadsOnGW(ctx context.Context, expiry time.Duration) {
|
||||
for {
|
||||
buckets, err := l.s3Objects.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, b := range buckets {
|
||||
expParts := l.getStalePartsForBucket(ctx, b.Name, expiry)
|
||||
for k := range expParts {
|
||||
l.s3Objects.DeleteObject(ctx, b.Name, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) getStalePartsForBucket(ctx context.Context, bucket string, expiry time.Duration) (expParts map[string]string) {
|
||||
var prefix, continuationToken, delimiter, startAfter string
|
||||
expParts = make(map[string]string)
|
||||
now := time.Now()
|
||||
for {
|
||||
loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
startAfter = obj.Name
|
||||
if !strings.Contains(obj.Name, defaultMinioGWPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isGWObject(obj.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// delete temporary part.meta or dare.meta files for incomplete uploads that are past expiry
|
||||
if (strings.HasSuffix(obj.Name, gwpartMetaJSON) || strings.HasSuffix(obj.Name, gwdareMetaJSON)) &&
|
||||
now.Sub(obj.ModTime) > expiry {
|
||||
expParts[obj.Name] = ""
|
||||
}
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
var prefix, continuationToken, delimiter, startAfter string
|
||||
expParts := make(map[string]string)
|
||||
|
||||
for {
|
||||
loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
startAfter = obj.Name
|
||||
if !strings.Contains(obj.Name, defaultMinioGWPrefix) {
|
||||
return minio.BucketNotEmpty{}
|
||||
}
|
||||
if isGWObject(obj.Name) {
|
||||
return minio.BucketNotEmpty{}
|
||||
}
|
||||
// delete temporary part.meta or dare.meta files for incomplete uploads
|
||||
if strings.HasSuffix(obj.Name, gwpartMetaJSON) || strings.HasSuffix(obj.Name, gwdareMetaJSON) {
|
||||
expParts[obj.Name] = ""
|
||||
}
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
for k := range expParts {
|
||||
l.s3Objects.DeleteObject(ctx, bucket, k)
|
||||
}
|
||||
err := l.Client.RemoveBucket(bucket)
|
||||
if err != nil {
|
||||
return minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 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 s3
|
||||
|
||||
import minio "github.com/minio/minio/cmd"
|
||||
|
||||
// List of header keys to be filtered, usually
|
||||
// from all S3 API http responses.
|
||||
var defaultFilterKeys = []string{
|
||||
"Connection",
|
||||
"Transfer-Encoding",
|
||||
"Accept-Ranges",
|
||||
"Date",
|
||||
"Server",
|
||||
"Vary",
|
||||
"x-amz-bucket-region",
|
||||
"x-amz-request-id",
|
||||
"x-amz-id-2",
|
||||
"Content-Security-Policy",
|
||||
"X-Xss-Protection",
|
||||
|
||||
// Add new headers to be ignored.
|
||||
}
|
||||
|
||||
// FromGatewayObjectPart converts ObjectInfo for custom part stored as object to PartInfo
|
||||
func FromGatewayObjectPart(partID int, oi minio.ObjectInfo) (pi minio.PartInfo) {
|
||||
return minio.PartInfo{
|
||||
Size: oi.Size,
|
||||
ETag: minio.CanonicalizeETag(oi.ETag),
|
||||
LastModified: oi.ModTime,
|
||||
PartNumber: partID,
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,13 @@ import (
|
||||
"github.com/minio/cli"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio-go/pkg/credentials"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -223,9 +224,20 @@ func (g *S3) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &s3Objects{
|
||||
s := s3Objects{
|
||||
Client: clnt,
|
||||
}, nil
|
||||
}
|
||||
// Enables single encyption of KMS is configured.
|
||||
if minio.GlobalKMS != nil {
|
||||
encS := s3EncObjects{s}
|
||||
|
||||
// Start stale enc multipart uploads cleanup routine.
|
||||
go encS.cleanupStaleEncMultipartUploads(context.Background(),
|
||||
minio.GlobalMultipartCleanupInterval, minio.GlobalMultipartExpiry, minio.GlobalServiceDoneCh)
|
||||
|
||||
return &encS, nil
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Production - s3 gateway is production ready.
|
||||
@@ -330,6 +342,7 @@ func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix strin
|
||||
|
||||
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
||||
func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
||||
|
||||
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
|
||||
if err != nil {
|
||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||
@@ -387,7 +400,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
||||
return minio.ErrorRespToObjectError(err, bucket, key)
|
||||
}
|
||||
defer object.Close()
|
||||
|
||||
if _, err := io.Copy(writer, object); err != nil {
|
||||
return minio.ErrorRespToObjectError(err, bucket, key)
|
||||
}
|
||||
@@ -396,7 +408,11 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{GetObjectOptions: miniogo.GetObjectOptions{ServerSideEncryption: opts.ServerSideEncryption}})
|
||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{
|
||||
GetObjectOptions: miniogo.GetObjectOptions{
|
||||
ServerSideEncryption: opts.ServerSideEncryption,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
@@ -426,6 +442,18 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
||||
// So preserve it by adding "REPLACE" directive to save all the metadata set by CopyObject API.
|
||||
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||
header := make(http.Header)
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||
}
|
||||
|
||||
if dstOpts.ServerSideEncryption != nil {
|
||||
dstOpts.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
}
|
||||
|
||||
if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
@@ -478,10 +506,21 @@ func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object str
|
||||
// existing object or a part of it.
|
||||
func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
||||
|
||||
srcInfo.UserDefined = map[string]string{
|
||||
"x-amz-copy-source-if-match": srcInfo.ETag,
|
||||
}
|
||||
header := make(http.Header)
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||
}
|
||||
|
||||
if dstOpts.ServerSideEncryption != nil {
|
||||
dstOpts.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
}
|
||||
|
||||
completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject,
|
||||
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
||||
if err != nil {
|
||||
@@ -493,7 +532,7 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi minio.ListPartsInfo, e error) {
|
||||
func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, e error) {
|
||||
result, err := l.Client.ListObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
if err != nil {
|
||||
return lpi, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
@@ -557,3 +596,8 @@ func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error
|
||||
func (l *s3Objects) IsCompressionSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (l *s3Objects) IsEncryptionSupported() bool {
|
||||
return minio.GlobalKMS != nil || len(minio.GlobalGatewaySSE) > 0
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ func TestS3ToObjectError(t *testing.T) {
|
||||
// Special test case for error that is not of type
|
||||
// miniogo.ErrorResponse
|
||||
{
|
||||
inputErr: fmt.Errorf("not a minio.ErrorResponse"),
|
||||
expectedErr: fmt.Errorf("not a minio.ErrorResponse"),
|
||||
inputErr: fmt.Errorf("not a ErrorResponse"),
|
||||
expectedErr: fmt.Errorf("not a ErrorResponse"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+47
-11
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -497,11 +496,6 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// A put method on path "/" doesn't make sense, ignore it.
|
||||
if r.Method == http.MethodPut && r.URL.Path == "/" {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Serve HTTP.
|
||||
h.handler.ServeHTTP(w, r)
|
||||
@@ -627,11 +621,52 @@ type bucketForwardingHandler struct {
|
||||
|
||||
func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if globalDNSConfig == nil || globalDomainName == "" ||
|
||||
guessIsBrowserReq(r) || guessIsHealthCheckReq(r) ||
|
||||
guessIsMetricsReq(r) || guessIsRPCReq(r) || isAdminReq(r) {
|
||||
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
|
||||
guessIsRPCReq(r) || isAdminReq(r) {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For browser requests, when federation is setup we need to
|
||||
// specifically handle download and upload for browser requests.
|
||||
if guessIsBrowserReq(r) && globalDNSConfig != nil && globalDomainName != "" {
|
||||
var bucket, _ string
|
||||
switch r.Method {
|
||||
case http.MethodPut:
|
||||
if getRequestAuthType(r) == authTypeJWT {
|
||||
bucket, _ = urlPath2BucketObjectName(strings.TrimPrefix(r.URL.Path, minioReservedBucketPath+"/upload"))
|
||||
}
|
||||
case http.MethodGet:
|
||||
if t := r.URL.Query().Get("token"); t != "" {
|
||||
bucket, _ = urlPath2BucketObjectName(strings.TrimPrefix(r.URL.Path, minioReservedBucketPath+"/download"))
|
||||
} else if getRequestAuthType(r) != authTypeJWT && !strings.HasPrefix(r.URL.Path, minioReservedBucketPath) {
|
||||
bucket, _ = urlPath2BucketObjectName(r.URL.Path)
|
||||
}
|
||||
}
|
||||
if bucket != "" {
|
||||
sr, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
writeErrorResponse(w, ErrNoSuchBucket, r.URL, guessIsBrowserReq(r))
|
||||
} else {
|
||||
writeErrorResponse(w, toAPIErrorCode(context.Background(), err), r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
r.URL.Scheme = "http"
|
||||
if globalIsSSL {
|
||||
r.URL.Scheme = "https"
|
||||
}
|
||||
r.URL.Host = getHostFromSrv(sr)
|
||||
f.fwd.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
bucket, object := urlPath2BucketObjectName(r.URL.Path)
|
||||
// ListBucket requests should be handled at current endpoint as
|
||||
// all buckets data can be fetched from here.
|
||||
@@ -663,12 +698,11 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
host, port := getRandomHostPort(sr)
|
||||
r.URL.Scheme = "http"
|
||||
if globalIsSSL {
|
||||
r.URL.Scheme = "https"
|
||||
}
|
||||
r.URL.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
r.URL.Host = getHostFromSrv(sr)
|
||||
f.fwd.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -743,7 +777,9 @@ func (s customHeaderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Set custom headers such as x-amz-request-id and x-minio-deployment-id
|
||||
// for each request.
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||
w.Header().Set(responseDeploymentIDKey, globalDeploymentID)
|
||||
if globalDeploymentID != "" {
|
||||
w.Header().Set(responseDeploymentIDKey, globalDeploymentID)
|
||||
}
|
||||
s.handler.ServeHTTP(logger.NewResponseWriter(w), r)
|
||||
}
|
||||
|
||||
|
||||
+25
-9
@@ -42,6 +42,8 @@ import (
|
||||
const (
|
||||
globalMinioCertExpireWarnDays = time.Hour * 24 * 30 // 30 days.
|
||||
|
||||
globalMinioDefaultPort = "9000"
|
||||
|
||||
globalMinioDefaultRegion = ""
|
||||
// This is a sha256 output of ``arn:aws:iam::minio:user/admin``,
|
||||
// this is kept in present form to be compatible with S3 owner ID
|
||||
@@ -77,10 +79,11 @@ const (
|
||||
// date and server date during signature verification.
|
||||
globalMaxSkewTime = 15 * time.Minute // 15 minutes skew allowed.
|
||||
|
||||
// Expiry duration after which the multipart uploads are deemed stale.
|
||||
globalMultipartExpiry = time.Hour * 24 * 14 // 2 weeks.
|
||||
// Cleanup interval when the stale multipart cleanup is initiated.
|
||||
globalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
// GlobalMultipartExpiry - Expiry duration after which the multipart uploads are deemed stale.
|
||||
GlobalMultipartExpiry = time.Hour * 24 * 14 // 2 weeks.
|
||||
// GlobalMultipartCleanupInterval - Cleanup interval when the stale multipart cleanup is initiated.
|
||||
GlobalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
|
||||
// Refresh interval to update in-memory bucket policy cache.
|
||||
globalRefreshBucketPolicyInterval = 5 * time.Minute
|
||||
// Refresh interval to update in-memory iam config cache.
|
||||
@@ -90,6 +93,12 @@ const (
|
||||
maxLocationConstraintSize = 3 * humanize.MiByte
|
||||
)
|
||||
|
||||
var globalCLIContext = struct {
|
||||
JSON, Quiet bool
|
||||
Anonymous bool
|
||||
Addr string
|
||||
}{}
|
||||
|
||||
var (
|
||||
// Indicates the total number of erasure coded sets configured.
|
||||
globalXLSetCount int
|
||||
@@ -127,7 +136,7 @@ var (
|
||||
// Minio local server address (in `host:port` format)
|
||||
globalMinioAddr = ""
|
||||
// Minio default port, can be changed through command line.
|
||||
globalMinioPort = "9000"
|
||||
globalMinioPort = globalMinioDefaultPort
|
||||
// Holds the host that was passed using --address
|
||||
globalMinioHost = ""
|
||||
|
||||
@@ -228,10 +237,14 @@ var (
|
||||
|
||||
// KMS key id
|
||||
globalKMSKeyID string
|
||||
// Allocated KMS
|
||||
globalKMS crypto.KMS
|
||||
// KMS config
|
||||
globalKMSConfig crypto.KMSConfig
|
||||
|
||||
// GlobalKMS initialized KMS configuration
|
||||
GlobalKMS crypto.KMS
|
||||
|
||||
// Auto-Encryption, if enabled, turns any non-SSE-C request
|
||||
// into an SSE-S3 request. If enabled a valid, non-empty KMS
|
||||
// configuration must be present.
|
||||
globalAutoEncryption bool
|
||||
|
||||
// Is compression include extensions/content-types set.
|
||||
globalIsEnvCompression bool
|
||||
@@ -258,6 +271,9 @@ var (
|
||||
// Deployment ID - unique per deployment
|
||||
globalDeploymentID string
|
||||
|
||||
// GlobalGatewaySSE sse options
|
||||
GlobalGatewaySSE gatewaySSE
|
||||
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
|
||||
@@ -134,6 +134,11 @@ func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set content-type to default value if it is not set.
|
||||
if _, ok := metadata["content-type"]; !ok {
|
||||
metadata["content-type"] = "application/octet-stream"
|
||||
}
|
||||
|
||||
// Success.
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -183,6 +188,13 @@ func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
|
||||
if cred.AccessKey == "" {
|
||||
cred, _, _ = getReqAccessKeyV2(r)
|
||||
}
|
||||
if cred.AccessKey == "" {
|
||||
claims, owner, _ := webRequestAuthenticate(r)
|
||||
if owner {
|
||||
return globalServerConfig.GetCredential()
|
||||
}
|
||||
cred, _ = globalIAMSys.GetUser(claims.Subject)
|
||||
}
|
||||
return cred
|
||||
}
|
||||
|
||||
@@ -194,6 +206,7 @@ func extractReqParams(r *http.Request) map[string]string {
|
||||
|
||||
region := globalServerConfig.GetRegion()
|
||||
cred := getReqAccessCred(r, region)
|
||||
|
||||
// Success.
|
||||
return map[string]string{
|
||||
"region": region,
|
||||
@@ -351,6 +364,12 @@ func getResource(path string, host string, domain string) (string, error) {
|
||||
return slashSeparator + pathJoin(bucket, path), nil
|
||||
}
|
||||
|
||||
// If none of the http routes match respond with MethodNotAllowed, in JSON
|
||||
func notFoundHandlerJSON(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// If none of the http routes match respond with MethodNotAllowed
|
||||
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorResponse(w, ErrMethodNotAllowed, r.URL, guessIsBrowserReq(r))
|
||||
|
||||
+2
-2
@@ -99,10 +99,10 @@ func (c *BufConn) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// newBufConn - creates a new connection object wrapping net.Conn.
|
||||
func newBufConn(c net.Conn, readTimeout, writeTimeout time.Duration) *BufConn {
|
||||
func newBufConn(c net.Conn, readTimeout, writeTimeout time.Duration, maxHeaderBytes int) *BufConn {
|
||||
return &BufConn{
|
||||
QuirkConn: QuirkConn{Conn: c},
|
||||
bufReader: bufio.NewReader(c),
|
||||
bufReader: bufio.NewReaderSize(c, maxHeaderBytes),
|
||||
readTimeout: readTimeout,
|
||||
writeTimeout: writeTimeout,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
bufconn := newBufConn(tcpConn, 1*time.Second, 1*time.Second, 4096)
|
||||
defer bufconn.Close()
|
||||
|
||||
// Read a line
|
||||
|
||||
@@ -178,6 +178,9 @@ type httpListener struct {
|
||||
// isRoutineNetErr returns true if error is due to a network timeout,
|
||||
// connect reset or io.EOF and false otherwise
|
||||
func isRoutineNetErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if nErr, ok := err.(*net.OpError); ok {
|
||||
// Check if the error is a tcp connection reset
|
||||
if syscallErr, ok := nErr.Err.(*os.SyscallError); ok {
|
||||
@@ -188,7 +191,8 @@ func isRoutineNetErr(err error) bool {
|
||||
// Check if the error is a timeout
|
||||
return nErr.Timeout()
|
||||
}
|
||||
return err == io.EOF
|
||||
// check for io.EOF and also some times io.EOF is wrapped is another error type.
|
||||
return err == io.EOF || err.Error() == "EOF"
|
||||
}
|
||||
|
||||
// start - starts separate goroutine for each TCP listener. A valid insecure/TLS HTTP new connection is passed to httpListener.acceptCh.
|
||||
@@ -218,7 +222,7 @@ func (listener *httpListener) start() {
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(listener.tcpKeepAliveTimeout)
|
||||
|
||||
bufconn := newBufConn(tcpConn, listener.readTimeout, listener.writeTimeout)
|
||||
bufconn := newBufConn(tcpConn, listener.readTimeout, listener.writeTimeout, listener.maxHeaderBytes)
|
||||
if listener.tlsConfig != nil {
|
||||
ok, err := getPlainText(bufconn)
|
||||
if err != nil {
|
||||
@@ -257,7 +261,7 @@ func (listener *httpListener) start() {
|
||||
return
|
||||
}
|
||||
|
||||
bufconn = newBufConn(tlsConn, listener.readTimeout, listener.writeTimeout)
|
||||
bufconn = newBufConn(tlsConn, listener.readTimeout, listener.writeTimeout, listener.maxHeaderBytes)
|
||||
}
|
||||
|
||||
method, resource, host, err := getMethodResourceHost(bufconn, listener.maxHeaderBytes)
|
||||
@@ -395,8 +399,8 @@ func newHTTPListener(serverAddrs []string,
|
||||
|
||||
for _, serverAddr := range serverAddrs {
|
||||
var l net.Listener
|
||||
if l, err = listen("tcp4", serverAddr); err != nil {
|
||||
if l, err = fallbackListen("tcp4", serverAddr); err != nil {
|
||||
if l, err = listen("tcp", serverAddr); err != nil {
|
||||
if l, err = fallbackListen("tcp", serverAddr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -801,6 +802,10 @@ func TestIgnoreErr(t *testing.T) {
|
||||
err: &net.OpError{Err: &myTimeoutErr{timeout: true}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
err: errors.New("EOF"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
err: &net.OpError{Err: &myTimeoutErr{timeout: false}},
|
||||
want: false,
|
||||
@@ -809,6 +814,10 @@ func TestIgnoreErr(t *testing.T) {
|
||||
err: io.ErrUnexpectedEOF,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
err: nil,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ const (
|
||||
DefaultShutdownTimeout = 5 * time.Second
|
||||
|
||||
// DefaultTCPKeepAliveTimeout - default TCP keep alive timeout for accepted connection.
|
||||
DefaultTCPKeepAliveTimeout = 10 * time.Second
|
||||
DefaultTCPKeepAliveTimeout = 30 * time.Second
|
||||
|
||||
// DefaultReadTimeout - default timout to read data from accepted connection.
|
||||
DefaultReadTimeout = 5 * time.Minute
|
||||
|
||||
+3
-3
@@ -60,9 +60,9 @@ type IAMSys struct {
|
||||
iamCannedPolicyMap map[string]iampolicy.Policy
|
||||
}
|
||||
|
||||
// Load - load iam.json
|
||||
// Load - loads iam subsystem
|
||||
func (sys *IAMSys) Load(objAPI ObjectLayer) error {
|
||||
return sys.Init(objAPI)
|
||||
return sys.refresh(objAPI)
|
||||
}
|
||||
|
||||
// Init - initializes config system from iam.json
|
||||
@@ -78,7 +78,7 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
sys.refresh(objAPI)
|
||||
|
||||
@@ -18,9 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"io/ioutil"
|
||||
@@ -76,15 +74,6 @@ func (lc localAdminClient) ServerInfo() (sid ServerInfoData, e error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetConfig - returns config.json of the local server.
|
||||
func (lc localAdminClient) GetConfig() ([]byte, error) {
|
||||
if globalServerConfig == nil {
|
||||
return nil, fmt.Errorf("config not present")
|
||||
}
|
||||
|
||||
return json.Marshal(globalServerConfig)
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling on the local server.
|
||||
func (lc localAdminClient) StartProfiling(profiler string) error {
|
||||
if globalProfiler != nil {
|
||||
|
||||
@@ -24,14 +24,6 @@ func TestLocalAdminClientSignalService(t *testing.T) {
|
||||
testAdminCmdRunnerSignalService(t, &localAdminClient{})
|
||||
}
|
||||
|
||||
func TestLocalAdminClientReInitFormat(t *testing.T) {
|
||||
testAdminCmdRunnerReInitFormat(t, &localAdminClient{})
|
||||
}
|
||||
|
||||
func TestLocalAdminClientServerInfo(t *testing.T) {
|
||||
testAdminCmdRunnerServerInfo(t, &localAdminClient{})
|
||||
}
|
||||
|
||||
func TestLocalAdminClientGetConfig(t *testing.T) {
|
||||
testAdminCmdRunnerGetConfig(t, &localAdminClient{})
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func startLockMaintenance(lkSrv *lockRPCReceiver) {
|
||||
for {
|
||||
// Verifies every minute for locks held more than 2minutes.
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
lkSrv.lockMaintenance(lockValidityCheckInterval)
|
||||
|
||||
+36
-1
@@ -18,17 +18,28 @@ package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"hash"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger/message/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// HighwayHash key for logging in anonymous mode
|
||||
magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
|
||||
// HighwayHash hasher for logging in anonymous mode
|
||||
loggerHighwayHasher hash.Hash
|
||||
)
|
||||
|
||||
// Disable disables all logging, false by default. (used for "go test")
|
||||
var Disable = false
|
||||
|
||||
@@ -94,7 +105,7 @@ func (level Level) String() string {
|
||||
// quietFlag: Hide startup messages if enabled
|
||||
// jsonFlag: Display in JSON format, if enabled
|
||||
var (
|
||||
quietFlag, jsonFlag bool
|
||||
quietFlag, jsonFlag, anonFlag bool
|
||||
// Custom function to format error
|
||||
errorFmtFunc func(string, error, bool) string
|
||||
)
|
||||
@@ -110,6 +121,12 @@ func EnableJSON() {
|
||||
quietFlag = true
|
||||
}
|
||||
|
||||
// EnableAnonymous - turns anonymous flag
|
||||
// to avoid printing sensitive information.
|
||||
func EnableAnonymous() {
|
||||
anonFlag = true
|
||||
}
|
||||
|
||||
// IsJSON - returns true if jsonFlag is true
|
||||
func IsJSON() bool {
|
||||
return jsonFlag
|
||||
@@ -187,6 +204,8 @@ func Init(goPath string, goRoot string) {
|
||||
// paths like "{GOROOT}/src/github.com/minio/minio"
|
||||
// and "{GOPATH}/src/github.com/minio/minio"
|
||||
trimStrings = append(trimStrings, filepath.Join("github.com", "minio", "minio")+string(filepath.Separator))
|
||||
|
||||
loggerHighwayHasher, _ = highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
||||
}
|
||||
|
||||
func trimTrace(f string) string {
|
||||
@@ -239,6 +258,14 @@ func getTrace(traceLevel int) []string {
|
||||
return trace
|
||||
}
|
||||
|
||||
// Return the highway hash of the passed string
|
||||
func hashString(input string) string {
|
||||
defer loggerHighwayHasher.Reset()
|
||||
loggerHighwayHasher.Write([]byte(input))
|
||||
checksum := loggerHighwayHasher.Sum(nil)
|
||||
return hex.EncodeToString(checksum)
|
||||
}
|
||||
|
||||
// LogAlwaysIf prints a detailed error message during
|
||||
// the execution of the server.
|
||||
func LogAlwaysIf(ctx context.Context, err error) {
|
||||
@@ -312,6 +339,14 @@ func logIf(ctx context.Context, err error) {
|
||||
},
|
||||
}
|
||||
|
||||
if anonFlag {
|
||||
entry.API.Args.Bucket = hashString(entry.API.Args.Bucket)
|
||||
entry.API.Args.Object = hashString(entry.API.Args.Object)
|
||||
entry.RemoteHost = hashString(entry.RemoteHost)
|
||||
entry.Message = reflect.TypeOf(err).String()
|
||||
entry.Trace.Variables = make(map[string]string)
|
||||
}
|
||||
|
||||
// Iterate over all logger targets to send the log entry
|
||||
for _, t := range Targets {
|
||||
t.Send(entry)
|
||||
|
||||
+11
-8
@@ -31,19 +31,22 @@ import (
|
||||
var globalFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config-dir, C",
|
||||
Value: getConfigDir(),
|
||||
Usage: func() string {
|
||||
usage := "Path to configuration directory."
|
||||
if getConfigDir() == "" {
|
||||
usage = usage + " This option must be set."
|
||||
}
|
||||
return usage
|
||||
}(),
|
||||
Value: defaultConfigDir.Get(),
|
||||
Usage: "[DEPRECATED] Path to legacy configuration directory.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "certs-dir, S",
|
||||
Value: defaultCertsDir.Get(),
|
||||
Usage: "Path to certs directory.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "Disable startup information.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "anonymous",
|
||||
Usage: "Hide sensitive information from logging.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "Output server logs and startup information in json format.",
|
||||
|
||||
+57
-20
@@ -37,14 +37,21 @@ import (
|
||||
// IPv4 addresses of local host.
|
||||
var localIP4 = mustGetLocalIP4()
|
||||
|
||||
// IPv6 address of local host.
|
||||
var localIP6 = mustGetLocalIP6()
|
||||
|
||||
// mustSplitHostPort is a wrapper to net.SplitHostPort() where error is assumed to be a fatal.
|
||||
func mustSplitHostPort(hostPort string) (host, port string) {
|
||||
host, port, err := net.SplitHostPort(hostPort)
|
||||
// Strip off IPv6 zone information.
|
||||
if i := strings.Index(host, "%"); i > -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
logger.FatalIf(err, "Unable to split host port %s", hostPort)
|
||||
return host, port
|
||||
}
|
||||
|
||||
// mustGetLocalIP4 returns IPv4 addresses of local host. It panics on error.
|
||||
// mustGetLocalIP4 returns IPv4 addresses of localhost. It panics on error.
|
||||
func mustGetLocalIP4() (ipList set.StringSet) {
|
||||
ipList = set.NewStringSet()
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
@@ -67,8 +74,31 @@ func mustGetLocalIP4() (ipList set.StringSet) {
|
||||
return ipList
|
||||
}
|
||||
|
||||
// getHostIP4 returns IPv4 address of given host.
|
||||
func getHostIP4(host string) (ipList set.StringSet, err error) {
|
||||
// mustGetLocalIP6 returns IPv6 addresses of localhost. It panics on error.
|
||||
func mustGetLocalIP6() (ipList set.StringSet) {
|
||||
ipList = set.NewStringSet()
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
logger.FatalIf(err, "Unable to get IP addresses of this host")
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
if ip.To4() == nil {
|
||||
ipList.Add(ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
return ipList
|
||||
}
|
||||
|
||||
// getHostIP returns IP address of given host.
|
||||
func getHostIP(host string) (ipList set.StringSet, err error) {
|
||||
var ips []net.IP
|
||||
|
||||
if ips, err = net.LookupIP(host); err != nil {
|
||||
@@ -110,9 +140,7 @@ func getHostIP4(host string) (ipList set.StringSet, err error) {
|
||||
|
||||
ipList = set.NewStringSet()
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
ipList.Add(ip.String())
|
||||
}
|
||||
ipList.Add(ip.String())
|
||||
}
|
||||
|
||||
return ipList, err
|
||||
@@ -169,29 +197,32 @@ func sortIPs(ipList []string) []string {
|
||||
return append(nonIPs, ips...)
|
||||
}
|
||||
|
||||
func getAPIEndpoints(serverAddr string) (apiEndpoints []string) {
|
||||
host, port := mustSplitHostPort(serverAddr)
|
||||
|
||||
func getAPIEndpoints() (apiEndpoints []string) {
|
||||
var ipList []string
|
||||
if host == "" {
|
||||
if globalMinioHost == "" {
|
||||
ipList = sortIPs(localIP4.ToSlice())
|
||||
ipList = append(ipList, localIP6.ToSlice()...)
|
||||
} else {
|
||||
ipList = []string{host}
|
||||
ipList = []string{globalMinioHost}
|
||||
}
|
||||
|
||||
for _, ip := range ipList {
|
||||
apiEndpoints = append(apiEndpoints, fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), ip, port))
|
||||
apiEndpoints = append(apiEndpoints, fmt.Sprintf("%s://%s", getURLScheme(globalIsSSL), net.JoinHostPort(ip, globalMinioPort)))
|
||||
}
|
||||
|
||||
return apiEndpoints
|
||||
}
|
||||
|
||||
// isHostIPv4 - helper for validating if the provided arg is an ip address.
|
||||
func isHostIPv4(ipAddress string) bool {
|
||||
// isHostIP - helper for validating if the provided arg is an ip address.
|
||||
func isHostIP(ipAddress string) bool {
|
||||
host, _, err := net.SplitHostPort(ipAddress)
|
||||
if err != nil {
|
||||
host = ipAddress
|
||||
}
|
||||
// Strip off IPv6 zone information.
|
||||
if i := strings.Index(host, "%"); i > -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
return net.ParseIP(host) != nil
|
||||
}
|
||||
|
||||
@@ -291,19 +322,20 @@ func extractHostPort(hostAddr string) (string, string, error) {
|
||||
// correspond to one of the local IP of the
|
||||
// current machine
|
||||
func isLocalHost(host string) (bool, error) {
|
||||
hostIPs, err := getHostIP4(host)
|
||||
hostIPs, err := getHostIP(host)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If intersection of two IP sets is not empty, then the host is local host.
|
||||
isLocal := !localIP4.Intersection(hostIPs).IsEmpty()
|
||||
return isLocal, nil
|
||||
// If intersection of two IP sets is not empty, then the host is localhost.
|
||||
isLocalv4 := !localIP4.Intersection(hostIPs).IsEmpty()
|
||||
isLocalv6 := !localIP6.Intersection(hostIPs).IsEmpty()
|
||||
return isLocalv4 || isLocalv6, nil
|
||||
}
|
||||
|
||||
// sameLocalAddrs - returns true if two addresses, even with different
|
||||
// formats, point to the same machine, e.g:
|
||||
// ':9000' and 'http://localhost:9000/' will return true
|
||||
// ':9000' and 'http://localhost:9000/' will return true
|
||||
func sameLocalAddrs(addr1, addr2 string) (bool, error) {
|
||||
|
||||
// Extract host & port from given parameters
|
||||
@@ -355,6 +387,11 @@ func CheckLocalServerAddr(serverAddr string) error {
|
||||
return uiErrInvalidAddressFlag(err)
|
||||
}
|
||||
|
||||
// Strip off IPv6 zone information.
|
||||
if i := strings.Index(host, "%"); i > -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
|
||||
// Check whether port is a valid port number.
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
@@ -366,7 +403,7 @@ func CheckLocalServerAddr(serverAddr string) error {
|
||||
// 0.0.0.0 is a wildcard address and refers to local network
|
||||
// addresses. I.e, 0.0.0.0:9000 like ":9000" refers to port
|
||||
// 9000 on localhost.
|
||||
if host != "" && host != net.IPv4zero.String() {
|
||||
if host != "" && host != net.IPv4zero.String() && host != net.IPv6zero.String() {
|
||||
isLocalHost, err := isLocalHost(host)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+14
-8
@@ -138,7 +138,7 @@ func TestGetHostIP(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
ipList, err := getHostIP4(testCase.host)
|
||||
ipList, err := getHostIP(testCase.host)
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
@@ -157,17 +157,22 @@ func TestGetHostIP(t *testing.T) {
|
||||
|
||||
// Tests finalize api endpoints.
|
||||
func TestGetAPIEndpoints(t *testing.T) {
|
||||
host, port := globalMinioHost, globalMinioAddr
|
||||
defer func() {
|
||||
globalMinioHost, globalMinioAddr = host, port
|
||||
}()
|
||||
testCases := []struct {
|
||||
serverAddr string
|
||||
host, port string
|
||||
expectedResult string
|
||||
}{
|
||||
{":80", "http://127.0.0.1:80"},
|
||||
{"127.0.0.1:80", "http://127.0.0.1:80"},
|
||||
{"localhost:80", "http://localhost:80"},
|
||||
{"", "80", "http://127.0.0.1:80"},
|
||||
{"127.0.0.1", "80", "http://127.0.0.1:80"},
|
||||
{"localhost", "80", "http://localhost:80"},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
apiEndpoints := getAPIEndpoints(testCase.serverAddr)
|
||||
globalMinioHost, globalMinioPort = testCase.host, testCase.port
|
||||
apiEndpoints := getAPIEndpoints()
|
||||
apiEndpointSet := set.CreateStringSet(apiEndpoints...)
|
||||
if !apiEndpointSet.Contains(testCase.expectedResult) {
|
||||
t.Fatalf("test %d: expected: Found, got: Not Found", i+1)
|
||||
@@ -330,7 +335,7 @@ func TestSameLocalAddrs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestIsHostIPv4(t *testing.T) {
|
||||
func TestIsHostIP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args string
|
||||
expectedResult bool
|
||||
@@ -341,10 +346,11 @@ func TestIsHostIPv4(t *testing.T) {
|
||||
{"http://192.168.1.0", false},
|
||||
{"http://192.168.1.0:9000", false},
|
||||
{"192.168.1.0", true},
|
||||
{"[2001:3984:3989::20%eth0]:9000", true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
ret := isHostIPv4(testCase.args)
|
||||
ret := isHostIP(testCase.args)
|
||||
if testCase.expectedResult != ret {
|
||||
t.Fatalf("expected: %v , got: %v", testCase.expectedResult, ret)
|
||||
}
|
||||
|
||||
+90
-3
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -89,6 +90,60 @@ func (sys *NotificationSys) DeleteBucket(ctx context.Context, bucketName string)
|
||||
}()
|
||||
}
|
||||
|
||||
// ReloadFormat - calls ReloadFormat RPC call on all peers.
|
||||
func (sys *NotificationSys) ReloadFormat(dryRun bool) map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load format in three attempts, before giving up.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.ReloadFormat(dryRun)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// LoadUsers - calls LoadUsers RPC call on all peers.
|
||||
func (sys *NotificationSys) LoadUsers() map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load users in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.LoadUsers()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// LoadCredentials - calls LoadCredentials RPC call on all peers.
|
||||
func (sys *NotificationSys) LoadCredentials() map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
@@ -97,7 +152,7 @@ func (sys *NotificationSys) LoadCredentials() map[xnet.Host]error {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to set credentials in three attempts.
|
||||
// Try to load credentials in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.LoadCredentials()
|
||||
if err == nil {
|
||||
@@ -457,6 +512,31 @@ func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
|
||||
return sys.send(args.BucketName, args.ToEvent(), targetIDs...)
|
||||
}
|
||||
|
||||
// DrivePerfInfo - Drive speed (read and write) information
|
||||
func (sys *NotificationSys) DrivePerfInfo() []ServerDrivesPerfInfo {
|
||||
reply := make([]ServerDrivesPerfInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
var i int
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, idx int) {
|
||||
defer wg.Done()
|
||||
di, err := client.DrivePerfInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
di.Addr = addr.String()
|
||||
di.Error = err.Error()
|
||||
}
|
||||
reply[idx] = di
|
||||
}(addr, client, i)
|
||||
i++
|
||||
}
|
||||
wg.Wait()
|
||||
return reply
|
||||
}
|
||||
|
||||
// NewNotificationSys - creates new notification system object.
|
||||
func NewNotificationSys(config *serverConfig, endpoints EndpointList) *NotificationSys {
|
||||
targetList := getNotificationTargets(config)
|
||||
@@ -488,10 +568,10 @@ func (args eventArgs) ToEvent() event.Event {
|
||||
host := globalMinioHost
|
||||
if host == "" {
|
||||
// FIXME: Send FQDN or hostname of this machine than sending IP address.
|
||||
host = localIP4.ToSlice()[0]
|
||||
host = sortIPs(localIP4.ToSlice())[0]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), host, globalMinioPort)
|
||||
return fmt.Sprintf("%s://%s", getURLScheme(globalIsSSL), net.JoinHostPort(host, globalMinioPort))
|
||||
}
|
||||
|
||||
eventTime := UTCNow()
|
||||
@@ -501,6 +581,10 @@ func (args eventArgs) ToEvent() event.Event {
|
||||
"x-amz-request-id": args.RespElements["requestId"],
|
||||
"x-minio-origin-endpoint": getOriginEndpoint(), // Minio specific custom elements.
|
||||
}
|
||||
// Add deployment as part of
|
||||
if globalDeploymentID != "" {
|
||||
respElements["x-minio-deployment-id"] = globalDeploymentID
|
||||
}
|
||||
if args.RespElements["content-length"] != "" {
|
||||
respElements["content-length"] = args.RespElements["content-length"]
|
||||
}
|
||||
@@ -537,6 +621,9 @@ func (args eventArgs) ToEvent() event.Event {
|
||||
if args.EventName != event.ObjectRemovedDelete {
|
||||
newEvent.S3.Object.ETag = args.Object.ETag
|
||||
newEvent.S3.Object.Size = args.Object.Size
|
||||
if args.Object.IsCompressed() {
|
||||
newEvent.S3.Object.Size = args.Object.GetActualSize()
|
||||
}
|
||||
newEvent.S3.Object.ContentType = args.Object.ContentType
|
||||
newEvent.S3.Object.UserMetadata = args.Object.UserDefined
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ type ObjectInfo struct {
|
||||
UserDefined map[string]string
|
||||
|
||||
// List of individual parts, maximum size of upto 10,000
|
||||
Parts []objectPartInfo `json:"-"`
|
||||
Parts []ObjectPartInfo `json:"-"`
|
||||
|
||||
// Implements writer and reader used by CopyObject API
|
||||
Writer io.WriteCloser `json:"-"`
|
||||
|
||||
@@ -47,7 +47,7 @@ func toObjectErr(err error, params ...string) error {
|
||||
Object: params[1],
|
||||
}
|
||||
}
|
||||
case errIsNotRegular, errFileAccessDenied:
|
||||
case errIsNotRegular:
|
||||
if len(params) >= 2 {
|
||||
err = ObjectExistsAsDirectory{
|
||||
Bucket: params[0],
|
||||
|
||||
@@ -75,7 +75,7 @@ type ObjectLayer interface {
|
||||
CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int,
|
||||
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (info PartInfo, err error)
|
||||
PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error)
|
||||
ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result ListPartsInfo, err error)
|
||||
ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error)
|
||||
AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error
|
||||
CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
|
||||
|
||||
@@ -574,6 +574,14 @@ func testListObjects(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||
t.Errorf("Test %d: %s: Expected IsTruncated flag to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.IsTruncated, result.IsTruncated)
|
||||
}
|
||||
|
||||
if testCase.result.IsTruncated && result.NextMarker == "" {
|
||||
t.Errorf("Test %d: %s: Expected NextContinuationToken to contain a string since listing is truncated, but instead found it to be empty", i+1, instanceType)
|
||||
}
|
||||
|
||||
if !testCase.result.IsTruncated && result.NextMarker != "" {
|
||||
t.Errorf("Test %d: %s: Expected NextContinuationToken to be empty since listing is not truncated, but instead found `%v`", i+1, instanceType, result.NextMarker)
|
||||
}
|
||||
|
||||
}
|
||||
// Take ListObject treeWalk go-routine to completion, if available in the treewalk pool.
|
||||
if result.IsTruncated {
|
||||
|
||||
@@ -1429,7 +1429,7 @@ func testListObjectPartsDiskNotFound(obj ObjectLayer, instanceType string, disks
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts)
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{})
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, actualErr.Error())
|
||||
}
|
||||
@@ -1667,7 +1667,7 @@ func testListObjectParts(obj ObjectLayer, instanceType string, t TestErrHandler)
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts)
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{})
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, actualErr.Error())
|
||||
}
|
||||
|
||||
+21
-5
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -299,11 +300,11 @@ func getHostsSlice(records []dns.SrvRecord) []string {
|
||||
return hosts
|
||||
}
|
||||
|
||||
// returns a random host (and corresponding port) from a slice of DNS records
|
||||
func getRandomHostPort(records []dns.SrvRecord) (string, int) {
|
||||
// returns a host (and corresponding port) from a slice of DNS records
|
||||
func getHostFromSrv(records []dns.SrvRecord) string {
|
||||
rand.Seed(time.Now().Unix())
|
||||
srvRecord := records[rand.Intn(len(records))]
|
||||
return srvRecord.Host, srvRecord.Port
|
||||
return net.JoinHostPort(srvRecord.Host, fmt.Sprintf("%d", srvRecord.Port))
|
||||
}
|
||||
|
||||
// IsCompressed returns true if the object is marked as compressed.
|
||||
@@ -486,7 +487,6 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, cleanUpFns ...func())
|
||||
// encrypted bytes. The header parameter is used to
|
||||
// provide encryption parameters.
|
||||
fn = func(inputReader io.Reader, h http.Header, cFns ...func()) (r *GetObjectReader, err error) {
|
||||
|
||||
copySource := h.Get(crypto.SSECopyAlgorithm) != ""
|
||||
|
||||
cFns = append(cleanUpFns, cFns...)
|
||||
@@ -502,6 +502,9 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, cleanUpFns ...func())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decrypt the ETag before top layer consumes this value.
|
||||
oi.ETag = getDecryptedETag(h, oi, copySource)
|
||||
|
||||
// Apply the skipLen and limit on the
|
||||
// decrypted stream
|
||||
decReader = io.LimitReader(ioutil.NewSkipReader(decReader, skipLen), decRangeLength)
|
||||
@@ -573,7 +576,6 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, cleanUpFns ...func())
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
return fn, off, length, nil
|
||||
}
|
||||
|
||||
@@ -659,3 +661,17 @@ func sealETagFn(key crypto.ObjectKey) SealMD5CurrFn {
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
// CleanMinioInternalMetadataKeys removes X-Amz-Meta- prefix from minio internal
|
||||
// encryption metadata that was sent by minio gateway
|
||||
func CleanMinioInternalMetadataKeys(metadata map[string]string) map[string]string {
|
||||
var newMeta = make(map[string]string, len(metadata))
|
||||
for k, v := range metadata {
|
||||
if strings.HasPrefix(k, "X-Amz-Meta-X-Minio-Internal-") {
|
||||
newMeta[strings.TrimPrefix(k, "X-Amz-Meta-")] = v
|
||||
} else {
|
||||
newMeta[k] = v
|
||||
}
|
||||
}
|
||||
return newMeta
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ func TestGetActualSize(t *testing.T) {
|
||||
"X-Minio-Internal-actual-size": "100000001",
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "b3ff3ef3789147152fbfbc50efba4bfd-2"},
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
@@ -450,7 +450,7 @@ func TestGetActualSize(t *testing.T) {
|
||||
"X-Minio-Internal-actual-size": "841",
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "b3ff3ef3789147152fbfbc50efba4bfd-2"},
|
||||
Parts: []objectPartInfo{},
|
||||
Parts: []ObjectPartInfo{},
|
||||
},
|
||||
result: 841,
|
||||
},
|
||||
@@ -459,7 +459,7 @@ func TestGetActualSize(t *testing.T) {
|
||||
UserDefined: map[string]string{"X-Minio-Internal-compression": "golang/snappy/LZ77",
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "b3ff3ef3789147152fbfbc50efba4bfd-2"},
|
||||
Parts: []objectPartInfo{},
|
||||
Parts: []ObjectPartInfo{},
|
||||
},
|
||||
result: -1,
|
||||
},
|
||||
@@ -482,7 +482,7 @@ func TestGetCompressedOffsets(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
objInfo: ObjectInfo{
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
@@ -499,7 +499,7 @@ func TestGetCompressedOffsets(t *testing.T) {
|
||||
},
|
||||
{
|
||||
objInfo: ObjectInfo{
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
@@ -516,7 +516,7 @@ func TestGetCompressedOffsets(t *testing.T) {
|
||||
},
|
||||
{
|
||||
objInfo: ObjectInfo{
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
|
||||
+342
-224
@@ -33,8 +33,8 @@ import (
|
||||
|
||||
snappy "github.com/golang/snappy"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/readahead"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -91,10 +91,26 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
// get gateway encryption options
|
||||
opts, err := getEncryptionOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
// Check for auth type to return S3 compatible error.
|
||||
// type to return the correct error (NoSuchKey vs AccessDenied)
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
|
||||
@@ -118,12 +134,7 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
}) {
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
_, err := getObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
_, err = getObjectInfo(ctx, bucket, object, opts)
|
||||
if toAPIErrorCode(ctx, err) == ErrNoSuchKey {
|
||||
s3Error = ErrNoSuchKey
|
||||
}
|
||||
@@ -144,18 +155,15 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, ErrEmptyRequestBody, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
var selectReq s3select.ObjectSelectRequest
|
||||
if err := xmlDecoder(r.Body, &selectReq, r.ContentLength); err != nil {
|
||||
writeErrorResponse(w, ErrMalformedXML, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.EqualFold(string(selectReq.ExpressionType), "SQL") {
|
||||
writeErrorResponse(w, ErrInvalidExpressionType, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if len(selectReq.Expression) >= s3select.MaxExpressionLength {
|
||||
writeErrorResponse(w, ErrExpressionTooLong, r.URL, guessIsBrowserReq(r))
|
||||
s3Select, err := s3select.NewS3Select(r.Body)
|
||||
if err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
w.WriteHeader(serr.HTTPStatusCode())
|
||||
w.Write(s3select.NewErrorMessage(serr.ErrorCode(), serr.ErrorMessage()))
|
||||
} else {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,121 +171,38 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectNInfo = api.CacheAPI().GetObjectNInfo
|
||||
}
|
||||
getObject := func(offset, length int64) (rc io.ReadCloser, err error) {
|
||||
isSuffixLength := false
|
||||
if offset < 0 {
|
||||
isSuffixLength = true
|
||||
}
|
||||
rs := &HTTPRangeSpec{
|
||||
IsSuffixLength: isSuffixLength,
|
||||
Start: offset,
|
||||
End: length,
|
||||
}
|
||||
|
||||
var opts ObjectOptions
|
||||
gr, err := getObjectNInfo(ctx, bucket, object, nil, r.Header, readLock, opts)
|
||||
return getObjectNInfo(ctx, bucket, object, rs, r.Header, readLock, ObjectOptions{})
|
||||
}
|
||||
|
||||
if err = s3Select.Open(getObject); err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
w.WriteHeader(serr.HTTPStatusCode())
|
||||
w.Write(s3select.NewErrorMessage(serr.ErrorCode(), serr.ErrorMessage()))
|
||||
} else {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s3Select.Evaluate(w)
|
||||
s3Select.Close()
|
||||
|
||||
objInfo, err := getObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
objInfo := gr.ObjInfo
|
||||
|
||||
if selectReq.InputSerialization.CompressionType == s3select.SelectCompressionGZIP {
|
||||
if !strings.Contains(objInfo.ContentType, "gzip") {
|
||||
writeErrorResponse(w, ErrInvalidDataSource, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if selectReq.InputSerialization.CompressionType == s3select.SelectCompressionBZIP {
|
||||
if !strings.Contains(objInfo.ContentType, "bzip") {
|
||||
writeErrorResponse(w, ErrInvalidDataSource, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if selectReq.InputSerialization.CompressionType == "" {
|
||||
selectReq.InputSerialization.CompressionType = s3select.SelectCompressionNONE
|
||||
if !strings.Contains(objInfo.ContentType, "text/csv") && !strings.Contains(objInfo.ContentType, "application/json") {
|
||||
writeErrorResponse(w, ErrInvalidDataSource, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if !strings.EqualFold(string(selectReq.ExpressionType), "SQL") {
|
||||
writeErrorResponse(w, ErrInvalidExpressionType, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if len(selectReq.Expression) >= s3select.MaxExpressionLength {
|
||||
writeErrorResponse(w, ErrExpressionTooLong, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if selectReq.InputSerialization.CSV == nil && selectReq.InputSerialization.JSON == nil {
|
||||
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if selectReq.OutputSerialization.CSV == nil && selectReq.OutputSerialization.JSON == nil {
|
||||
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if selectReq.InputSerialization.CSV != nil {
|
||||
if selectReq.InputSerialization.CSV.FileHeaderInfo != s3select.CSVFileHeaderInfoUse &&
|
||||
selectReq.InputSerialization.CSV.FileHeaderInfo != s3select.CSVFileHeaderInfoNone &&
|
||||
selectReq.InputSerialization.CSV.FileHeaderInfo != s3select.CSVFileHeaderInfoIgnore &&
|
||||
selectReq.InputSerialization.CSV.FileHeaderInfo != "" {
|
||||
writeErrorResponse(w, ErrInvalidFileHeaderInfo, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAlways &&
|
||||
selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAsNeeded &&
|
||||
selectReq.OutputSerialization.CSV.QuoteFields != "" {
|
||||
writeErrorResponse(w, ErrInvalidQuoteFields, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if len(selectReq.InputSerialization.CSV.RecordDelimiter) > 2 {
|
||||
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
if selectReq.InputSerialization.JSON != nil {
|
||||
if selectReq.InputSerialization.JSON.Type != s3select.JSONLinesType {
|
||||
writeErrorResponse(w, ErrInvalidJSONType, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Set encryption response headers
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if crypto.IsEncrypted(objInfo.UserDefined) {
|
||||
switch {
|
||||
case crypto.S3.IsEncrypted(objInfo.UserDefined):
|
||||
w.Header().Set(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
case crypto.SSEC.IsEncrypted(objInfo.UserDefined):
|
||||
w.Header().Set(crypto.SSECAlgorithm, r.Header.Get(crypto.SSECAlgorithm))
|
||||
w.Header().Set(crypto.SSECKeyMD5, r.Header.Get(crypto.SSECKeyMD5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := readahead.NewReader(gr)
|
||||
defer reader.Close()
|
||||
|
||||
size := objInfo.Size
|
||||
if objInfo.IsCompressed() {
|
||||
size = objInfo.GetActualSize()
|
||||
if size < 0 {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, errInvalidDecompressedSize), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s3s, err := s3select.New(reader, size, selectReq)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Parses the select query and checks for an error
|
||||
_, _, _, _, _, _, err = s3select.ParseSelect(s3s)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Executes the query on data-set
|
||||
s3select.Execute(w, s3s)
|
||||
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||
@@ -316,15 +241,26 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
var (
|
||||
opts ObjectOptions
|
||||
err error
|
||||
)
|
||||
if vid := r.URL.Query().Get("versionId"); vid != "" && vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// get gateway encryption options
|
||||
opts, err := getEncryptionOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Check for auth type to return S3 compatible error.
|
||||
// type to return the correct error (NoSuchKey vs AccessDenied)
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
|
||||
@@ -395,6 +331,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
objInfo := gr.ObjInfo
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined)
|
||||
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -481,23 +418,32 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
|
||||
writeErrorResponseHeadersOnly(w, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
if vid := r.URL.Query().Get("versionId"); vid != "" && vid != "null" {
|
||||
writeErrorResponseHeadersOnly(w, ErrNoSuchVersion)
|
||||
return
|
||||
}
|
||||
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
var (
|
||||
opts ObjectOptions
|
||||
err error
|
||||
)
|
||||
opts, err := getEncryptionOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
|
||||
if getRequestAuthType(r) == authTypeAnonymous {
|
||||
@@ -552,12 +498,12 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined)
|
||||
}
|
||||
|
||||
// Set encryption response headers
|
||||
@@ -604,7 +550,6 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
|
||||
// Notify object accessed via a HEAD request.
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectAccessedHead,
|
||||
@@ -628,6 +573,9 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
||||
defaultMeta[k] = v
|
||||
}
|
||||
|
||||
// remove SSE Headers from source info
|
||||
crypto.RemoveSSEHeaders(defaultMeta)
|
||||
|
||||
// if x-amz-metadata-directive says REPLACE then
|
||||
// we extract metadata from the input headers.
|
||||
if isMetadataReplace(r.Header) {
|
||||
@@ -646,11 +594,25 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
||||
|
||||
// Returns a minio-go Client configured to access remote host described by destDNSRecord
|
||||
// Applicable only in a federated deployment
|
||||
var getRemoteInstanceClient = func(r *http.Request, host string, port int) (*miniogo.Core, error) {
|
||||
// In a federated deployment, all the instances share config files and hence expected to have same
|
||||
// credentials, make sure to send the same credentials for which the request came in.
|
||||
var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
|
||||
cred := getReqAccessCred(r, globalServerConfig.GetRegion())
|
||||
return miniogo.NewCore(net.JoinHostPort(host, strconv.Itoa(port)), cred.AccessKey, cred.SecretKey, globalIsSSL)
|
||||
// In a federated deployment, all the instances share config files
|
||||
// and hence expected to have same credentials.
|
||||
core, err := miniogo.NewCore(host, cred.AccessKey, cred.SecretKey, globalIsSSL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
core.SetCustomTransport(NewCustomHTTPTransport())
|
||||
return core, nil
|
||||
}
|
||||
|
||||
// Check if the bucket is on a remote site, this code only gets executed when federation is enabled.
|
||||
var isRemoteCallRequired = func(ctx context.Context, bucket string, objAPI ObjectLayer) bool {
|
||||
if globalDNSConfig == nil {
|
||||
return false
|
||||
}
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
return err == toObjectErr(errVolumeNotFound, bucket)
|
||||
}
|
||||
|
||||
// CopyObjectHandler - Copy Object
|
||||
@@ -676,7 +638,10 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
|
||||
if !api.EncryptionEnabled() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
dstBucket := vars["bucket"]
|
||||
dstObject := vars["object"]
|
||||
@@ -695,6 +660,22 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
|
||||
}
|
||||
|
||||
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
|
||||
// Regardless of whether you have enabled versioning, each object in your bucket
|
||||
// has a version ID. If you have not enabled versioning, Amazon S3 sets the value
|
||||
// of the version ID to null. If you have enabled versioning, Amazon S3 assigns a
|
||||
// unique version ID value for the object.
|
||||
if u, err := url.Parse(cpSrcPath); err == nil {
|
||||
// Check if versionId query param was added, if yes then check if
|
||||
// its non "null" value, we should error out since we do not support
|
||||
// any versions other than "null".
|
||||
if vid := u.Query().Get("versionId"); vid != "" && vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
cpSrcPath = u.Path
|
||||
}
|
||||
|
||||
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
|
||||
// If source object is empty or bucket is empty, reply back invalid copy source.
|
||||
if srcObject == "" || srcBucket == "" {
|
||||
@@ -712,8 +693,30 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, ErrInvalidMetadataDirective, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
|
||||
var srcOpts, dstOpts ObjectOptions
|
||||
srcOpts, err = copySrcEncryptionOpts(ctx, r, srcBucket, srcObject)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// convert copy src encryption options for GET calls
|
||||
var getOpts = ObjectOptions{}
|
||||
getSSE := encrypt.SSE(srcOpts.ServerSideEncryption)
|
||||
if getSSE != srcOpts.ServerSideEncryption {
|
||||
getOpts.ServerSideEncryption = getSSE
|
||||
}
|
||||
dstOpts, err = copyDstEncryptionOpts(ctx, r, dstBucket, dstObject, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -736,7 +739,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
var rs *HTTPRangeSpec
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, lock, srcOpts)
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, lock, getOpts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -761,17 +764,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
srcInfo.metadataOnly = true
|
||||
}
|
||||
|
||||
// Checks if a remote putobject call is needed for CopyObject operation
|
||||
// 1. If source and destination bucket names are same, it means no call needed to etcd to get destination info
|
||||
// 2. If destination bucket doesn't exist locally, only then a etcd call is needed
|
||||
var isRemoteCallRequired = func(ctx context.Context, src, dst string, objAPI ObjectLayer) bool {
|
||||
if src == dst {
|
||||
return false
|
||||
}
|
||||
_, berr := objAPI.GetBucketInfo(ctx, dst)
|
||||
return berr == toObjectErr(errVolumeNotFound, dst)
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
var length = srcInfo.Size
|
||||
|
||||
@@ -786,13 +778,32 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
length = actualSize
|
||||
}
|
||||
|
||||
// Check if the destination bucket is on a remote site, this code only gets executed
|
||||
// when federation is enabled, ie when globalDNSConfig is non 'nil'.
|
||||
//
|
||||
// This function is similar to isRemoteCallRequired but specifically for COPY object API
|
||||
// if destination and source are same we do not need to check for destnation bucket
|
||||
// to exist locally.
|
||||
var isRemoteCopyRequired = func(ctx context.Context, srcBucket, dstBucket string, objAPI ObjectLayer) bool {
|
||||
if globalDNSConfig == nil {
|
||||
return false
|
||||
}
|
||||
if srcBucket == dstBucket {
|
||||
return false
|
||||
}
|
||||
_, err := objAPI.GetBucketInfo(ctx, dstBucket)
|
||||
return err == toObjectErr(errVolumeNotFound, dstBucket)
|
||||
}
|
||||
|
||||
var compressMetadata map[string]string
|
||||
// No need to compress for remote etcd calls
|
||||
// Pass the decompressed stream to such calls.
|
||||
isCompressed := objectAPI.IsCompressionSupported() && isCompressible(r.Header, srcObject) && !isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI)
|
||||
isCompressed := objectAPI.IsCompressionSupported() && isCompressible(r.Header, srcObject) && !isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
|
||||
if isCompressed {
|
||||
// Storing the compression metadata.
|
||||
srcInfo.UserDefined[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV1
|
||||
srcInfo.UserDefined[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10)
|
||||
compressMetadata = make(map[string]string, 2)
|
||||
// Preserving the compression metadata.
|
||||
compressMetadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV1
|
||||
compressMetadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10)
|
||||
// Remove all source encrypted related metadata to
|
||||
// avoid copying them in target object.
|
||||
crypto.RemoveInternalEntries(srcInfo.UserDefined)
|
||||
@@ -861,6 +872,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
// - the object is encrypted using SSE-C and two different SSE-C keys are present
|
||||
// - the object is encrypted using SSE-S3 and the SSE-S3 header is present
|
||||
// than execute a key rotation.
|
||||
var keyRotation bool
|
||||
if cpSrcDstSame && ((sseCopyC && sseC) || (sseS3 && sseCopyS3)) {
|
||||
if sseCopyC && sseC {
|
||||
oldKey, err = ParseSSECopyCustomerRequest(r.Header, srcInfo.UserDefined)
|
||||
@@ -869,8 +881,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range srcInfo.UserDefined {
|
||||
encMetadata[k] = v
|
||||
if hasPrefix(k, ReservedMetadataPrefix) {
|
||||
encMetadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// In case of SSE-S3 oldKey and newKey aren't used - the KMS manages the keys.
|
||||
@@ -881,12 +896,15 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Since we are rotating the keys, make sure to update the metadata.
|
||||
srcInfo.metadataOnly = true
|
||||
keyRotation = true
|
||||
} else {
|
||||
if isSourceEncrypted || isTargetEncrypted {
|
||||
// We are not only copying just metadata instead
|
||||
// we are creating a new object at this point, even
|
||||
// if source and destination are same objects.
|
||||
srcInfo.metadataOnly = false
|
||||
if !keyRotation {
|
||||
srcInfo.metadataOnly = false
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the size of the target object
|
||||
@@ -896,7 +914,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
case !isSourceEncrypted && !isTargetEncrypted:
|
||||
targetSize = srcInfo.Size
|
||||
case isSourceEncrypted && isTargetEncrypted:
|
||||
targetSize = srcInfo.Size
|
||||
objInfo := ObjectInfo{Size: actualSize}
|
||||
targetSize = objInfo.EncryptedSize()
|
||||
case !isSourceEncrypted && isTargetEncrypted:
|
||||
targetSize = srcInfo.EncryptedSize()
|
||||
case isSourceEncrypted && !isTargetEncrypted:
|
||||
@@ -935,6 +954,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// Store the preserved compression metadata.
|
||||
for k, v := range compressMetadata {
|
||||
srcInfo.UserDefined[k] = v
|
||||
}
|
||||
|
||||
// We need to preserve the encryption headers set in EncryptRequest,
|
||||
// so we do not want to override them, copy them instead.
|
||||
for k, v := range encMetadata {
|
||||
@@ -943,7 +967,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Ensure that metadata does not contain sensitive information
|
||||
crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
|
||||
|
||||
// Check if x-amz-metadata-directive was not set to REPLACE and source,
|
||||
// desination are same objects. Apply this restriction also when
|
||||
// metadataOnly is true indicating that we are not overwriting the object.
|
||||
@@ -958,29 +981,28 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
var objInfo ObjectInfo
|
||||
|
||||
if isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI) {
|
||||
if globalDNSConfig == nil {
|
||||
writeErrorResponse(w, ErrNoSuchBucket, r.URL, guessIsBrowserReq(r))
|
||||
if isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI) {
|
||||
var dstRecords []dns.SrvRecord
|
||||
dstRecords, err = globalDNSConfig.Get(dstBucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
var dstRecords []dns.SrvRecord
|
||||
if dstRecords, err = globalDNSConfig.Get(dstBucket); err == nil {
|
||||
// Send PutObject request to appropriate instance (in federated deployment)
|
||||
host, port := getRandomHostPort(dstRecords)
|
||||
client, rerr := getRemoteInstanceClient(r, host, port)
|
||||
if rerr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
remoteObjInfo, rerr := client.PutObject(dstBucket, dstObject, srcInfo.Reader,
|
||||
srcInfo.Size, "", "", srcInfo.UserDefined, dstOpts.ServerSideEncryption)
|
||||
if rerr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
objInfo.ETag = remoteObjInfo.ETag
|
||||
objInfo.ModTime = remoteObjInfo.LastModified
|
||||
|
||||
// Send PutObject request to appropriate instance (in federated deployment)
|
||||
client, rerr := getRemoteInstanceClient(r, getHostFromSrv(dstRecords))
|
||||
if rerr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
remoteObjInfo, rerr := client.PutObject(dstBucket, dstObject, srcInfo.Reader,
|
||||
srcInfo.Size, "", "", srcInfo.UserDefined, dstOpts.ServerSideEncryption)
|
||||
if rerr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
objInfo.ETag = remoteObjInfo.ETag
|
||||
objInfo.ModTime = remoteObjInfo.LastModified
|
||||
} else {
|
||||
// Copy source object to destination, if source and destination
|
||||
// object is same then only metadata is updated.
|
||||
@@ -1042,7 +1064,10 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
@@ -1200,7 +1225,17 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -1209,6 +1244,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var objectEncryptionKey []byte
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasServerSideEncryptionHeader(r.Header) && !hasSuffix(object, slashSeparator) { // handle SSE requests
|
||||
@@ -1241,15 +1277,15 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
etag := objInfo.ETag
|
||||
if objInfo.IsCompressed() {
|
||||
// Ignore compressed ETag.
|
||||
objInfo.ETag = objInfo.ETag + "-1"
|
||||
}
|
||||
if hasServerSideEncryptionHeader(r.Header) {
|
||||
w.Header().Set("ETag", "\""+getDecryptedETag(r.Header, objInfo, false)+"\"")
|
||||
} else {
|
||||
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
||||
etag = objInfo.ETag + "-1"
|
||||
} else if hasServerSideEncryptionHeader(r.Header) {
|
||||
etag = getDecryptedETag(r.Header, objInfo, false)
|
||||
}
|
||||
w.Header().Set("ETag", "\""+etag+"\"")
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if crypto.IsEncrypted(objInfo.UserDefined) {
|
||||
switch {
|
||||
@@ -1305,15 +1341,29 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
var (
|
||||
opts ObjectOptions
|
||||
err error
|
||||
)
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
var err error
|
||||
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, bucket, object); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1402,6 +1452,10 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
if !api.EncryptionEnabled() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
dstBucket := vars["bucket"]
|
||||
@@ -1419,6 +1473,22 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
|
||||
}
|
||||
|
||||
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
|
||||
// Regardless of whether you have enabled versioning, each object in your bucket
|
||||
// has a version ID. If you have not enabled versioning, Amazon S3 sets the value
|
||||
// of the version ID to null. If you have enabled versioning, Amazon S3 assigns a
|
||||
// unique version ID value for the object.
|
||||
if u, err := url.Parse(cpSrcPath); err == nil {
|
||||
// Check if versionId query param was added, if yes then check if
|
||||
// its non "null" value, we should error out since we do not support
|
||||
// any versions other than "null".
|
||||
if vid := u.Query().Get("versionId"); vid != "" && vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
cpSrcPath = u.Path
|
||||
}
|
||||
|
||||
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
|
||||
// If source object is empty or bucket is empty, reply back invalid copy source.
|
||||
if srcObject == "" || srcBucket == "" {
|
||||
@@ -1447,6 +1517,21 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
var srcOpts, dstOpts ObjectOptions
|
||||
srcOpts, err = copySrcEncryptionOpts(ctx, r, srcBucket, srcObject)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// convert copy src and dst encryption options for GET/PUT calls
|
||||
var getOpts = ObjectOptions{}
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
getOpts.ServerSideEncryption = encrypt.SSE(srcOpts.ServerSideEncryption)
|
||||
}
|
||||
dstOpts, err = copyDstEncryptionOpts(ctx, r, dstBucket, dstObject, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -1477,7 +1562,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, readLock, srcOpts)
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, readLock, getOpts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1522,7 +1607,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
var reader io.Reader
|
||||
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, dstBucket, dstObject, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, dstBucket, dstObject, uploadID, 0, 1, dstOpts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1564,7 +1649,19 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
isEncrypted := false
|
||||
var objectEncryptionKey []byte
|
||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||
li, lerr := objectAPI.ListObjectParts(ctx, dstBucket, dstObject, uploadID, 0, 1, dstOpts)
|
||||
if lerr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, lerr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
li.UserDefined = CleanMinioInternalMetadataKeys(li.UserDefined)
|
||||
dstOpts, err = copyDstEncryptionOpts(ctx, r, dstBucket, dstObject, li.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if crypto.IsEncrypted(li.UserDefined) {
|
||||
isEncrypted = true
|
||||
if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(li.UserDefined) {
|
||||
writeErrorResponse(w, ErrSSEMultipartEncrypted, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1609,7 +1706,6 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
pReader = NewPutObjReader(rawReader, srcInfo.Reader, objectEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
srcInfo.PutObjReader = pReader
|
||||
// Copy source object to destination, if source and destination
|
||||
// object is same then only metadata is updated.
|
||||
@@ -1619,6 +1715,9 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if isEncrypted {
|
||||
partInfo.ETag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
|
||||
}
|
||||
|
||||
if isEncrypted {
|
||||
partInfo.ETag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
|
||||
@@ -1646,7 +1745,10 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
@@ -1748,8 +1850,17 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
var pipeReader *io.PipeReader
|
||||
var pipeWriter *io.PipeWriter
|
||||
|
||||
// get encryption options
|
||||
var opts ObjectOptions
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1789,10 +1900,8 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
var opts ObjectOptions
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -1806,17 +1915,25 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
var objectEncryptionKey []byte
|
||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
li.UserDefined = CleanMinioInternalMetadataKeys(li.UserDefined)
|
||||
if crypto.IsEncrypted(li.UserDefined) {
|
||||
if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(li.UserDefined) {
|
||||
writeErrorResponse(w, ErrSSEMultipartEncrypted, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
isEncrypted = true // to detect SSE-S3 encryption
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, li.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var key []byte
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
key, err = ParseSSECustomerRequest(r)
|
||||
@@ -1832,7 +1949,6 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var partIDbin [4]byte
|
||||
binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
|
||||
|
||||
@@ -1865,19 +1981,16 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
etag := partInfo.ETag
|
||||
if isCompressed {
|
||||
pipeWriter.Close()
|
||||
// Suppress compressed ETag.
|
||||
partInfo.ETag = partInfo.ETag + "-1"
|
||||
}
|
||||
|
||||
if partInfo.ETag != "" {
|
||||
if isEncrypted {
|
||||
w.Header().Set("ETag", "\""+tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))+"\"")
|
||||
} else {
|
||||
w.Header().Set("ETag", "\""+partInfo.ETag+"\"")
|
||||
}
|
||||
etag = partInfo.ETag + "-1"
|
||||
} else if isEncrypted {
|
||||
etag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
|
||||
}
|
||||
w.Header().Set("ETag", "\""+etag+"\"")
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
@@ -1962,16 +2075,16 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
|
||||
writeErrorResponse(w, ErrInvalidMaxParts, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
var opts ObjectOptions
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var ssec bool
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2064,10 +2177,9 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
var objectEncryptionKey []byte
|
||||
var opts ObjectOptions
|
||||
var isEncrypted, ssec bool
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2076,6 +2188,8 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
isEncrypted = true
|
||||
ssec = crypto.SSEC.IsEncrypted(li.UserDefined)
|
||||
var key []byte
|
||||
isEncrypted = true
|
||||
ssec = crypto.SSEC.IsEncrypted(li.UserDefined)
|
||||
if crypto.S3.IsEncrypted(li.UserDefined) {
|
||||
// Calculating object encryption key
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, li.UserDefined)
|
||||
@@ -2088,13 +2202,11 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
}
|
||||
|
||||
partsMap := make(map[string]PartInfo)
|
||||
var listPartsInfo ListPartsInfo
|
||||
|
||||
if isEncrypted {
|
||||
var partNumberMarker int
|
||||
maxParts := 1000
|
||||
for {
|
||||
listPartsInfo, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2108,6 +2220,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete parts.
|
||||
var completeParts []CompletePart
|
||||
for _, part := range complMultipartUpload.Parts {
|
||||
@@ -2207,6 +2320,11 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
if vid := r.URL.Query().Get("versionId"); vid != "" && vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
// Not required to check whether given object exists or not, because
|
||||
|
||||
@@ -1731,7 +1731,7 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
// See if the new part has been uploaded.
|
||||
// testing whether the copy was successful.
|
||||
var results ListPartsInfo
|
||||
results, err = obj.ListObjectParts(context.Background(), testCase.bucketName, testObject, testCase.uploadID, 0, 1)
|
||||
results, err = obj.ListObjectParts(context.Background(), testCase.bucketName, testObject, testCase.uploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %s: Failed to look for copied object part: <ERROR> %s", i+1, instanceType, err)
|
||||
}
|
||||
@@ -2245,7 +2245,7 @@ func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
t.Fatalf("Error decoding the recorded response Body")
|
||||
}
|
||||
// verify the uploadID my making an attempt to list parts.
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1)
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid UploadID: <ERROR> %s", err)
|
||||
}
|
||||
@@ -2297,7 +2297,7 @@ func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
t.Fatalf("Error decoding the recorded response Body")
|
||||
}
|
||||
// verify the uploadID my making an attempt to list parts.
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1)
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid UploadID: <ERROR> %s", err)
|
||||
}
|
||||
@@ -2412,7 +2412,7 @@ func testAPINewMultipartHandlerParallel(obj ObjectLayer, instanceType, bucketNam
|
||||
wg.Wait()
|
||||
// Validate the upload ID by an attempt to list parts using it.
|
||||
for _, uploadID := range testUploads.uploads {
|
||||
_, err := obj.ListObjectParts(context.Background(), bucketName, objectName, uploadID, 0, 1)
|
||||
_, err := obj.ListObjectParts(context.Background(), bucketName, objectName, uploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid UploadID: <ERROR> %s", err)
|
||||
}
|
||||
|
||||
+28
-1
@@ -115,12 +115,39 @@ func (rpcClient *PeerRPCClient) SendEvent(bucketName string, targetID, remoteTar
|
||||
return err
|
||||
}
|
||||
|
||||
// ReloadFormat - calls reload format RPC.
|
||||
func (rpcClient *PeerRPCClient) ReloadFormat(dryRun bool) error {
|
||||
args := ReloadFormatArgs{
|
||||
DryRun: dryRun,
|
||||
}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(peerServiceName+".ReloadFormat", &args, &reply)
|
||||
}
|
||||
|
||||
// LoadUsers - calls load users RPC.
|
||||
func (rpcClient *PeerRPCClient) LoadUsers() error {
|
||||
args := AuthArgs{}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(peerServiceName+".LoadUsers", &args, &reply)
|
||||
}
|
||||
|
||||
// LoadCredentials - calls load credentials RPC.
|
||||
func (rpcClient *PeerRPCClient) LoadCredentials() error {
|
||||
args := AuthArgs{}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(peerServiceName+".SetCredentials", &args, &reply)
|
||||
return rpcClient.Call(peerServiceName+".LoadCredentials", &args, &reply)
|
||||
}
|
||||
|
||||
// DrivePerfInfo - returns drive performance info for remote server.
|
||||
func (rpcClient *PeerRPCClient) DrivePerfInfo() (ServerDrivesPerfInfo, error) {
|
||||
args := AuthArgs{}
|
||||
var reply ServerDrivesPerfInfo
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".DrivePerfInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// NewPeerRPCClient - returns new peer RPC client.
|
||||
|
||||
@@ -192,6 +192,30 @@ func (receiver *peerRPCReceiver) SendEvent(args *SendEventArgs, reply *bool) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadFormatArgs - send event RPC arguments.
|
||||
type ReloadFormatArgs struct {
|
||||
AuthArgs
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// ReloadFormat - handles reload format RPC call, reloads latest `format.json`
|
||||
func (receiver *peerRPCReceiver) ReloadFormat(args *ReloadFormatArgs, reply *VoidReply) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
return objAPI.ReloadFormat(context.Background(), args.DryRun)
|
||||
}
|
||||
|
||||
// LoadUsers - handles load users RPC call.
|
||||
func (receiver *peerRPCReceiver) LoadUsers(args *AuthArgs, reply *VoidReply) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
return globalIAMSys.Load(objAPI)
|
||||
}
|
||||
|
||||
// LoadCredentials - handles load credentials RPC call.
|
||||
func (receiver *peerRPCReceiver) LoadCredentials(args *AuthArgs, reply *VoidReply) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
@@ -214,6 +238,17 @@ func (receiver *peerRPCReceiver) LoadCredentials(args *AuthArgs, reply *VoidRepl
|
||||
return globalConfigSys.Load(newObjectLayerFn())
|
||||
}
|
||||
|
||||
// DrivePerfInfo - handles drive performance RPC call
|
||||
func (receiver *peerRPCReceiver) DrivePerfInfo(args *AuthArgs, reply *ServerDrivesPerfInfo) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
*reply = localEndpointsPerf(globalEndpoints)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPeerRPCServer - returns new peer RPC server.
|
||||
func NewPeerRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
|
||||
+8
-4
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -140,7 +141,7 @@ func (sys *PolicySys) Init(objAPI ObjectLayer) error {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
sys.refresh(objAPI)
|
||||
@@ -183,7 +184,12 @@ func NewPolicySys() *PolicySys {
|
||||
}
|
||||
|
||||
func getConditionValues(request *http.Request, locationConstraint string) map[string][]string {
|
||||
args := make(map[string][]string)
|
||||
args := map[string][]string{
|
||||
"SourceIp": {handlers.GetSourceIP(request)},
|
||||
"SecureTransport": {fmt.Sprintf("%t", request.TLS != nil)},
|
||||
"UserAgent": {request.UserAgent()},
|
||||
"Referer": {request.Referer()},
|
||||
}
|
||||
|
||||
for key, values := range request.Header {
|
||||
if existingValues, found := args[key]; found {
|
||||
@@ -201,8 +207,6 @@ func getConditionValues(request *http.Request, locationConstraint string) map[st
|
||||
}
|
||||
}
|
||||
|
||||
args["SourceIp"] = []string{handlers.GetSourceIP(request)}
|
||||
|
||||
if locationConstraint != "" {
|
||||
args["LocationConstraint"] = []string{locationConstraint}
|
||||
}
|
||||
|
||||
+19
-3
@@ -71,6 +71,7 @@ type posix struct {
|
||||
connected bool
|
||||
|
||||
diskMount bool // indicates if the path is an actual mount.
|
||||
driveSync bool // indicates if the backend is synchronous.
|
||||
|
||||
// Disk usage metrics
|
||||
stopUsageCh chan struct{}
|
||||
@@ -191,8 +192,17 @@ func newPosix(path string) (*posix, error) {
|
||||
diskMount: mountinfo.IsLikelyMountPoint(path),
|
||||
}
|
||||
|
||||
var pf BoolFlag
|
||||
if driveSync := os.Getenv("MINIO_DRIVE_SYNC"); driveSync != "" {
|
||||
pf, err = ParseBoolFlag(driveSync)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.driveSync = bool(pf)
|
||||
}
|
||||
|
||||
if !p.diskMount {
|
||||
go p.diskUsage(globalServiceDoneCh)
|
||||
go p.diskUsage(GlobalServiceDoneCh)
|
||||
}
|
||||
|
||||
// Success.
|
||||
@@ -1013,8 +1023,14 @@ func (s *posix) AppendFile(volume, path string, buf []byte) (err error) {
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
// Create file if not found
|
||||
w, err := s.openFile(volume, path, os.O_CREATE|os.O_APPEND|os.O_WRONLY)
|
||||
var w *os.File
|
||||
// Create file if not found, additionally also enables synchronous
|
||||
// operation if asked by the user.
|
||||
if s.driveSync {
|
||||
w, err = s.openFile(volume, path, os.O_CREATE|os.O_SYNC|os.O_APPEND|os.O_WRONLY)
|
||||
} else {
|
||||
w, err = s.openFile(volume, path, os.O_CREATE|os.O_APPEND|os.O_WRONLY)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ import (
|
||||
|
||||
// startWithConds - map which indicates if a given condition supports starts-with policy operator
|
||||
var startsWithConds = map[string]bool{
|
||||
"$acl": true,
|
||||
"$bucket": false,
|
||||
"$cache-control": true,
|
||||
"$content-type": true,
|
||||
"$content-disposition": true,
|
||||
"$content-encoding": true,
|
||||
"$expires": true,
|
||||
"$key": true,
|
||||
"$acl": true,
|
||||
"$bucket": false,
|
||||
"$cache-control": true,
|
||||
"$content-type": true,
|
||||
"$content-disposition": true,
|
||||
"$content-encoding": true,
|
||||
"$expires": true,
|
||||
"$key": true,
|
||||
"$success_action_redirect": true,
|
||||
"$redirect": true,
|
||||
"$success_action_status": false,
|
||||
|
||||
+5
-5
@@ -101,14 +101,14 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
|
||||
registerDistXLRouters(router, endpoints)
|
||||
}
|
||||
|
||||
// Add STS router only enabled if etcd is configured.
|
||||
// Add STS router always.
|
||||
registerSTSRouter(router)
|
||||
|
||||
// Add Admin RPC router
|
||||
registerAdminRPCRouter(router)
|
||||
|
||||
// Add Admin router.
|
||||
registerAdminRouter(router)
|
||||
// Add Admin router, all APIs are enabled in server mode.
|
||||
registerAdminRouter(router, true)
|
||||
|
||||
// Add healthcheck router
|
||||
registerHealthCheckRouter(router)
|
||||
@@ -123,8 +123,8 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add API router.
|
||||
registerAPIRouter(router)
|
||||
// Add API router, additionally all server mode support encryption.
|
||||
registerAPIRouter(router, true)
|
||||
|
||||
// Register rest of the handlers.
|
||||
return registerHandlers(router, globalHandlers...), nil
|
||||
|
||||
+11
-26
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -40,7 +41,7 @@ func init() {
|
||||
var serverFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "address",
|
||||
Value: ":" + globalMinioPort,
|
||||
Value: ":" + globalMinioDefaultPort,
|
||||
Usage: "bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname",
|
||||
},
|
||||
}
|
||||
@@ -147,9 +148,7 @@ func serverHandleCmdArgs(ctx *cli.Context) {
|
||||
// Handle common command args.
|
||||
handleCommonCmdArgs(ctx)
|
||||
|
||||
// Server address.
|
||||
serverAddr := ctx.String("address")
|
||||
logger.FatalIf(CheckLocalServerAddr(serverAddr), "Unable to validate passed arguments")
|
||||
logger.FatalIf(CheckLocalServerAddr(globalCLIContext.Addr), "Unable to validate passed arguments")
|
||||
|
||||
var setupType SetupType
|
||||
var err error
|
||||
@@ -162,9 +161,9 @@ func serverHandleCmdArgs(ctx *cli.Context) {
|
||||
|
||||
endpoints := strings.Fields(os.Getenv("MINIO_ENDPOINTS"))
|
||||
if len(endpoints) > 0 {
|
||||
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(serverAddr, endpoints...)
|
||||
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(globalCLIContext.Addr, endpoints...)
|
||||
} else {
|
||||
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(serverAddr, ctx.Args()...)
|
||||
globalMinioAddr, globalEndpoints, setupType, globalXLSetCount, globalXLSetDriveCount, err = createServerEndpoints(globalCLIContext.Addr, ctx.Args()...)
|
||||
}
|
||||
logger.FatalIf(err, "Invalid command line arguments")
|
||||
|
||||
@@ -205,32 +204,16 @@ func serverMain(ctx *cli.Context) {
|
||||
// error during initialization will be shown as a fatal message
|
||||
logger.Disable = true
|
||||
|
||||
// Get "json" flag from command line argument and
|
||||
// enable json and quite modes if jason flag is turned on.
|
||||
jsonFlag := ctx.IsSet("json") || ctx.GlobalIsSet("json")
|
||||
if jsonFlag {
|
||||
logger.EnableJSON()
|
||||
}
|
||||
|
||||
// Get quiet flag from command line argument.
|
||||
quietFlag := ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
|
||||
if quietFlag {
|
||||
logger.EnableQuiet()
|
||||
}
|
||||
|
||||
// Handle all server command args.
|
||||
serverHandleCmdArgs(ctx)
|
||||
|
||||
// Create certs path.
|
||||
logger.FatalIf(createConfigDir(), "Unable to initialize configuration files")
|
||||
|
||||
// Check and load TLS certificates.
|
||||
var err error
|
||||
globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig()
|
||||
logger.FatalIf(err, "Unable to load the TLS configuration")
|
||||
|
||||
// Check and load Root CAs.
|
||||
globalRootCAs, err = getRootCAs(getCADir())
|
||||
globalRootCAs, err = getRootCAs(globalCertsCADir.Get())
|
||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||
|
||||
// Handle all server environment vars.
|
||||
@@ -246,7 +229,7 @@ func serverMain(ctx *cli.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if !quietFlag {
|
||||
if !globalCLIContext.Quiet {
|
||||
// Check for new updates from dl.minio.io.
|
||||
mode := globalMinioModeFS
|
||||
if globalIsDistXL {
|
||||
@@ -383,14 +366,16 @@ func serverMain(ctx *cli.Context) {
|
||||
if err = globalNotificationSys.Init(newObject); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
}
|
||||
if globalAutoEncryption && !newObject.IsEncryptionSupported() {
|
||||
logger.Fatal(errors.New("Invalid KMS configuration"), "auto-encryption is enabled but server does not support encryption")
|
||||
}
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = newObject
|
||||
globalObjLayerMutex.Unlock()
|
||||
|
||||
// Prints the formatted startup message once object layer is initialized.
|
||||
apiEndpoints := getAPIEndpoints(globalMinioAddr)
|
||||
printStartupMessage(apiEndpoints)
|
||||
printStartupMessage(getAPIEndpoints())
|
||||
|
||||
// Set uptime time after object layer has initialized.
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
+21
-14
@@ -20,12 +20,13 @@ import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// Documentation links, these are part of message printing code.
|
||||
@@ -76,6 +77,19 @@ func printStartupMessage(apiEndPoints []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if input is not IPv4, false if it is.
|
||||
func isNotIPv4(host string) bool {
|
||||
h, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
h = host
|
||||
}
|
||||
ip := net.ParseIP(h)
|
||||
ok := ip.To4() != nil // This is always true of IP is IPv4
|
||||
|
||||
// Returns true if input is not IPv4.
|
||||
return !ok
|
||||
}
|
||||
|
||||
// strip api endpoints list with standard ports such as
|
||||
// port "80" and "443" before displaying on the startup
|
||||
// banner. Returns a new list of API endpoints.
|
||||
@@ -83,23 +97,16 @@ func stripStandardPorts(apiEndpoints []string) (newAPIEndpoints []string) {
|
||||
newAPIEndpoints = make([]string, len(apiEndpoints))
|
||||
// Check all API endpoints for standard ports and strip them.
|
||||
for i, apiEndpoint := range apiEndpoints {
|
||||
url, err := url.Parse(apiEndpoint)
|
||||
u, err := xnet.ParseURL(apiEndpoint)
|
||||
if err != nil {
|
||||
newAPIEndpoints[i] = apiEndpoint
|
||||
continue
|
||||
}
|
||||
host, port := mustSplitHostPort(url.Host)
|
||||
// For standard HTTP(s) ports such as "80" and "443"
|
||||
// apiEndpoints should only be host without port.
|
||||
switch {
|
||||
case url.Scheme == "http" && port == "80":
|
||||
fallthrough
|
||||
case url.Scheme == "https" && port == "443":
|
||||
url.Host = host
|
||||
newAPIEndpoints[i] = url.String()
|
||||
default:
|
||||
newAPIEndpoints[i] = apiEndpoint
|
||||
if globalMinioHost == "" && isNotIPv4(u.Host) {
|
||||
// Skip all non-IPv4 endpoints when we bind to all interfaces.
|
||||
continue
|
||||
}
|
||||
newAPIEndpoints[i] = u.String()
|
||||
}
|
||||
return newAPIEndpoints
|
||||
}
|
||||
@@ -116,7 +123,7 @@ func printServerCommonMsg(apiEndpoints []string) {
|
||||
|
||||
// Colorize the message and print.
|
||||
logger.StartupMessage(colorBlue("Endpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
||||
if isTerminal() {
|
||||
if isTerminal() && !globalCLIContext.Anonymous {
|
||||
logger.StartupMessage(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
||||
logger.StartupMessage(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
||||
if region != "" {
|
||||
|
||||
+3
-3
@@ -34,12 +34,12 @@ const (
|
||||
// Global service signal channel.
|
||||
var globalServiceSignalCh chan serviceSignal
|
||||
|
||||
// Global service done channel.
|
||||
var globalServiceDoneCh chan struct{}
|
||||
// GlobalServiceDoneCh - Global service done channel.
|
||||
var GlobalServiceDoneCh chan struct{}
|
||||
|
||||
// Initialize service mutex once.
|
||||
func init() {
|
||||
globalServiceDoneCh = make(chan struct{}, 1)
|
||||
GlobalServiceDoneCh = make(chan struct{}, 1)
|
||||
globalServiceSignalCh = make(chan serviceSignal)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 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 (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// AssumedRoleUser - The identifiers for the temporary security credentials that
|
||||
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
|
||||
type AssumedRoleUser struct {
|
||||
// The ARN of the temporary security credentials that are returned from the
|
||||
// AssumeRole action. For more information about ARNs and how to use them in
|
||||
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html)
|
||||
// in Using IAM.
|
||||
//
|
||||
// Arn is a required field
|
||||
Arn string
|
||||
|
||||
// A unique identifier that contains the role ID and the role session name of
|
||||
// the role that is being assumed. The role ID is generated by AWS when the
|
||||
// role is created.
|
||||
//
|
||||
// AssumedRoleId is a required field
|
||||
AssumedRoleID string `xml:"AssumeRoleId"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
|
||||
// AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request.
|
||||
type AssumeRoleWithWebIdentityResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
|
||||
Result WebIdentityResult `xml:"AssumeRoleWithWebIdentityResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// WebIdentityResult - Contains the response to a successful AssumeRoleWithWebIdentity
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type WebIdentityResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithWebIdentity request.
|
||||
Provider string `xml:",omitempty"`
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithWebIdentity call. The identifier is typically unique to
|
||||
// the user and the application that acquired the WebIdentityToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromWebIdentityToken string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request.
|
||||
type AssumeRoleWithClientGrantsResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
|
||||
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type ClientGrantsResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithClientGrants request.
|
||||
Provider string `xml:",omitempty"`
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to
|
||||
// the user and the application that acquired the ClientGrantsToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromToken string `xml:",omitempty"`
|
||||
}
|
||||
+7
-1
@@ -56,6 +56,7 @@ const (
|
||||
ErrSTSNone STSErrorCode = iota
|
||||
ErrSTSMissingParameter
|
||||
ErrSTSInvalidParameterValue
|
||||
ErrSTSWebIdentityExpiredToken
|
||||
ErrSTSClientGrantsExpiredToken
|
||||
ErrSTSInvalidClientGrantsToken
|
||||
ErrSTSMalformedPolicyDocument
|
||||
@@ -76,9 +77,14 @@ var stsErrCodeResponse = map[STSErrorCode]STSError{
|
||||
Description: "An invalid or out-of-range value was supplied for the input parameter.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSWebIdentityExpiredToken: {
|
||||
Code: "ExpiredToken",
|
||||
Description: "The web identity token that was passed is expired or is not valid. Get a new identity token from the identity provider and then retry the request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSClientGrantsExpiredToken: {
|
||||
Code: "ExpiredToken",
|
||||
Description: "The client grants that was passed is expired or is not valid.",
|
||||
Description: "The client grants that was passed is expired or is not valid. Get a new client grants token from the identity provider and then retry the request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSInvalidClientGrantsToken: {
|
||||
|
||||
+100
-63
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -45,82 +44,104 @@ func registerSTSRouter(router *mux.Router) {
|
||||
// AssumeRoleWithClientGrants
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
|
||||
Queries("Action", "AssumeRoleWithClientGrants").
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("Token", "{Token:.*}")
|
||||
|
||||
// AssumeRoleWithWebIdentity
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)).
|
||||
Queries("Action", "AssumeRoleWithWebIdentity").
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("WebIdentityToken", "{Token:.*}")
|
||||
|
||||
}
|
||||
|
||||
// AssumedRoleUser - The identifiers for the temporary security credentials that
|
||||
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
|
||||
type AssumedRoleUser struct {
|
||||
// The ARN of the temporary security credentials that are returned from the
|
||||
// AssumeRole action. For more information about ARNs and how to use them in
|
||||
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html)
|
||||
// in Using IAM.
|
||||
//
|
||||
// Arn is a required field
|
||||
Arn string
|
||||
// AssumeRoleWithWebIdentity - implementation of AWS STS API supporting OAuth2.0
|
||||
// users from web identity provider such as Facebook, Google, or any OpenID
|
||||
// Connect-compatible identity provider.
|
||||
//
|
||||
// Eg:-
|
||||
// $ curl https://minio:9000/?Action=AssumeRoleWithWebIdentity&WebIdentityToken=<jwt>
|
||||
func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AssumeRoleWithWebIdentity")
|
||||
|
||||
// A unique identifier that contains the role ID and the role session name of
|
||||
// the role that is being assumed. The role ID is generated by AWS when the
|
||||
// role is created.
|
||||
//
|
||||
// AssumedRoleId is a required field
|
||||
AssumedRoleID string `xml:"AssumeRoleId"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
defer logger.AuditLog(w, r, "AssumeRoleWithWebIdentity", nil)
|
||||
|
||||
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request.
|
||||
type AssumeRoleWithClientGrantsResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
|
||||
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
if globalIAMValidators == nil {
|
||||
writeSTSErrorResponse(w, ErrSTSNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type ClientGrantsResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
// NOTE: this API only accepts JWT tokens.
|
||||
v, err := globalIAMValidators.Get("jwt")
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
return
|
||||
}
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
vars := mux.Vars(r)
|
||||
m, err := v.Validate(vars["Token"], r.URL.Query().Get("DurationSeconds"))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case validator.ErrTokenExpired:
|
||||
writeSTSErrorResponse(w, ErrSTSWebIdentityExpiredToken)
|
||||
case validator.ErrInvalidDuration:
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
secret := globalServerConfig.GetCredential().SecretKey
|
||||
cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
// JWT has requested a custom claim with policy value set.
|
||||
// This is a Minio STS API specific value, this value should
|
||||
// be set and configured on your identity provider as part of
|
||||
// JWT custom claims.
|
||||
var policyName string
|
||||
if v, ok := m["policy"]; ok {
|
||||
policyName, _ = v.(string)
|
||||
}
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithClientGrants request.
|
||||
Provider string `xml:",omitempty"`
|
||||
var subFromToken string
|
||||
if v, ok := m["sub"]; ok {
|
||||
subFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to
|
||||
// the user and the application that acquired the ClientGrantsToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromToken string `xml:",omitempty"`
|
||||
// Set the newly generated credentials.
|
||||
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload temp users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
encodedSuccessResponse := encodeResponse(&AssumeRoleWithWebIdentityResponse{
|
||||
Result: WebIdentityResult{
|
||||
Credentials: cred,
|
||||
SubjectFromWebIdentityToken: subFromToken,
|
||||
},
|
||||
})
|
||||
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
// AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting
|
||||
// OAuth2.0 client grants.
|
||||
// OAuth2.0 client credential grants.
|
||||
//
|
||||
// Eg:-
|
||||
// $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token=<jwt>
|
||||
@@ -173,6 +194,11 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
|
||||
policyName, _ = v.(string)
|
||||
}
|
||||
|
||||
var subFromToken string
|
||||
if v, ok := m["sub"]; ok {
|
||||
subFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
// Set the newly generated credentials.
|
||||
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -180,8 +206,19 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload temp users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
encodedSuccessResponse := encodeResponse(&AssumeRoleWithClientGrantsResponse{
|
||||
Result: ClientGrantsResult{Credentials: cred},
|
||||
Result: ClientGrantsResult{
|
||||
Credentials: cred,
|
||||
SubjectFromToken: subFromToken,
|
||||
},
|
||||
})
|
||||
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user