mirror of
https://github.com/pgsty/minio.git
synced 2026-11-09 09:11:51 +03:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf66e9a529 | |||
| fde8c38638 | |||
| e6252dee5a | |||
| ecb042aa1c | |||
| e29009d347 | |||
| 9631d65552 | |||
| 7b7be66fa1 | |||
| b99aaab42e | |||
| 32bd1b31e9 | |||
| f287b15e71 | |||
| 586466584f | |||
| c0b4bf0a3e | |||
| acf46cc3b5 | |||
| 06ef8248c3 | |||
| 7c2ae4eaf7 | |||
| 989d7af9ac | |||
| 8a6c3aa3cd | |||
| 62b560510b | |||
| 0edfb32621 | |||
| 7e0f1eb8b5 | |||
| b43e8337b1 | |||
| 30040fba45 | |||
| 44cf9ac62f | |||
| 3b55357045 | |||
| 18d9a20ff6 | |||
| 6590aba6d2 | |||
| 2e81f27d27 | |||
| ae3c05aa37 | |||
| cef044178c | |||
| c998d1ac8c | |||
| 3457e504cf | |||
| 7f0cca075a | |||
| 088c595e01 | |||
| 26b4b466df | |||
| fdf691fdcc | |||
| 88c8c2d6cd | |||
| ef585037a0 | |||
| 362ebdcbab | |||
| b251454dd6 | |||
| 21c8693d9c | |||
| 1e7e5e297c | |||
| c7f180ffa9 | |||
| b8bd8d6a03 | |||
| baec331e84 | |||
| 557f382477 | |||
| 23b166b318 | |||
| 81a481e098 | |||
| 5b3090dffc | |||
| 3ef3fefd54 | |||
| 28e25eac78 | |||
| b0c9ae7490 | |||
| 143e7fe300 | |||
| f09e7ca764 | |||
| fae284d6b9 | |||
| 83d8e01c81 | |||
| 110458cd10 | |||
| e3eec89d24 | |||
| 54ae364def | |||
| 16a100b597 | |||
| cbc5d78a09 | |||
| d8a2975a68 | |||
| 66d911653f | |||
| ca6f795504 | |||
| 2af0f11731 | |||
| c3408f4f04 | |||
| b92c324254 | |||
| 81bee93b8d | |||
| 670f9788e3 | |||
| f187a16962 | |||
| e031f2b614 | |||
| c2b7b82ef4 | |||
| 02ad9d6072 | |||
| ea9408ccbb | |||
| 307765591d | |||
| 5d859b2178 | |||
| 223967fd32 | |||
| 274b35154c | |||
| c05ced08bb | |||
| c7722fbb1b | |||
| f163bed40d | |||
| b4772849f9 | |||
| 839a758a36 | |||
| aebfceeafb | |||
| 83d7ec09c1 | |||
| 8c29f69b00 | |||
| ce9d36d954 | |||
| 5c765bc63e | |||
| 6c7c6bec91 | |||
| ed703c065d | |||
| 387584356f | |||
| 1111419d4a | |||
| 20378821cf | |||
| ce1bfa6de8 | |||
| 6c26227081 | |||
| aa4e2b1542 | |||
| 1e5ac39ff3 | |||
| ec2295c3dc | |||
| 8cf7b88cc5 | |||
| 48bfebe442 | |||
| df60b3c733 | |||
| 584cb61bb8 | |||
| c1798cc89a | |||
| 3c8fabd116 | |||
| 36e51d0cee | |||
| 7d0645fb3a | |||
| 7c339e248a | |||
| b62ed5dc90 | |||
| f0641a0406 | |||
| 3d060f8b64 | |||
| 052a7b8eec | |||
| 9531cddb06 | |||
| 6fe9a613c0 | |||
| b729a4e83c | |||
| a0683d3c1f | |||
| 66fda7a37f | |||
| a63bc9254d | |||
| 14fa0097b0 | |||
| d99c397746 | |||
| e3777b1dd9 | |||
| 63c03758e6 |
+12
-8
@@ -1,10 +1,4 @@
|
|||||||
go_import_path: github.com/minio/minio
|
go_import_path: github.com/minio/minio
|
||||||
sudo: required
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
dist: trusty
|
|
||||||
|
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
@@ -18,9 +12,11 @@ branches:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: linux
|
- os: linux
|
||||||
|
dist: trusty
|
||||||
|
sudo: required
|
||||||
env:
|
env:
|
||||||
- ARCH=x86_64
|
- ARCH=x86_64
|
||||||
go: 1.10.3
|
go: 1.10.4
|
||||||
script:
|
script:
|
||||||
- make
|
- make
|
||||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||||
@@ -29,9 +25,17 @@ matrix:
|
|||||||
- make verify
|
- make verify
|
||||||
- make coverage
|
- make coverage
|
||||||
- cd browser && yarn && yarn test && cd ..
|
- cd browser && yarn && yarn test && cd ..
|
||||||
|
- os: windows
|
||||||
|
env:
|
||||||
|
- ARCH=x86_64
|
||||||
|
go: 1.10.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
|
||||||
|
- bash buildscripts/go-coverage.sh
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- nvm install stable
|
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then nvm install stable ; fi
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|||||||
+8
-15
@@ -1,27 +1,20 @@
|
|||||||
FROM golang:1.10.1-alpine3.7
|
FROM alpine:3.7
|
||||||
|
|
||||||
LABEL maintainer="Minio Inc <dev@minio.io>"
|
LABEL maintainer="Minio Inc <dev@minio.io>"
|
||||||
|
|
||||||
ENV GOPATH /go
|
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||||
ENV PATH $PATH:$GOPATH/bin
|
COPY minio /usr/bin/
|
||||||
ENV CGO_ENABLED 0
|
|
||||||
ENV MINIO_UPDATE off
|
ENV MINIO_UPDATE off
|
||||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
MINIO_SECRET_KEY_FILE=secret_key
|
MINIO_SECRET_KEY_FILE=secret_key
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/minio/
|
RUN \
|
||||||
|
|
||||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
|
||||||
|
|
||||||
COPY . /go/src/github.com/minio/minio
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
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 && \
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
cd /go/src/github.com/minio/minio && \
|
chmod +x /usr/bin/minio && \
|
||||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
chmod +x /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ checks:
|
|||||||
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
||||||
|
|
||||||
getdeps:
|
getdeps:
|
||||||
@echo "Installing golint" && go get -u github.com/golang/lint/golint
|
@echo "Installing golint" && go get -u golang.org/x/lint/golint
|
||||||
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
||||||
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||||
@@ -75,6 +75,9 @@ build: checks
|
|||||||
@echo "Building minio binary to './minio'"
|
@echo "Building minio binary to './minio'"
|
||||||
@CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
@CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||||
|
|
||||||
|
docker: build
|
||||||
|
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||||
|
|
||||||
pkg-add:
|
pkg-add:
|
||||||
@echo "Adding new package $(PKG)"
|
@echo "Adding new package $(PKG)"
|
||||||
@${GOPATH}/bin/govendor add $(PKG)
|
@${GOPATH}/bin/govendor add $(PKG)
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# version format
|
|
||||||
version: "{build}"
|
|
||||||
|
|
||||||
# Operating system (build VM template)
|
|
||||||
os: Windows Server 2012 R2
|
|
||||||
|
|
||||||
# Platform.
|
|
||||||
platform: x64
|
|
||||||
|
|
||||||
clone_folder: c:\gopath\src\github.com\minio\minio
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
environment:
|
|
||||||
GOPATH: c:\gopath
|
|
||||||
GOROOT: c:\go110
|
|
||||||
|
|
||||||
# scripts that run after cloning repository
|
|
||||||
install:
|
|
||||||
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
|
||||||
- go version
|
|
||||||
- go env
|
|
||||||
- python --version
|
|
||||||
|
|
||||||
# To run your custom scripts instead of automatic MSBuild
|
|
||||||
build_script:
|
|
||||||
# Compile
|
|
||||||
# We need to disable firewall - https://github.com/appveyor/ci/issues/1579#issuecomment-309830648
|
|
||||||
- ps: Disable-NetFirewallRule -DisplayName 'File and Printer Sharing (SMB-Out)'
|
|
||||||
- appveyor AddCompilationMessage "Starting Compile"
|
|
||||||
- cd c:\gopath\src\github.com\minio\minio
|
|
||||||
- go run buildscripts/gen-ldflags.go > temp.txt
|
|
||||||
- set /p BUILD_LDFLAGS=<temp.txt
|
|
||||||
- go build -ldflags="%BUILD_LDFLAGS%" -o %GOPATH%\bin\minio.exe
|
|
||||||
- appveyor AddCompilationMessage "Compile Success"
|
|
||||||
|
|
||||||
# To run your custom scripts instead of automatic tests
|
|
||||||
test_script:
|
|
||||||
# Unit tests
|
|
||||||
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
|
|
||||||
- mkdir build\coverage
|
|
||||||
- for /f "" %%G in ('go list github.com/minio/minio/... ^| find /i /v "browser/"') do ( go test -v -timeout 20m -race %%G )
|
|
||||||
- go test -v -timeout 20m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
|
||||||
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed
|
|
||||||
|
|
||||||
after_test:
|
|
||||||
- go tool cover -html=build\coverage\coverage.txt -o build\coverage\coverage.html
|
|
||||||
- ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
|
||||||
- ps: Push-AppveyorArtifact build\coverage\coverage.html
|
|
||||||
# Upload coverage report.
|
|
||||||
- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
|
||||||
- pip install codecov
|
|
||||||
- codecov -X gcov -f "build\coverage\coverage.txt"
|
|
||||||
|
|
||||||
# to disable deployment
|
|
||||||
deploy: off
|
|
||||||
@@ -22,7 +22,7 @@ export let alertId = 0
|
|||||||
export const set = alert => {
|
export const set = alert => {
|
||||||
const id = alertId++
|
const id = alertId++
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (alert.type !== "danger") {
|
if (alert.type !== "danger" || alert.autoClear) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CLEAR,
|
type: CLEAR,
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return {
|
return {
|
||||||
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()),
|
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()),
|
||||||
setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)),
|
setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)),
|
||||||
selectBucket: bucket => dispatch(actionsBuckets.selectBucket(bucket))
|
selectBucket: (bucket, prefix) =>
|
||||||
|
dispatch(actionsBuckets.selectBucket(bucket, prefix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BucketList)
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BucketList)
|
||||||
|
|||||||
@@ -123,9 +123,6 @@ describe("Objects actions", () => {
|
|||||||
objects: { currentPrefix: "" }
|
objects: { currentPrefix: "" }
|
||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
|
||||||
type: "alert/CLEAR"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "objects/SET_LIST",
|
type: "objects/SET_LIST",
|
||||||
objects: [{ name: "test1" }, { name: "test2" }],
|
objects: [{ name: "test1" }, { name: "test2" }],
|
||||||
@@ -157,9 +154,6 @@ describe("Objects actions", () => {
|
|||||||
objects: { currentPrefix: "" }
|
objects: { currentPrefix: "" }
|
||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
|
||||||
type: "alert/CLEAR"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "objects/APPEND_LIST",
|
type: "objects/APPEND_LIST",
|
||||||
objects: [{ name: "test1" }, { name: "test2" }],
|
objects: [{ name: "test1" }, { name: "test2" }],
|
||||||
@@ -183,15 +177,13 @@ describe("Objects actions", () => {
|
|||||||
objects: { currentPrefix: "" }
|
objects: { currentPrefix: "" }
|
||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
|
||||||
type: "alert/CLEAR"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "alert/SET",
|
type: "alert/SET",
|
||||||
alert: {
|
alert: {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: "listobjects is denied",
|
message: "listobjects is denied",
|
||||||
id: alertActions.alertId
|
id: alertActions.alertId,
|
||||||
|
autoClear: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,7 +244,6 @@ describe("Objects actions", () => {
|
|||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" },
|
{ type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" },
|
||||||
{ type: "alert/CLEAR" },
|
|
||||||
{ type: "objects/CHECKED_LIST_RESET" }
|
{ type: "objects/CHECKED_LIST_RESET" }
|
||||||
]
|
]
|
||||||
store.dispatch(actionsObjects.selectPrefix("abc/"))
|
store.dispatch(actionsObjects.selectPrefix("abc/"))
|
||||||
|
|||||||
@@ -64,43 +64,47 @@ export const fetchObjects = append => {
|
|||||||
buckets: { currentBucket },
|
buckets: { currentBucket },
|
||||||
objects: { currentPrefix, marker }
|
objects: { currentPrefix, marker }
|
||||||
} = getState()
|
} = getState()
|
||||||
dispatch(alertActions.clear())
|
|
||||||
if (currentBucket) {
|
if (currentBucket) {
|
||||||
return web
|
return web
|
||||||
.ListObjects({
|
.ListObjects({
|
||||||
bucketName: currentBucket,
|
bucketName: currentBucket,
|
||||||
prefix: currentPrefix,
|
prefix: currentPrefix,
|
||||||
marker: append ? marker : ""
|
marker: append ? marker : ""
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let objects = []
|
let objects = []
|
||||||
if (res.objects) {
|
if (res.objects) {
|
||||||
objects = res.objects.map(object => {
|
objects = res.objects.map(object => {
|
||||||
return {
|
return {
|
||||||
...object,
|
...object,
|
||||||
name: object.name.replace(currentPrefix, "")
|
name: object.name.replace(currentPrefix, "")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (append) {
|
if (append) {
|
||||||
dispatch(appendList(objects, res.nextmarker, res.istruncated))
|
dispatch(appendList(objects, res.nextmarker, res.istruncated))
|
||||||
} else {
|
} else {
|
||||||
dispatch(setList(objects, res.nextmarker, res.istruncated))
|
dispatch(setList(objects, res.nextmarker, res.istruncated))
|
||||||
dispatch(setSortBy(""))
|
dispatch(setSortBy(""))
|
||||||
dispatch(setSortOrder(false))
|
dispatch(setSortOrder(false))
|
||||||
}
|
}
|
||||||
dispatch(setPrefixWritable(res.writable))
|
dispatch(setPrefixWritable(res.writable))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (web.LoggedIn()) {
|
if (web.LoggedIn()) {
|
||||||
dispatch(alertActions.set({ type: "danger", message: err.message }))
|
dispatch(
|
||||||
dispatch(resetList())
|
alertActions.set({
|
||||||
}
|
type: "danger",
|
||||||
else {
|
message: err.message,
|
||||||
history.push("/login")
|
autoClear: true
|
||||||
}
|
})
|
||||||
})
|
)
|
||||||
}
|
dispatch(resetList())
|
||||||
|
} else {
|
||||||
|
history.push("/login")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-23
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ set -e
|
|||||||
echo "" > coverage.txt
|
echo "" > coverage.txt
|
||||||
|
|
||||||
for d in $(go list ./... | grep -v browser); do
|
for d in $(go list ./... | grep -v browser); do
|
||||||
go test -coverprofile=profile.out -covermode=atomic "$d"
|
go test -v -coverprofile=profile.out -covermode=atomic "$d"
|
||||||
if [ -f profile.out ]; then
|
if [ -f profile.out ]; then
|
||||||
cat profile.out >> coverage.txt
|
cat profile.out >> coverage.txt
|
||||||
rm profile.out
|
rm profile.out
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,6 +57,8 @@ type accessControlPolicy struct {
|
|||||||
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketACL")
|
ctx := newContext(r, w, "GetBucketACL")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -103,6 +106,8 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetObjectACL")
|
ctx := newContext(r, w, "GetObjectACL")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
object := vars["object"]
|
object := vars["object"]
|
||||||
|
|||||||
+491
-38
@@ -17,13 +17,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -33,6 +36,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -40,7 +44,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxConfigJSONSize = 256 * 1024 // 256KiB
|
maxEConfigJSONSize = 262272
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type-safe query params.
|
// Type-safe query params.
|
||||||
@@ -102,7 +106,7 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
|||||||
// of read-quorum availability.
|
// of read-quorum availability.
|
||||||
uptime, err := getPeerUptimes(globalAdminPeers)
|
uptime, err := getPeerUptimes(globalAdminPeers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(context.Background(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -275,6 +279,127 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
writeSuccessResponseJSON(w, jsonBytes)
|
writeSuccessResponseJSON(w, jsonBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartProfilingResult contains the status of the starting
|
||||||
|
// profiling action in a given server
|
||||||
|
type StartProfilingResult struct {
|
||||||
|
NodeName string `json:"nodeName"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartProfilingHandler - POST /minio/admin/v1/profiling/start?profilerType={profilerType}
|
||||||
|
// ----------
|
||||||
|
// Enable server profiling
|
||||||
|
func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
profiler := vars["profilerType"]
|
||||||
|
|
||||||
|
startProfilingResult := make([]StartProfilingResult, len(globalAdminPeers))
|
||||||
|
|
||||||
|
// Call StartProfiling function on all nodes and save results
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i, peer := range globalAdminPeers {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, peer adminPeer) {
|
||||||
|
defer wg.Done()
|
||||||
|
result := StartProfilingResult{NodeName: peer.addr}
|
||||||
|
if err := peer.cmdRunner.StartProfiling(profiler); err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.Success = true
|
||||||
|
startProfilingResult[idx] = result
|
||||||
|
}(i, peer)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Create JSON result and send it to the client
|
||||||
|
startProfilingResultInBytes, err := json.Marshal(startProfilingResult)
|
||||||
|
if err != nil {
|
||||||
|
writeCustomErrorResponseJSON(w, http.StatusInternalServerError, err.Error(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccessResponseJSON(w, []byte(startProfilingResultInBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// dummyFileInfo represents a dummy representation of a profile data file
|
||||||
|
// present only in memory, it helps to generate the zip stream.
|
||||||
|
type dummyFileInfo struct {
|
||||||
|
name string
|
||||||
|
size int64
|
||||||
|
mode os.FileMode
|
||||||
|
modTime time.Time
|
||||||
|
isDir bool
|
||||||
|
sys interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f dummyFileInfo) Name() string { return f.name }
|
||||||
|
func (f dummyFileInfo) Size() int64 { return f.size }
|
||||||
|
func (f dummyFileInfo) Mode() os.FileMode { return f.mode }
|
||||||
|
func (f dummyFileInfo) ModTime() time.Time { return f.modTime }
|
||||||
|
func (f dummyFileInfo) IsDir() bool { return f.isDir }
|
||||||
|
func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
||||||
|
|
||||||
|
// DownloadProfilingHandler - POST /minio/admin/v1/profiling/download
|
||||||
|
// ----------
|
||||||
|
// Download profiling information of all nodes in a zip format
|
||||||
|
func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profilingDataFound := false
|
||||||
|
|
||||||
|
// Initialize a zip writer which will provide a zipped content
|
||||||
|
// of profiling data of all nodes
|
||||||
|
zipWriter := zip.NewWriter(w)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
for i, peer := range globalAdminPeers {
|
||||||
|
// Get profiling data from a node
|
||||||
|
data, err := peer.cmdRunner.DownloadProfilingData()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(context.Background(), fmt.Errorf("Unable to download profiling data from node `%s`, reason: %s", peer.addr, err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profilingDataFound = true
|
||||||
|
|
||||||
|
// Send profiling data to zip as file
|
||||||
|
header, err := zip.FileInfoHeader(dummyFileInfo{
|
||||||
|
name: fmt.Sprintf("profiling-%d", i),
|
||||||
|
size: int64(len(data)),
|
||||||
|
mode: 0600,
|
||||||
|
modTime: time.Now().UTC(),
|
||||||
|
isDir: false,
|
||||||
|
sys: nil,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writer, err := zipWriter.CreateHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(writer, bytes.NewBuffer(data)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !profilingDataFound {
|
||||||
|
writeErrorResponseJSON(w, ErrAdminProfilerNotEnabled, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extractHealInitParams - Validates params for heal init API.
|
// extractHealInitParams - Validates params for heal init API.
|
||||||
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
||||||
hs madmin.HealOpts, clientToken string, forceStart bool,
|
hs madmin.HealOpts, clientToken string, forceStart bool,
|
||||||
@@ -473,7 +598,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
password := config.GetCredential().SecretKey
|
password := config.GetCredential().SecretKey
|
||||||
econfigData, err := madmin.EncryptServerConfigData(password, configData)
|
econfigData, err := madmin.EncryptData(password, configData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
@@ -551,13 +676,13 @@ func (a adminAPIHandlers) GetConfigKeysHandler(w http.ResponseWriter, r *http.Re
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val := gjson.Get(configStr, key)
|
val := gjson.Get(configStr, key)
|
||||||
if j, err := sjson.Set(newConfigStr, normalizeJSONKey(key), val.Value()); err == nil {
|
if j, ierr := sjson.Set(newConfigStr, normalizeJSONKey(key), val.Value()); ierr == nil {
|
||||||
newConfigStr = j
|
newConfigStr = j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
password := config.GetCredential().SecretKey
|
password := config.GetCredential().SecretKey
|
||||||
econfigData, err := madmin.EncryptServerConfigData(password, []byte(newConfigStr))
|
econfigData, err := madmin.EncryptData(password, []byte(newConfigStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
@@ -578,6 +703,348 @@ func toAdminAPIErrCode(err error) APIErrorCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveUser - DELETE /minio/admin/v1/remove-user?accessKey=<access_key>
|
||||||
|
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "RemoveUser")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny if WORM is enabled
|
||||||
|
if globalWORMEnabled {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
accessKey := vars["accessKey"]
|
||||||
|
if err := globalIAMSys.DeleteUser(accessKey); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers - GET /minio/admin/v1/list-users
|
||||||
|
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ListUsers")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allCredentials, err := globalIAMSys.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(allCredentials)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
|
econfigData, err := madmin.EncryptData(password, data)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserStatus - PUT /minio/admin/v1/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
|
||||||
|
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SetUserStatus")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny if WORM is enabled
|
||||||
|
if globalWORMEnabled {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
accessKey := vars["accessKey"]
|
||||||
|
status := vars["status"]
|
||||||
|
|
||||||
|
// Custom IAM policies not allowed for admin user.
|
||||||
|
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||||
|
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalIAMSys.SetUserStatus(accessKey, madmin.AccountStatus(status)); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUser - PUT /minio/admin/v1/add-user?accessKey=<access_key>
|
||||||
|
func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "AddUser")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny if WORM is enabled
|
||||||
|
if globalWORMEnabled {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
accessKey := vars["accessKey"]
|
||||||
|
|
||||||
|
// Custom IAM policies not allowed for admin user.
|
||||||
|
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||||
|
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
|
// More than maxConfigSize bytes were available
|
||||||
|
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
|
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var uinfo madmin.UserInfo
|
||||||
|
if err = json.Unmarshal(configBytes, &uinfo); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = globalIAMSys.SetUser(accessKey, uinfo); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCannedPolicies - GET /minio/admin/v1/list-canned-policies
|
||||||
|
func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ListCannedPolicies")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policies, err := globalIAMSys.ListCannedPolicies()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.NewEncoder(w).Encode(policies); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCannedPolicy - DELETE /minio/admin/v1/remove-canned-policy?name=<policy_name>
|
||||||
|
func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "RemoveCannedPolicy")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
policyName := vars["name"]
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny if WORM is enabled
|
||||||
|
if globalWORMEnabled {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalIAMSys.DeleteCannedPolicy(policyName); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCannedPolicy - PUT /minio/admin/v1/add-canned-policy?name=<policy_name>
|
||||||
|
func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "AddCannedPolicy")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
policyName := vars["name"]
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny if WORM is enabled
|
||||||
|
if globalWORMEnabled {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error out if Content-Length is missing.
|
||||||
|
if r.ContentLength <= 0 {
|
||||||
|
writeErrorResponseJSON(w, ErrMissingContentLength, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error out if Content-Length is beyond allowed size.
|
||||||
|
if r.ContentLength > maxBucketPolicySize {
|
||||||
|
writeErrorResponseJSON(w, ErrEntityTooLarge, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iamPolicy, err := iampolicy.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(w, ErrMalformedPolicy, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version in policy must not be empty
|
||||||
|
if iamPolicy.Version == "" {
|
||||||
|
writeErrorResponseJSON(w, ErrMalformedPolicy, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = globalIAMSys.SetCannedPolicy(policyName, *iamPolicy); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserPolicy - PUT /minio/admin/v1/set-user-policy?accessKey=<access_key>&name=<policy_name>
|
||||||
|
func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SetUserPolicy")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
accessKey := vars["accessKey"]
|
||||||
|
policyName := vars["name"]
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny if WORM is enabled
|
||||||
|
if globalWORMEnabled {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom IAM policies not allowed for admin user.
|
||||||
|
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||||
|
writeErrorResponseJSON(w, ErrInvalidRequest, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalIAMSys.SetUserPolicy(accessKey, policyName); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetConfigHandler - PUT /minio/admin/v1/config
|
// SetConfigHandler - PUT /minio/admin/v1/config
|
||||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "SetConfigHandler")
|
ctx := newContext(r, w, "SetConfigHandler")
|
||||||
@@ -602,22 +1069,14 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read configuration bytes from request body.
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
configBuf := make([]byte, maxConfigJSONSize+1)
|
|
||||||
n, err := io.ReadFull(r.Body, configBuf)
|
|
||||||
if err == nil {
|
|
||||||
// More than maxConfigSize bytes were available
|
// More than maxConfigSize bytes were available
|
||||||
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != io.ErrUnexpectedEOF {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
password := globalServerConfig.GetCredential().SecretKey
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
@@ -633,8 +1092,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config serverConfig
|
var config serverConfig
|
||||||
err = json.Unmarshal(configBytes, &config)
|
if err = json.Unmarshal(configBytes, &config); err != nil {
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
@@ -656,6 +1114,11 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = config.TestNotificationTargets(); err != nil {
|
||||||
|
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = saveServerConfig(ctx, objectAPI, &config); err != nil {
|
if err = saveServerConfig(ctx, objectAPI, &config); err != nil {
|
||||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -663,8 +1126,6 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// Reply to the client before restarting minio server.
|
// Reply to the client before restarting minio server.
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
|
||||||
sendServiceCmd(globalAdminPeers, serviceRestart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertValueType(elem []byte, jsonType gjson.Type) (interface{}, error) {
|
func convertValueType(elem []byte, jsonType gjson.Type) (interface{}, error) {
|
||||||
@@ -738,7 +1199,7 @@ func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Re
|
|||||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
elem, dErr := madmin.DecryptServerConfigData(password, bytes.NewBuffer([]byte(encryptedElem)))
|
elem, dErr := madmin.DecryptData(password, bytes.NewBuffer([]byte(encryptedElem)))
|
||||||
if dErr != nil {
|
if dErr != nil {
|
||||||
logger.LogIf(ctx, dErr)
|
logger.LogIf(ctx, dErr)
|
||||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
@@ -796,15 +1257,12 @@ func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
// Send success response
|
// Send success response
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
|
||||||
sendServiceCmd(globalAdminPeers, serviceRestart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCredsHandler - POST /minio/admin/v1/config/credential
|
// UpdateAdminCredsHandler - POST /minio/admin/v1/config/credential
|
||||||
// ----------
|
// ----------
|
||||||
// Update credentials in a minio server. In a distributed setup,
|
// Update admin credentials in a minio server
|
||||||
// update all the servers in the cluster.
|
func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter,
|
||||||
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|
||||||
r *http.Request) {
|
r *http.Request) {
|
||||||
|
|
||||||
ctx := newContext(r, w, "UpdateCredentialsHandler")
|
ctx := newContext(r, w, "UpdateCredentialsHandler")
|
||||||
@@ -818,7 +1276,7 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
// Avoid setting new credentials when they are already passed
|
// Avoid setting new credentials when they are already passed
|
||||||
// by the environment. Deny if WORM is enabled.
|
// by the environment. Deny if WORM is enabled.
|
||||||
if globalIsEnvCreds {
|
if globalIsEnvCreds || globalWORMEnabled {
|
||||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -830,22 +1288,14 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read configuration bytes from request body.
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
configBuf := make([]byte, maxConfigJSONSize+1)
|
|
||||||
n, err := io.ReadFull(r.Body, configBuf)
|
|
||||||
if err == nil {
|
|
||||||
// More than maxConfigSize bytes were available
|
// More than maxConfigSize bytes were available
|
||||||
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != io.ErrUnexpectedEOF {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
password := globalServerConfig.GetCredential().SecretKey
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
@@ -862,7 +1312,7 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
creds, err := auth.CreateCredentials(req.AccessKey, req.SecretKey)
|
creds, err := auth.CreateCredentials(req.AccessKey, req.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,6 +1323,9 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|||||||
// Update local credentials in memory.
|
// Update local credentials in memory.
|
||||||
globalServerConfig.SetCredential(creds)
|
globalServerConfig.SetCredential(creds)
|
||||||
|
|
||||||
|
// Set active creds.
|
||||||
|
globalActiveCred = creds
|
||||||
|
|
||||||
if err = saveServerConfig(ctx, objectAPI, globalServerConfig); err != nil {
|
if err = saveServerConfig(ctx, objectAPI, globalServerConfig); err != nil {
|
||||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
return
|
return
|
||||||
|
|||||||
+195
-173
@@ -27,6 +27,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -37,170 +38,184 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
configJSON = []byte(`{
|
configJSON = []byte(`{
|
||||||
"version": "28",
|
"version": "31",
|
||||||
"credential": {
|
"credential": {
|
||||||
"accessKey": "minio",
|
"accessKey": "minio",
|
||||||
"secretKey": "minio123"
|
"secretKey": "minio123"
|
||||||
},
|
},
|
||||||
"region": "",
|
"region": "us-east-1",
|
||||||
"browser": "on",
|
"worm": "off",
|
||||||
"worm": "off",
|
"storageclass": {
|
||||||
"domain": "",
|
"standard": "",
|
||||||
"storageclass": {
|
"rrs": ""
|
||||||
"standard": "",
|
},
|
||||||
"rrs": ""
|
"cache": {
|
||||||
},
|
"drives": [],
|
||||||
"cache": {
|
"expiry": 90,
|
||||||
"drives": [],
|
"maxuse": 80,
|
||||||
"expiry": 90,
|
"exclude": []
|
||||||
"maxuse": 80,
|
},
|
||||||
"exclude": []
|
"kms": {
|
||||||
},
|
"vault": {
|
||||||
"kms": {
|
"endpoint": "",
|
||||||
"vault": {
|
"auth": {
|
||||||
"endpoint": "",
|
"type": "",
|
||||||
"auth": {
|
"approle": {
|
||||||
"type": "",
|
"id": "",
|
||||||
"approle": {
|
"secret": ""
|
||||||
"id": "",
|
}
|
||||||
"secret": ""
|
},
|
||||||
}
|
"key-id": {
|
||||||
},
|
"name": "",
|
||||||
"key-id": {
|
"version": 0
|
||||||
"name": "",
|
}
|
||||||
"version": 0
|
}
|
||||||
}
|
},
|
||||||
}
|
"notify": {
|
||||||
},
|
"amqp": {
|
||||||
"notify": {
|
"1": {
|
||||||
"amqp": {
|
"enable": false,
|
||||||
"1": {
|
"url": "",
|
||||||
"enable": false,
|
"exchange": "",
|
||||||
"url": "",
|
"routingKey": "",
|
||||||
"exchange": "",
|
"exchangeType": "",
|
||||||
"routingKey": "",
|
"deliveryMode": 0,
|
||||||
"exchangeType": "",
|
"mandatory": false,
|
||||||
"deliveryMode": 0,
|
"immediate": false,
|
||||||
"mandatory": false,
|
"durable": false,
|
||||||
"immediate": false,
|
"internal": false,
|
||||||
"durable": false,
|
"noWait": false,
|
||||||
"internal": false,
|
"autoDeleted": false
|
||||||
"noWait": false,
|
}
|
||||||
"autoDeleted": false
|
},
|
||||||
}
|
"elasticsearch": {
|
||||||
},
|
"1": {
|
||||||
"elasticsearch": {
|
"enable": false,
|
||||||
"1": {
|
"format": "namespace",
|
||||||
"enable": false,
|
"url": "",
|
||||||
"format": "",
|
"index": ""
|
||||||
"url": "",
|
}
|
||||||
"index": ""
|
},
|
||||||
}
|
"kafka": {
|
||||||
},
|
"1": {
|
||||||
"kafka": {
|
"enable": false,
|
||||||
"1": {
|
"brokers": null,
|
||||||
"enable": false,
|
"topic": "",
|
||||||
"brokers": null,
|
"tls": {
|
||||||
"topic": "",
|
"enable": false,
|
||||||
"tls" : {
|
"skipVerify": false,
|
||||||
"enable" : false,
|
"clientAuth": 0
|
||||||
"skipVerify" : false,
|
},
|
||||||
"clientAuth" : 0
|
"sasl": {
|
||||||
},
|
"enable": false,
|
||||||
"sasl" : {
|
"username": "",
|
||||||
"enable" : false,
|
"password": ""
|
||||||
"username" : "",
|
}
|
||||||
"password" : ""
|
}
|
||||||
}
|
},
|
||||||
}
|
"mqtt": {
|
||||||
},
|
"1": {
|
||||||
"mqtt": {
|
"enable": false,
|
||||||
"1": {
|
"broker": "",
|
||||||
"enable": false,
|
"topic": "",
|
||||||
"broker": "",
|
"qos": 0,
|
||||||
"topic": "",
|
"clientId": "",
|
||||||
"qos": 0,
|
"username": "",
|
||||||
"clientId": "",
|
"password": "",
|
||||||
"username": "",
|
"reconnectInterval": 0,
|
||||||
"password": "",
|
"keepAliveInterval": 0
|
||||||
"reconnectInterval": 0,
|
}
|
||||||
"keepAliveInterval": 0
|
},
|
||||||
}
|
"mysql": {
|
||||||
},
|
"1": {
|
||||||
"mysql": {
|
"enable": false,
|
||||||
"1": {
|
"format": "namespace",
|
||||||
"enable": false,
|
"dsnString": "",
|
||||||
"format": "",
|
"table": "",
|
||||||
"dsnString": "",
|
"host": "",
|
||||||
"table": "",
|
"port": "",
|
||||||
"host": "",
|
"user": "",
|
||||||
"port": "",
|
"password": "",
|
||||||
"user": "",
|
"database": ""
|
||||||
"password": "",
|
}
|
||||||
"database": ""
|
},
|
||||||
}
|
"nats": {
|
||||||
},
|
"1": {
|
||||||
"nats": {
|
"enable": false,
|
||||||
"1": {
|
"address": "",
|
||||||
"enable": false,
|
"subject": "",
|
||||||
"address": "",
|
"username": "",
|
||||||
"subject": "",
|
"password": "",
|
||||||
"username": "",
|
"token": "",
|
||||||
"password": "",
|
"secure": false,
|
||||||
"token": "",
|
"pingInterval": 0,
|
||||||
"secure": false,
|
"streaming": {
|
||||||
"pingInterval": 0,
|
"enable": false,
|
||||||
"streaming": {
|
"clusterID": "",
|
||||||
"enable": false,
|
"clientID": "",
|
||||||
"clusterID": "",
|
"async": false,
|
||||||
"clientID": "",
|
"maxPubAcksInflight": 0
|
||||||
"async": false,
|
}
|
||||||
"maxPubAcksInflight": 0
|
}
|
||||||
}
|
},
|
||||||
}
|
"postgresql": {
|
||||||
},
|
"1": {
|
||||||
"postgresql": {
|
"enable": false,
|
||||||
"1": {
|
"format": "namespace",
|
||||||
"enable": false,
|
"connectionString": "",
|
||||||
"format": "",
|
"table": "",
|
||||||
"connectionString": "",
|
"host": "",
|
||||||
"table": "",
|
"port": "",
|
||||||
"host": "",
|
"user": "",
|
||||||
"port": "",
|
"password": "",
|
||||||
"user": "",
|
"database": ""
|
||||||
"password": "",
|
}
|
||||||
"database": ""
|
},
|
||||||
}
|
"redis": {
|
||||||
},
|
"1": {
|
||||||
"redis": {
|
"enable": false,
|
||||||
"1": {
|
"format": "namespace",
|
||||||
"enable": false,
|
"address": "",
|
||||||
"format": "",
|
"password": "",
|
||||||
"address": "",
|
"key": ""
|
||||||
"password": "",
|
}
|
||||||
"key": ""
|
},
|
||||||
}
|
"webhook": {
|
||||||
},
|
"1": {
|
||||||
"webhook": {
|
"enable": false,
|
||||||
"1": {
|
"endpoint": ""
|
||||||
"enable": false,
|
}
|
||||||
"endpoint": ""
|
}
|
||||||
}
|
},
|
||||||
}
|
"logger": {
|
||||||
},
|
"console": {
|
||||||
"logger": {
|
"enabled": true
|
||||||
"console": {
|
},
|
||||||
"enabled": true
|
"http": {
|
||||||
},
|
"1": {
|
||||||
"http": {
|
"enabled": false,
|
||||||
"1": {
|
"endpoint": "https://username:password@example.com/api"
|
||||||
"enabled": false,
|
}
|
||||||
"endpoint": "http://user:example@localhost:9001/api/endpoint"
|
}
|
||||||
}
|
},
|
||||||
}
|
"compress": {
|
||||||
}
|
"enabled": false,
|
||||||
|
"extensions":[".txt",".log",".csv",".json"],
|
||||||
}`)
|
"mime-types":["text/csv","text/plain","application/json"]
|
||||||
|
},
|
||||||
|
"openid": {
|
||||||
|
"jwks": {
|
||||||
|
"url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"opa": {
|
||||||
|
"url": "",
|
||||||
|
"authToken": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
||||||
@@ -482,6 +497,8 @@ func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*htt
|
|||||||
|
|
||||||
// Set body
|
// Set body
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||||
|
req.ContentLength = int64(len(body))
|
||||||
|
|
||||||
// Set sha-sum header
|
// Set sha-sum header
|
||||||
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
||||||
|
|
||||||
@@ -508,10 +525,16 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
|||||||
globalMinioAddr = "127.0.0.1:9000"
|
globalMinioAddr = "127.0.0.1:9000"
|
||||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Setting up a go routine to simulate ServerRouter's
|
// Setting up a go routine to simulate ServerRouter's
|
||||||
// handleServiceSignals for stop and restart commands.
|
// handleServiceSignals for stop and restart commands.
|
||||||
if cmd == restartCmd {
|
if cmd == restartCmd {
|
||||||
go testServiceSignalReceiver(cmd, t)
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
testServiceSignalReceiver(cmd, t)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
credentials := globalServerConfig.GetCredential()
|
credentials := globalServerConfig.GetCredential()
|
||||||
|
|
||||||
@@ -547,6 +570,9 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
|||||||
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
||||||
http.StatusOK, rec.Code, string(resp))
|
http.StatusOK, rec.Code, string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait until testServiceSignalReceiver() called in a goroutine quits.
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test for service status management REST API.
|
// Test for service status management REST API.
|
||||||
@@ -603,7 +629,7 @@ func TestServiceSetCreds(t *testing.T) {
|
|||||||
t.Fatalf("JSONify err: %v", err)
|
t.Fatalf("JSONify err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ebody, err := madmin.EncryptServerConfigData(credentials.SecretKey, body)
|
ebody, err := madmin.EncryptData(credentials.SecretKey, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -701,16 +727,12 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
globalMinioAddr = "127.0.0.1:9000"
|
globalMinioAddr = "127.0.0.1:9000"
|
||||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||||
|
|
||||||
// SetConfigHandler restarts minio setup - need to start a
|
|
||||||
// signal receiver to receive on globalServiceSignalCh.
|
|
||||||
go testServiceSignalReceiver(restartCmd, t)
|
|
||||||
|
|
||||||
// Prepare query params for set-config mgmt REST API.
|
// Prepare query params for set-config mgmt REST API.
|
||||||
queryVal := url.Values{}
|
queryVal := url.Values{}
|
||||||
queryVal.Set("config", "")
|
queryVal.Set("config", "")
|
||||||
|
|
||||||
password := globalServerConfig.GetCredential().SecretKey
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
econfigJSON, err := madmin.EncryptServerConfigData(password, configJSON)
|
econfigJSON, err := madmin.EncryptData(password, configJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -730,7 +752,7 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
// Check that a very large config file returns an error.
|
// Check that a very large config file returns an error.
|
||||||
{
|
{
|
||||||
// Make a large enough config string
|
// Make a large enough config string
|
||||||
invalidCfg := []byte(strings.Repeat("A", maxConfigJSONSize+1))
|
invalidCfg := []byte(strings.Repeat("A", maxEConfigJSONSize+1))
|
||||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||||
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -760,7 +782,7 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
adminTestBed.router.ServeHTTP(rec, req)
|
adminTestBed.router.ServeHTTP(rec, req)
|
||||||
respBody := string(rec.Body.Bytes())
|
respBody := string(rec.Body.Bytes())
|
||||||
if rec.Code != http.StatusBadRequest ||
|
if rec.Code != http.StatusBadRequest ||
|
||||||
!strings.Contains(respBody, "JSON configuration provided has objects with duplicate keys") {
|
!strings.Contains(respBody, "JSON configuration provided is of incorrect format") {
|
||||||
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-10
@@ -20,12 +20,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// healStatusSummary - overall short summary of a healing sequence
|
// healStatusSummary - overall short summary of a healing sequence
|
||||||
@@ -383,7 +385,6 @@ func (h *healSequence) stop() {
|
|||||||
// sequence automatically resumes. The return value indicates if the
|
// sequence automatically resumes. The return value indicates if the
|
||||||
// operation succeeded.
|
// operation succeeded.
|
||||||
func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||||
|
|
||||||
// start a timer to keep an upper time limit to find an empty
|
// start a timer to keep an upper time limit to find an empty
|
||||||
// slot to add the given heal result - if no slot is found it
|
// slot to add the given heal result - if no slot is found it
|
||||||
// means that the server is holding the maximum amount of
|
// means that the server is holding the maximum amount of
|
||||||
@@ -519,6 +520,9 @@ func (h *healSequence) traverseAndHeal() {
|
|||||||
// Start with format healing
|
// Start with format healing
|
||||||
checkErr(h.healDiskFormat)
|
checkErr(h.healDiskFormat)
|
||||||
|
|
||||||
|
// Start healing the config.
|
||||||
|
checkErr(h.healConfig)
|
||||||
|
|
||||||
// Heal buckets and objects
|
// Heal buckets and objects
|
||||||
checkErr(h.healBuckets)
|
checkErr(h.healBuckets)
|
||||||
|
|
||||||
@@ -529,9 +533,72 @@ func (h *healSequence) traverseAndHeal() {
|
|||||||
close(h.traverseAndHealDoneCh)
|
close(h.traverseAndHealDoneCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// healConfig - heals config.json, retrun value indicates if a failure occurred.
|
||||||
|
func (h *healSequence) healConfig() error {
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Healing on configs is run regardless
|
||||||
|
// of any bucket being selected, this is to ensure that
|
||||||
|
// configs are always uptodate and correct.
|
||||||
|
marker := ""
|
||||||
|
isTruncated := true
|
||||||
|
for isTruncated {
|
||||||
|
if globalHTTPServer != nil {
|
||||||
|
// Wait at max 1 minute for an inprogress request
|
||||||
|
// before proceeding to heal
|
||||||
|
waitCount := 60
|
||||||
|
// Any requests in progress, delay the heal.
|
||||||
|
for globalHTTPServer.GetRequestCount() > 0 && waitCount > 0 {
|
||||||
|
waitCount--
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists all objects under `config` prefix.
|
||||||
|
objectInfos, err := objectAPI.ListObjectsHeal(h.ctx, minioMetaBucket, minioConfigPrefix,
|
||||||
|
marker, "", 1000)
|
||||||
|
if err != nil {
|
||||||
|
return errFnHealFromAPIErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range objectInfos.Objects {
|
||||||
|
if h.isQuitting() {
|
||||||
|
return errHealStopSignalled
|
||||||
|
}
|
||||||
|
o := objectInfos.Objects[index]
|
||||||
|
res, herr := objectAPI.HealObject(h.ctx, o.Bucket, o.Name, h.settings.DryRun)
|
||||||
|
// Object might have been deleted, by the time heal
|
||||||
|
// was attempted we ignore this file an move on.
|
||||||
|
if isErrObjectNotFound(herr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if herr != nil {
|
||||||
|
return herr
|
||||||
|
}
|
||||||
|
res.Type = madmin.HealItemBucketMetadata
|
||||||
|
if err = h.pushHealResultItem(res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTruncated = objectInfos.IsTruncated
|
||||||
|
marker = objectInfos.NextMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// healDiskFormat - heals format.json, return value indicates if a
|
// healDiskFormat - heals format.json, return value indicates if a
|
||||||
// failure error occurred.
|
// failure error occurred.
|
||||||
func (h *healSequence) healDiskFormat() error {
|
func (h *healSequence) healDiskFormat() error {
|
||||||
|
if h.isQuitting() {
|
||||||
|
return errHealStopSignalled
|
||||||
|
}
|
||||||
|
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -557,6 +624,10 @@ func (h *healSequence) healDiskFormat() error {
|
|||||||
|
|
||||||
// healBuckets - check for all buckets heal or just particular bucket.
|
// healBuckets - check for all buckets heal or just particular bucket.
|
||||||
func (h *healSequence) healBuckets() error {
|
func (h *healSequence) healBuckets() error {
|
||||||
|
if h.isQuitting() {
|
||||||
|
return errHealStopSignalled
|
||||||
|
}
|
||||||
|
|
||||||
// 1. If a bucket was specified, heal only the bucket.
|
// 1. If a bucket was specified, heal only the bucket.
|
||||||
if h.bucket != "" {
|
if h.bucket != "" {
|
||||||
return h.healBucket(h.bucket)
|
return h.healBucket(h.bucket)
|
||||||
@@ -584,10 +655,6 @@ func (h *healSequence) healBuckets() error {
|
|||||||
|
|
||||||
// healBucket - traverses and heals given bucket
|
// healBucket - traverses and heals given bucket
|
||||||
func (h *healSequence) healBucket(bucket string) error {
|
func (h *healSequence) healBucket(bucket string) error {
|
||||||
if h.isQuitting() {
|
|
||||||
return errHealStopSignalled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -612,8 +679,7 @@ func (h *healSequence) healBucket(bucket string) error {
|
|||||||
// and if so heal it.
|
// and if so heal it.
|
||||||
_, err = objectAPI.GetObjectInfo(h.ctx, bucket, h.objPrefix, ObjectOptions{})
|
_, err = objectAPI.GetObjectInfo(h.ctx, bucket, h.objPrefix, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = h.healObject(bucket, h.objPrefix)
|
if err = h.healObject(bucket, h.objPrefix); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,17 +688,40 @@ func (h *healSequence) healBucket(bucket string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entries := runtime.NumCPU() * globalEndpoints.Nodes()
|
||||||
|
|
||||||
marker := ""
|
marker := ""
|
||||||
isTruncated := true
|
isTruncated := true
|
||||||
for isTruncated {
|
for isTruncated {
|
||||||
|
if globalHTTPServer != nil {
|
||||||
|
// Wait at max 1 minute for an inprogress request
|
||||||
|
// before proceeding to heal
|
||||||
|
waitCount := 60
|
||||||
|
// Any requests in progress, delay the heal.
|
||||||
|
for globalHTTPServer.GetRequestCount() > 0 && waitCount > 0 {
|
||||||
|
waitCount--
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heal numCPU * nodes objects at a time.
|
||||||
objectInfos, err := objectAPI.ListObjectsHeal(h.ctx, bucket,
|
objectInfos, err := objectAPI.ListObjectsHeal(h.ctx, bucket,
|
||||||
h.objPrefix, marker, "", 1000)
|
h.objPrefix, marker, "", entries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errFnHealFromAPIErr(err)
|
return errFnHealFromAPIErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range objectInfos.Objects {
|
g := errgroup.WithNErrs(len(objectInfos.Objects))
|
||||||
if err := h.healObject(o.Bucket, o.Name); err != nil {
|
for index := range objectInfos.Objects {
|
||||||
|
index := index
|
||||||
|
g.Go(func() error {
|
||||||
|
o := objectInfos.Objects[index]
|
||||||
|
return h.healObject(o.Bucket, o.Name)
|
||||||
|
}, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, err := range g.Wait() {
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,6 +745,9 @@ func (h *healSequence) healObject(bucket, object string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hri, err := objectAPI.HealObject(h.ctx, bucket, object, h.settings.DryRun)
|
hri, err := objectAPI.HealObject(h.ctx, bucket, object, h.settings.DryRun)
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hri.Detail = err.Error()
|
hri.Detail = err.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-6
@@ -53,17 +53,24 @@ func registerAdminRouter(router *mux.Router) {
|
|||||||
// Info operations
|
// Info operations
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||||
|
|
||||||
/// Heal operations
|
if globalIsDistXL || globalIsXL {
|
||||||
|
/// Heal operations
|
||||||
|
|
||||||
// Heal processing endpoint.
|
// Heal processing endpoint.
|
||||||
adminV1Router.Methods(http.MethodPost).Path("/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
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}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profiling operations
|
||||||
|
adminV1Router.Methods(http.MethodPost).Path("/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
||||||
|
Queries("profilerType", "{profilerType:.*}")
|
||||||
|
adminV1Router.Methods(http.MethodGet).Path("/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
||||||
|
|
||||||
/// Config operations
|
/// Config operations
|
||||||
|
|
||||||
// Update credentials
|
// Update credentials
|
||||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateCredentialsHandler))
|
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
|
||||||
// Get config
|
// Get config
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||||
// Set config
|
// Set config
|
||||||
@@ -73,4 +80,31 @@ func registerAdminRouter(router *mux.Router) {
|
|||||||
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||||
// Set config keys/values
|
// Set config keys/values
|
||||||
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||||
|
|
||||||
|
// -- IAM APIs --
|
||||||
|
|
||||||
|
// 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:.*}")
|
||||||
|
|
||||||
|
// 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:.*}")
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
// If none of the routes match.
|
||||||
|
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandler))
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -68,6 +68,22 @@ func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
|
|||||||
return reply, err
|
return reply, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartProfiling - starts profiling in the remote server.
|
||||||
|
func (rpcClient *AdminRPCClient) StartProfiling(profiler string) error {
|
||||||
|
args := StartProfilingArgs{Profiler: profiler}
|
||||||
|
reply := VoidReply{}
|
||||||
|
return rpcClient.Call(adminServiceName+".StartProfiling", &args, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadProfilingData - returns profiling data of the remote server.
|
||||||
|
func (rpcClient *AdminRPCClient) DownloadProfilingData() ([]byte, error) {
|
||||||
|
args := AuthArgs{}
|
||||||
|
var reply []byte
|
||||||
|
|
||||||
|
err := rpcClient.Call(adminServiceName+".DownloadProfilingData", &args, &reply)
|
||||||
|
return reply, err
|
||||||
|
}
|
||||||
|
|
||||||
// NewAdminRPCClient - returns new admin RPC client.
|
// NewAdminRPCClient - returns new admin RPC client.
|
||||||
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
@@ -112,6 +128,8 @@ type adminCmdRunner interface {
|
|||||||
ReInitFormat(dryRun bool) error
|
ReInitFormat(dryRun bool) error
|
||||||
ServerInfo() (ServerInfoData, error)
|
ServerInfo() (ServerInfoData, error)
|
||||||
GetConfig() ([]byte, error)
|
GetConfig() ([]byte, error)
|
||||||
|
StartProfiling(string) error
|
||||||
|
DownloadProfilingData() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminPeer - represents an entity that implements admin API RPCs.
|
// adminPeer - represents an entity that implements admin API RPCs.
|
||||||
@@ -139,9 +157,9 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
|||||||
|
|
||||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||||
host, err := xnet.ParseHost(hostStr)
|
host, err := xnet.ParseHost(hostStr)
|
||||||
logger.FatalIf(err, "Unable to parse Admin RPC Host", context.Background())
|
logger.FatalIf(err, "Unable to parse Admin RPC Host")
|
||||||
rpcClient, err := NewAdminRPCClient(host)
|
rpcClient, err := NewAdminRPCClient(host)
|
||||||
logger.FatalIf(err, "Unable to initialize Admin RPC Client", context.Background())
|
logger.FatalIf(err, "Unable to initialize Admin RPC Client")
|
||||||
adminPeerList = append(adminPeerList, adminPeer{
|
adminPeerList = append(adminPeerList, adminPeer{
|
||||||
addr: hostStr,
|
addr: hostStr,
|
||||||
cmdRunner: rpcClient,
|
cmdRunner: rpcClient,
|
||||||
|
|||||||
+18
-2
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -52,6 +51,23 @@ func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoDa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartProfilingArgs - holds the RPC argument for StartingProfiling RPC call
|
||||||
|
type StartProfilingArgs struct {
|
||||||
|
AuthArgs
|
||||||
|
Profiler string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartProfiling - starts profiling of this server
|
||||||
|
func (receiver *adminRPCReceiver) StartProfiling(args *StartProfilingArgs, reply *VoidReply) error {
|
||||||
|
return receiver.local.StartProfiling(args.Profiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadProfilingData - stops and returns profiling data of this server
|
||||||
|
func (receiver *adminRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[]byte) (err error) {
|
||||||
|
*reply, err = receiver.local.DownloadProfilingData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetConfig - returns the config.json of this server.
|
// GetConfig - returns the config.json of this server.
|
||||||
func (receiver *adminRPCReceiver) GetConfig(args *AuthArgs, reply *[]byte) (err error) {
|
func (receiver *adminRPCReceiver) GetConfig(args *AuthArgs, reply *[]byte) (err error) {
|
||||||
*reply, err = receiver.local.GetConfig()
|
*reply, err = receiver.local.GetConfig()
|
||||||
@@ -81,7 +97,7 @@ func NewAdminRPCServer() (*xrpc.Server, error) {
|
|||||||
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
||||||
func registerAdminRPCRouter(router *mux.Router) {
|
func registerAdminRPCRouter(router *mux.Router) {
|
||||||
rpcServer, err := NewAdminRPCServer()
|
rpcServer, err := NewAdminRPCServer()
|
||||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
logger.FatalIf(err, "Unable to initialize Lock RPC Server")
|
||||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||||
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,87 +25,6 @@ const (
|
|||||||
responseRequestIDKey = "x-amz-request-id"
|
responseRequestIDKey = "x-amz-request-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSVFileHeaderInfo -Can be either USE IGNORE OR NONE, defines what to do with
|
|
||||||
// the first row
|
|
||||||
type CSVFileHeaderInfo string
|
|
||||||
|
|
||||||
// Constants for file header info.
|
|
||||||
const (
|
|
||||||
CSVFileHeaderInfoNone CSVFileHeaderInfo = "NONE"
|
|
||||||
CSVFileHeaderInfoIgnore = "IGNORE"
|
|
||||||
CSVFileHeaderInfoUse = "USE"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SelectCompressionType - ONLY GZIP is supported
|
|
||||||
type SelectCompressionType string
|
|
||||||
|
|
||||||
// Constants for compression types under select API.
|
|
||||||
const (
|
|
||||||
SelectCompressionNONE SelectCompressionType = "NONE"
|
|
||||||
SelectCompressionGZIP = "GZIP"
|
|
||||||
SelectCompressionBZIP = "BZIP2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CSVQuoteFields - Can be either Always or AsNeeded
|
|
||||||
type CSVQuoteFields string
|
|
||||||
|
|
||||||
// Constants for csv quote styles.
|
|
||||||
const (
|
|
||||||
CSVQuoteFieldsAlways CSVQuoteFields = "Always"
|
|
||||||
CSVQuoteFieldsAsNeeded = "AsNeeded"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueryExpressionType - Currently can only be SQL
|
|
||||||
type QueryExpressionType string
|
|
||||||
|
|
||||||
// Constants for expression type.
|
|
||||||
const (
|
|
||||||
QueryExpressionTypeSQL QueryExpressionType = "SQL"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONType determines json input serialization type.
|
|
||||||
type JSONType string
|
|
||||||
|
|
||||||
// Constants for JSONTypes.
|
|
||||||
const (
|
|
||||||
JSONDocumentType JSONType = "Document"
|
|
||||||
JSONStreamType = "Stream"
|
|
||||||
JSONLinesType = "Lines"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ObjectSelectRequest - represents the input select body
|
|
||||||
type ObjectSelectRequest struct {
|
|
||||||
XMLName xml.Name `xml:"SelectObjectContentRequest" json:"-"`
|
|
||||||
Expression string
|
|
||||||
ExpressionType QueryExpressionType
|
|
||||||
InputSerialization struct {
|
|
||||||
CompressionType SelectCompressionType
|
|
||||||
CSV *struct {
|
|
||||||
FileHeaderInfo CSVFileHeaderInfo
|
|
||||||
RecordDelimiter string
|
|
||||||
FieldDelimiter string
|
|
||||||
QuoteCharacter string
|
|
||||||
QuoteEscapeCharacter string
|
|
||||||
Comments string
|
|
||||||
}
|
|
||||||
JSON *struct {
|
|
||||||
Type JSONType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutputSerialization struct {
|
|
||||||
CSV *struct {
|
|
||||||
QuoteFields CSVQuoteFields
|
|
||||||
RecordDelimiter string
|
|
||||||
FieldDelimiter string
|
|
||||||
QuoteCharacter string
|
|
||||||
QuoteEscapeCharacter string
|
|
||||||
}
|
|
||||||
JSON *struct {
|
|
||||||
RecordDelimiter string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectIdentifier carries key name for the object to delete.
|
// ObjectIdentifier carries key name for the object to delete.
|
||||||
type ObjectIdentifier struct {
|
type ObjectIdentifier struct {
|
||||||
ObjectName string `xml:"Key"`
|
ObjectName string `xml:"Key"`
|
||||||
|
|||||||
+57
-20
@@ -187,11 +187,15 @@ const (
|
|||||||
// new error codes here.
|
// new error codes here.
|
||||||
|
|
||||||
ErrMalformedJSON
|
ErrMalformedJSON
|
||||||
|
ErrAdminNoSuchUser
|
||||||
|
ErrAdminNoSuchPolicy
|
||||||
|
ErrAdminInvalidArgument
|
||||||
ErrAdminInvalidAccessKey
|
ErrAdminInvalidAccessKey
|
||||||
ErrAdminInvalidSecretKey
|
ErrAdminInvalidSecretKey
|
||||||
ErrAdminConfigNoQuorum
|
ErrAdminConfigNoQuorum
|
||||||
ErrAdminConfigTooLarge
|
ErrAdminConfigTooLarge
|
||||||
ErrAdminConfigBadJSON
|
ErrAdminConfigBadJSON
|
||||||
|
ErrAdminConfigDuplicateKeys
|
||||||
ErrAdminCredentialsMismatch
|
ErrAdminCredentialsMismatch
|
||||||
ErrInsecureClientRequest
|
ErrInsecureClientRequest
|
||||||
ErrObjectTampered
|
ErrObjectTampered
|
||||||
@@ -249,7 +253,6 @@ const (
|
|||||||
ErrParseUnsupportedAlias
|
ErrParseUnsupportedAlias
|
||||||
ErrParseUnsupportedSyntax
|
ErrParseUnsupportedSyntax
|
||||||
ErrParseUnknownOperator
|
ErrParseUnknownOperator
|
||||||
ErrParseInvalidPathComponent
|
|
||||||
ErrParseMissingIdentAfterAt
|
ErrParseMissingIdentAfterAt
|
||||||
ErrParseUnexpectedOperator
|
ErrParseUnexpectedOperator
|
||||||
ErrParseUnexpectedTerm
|
ErrParseUnexpectedTerm
|
||||||
@@ -288,9 +291,10 @@ const (
|
|||||||
ErrEvaluatorInvalidTimestampFormatPatternToken
|
ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||||
ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||||
ErrEvaluatorBindingDoesNotExist
|
ErrEvaluatorBindingDoesNotExist
|
||||||
ErrInvalidColumnIndex
|
|
||||||
ErrMissingHeaders
|
ErrMissingHeaders
|
||||||
ErrAdminConfigNotificationTargetsFailed
|
ErrAdminConfigNotificationTargetsFailed
|
||||||
|
ErrAdminProfilerNotEnabled
|
||||||
|
ErrInvalidDecompressedSize
|
||||||
)
|
)
|
||||||
|
|
||||||
// error code to APIError structure, these fields carry respective
|
// error code to APIError structure, these fields carry respective
|
||||||
@@ -861,6 +865,21 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The JSON you provided was not well-formed or did not validate against our published format.",
|
Description: "The JSON you provided was not well-formed or did not validate against our published format.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminNoSuchUser: {
|
||||||
|
Code: "XMinioAdminNoSuchUser",
|
||||||
|
Description: "The specified user does not exist.",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrAdminNoSuchPolicy: {
|
||||||
|
Code: "XMinioAdminNoSuchPolicy",
|
||||||
|
Description: "The canned policy does not exist.",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrAdminInvalidArgument: {
|
||||||
|
Code: "XMinioAdminInvalidArgument",
|
||||||
|
Description: "Invalid arguments specified.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminInvalidAccessKey: {
|
ErrAdminInvalidAccessKey: {
|
||||||
Code: "XMinioAdminInvalidAccessKey",
|
Code: "XMinioAdminInvalidAccessKey",
|
||||||
Description: "The access key is invalid.",
|
Description: "The access key is invalid.",
|
||||||
@@ -879,11 +898,16 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
ErrAdminConfigTooLarge: {
|
ErrAdminConfigTooLarge: {
|
||||||
Code: "XMinioAdminConfigTooLarge",
|
Code: "XMinioAdminConfigTooLarge",
|
||||||
Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
|
Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
|
||||||
maxConfigJSONSize),
|
maxEConfigJSONSize),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrAdminConfigBadJSON: {
|
ErrAdminConfigBadJSON: {
|
||||||
Code: "XMinioAdminConfigBadJSON",
|
Code: "XMinioAdminConfigBadJSON",
|
||||||
|
Description: "JSON configuration provided is of incorrect format",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrAdminConfigDuplicateKeys: {
|
||||||
|
Code: "XMinioAdminConfigDuplicateKeys",
|
||||||
Description: "JSON configuration provided has objects with duplicate keys",
|
Description: "JSON configuration provided has objects with duplicate keys",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
@@ -892,6 +916,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers",
|
Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminProfilerNotEnabled: {
|
||||||
|
Code: "XMinioAdminProfilerNotEnabled",
|
||||||
|
Description: "Unable to perform the requested operation because profiling is not enabled",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminCredentialsMismatch: {
|
ErrAdminCredentialsMismatch: {
|
||||||
Code: "XMinioAdminCredentialsMismatch",
|
Code: "XMinioAdminCredentialsMismatch",
|
||||||
Description: "Credentials in config mismatch with server environment variables",
|
Description: "Credentials in config mismatch with server environment variables",
|
||||||
@@ -1192,11 +1221,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The SQL expression contains an invalid operator.",
|
Description: "The SQL expression contains an invalid operator.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrParseInvalidPathComponent: {
|
|
||||||
Code: "ParseInvalidPathComponent",
|
|
||||||
Description: "The SQL expression contains an invalid path component.",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
ErrParseMissingIdentAfterAt: {
|
ErrParseMissingIdentAfterAt: {
|
||||||
Code: "ParseMissingIdentAfterAt",
|
Code: "ParseMissingIdentAfterAt",
|
||||||
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
|
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
|
||||||
@@ -1382,11 +1406,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
|
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidColumnIndex: {
|
|
||||||
Code: "InvalidColumnIndex",
|
|
||||||
Description: "Column index in the SQL expression is invalid.",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
ErrEvaluatorBindingDoesNotExist: {
|
ErrEvaluatorBindingDoesNotExist: {
|
||||||
Code: "ErrEvaluatorBindingDoesNotExist",
|
Code: "ErrEvaluatorBindingDoesNotExist",
|
||||||
Description: "A column name or a path provided does not exist in the SQL expression",
|
Description: "A column name or a path provided does not exist in the SQL expression",
|
||||||
@@ -1397,6 +1416,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Some headers in the query are missing from the file. Check the file and try again.",
|
Description: "Some headers in the query are missing from the file. Check the file and try again.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrInvalidDecompressedSize: {
|
||||||
|
Code: "XMinioInvalidDecompressedSize",
|
||||||
|
Description: "The data provided is unfit for decompression",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
// Add your error structure here.
|
// Add your error structure here.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,8 +1434,16 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// Verify if the underlying error is signature mismatch.
|
// Verify if the underlying error is signature mismatch.
|
||||||
switch err {
|
switch err {
|
||||||
|
case errInvalidArgument:
|
||||||
|
apiErr = ErrAdminInvalidArgument
|
||||||
|
case errNoSuchUser:
|
||||||
|
apiErr = ErrAdminNoSuchUser
|
||||||
|
case errNoSuchPolicy:
|
||||||
|
apiErr = ErrAdminNoSuchPolicy
|
||||||
case errSignatureMismatch:
|
case errSignatureMismatch:
|
||||||
apiErr = ErrSignatureDoesNotMatch
|
apiErr = ErrSignatureDoesNotMatch
|
||||||
|
case errInvalidRange:
|
||||||
|
apiErr = ErrInvalidRange
|
||||||
case errDataTooLarge:
|
case errDataTooLarge:
|
||||||
apiErr = ErrEntityTooLarge
|
apiErr = ErrEntityTooLarge
|
||||||
case errDataTooSmall:
|
case errDataTooSmall:
|
||||||
@@ -1421,10 +1453,10 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
case auth.ErrInvalidSecretKeyLength:
|
case auth.ErrInvalidSecretKeyLength:
|
||||||
apiErr = ErrAdminInvalidSecretKey
|
apiErr = ErrAdminInvalidSecretKey
|
||||||
// SSE errors
|
// SSE errors
|
||||||
|
case errInvalidEncryptionParameters:
|
||||||
|
apiErr = ErrInvalidEncryptionParameters
|
||||||
case crypto.ErrInvalidEncryptionMethod:
|
case crypto.ErrInvalidEncryptionMethod:
|
||||||
apiErr = ErrInvalidEncryptionMethod
|
apiErr = ErrInvalidEncryptionMethod
|
||||||
case errInsecureSSERequest:
|
|
||||||
apiErr = ErrInsecureSSECustomerRequest
|
|
||||||
case crypto.ErrInvalidCustomerAlgorithm:
|
case crypto.ErrInvalidCustomerAlgorithm:
|
||||||
apiErr = ErrInvalidSSECustomerAlgorithm
|
apiErr = ErrInvalidSSECustomerAlgorithm
|
||||||
case crypto.ErrInvalidCustomerKey:
|
case crypto.ErrInvalidCustomerKey:
|
||||||
@@ -1533,8 +1565,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrParseUnsupportedSyntax
|
apiErr = ErrParseUnsupportedSyntax
|
||||||
case s3select.ErrParseUnknownOperator:
|
case s3select.ErrParseUnknownOperator:
|
||||||
apiErr = ErrParseUnknownOperator
|
apiErr = ErrParseUnknownOperator
|
||||||
case s3select.ErrParseInvalidPathComponent:
|
|
||||||
apiErr = ErrParseInvalidPathComponent
|
|
||||||
case s3select.ErrParseMissingIdentAfterAt:
|
case s3select.ErrParseMissingIdentAfterAt:
|
||||||
apiErr = ErrParseMissingIdentAfterAt
|
apiErr = ErrParseMissingIdentAfterAt
|
||||||
case s3select.ErrParseUnexpectedOperator:
|
case s3select.ErrParseUnexpectedOperator:
|
||||||
@@ -1607,8 +1637,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
||||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||||
case s3select.ErrInvalidColumnIndex:
|
|
||||||
apiErr = ErrInvalidColumnIndex
|
|
||||||
case s3select.ErrEvaluatorBindingDoesNotExist:
|
case s3select.ErrEvaluatorBindingDoesNotExist:
|
||||||
apiErr = ErrEvaluatorBindingDoesNotExist
|
apiErr = ErrEvaluatorBindingDoesNotExist
|
||||||
case s3select.ErrMissingHeaders:
|
case s3select.ErrMissingHeaders:
|
||||||
@@ -1616,6 +1644,12 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compression errors
|
||||||
|
switch err {
|
||||||
|
case errInvalidDecompressedSize:
|
||||||
|
apiErr = ErrInvalidDecompressedSize
|
||||||
|
}
|
||||||
|
|
||||||
if apiErr != ErrNone {
|
if apiErr != ErrNone {
|
||||||
// If there was a match in the above switch case.
|
// If there was a match in the above switch case.
|
||||||
return apiErr
|
return apiErr
|
||||||
@@ -1727,7 +1761,10 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// getAPIError provides API Error for input API error code.
|
// getAPIError provides API Error for input API error code.
|
||||||
func getAPIError(code APIErrorCode) APIError {
|
func getAPIError(code APIErrorCode) APIError {
|
||||||
return errorCodeResponse[code]
|
if apiErr, ok := errorCodeResponse[code]; ok {
|
||||||
|
return apiErr
|
||||||
|
}
|
||||||
|
return errorCodeResponse[ErrInternalError]
|
||||||
}
|
}
|
||||||
|
|
||||||
// getErrorResponse gets in standard error and resource value and
|
// getErrorResponse gets in standard error and resource value and
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ var toAPIErrorCodeTests = []struct {
|
|||||||
{err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
|
{err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
|
||||||
|
|
||||||
// SSE-C errors
|
// SSE-C errors
|
||||||
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
|
||||||
{err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
{err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||||
{err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
|
{err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
|
||||||
{err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
|
{err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
|
||||||
|
|||||||
+35
-9
@@ -24,6 +24,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns a hexadecimal representation of time at the
|
// Returns a hexadecimal representation of time at the
|
||||||
@@ -41,6 +43,9 @@ func setCommonHeaders(w http.ResponseWriter) {
|
|||||||
w.Header().Set("X-Amz-Bucket-Region", region)
|
w.Header().Set("X-Amz-Bucket-Region", region)
|
||||||
}
|
}
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
|
||||||
|
// Remove sensitive information
|
||||||
|
crypto.RemoveSensitiveHeaders(w.Header())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encodes the response headers into XML format.
|
// Encodes the response headers into XML format.
|
||||||
@@ -61,13 +66,10 @@ func encodeResponseJSON(response interface{}) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write object header
|
// Write object header
|
||||||
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *httpRange) {
|
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSpec) (err error) {
|
||||||
// set common headers
|
// set common headers
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
|
|
||||||
// Set content length.
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
|
|
||||||
|
|
||||||
// Set last modified time.
|
// Set last modified time.
|
||||||
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
|
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
|
||||||
w.Header().Set("Last-Modified", lastModified)
|
w.Header().Set("Last-Modified", lastModified)
|
||||||
@@ -95,10 +97,34 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
|||||||
w.Header().Set(k, v)
|
w.Header().Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// for providing ranged content
|
var totalObjectSize int64
|
||||||
if contentRange != nil && contentRange.offsetBegin > -1 {
|
switch {
|
||||||
// Override content-length
|
case crypto.IsEncrypted(objInfo.UserDefined):
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
|
totalObjectSize, err = objInfo.DecryptedSize()
|
||||||
w.Header().Set("Content-Range", contentRange.String())
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case objInfo.IsCompressed():
|
||||||
|
totalObjectSize = objInfo.GetActualSize()
|
||||||
|
if totalObjectSize < 0 {
|
||||||
|
return errInvalidDecompressedSize
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
totalObjectSize = objInfo.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for providing ranged content
|
||||||
|
start, rangeLen, err := rs.GetOffsetLength(totalObjectSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content length.
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(rangeLen, 10))
|
||||||
|
if rs != nil {
|
||||||
|
contentRange := fmt.Sprintf("bytes %d-%d/%d", start, start+rangeLen-1, totalObjectSize)
|
||||||
|
w.Header().Set("Content-Range", contentRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-20
@@ -22,15 +22,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Parse bucket url queries
|
// Parse bucket url queries
|
||||||
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
|
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string, errCode APIErrorCode) {
|
||||||
prefix = values.Get("prefix")
|
errCode = ErrNone
|
||||||
marker = values.Get("marker")
|
|
||||||
delimiter = values.Get("delimiter")
|
|
||||||
if values.Get("max-keys") != "" {
|
if values.Get("max-keys") != "" {
|
||||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
var err error
|
||||||
|
if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
maxkeys = maxObjectList
|
maxkeys = maxObjectList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix = values.Get("prefix")
|
||||||
|
marker = values.Get("marker")
|
||||||
|
delimiter = values.Get("delimiter")
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,44 +54,69 @@ func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if values.Get("max-keys") != "" {
|
||||||
|
var err error
|
||||||
|
if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxkeys = maxObjectList
|
||||||
|
}
|
||||||
|
|
||||||
prefix = values.Get("prefix")
|
prefix = values.Get("prefix")
|
||||||
token = values.Get("continuation-token")
|
token = values.Get("continuation-token")
|
||||||
startAfter = values.Get("start-after")
|
startAfter = values.Get("start-after")
|
||||||
delimiter = values.Get("delimiter")
|
delimiter = values.Get("delimiter")
|
||||||
if values.Get("max-keys") != "" {
|
|
||||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
|
||||||
} else {
|
|
||||||
maxkeys = maxObjectList
|
|
||||||
}
|
|
||||||
fetchOwner = values.Get("fetch-owner") == "true"
|
fetchOwner = values.Get("fetch-owner") == "true"
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse bucket url queries for ?uploads
|
// Parse bucket url queries for ?uploads
|
||||||
func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string) {
|
func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string, errCode APIErrorCode) {
|
||||||
|
errCode = ErrNone
|
||||||
|
|
||||||
|
if values.Get("max-uploads") != "" {
|
||||||
|
var err error
|
||||||
|
if maxUploads, err = strconv.Atoi(values.Get("max-uploads")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxUploads
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxUploads = maxUploadsList
|
||||||
|
}
|
||||||
|
|
||||||
prefix = values.Get("prefix")
|
prefix = values.Get("prefix")
|
||||||
keyMarker = values.Get("key-marker")
|
keyMarker = values.Get("key-marker")
|
||||||
uploadIDMarker = values.Get("upload-id-marker")
|
uploadIDMarker = values.Get("upload-id-marker")
|
||||||
delimiter = values.Get("delimiter")
|
delimiter = values.Get("delimiter")
|
||||||
if values.Get("max-uploads") != "" {
|
|
||||||
maxUploads, _ = strconv.Atoi(values.Get("max-uploads"))
|
|
||||||
} else {
|
|
||||||
maxUploads = maxUploadsList
|
|
||||||
}
|
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse object url queries
|
// Parse object url queries
|
||||||
func getObjectResources(values url.Values) (uploadID string, partNumberMarker, maxParts int, encodingType string) {
|
func getObjectResources(values url.Values) (uploadID string, partNumberMarker, maxParts int, encodingType string, errCode APIErrorCode) {
|
||||||
uploadID = values.Get("uploadId")
|
var err error
|
||||||
partNumberMarker, _ = strconv.Atoi(values.Get("part-number-marker"))
|
errCode = ErrNone
|
||||||
|
|
||||||
if values.Get("max-parts") != "" {
|
if values.Get("max-parts") != "" {
|
||||||
maxParts, _ = strconv.Atoi(values.Get("max-parts"))
|
if maxParts, err = strconv.Atoi(values.Get("max-parts")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxParts
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
maxParts = maxPartsList
|
maxParts = maxPartsList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if values.Get("part-number-marker") != "" {
|
||||||
|
if partNumberMarker, err = strconv.Atoi(values.Get("part-number-marker")); err != nil {
|
||||||
|
errCode = ErrInvalidPartNumberMarker
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadID = values.Get("uploadId")
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ func TestListObjectsV1Resources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
prefix, marker, delimiter, maxKeys, encodingType := getListObjectsV1Args(testCase.values)
|
prefix, marker, delimiter, maxKeys, encodingType, argsErr := getListObjectsV1Args(testCase.values)
|
||||||
|
if argsErr != ErrNone {
|
||||||
|
t.Errorf("Test %d: argument parsing failed, got %v", i+1, argsErr)
|
||||||
|
}
|
||||||
if prefix != testCase.prefix {
|
if prefix != testCase.prefix {
|
||||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
|
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
|
||||||
}
|
}
|
||||||
@@ -198,7 +201,10 @@ func TestGetObjectsResources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
uploadID, partNumberMarker, maxParts, encodingType := getObjectResources(testCase.values)
|
uploadID, partNumberMarker, maxParts, encodingType, argsErr := getObjectResources(testCase.values)
|
||||||
|
if argsErr != ErrNone {
|
||||||
|
t.Errorf("Test %d: argument parsing failed, got %v", i+1, argsErr)
|
||||||
|
}
|
||||||
if uploadID != testCase.uploadID {
|
if uploadID != testCase.uploadID {
|
||||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.uploadID, uploadID)
|
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.uploadID, uploadID)
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-21
@@ -27,8 +27,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jwtgo "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ const (
|
|||||||
authTypeSigned
|
authTypeSigned
|
||||||
authTypeSignedV2
|
authTypeSignedV2
|
||||||
authTypeJWT
|
authTypeJWT
|
||||||
|
authTypeSTS
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get request authentication type.
|
// Get request authentication type.
|
||||||
@@ -106,6 +109,8 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
return authTypePostPolicy
|
return authTypePostPolicy
|
||||||
} else if _, ok := r.Header["Authorization"]; !ok {
|
} else if _, ok := r.Header["Authorization"]; !ok {
|
||||||
return authTypeAnonymous
|
return authTypeAnonymous
|
||||||
|
} else if _, ok := r.URL.Query()["Action"]; ok {
|
||||||
|
return authTypeSTS
|
||||||
}
|
}
|
||||||
return authTypeUnknown
|
return authTypeUnknown
|
||||||
}
|
}
|
||||||
@@ -114,7 +119,21 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
// It does not accept presigned or JWT or anonymous requests.
|
// It does not accept presigned or JWT or anonymous requests.
|
||||||
func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
||||||
s3Err := ErrAccessDenied
|
s3Err := ErrAccessDenied
|
||||||
if _, ok := r.Header["X-Amz-Content-Sha256"]; ok && getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) { // we only support V4 (no presign) with auth. body
|
if _, ok := r.Header["X-Amz-Content-Sha256"]; ok &&
|
||||||
|
getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) {
|
||||||
|
// We only support admin credentials to access admin APIs.
|
||||||
|
|
||||||
|
var owner bool
|
||||||
|
_, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !owner {
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only support V4 (no presign) with auth body
|
||||||
s3Err = isReqAuthenticated(r, region)
|
s3Err = isReqAuthenticated(r, region)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
@@ -125,30 +144,62 @@ func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
|||||||
return s3Err
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) APIErrorCode {
|
// Fetch the security token set by the client.
|
||||||
isOwner := true
|
func getSessionToken(r *http.Request) (token string) {
|
||||||
accountName := globalServerConfig.GetCredential().AccessKey
|
token = r.Header.Get("X-Amz-Security-Token")
|
||||||
|
if token != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
return r.URL.Query().Get("X-Amz-Security-Token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch claims in the security token returned by the client and validate the token.
|
||||||
|
func getClaimsFromToken(r *http.Request) (map[string]interface{}, APIErrorCode) {
|
||||||
|
claims := make(map[string]interface{})
|
||||||
|
token := getSessionToken(r)
|
||||||
|
if token == "" {
|
||||||
|
return nil, ErrNone
|
||||||
|
}
|
||||||
|
p := &jwtgo.Parser{}
|
||||||
|
jtoken, err := p.ParseWithClaims(token, jwtgo.MapClaims(claims), stsTokenCallback)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toAPIErrorCode(errAuthentication)
|
||||||
|
}
|
||||||
|
if !jtoken.Valid {
|
||||||
|
return nil, toAPIErrorCode(errAuthentication)
|
||||||
|
}
|
||||||
|
return claims, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check request auth type verifies the incoming http request
|
||||||
|
// - validates the request signature
|
||||||
|
// - validates the policy action if anonymous tests bucket policies if any,
|
||||||
|
// for authenticated requests validates IAM policies.
|
||||||
|
// returns APIErrorCode if any to be replied to the client.
|
||||||
|
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) (s3Err APIErrorCode) {
|
||||||
|
var accessKey string
|
||||||
|
var owner bool
|
||||||
switch getRequestAuthType(r) {
|
switch getRequestAuthType(r) {
|
||||||
case authTypeUnknown:
|
case authTypeUnknown, authTypeStreamingSigned:
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
case authTypePresignedV2, authTypeSignedV2:
|
case authTypePresignedV2, authTypeSignedV2:
|
||||||
if errorCode := isReqAuthenticatedV2(r); errorCode != ErrNone {
|
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||||
return errorCode
|
return s3Err
|
||||||
}
|
}
|
||||||
|
accessKey, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeSigned, authTypePresigned:
|
case authTypeSigned, authTypePresigned:
|
||||||
region := globalServerConfig.GetRegion()
|
region := globalServerConfig.GetRegion()
|
||||||
switch action {
|
switch action {
|
||||||
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
||||||
region = ""
|
region = ""
|
||||||
}
|
}
|
||||||
|
if s3Err = isReqAuthenticated(r, region); s3Err != ErrNone {
|
||||||
if errorCode := isReqAuthenticated(r, region); errorCode != ErrNone {
|
return s3Err
|
||||||
return errorCode
|
|
||||||
}
|
}
|
||||||
default:
|
accessKey, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||||
isOwner = false
|
}
|
||||||
accountName = ""
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocationConstraint is valid only for CreateBucketAction.
|
// LocationConstraint is valid only for CreateBucketAction.
|
||||||
@@ -174,17 +225,36 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac
|
|||||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalPolicySys.IsAllowed(policy.Args{
|
claims, s3Err := getClaimsFromToken(r)
|
||||||
AccountName: accountName,
|
if s3Err != ErrNone {
|
||||||
Action: action,
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessKey == "" {
|
||||||
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
|
AccountName: accessKey,
|
||||||
|
Action: action,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, locationConstraint),
|
||||||
|
IsOwner: false,
|
||||||
|
ObjectName: objectName,
|
||||||
|
}) {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: accessKey,
|
||||||
|
Action: iampolicy.Action(action),
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, locationConstraint),
|
ConditionValues: getConditionValues(r, ""),
|
||||||
IsOwner: isOwner,
|
|
||||||
ObjectName: objectName,
|
ObjectName: objectName,
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: claims,
|
||||||
}) {
|
}) {
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +314,7 @@ func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
|
|||||||
|
|
||||||
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
|
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
|
||||||
// The verification happens implicit during reading.
|
// The verification happens implicit during reading.
|
||||||
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256))
|
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256), -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toAPIErrorCode(err)
|
return toAPIErrorCode(err)
|
||||||
}
|
}
|
||||||
@@ -288,7 +358,7 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
} else if aType == authTypeJWT {
|
} else if aType == authTypeJWT {
|
||||||
// Validate Authorization header if its valid for JWT request.
|
// Validate Authorization header if its valid for JWT request.
|
||||||
if !isHTTPRequestValid(r) {
|
if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -297,3 +367,55 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeErrorResponse(w, ErrSignatureVersionNotSupported, r.URL)
|
writeErrorResponse(w, ErrSignatureVersionNotSupported, r.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isPutAllowed - check if PUT operation is allowed on the resource, this
|
||||||
|
// call verifies bucket policies and IAM policies, supports multi user
|
||||||
|
// checks etc.
|
||||||
|
func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request) (s3Err APIErrorCode) {
|
||||||
|
var accessKey string
|
||||||
|
var owner bool
|
||||||
|
switch atype {
|
||||||
|
case authTypeUnknown:
|
||||||
|
return ErrAccessDenied
|
||||||
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
|
accessKey, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||||
|
region := globalServerConfig.GetRegion()
|
||||||
|
accessKey, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||||
|
}
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, s3Err := getClaimsFromToken(r)
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessKey == "" {
|
||||||
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
|
AccountName: accessKey,
|
||||||
|
Action: policy.PutObjectAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, ""),
|
||||||
|
IsOwner: false,
|
||||||
|
ObjectName: objectName,
|
||||||
|
}) {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: accessKey,
|
||||||
|
Action: policy.PutObjectAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, ""),
|
||||||
|
ObjectName: objectName,
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -138,7 +138,9 @@ func (b *bitrotReader) ReadChunk(offset int64, length int64) ([]byte, error) {
|
|||||||
if b.buf == nil {
|
if b.buf == nil {
|
||||||
b.buf = make([]byte, b.endOffset-offset)
|
b.buf = make([]byte, b.endOffset-offset)
|
||||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
ctx := context.Background()
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
@@ -58,6 +59,8 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
|||||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV2")
|
ctx := newContext(r, w, "ListObjectsV2")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -102,7 +105,17 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
for i := range listObjectsV2Info.Objects {
|
||||||
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
var actualSize int64
|
||||||
|
if listObjectsV2Info.Objects[i].IsCompressed() {
|
||||||
|
// Read the decompressed size from the meta.json.
|
||||||
|
actualSize = listObjectsV2Info.Objects[i].GetActualSize()
|
||||||
|
if actualSize < 0 {
|
||||||
|
writeErrorResponse(w, ErrInvalidDecompressedSize, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set the info.Size to the actualSize.
|
||||||
|
listObjectsV2Info.Objects[i].Size = actualSize
|
||||||
|
} else if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -127,6 +140,8 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV1")
|
ctx := newContext(r, w, "ListObjectsV1")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -142,7 +157,11 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract all the litsObjectsV1 query params to their native values.
|
// Extract all the litsObjectsV1 query params to their native values.
|
||||||
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
prefix, marker, delimiter, maxKeys, _, s3Error := getListObjectsV1Args(r.URL.Query())
|
||||||
|
if s3Error != ErrNone {
|
||||||
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the maxKeys lowerbound. When maxKeys > 1000, S3 returns 1000 but
|
// Validate the maxKeys lowerbound. When maxKeys > 1000, S3 returns 1000 but
|
||||||
// does not throw an error.
|
// does not throw an error.
|
||||||
@@ -168,7 +187,17 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsInfo.Objects {
|
for i := range listObjectsInfo.Objects {
|
||||||
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
var actualSize int64
|
||||||
|
if listObjectsInfo.Objects[i].IsCompressed() {
|
||||||
|
// Read the decompressed size from the meta.json.
|
||||||
|
actualSize = listObjectsInfo.Objects[i].GetActualSize()
|
||||||
|
if actualSize < 0 {
|
||||||
|
writeErrorResponse(w, ErrInvalidDecompressedSize, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set the info.Size to the actualSize.
|
||||||
|
listObjectsInfo.Objects[i].Size = actualSize
|
||||||
|
} else if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
||||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -176,7 +205,6 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
|
|||||||
+27
-3
@@ -89,6 +89,8 @@ func initFederatorBackend(objLayer ObjectLayer) {
|
|||||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketLocation")
|
ctx := newContext(r, w, "GetBucketLocation")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -137,6 +139,8 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
|||||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListMultipartUploads")
|
ctx := newContext(r, w, "ListMultipartUploads")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -151,7 +155,11 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _ := getBucketMultipartResources(r.URL.Query())
|
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _, s3Error := getBucketMultipartResources(r.URL.Query())
|
||||||
|
if s3Error != ErrNone {
|
||||||
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
if maxUploads < 0 {
|
if maxUploads < 0 {
|
||||||
writeErrorResponse(w, ErrInvalidMaxUploads, r.URL)
|
writeErrorResponse(w, ErrInvalidMaxUploads, r.URL)
|
||||||
return
|
return
|
||||||
@@ -184,6 +192,8 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListBuckets")
|
ctx := newContext(r, w, "ListBuckets")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
@@ -384,6 +394,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PutBucket")
|
ctx := newContext(r, w, "PutBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
@@ -460,6 +472,8 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PostPolicyBucket")
|
ctx := newContext(r, w, "PostPolicyBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
@@ -590,7 +604,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "")
|
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -614,7 +628,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := ObjectInfo{Size: fileSize}
|
info := ObjectInfo{Size: fileSize}
|
||||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "") // do not try to verify encrypted content
|
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize) // do not try to verify encrypted content
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -671,6 +685,12 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
default:
|
default:
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range objInfo.UserDefined {
|
||||||
|
logger.GetReqInfo(ctx).SetTags(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.GetReqInfo(ctx).SetTags("etag", objInfo.ETag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeadBucketHandler - HEAD Bucket
|
// HeadBucketHandler - HEAD Bucket
|
||||||
@@ -682,6 +702,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "HeadBucket")
|
ctx := newContext(r, w, "HeadBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -712,6 +734,8 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
|||||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DeleteBucket")
|
ctx := newContext(r, w, "DeleteBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -84,7 +84,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// construct HTTP request for Get bucket location.
|
// construct HTTP request for Get bucket location.
|
||||||
req, err := newTestSignedRequestV4("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
req, err := newTestSignedRequestV4("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
@@ -220,7 +220,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// construct HTTP request for HEAD bucket.
|
// construct HTTP request for HEAD bucket.
|
||||||
req, err := newTestSignedRequestV4("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
req, err := newTestSignedRequestV4("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for HeadBucketHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for HeadBucketHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -235,7 +235,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
@@ -437,7 +437,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
|
|
||||||
// construct HTTP request for List multipart uploads endpoint.
|
// construct HTTP request for List multipart uploads endpoint.
|
||||||
u := getListMultipartUploadsURLWithParams("", testCase.bucket, testCase.prefix, testCase.keyMarker, testCase.uploadIDMarker, testCase.delimiter, testCase.maxUploads)
|
u := getListMultipartUploadsURLWithParams("", testCase.bucket, testCase.prefix, testCase.keyMarker, testCase.uploadIDMarker, testCase.delimiter, testCase.maxUploads)
|
||||||
req, gerr := newTestSignedRequestV4("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
|
req, gerr := newTestSignedRequestV4("GET", u, 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if gerr != nil {
|
if gerr != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", i+1, instanceType, gerr)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", i+1, instanceType, gerr)
|
||||||
}
|
}
|
||||||
@@ -454,7 +454,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
|
|
||||||
// verify response for V2 signed HTTP request.
|
// verify response for V2 signed HTTP request.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -471,7 +471,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
|
|
||||||
// construct HTTP request for List multipart uploads endpoint.
|
// construct HTTP request for List multipart uploads endpoint.
|
||||||
u := getListMultipartUploadsURLWithParams("", bucketName, "", "", "", "", "")
|
u := getListMultipartUploadsURLWithParams("", bucketName, "", "", "", "", "")
|
||||||
req, err := newTestSignedRequestV4("GET", u, 0, nil, "", "") // Generate an anonymous request.
|
req, err := newTestSignedRequestV4("GET", u, 0, nil, "", "", nil) // Generate an anonymous request.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", instanceType, err)
|
t.Fatalf("Test %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -551,7 +551,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req, lerr := newTestSignedRequestV4("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey)
|
req, lerr := newTestSignedRequestV4("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if lerr != nil {
|
if lerr != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListBucketsHandler: <ERROR> %v", i+1, instanceType, lerr)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListBucketsHandler: <ERROR> %v", i+1, instanceType, lerr)
|
||||||
}
|
}
|
||||||
@@ -568,7 +568,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
|
|
||||||
// verify response for V2 signed HTTP request.
|
// verify response for V2 signed HTTP request.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
@@ -745,7 +745,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
// Generate a signed or anonymous request based on the testCase
|
// Generate a signed or anonymous request based on the testCase
|
||||||
if testCase.accessKey != "" {
|
if testCase.accessKey != "" {
|
||||||
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey)
|
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey, nil)
|
||||||
} else {
|
} else {
|
||||||
req, err = newTestRequest("POST", getDeleteMultipleObjectsURL("", bucketName),
|
req, err = newTestRequest("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
|
int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
|
||||||
@@ -785,7 +785,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
nilObject := ""
|
nilObject := ""
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", nilBucket), 0, nil, "", "")
|
nilReq, err := newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", nilBucket), 0, nil, "", "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ var errNoSuchNotifications = errors.New("The specified bucket does not have buck
|
|||||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketNotification")
|
ctx := newContext(r, w, "GetBucketNotification")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucketName := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
@@ -96,6 +98,8 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PutBucketNotification")
|
ctx := newContext(r, w, "PutBucketNotification")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
@@ -139,7 +143,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveNotificationConfig(objectAPI, bucketName, config); err != nil {
|
if err = saveNotificationConfig(ctx, objectAPI, bucketName, config); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -156,6 +160,8 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListenBucketNotification")
|
ctx := newContext(r, w, "ListenBucketNotification")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
// Validate if bucket exists.
|
// Validate if bucket exists.
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ const (
|
|||||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PutBucketPolicy")
|
ctx := newContext(r, w, "PutBucketPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
@@ -101,6 +103,8 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DeleteBucketPolicy")
|
ctx := newContext(r, w, "DeleteBucketPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
@@ -137,6 +141,8 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketPolicy")
|
ctx := newContext(r, w, "GetBucketPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, r)
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testCase.bucketName),
|
||||||
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey)
|
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testCase.bucketName),
|
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testCase.bucketName),
|
||||||
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey)
|
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -304,7 +304,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", nilBucket),
|
||||||
0, nil, "", "")
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
@@ -346,7 +346,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -417,7 +417,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("GET", getGetPolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4("GET", getGetPolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
@@ -456,7 +456,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", getGetPolicyURL("", testCase.bucketName),
|
reqV2, err := newTestSignedRequestV2("GET", getGetPolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -511,7 +511,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("GET", getGetPolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4("GET", getGetPolicyURL("", nilBucket),
|
||||||
0, nil, "", "")
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
@@ -591,7 +591,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -641,7 +641,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for Delete bucket policy endpoint.
|
// construct HTTP request for Delete bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -663,7 +663,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -680,7 +680,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for Delete bucket policy endpoint.
|
// construct HTTP request for Delete bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
reqV2, err := newTestSignedRequestV2("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -714,7 +714,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", nilBucket),
|
||||||
0, nil, "", "")
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
|
|||||||
+55
-5
@@ -26,13 +26,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/clientv3"
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
|
dns2 "github.com/miekg/dns"
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
|
"github.com/minio/minio-go/pkg/set"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check for updates and print a notification message
|
// Check for updates and print a notification message
|
||||||
@@ -49,10 +49,17 @@ func checkUpdate(mode string) {
|
|||||||
|
|
||||||
// Load logger targets based on user's configuration
|
// Load logger targets based on user's configuration
|
||||||
func loadLoggers() {
|
func loadLoggers() {
|
||||||
|
if endpoint, ok := os.LookupEnv("MINIO_LOGGER_HTTP_ENDPOINT"); ok {
|
||||||
|
// Enable http logging through ENV, this is specifically added gateway audit logging.
|
||||||
|
logger.AddTarget(logger.NewHTTP(endpoint, NewCustomHTTPTransport()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if globalServerConfig.Logger.Console.Enabled {
|
if globalServerConfig.Logger.Console.Enabled {
|
||||||
// Enable console logging
|
// Enable console logging
|
||||||
logger.AddTarget(logger.NewConsole())
|
logger.AddTarget(logger.NewConsole())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, l := range globalServerConfig.Logger.HTTP {
|
for _, l := range globalServerConfig.Logger.HTTP {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
// Enable http logging
|
// Enable http logging
|
||||||
@@ -95,10 +102,23 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
setConfigDir(configDirAbs)
|
setConfigDir(configDirAbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses the given compression exclude list `extensions` or `content-types`.
|
||||||
|
func parseCompressIncludes(includes []string) ([]string, error) {
|
||||||
|
for _, e := range includes {
|
||||||
|
if len(e) == 0 {
|
||||||
|
return nil, uiErrInvalidCompressionIncludesValue(nil).Msg("extension/mime-type (%s) cannot be empty", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return includes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleCommonEnvVars() {
|
func handleCommonEnvVars() {
|
||||||
|
compressEnvDelimiter := ","
|
||||||
// Start profiler if env is set.
|
// Start profiler if env is set.
|
||||||
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
|
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
|
||||||
globalProfiler = startProfiler(profiler)
|
var err error
|
||||||
|
globalProfiler, err = startProfiler(profiler, "")
|
||||||
|
logger.FatalIf(err, "Unable to setup a profiler")
|
||||||
}
|
}
|
||||||
|
|
||||||
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
||||||
@@ -108,6 +128,7 @@ func handleCommonEnvVars() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(uiErrInvalidCredentials(err), "Unable to validate credentials inherited from the shell environment")
|
logger.Fatal(uiErrInvalidCredentials(err), "Unable to validate credentials inherited from the shell environment")
|
||||||
}
|
}
|
||||||
|
cred.Expiration = timeSentinel
|
||||||
|
|
||||||
// credential Envs are set globally.
|
// credential Envs are set globally.
|
||||||
globalIsEnvCreds = true
|
globalIsEnvCreds = true
|
||||||
@@ -117,7 +138,7 @@ func handleCommonEnvVars() {
|
|||||||
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
||||||
browserFlag, err := ParseBoolFlag(browser)
|
browserFlag, err := ParseBoolFlag(browser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Unable to validate MINIO_BROWSER environment variable")
|
logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Invalid MINIO_BROWSER value in environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// browser Envs are set globally, this does not represent
|
// browser Envs are set globally, this does not represent
|
||||||
@@ -146,6 +167,11 @@ func handleCommonEnvVars() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
|
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
|
||||||
|
if globalDomainName != "" {
|
||||||
|
if _, ok = dns2.IsDomainName(globalDomainName); !ok {
|
||||||
|
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN value in environment variable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
||||||
if ok {
|
if ok {
|
||||||
@@ -238,7 +264,7 @@ func handleCommonEnvVars() {
|
|||||||
if worm := os.Getenv("MINIO_WORM"); worm != "" {
|
if worm := os.Getenv("MINIO_WORM"); worm != "" {
|
||||||
wormFlag, err := ParseBoolFlag(worm)
|
wormFlag, err := ParseBoolFlag(worm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Unable to validate MINIO_WORM environment variable")
|
logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Invalid MINIO_WORM value in environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// worm Envs are set globally, this does not represent
|
// worm Envs are set globally, this does not represent
|
||||||
@@ -260,4 +286,28 @@ func handleCommonEnvVars() {
|
|||||||
globalKMSKeyID = kmsConf.Vault.Key.Name
|
globalKMSKeyID = kmsConf.Vault.Key.Name
|
||||||
globalKMSConfig = kmsConf
|
globalKMSConfig = kmsConf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if compress := os.Getenv("MINIO_COMPRESS"); compress != "" {
|
||||||
|
globalIsCompressionEnabled = strings.EqualFold(compress, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
compressExtensions := os.Getenv("MINIO_COMPRESS_EXTENSIONS")
|
||||||
|
compressMimeTypes := os.Getenv("MINIO_COMPRESS_MIMETYPES")
|
||||||
|
if compressExtensions != "" || compressMimeTypes != "" {
|
||||||
|
globalIsEnvCompression = true
|
||||||
|
if compressExtensions != "" {
|
||||||
|
extensions, err := parseCompressIncludes(strings.Split(compressExtensions, compressEnvDelimiter))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Invalid MINIO_COMPRESS_EXTENSIONS value (`%s`)", extensions)
|
||||||
|
}
|
||||||
|
globalCompressExtensions = extensions
|
||||||
|
}
|
||||||
|
if compressMimeTypes != "" {
|
||||||
|
contenttypes, err := parseCompressIncludes(strings.Split(compressMimeTypes, compressEnvDelimiter))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Invalid MINIO_COMPRESS_MIMETYPES value (`%s`)", contenttypes)
|
||||||
|
}
|
||||||
|
globalCompressMimeTypes = contenttypes
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errConfigNotFound = errors.New("config file not found")
|
||||||
|
|
||||||
|
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
// Read entire content by setting size to -1
|
||||||
|
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, "", ObjectOptions{}); err != nil {
|
||||||
|
// Treat object not found as config not found.
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return config not found on empty content.
|
||||||
|
if buffer.Len() == 0 {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
|
||||||
|
_, err := client.Delete(ctx, configFile)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
||||||
|
return objAPI.DeleteObject(ctx, minioMetaBucket, configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfigEtcd(ctx context.Context, client *etcd.Client, configFile string, data []byte) error {
|
||||||
|
_, err := client.Put(ctx, configFile, string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
||||||
|
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objAPI.PutObject(ctx, minioMetaBucket, configFile, hashReader, nil, ObjectOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) ([]byte, error) {
|
||||||
|
resp, err := client.Get(ctx, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.Count == 0 {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
for _, ev := range resp.Kvs {
|
||||||
|
if string(ev.Key) == configFile {
|
||||||
|
return ev.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchConfig - watches for changes on `configFile` on etcd and loads them.
|
||||||
|
func watchConfig(objAPI ObjectLayer, configFile string, loadCfgFn func(ObjectLayer) error) {
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
for watchResp := range globalEtcdClient.Watch(context.Background(), configFile) {
|
||||||
|
for _, event := range watchResp.Events {
|
||||||
|
if event.IsModify() || event.IsCreate() {
|
||||||
|
loadCfgFn(objAPI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
|
||||||
|
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Count == 0 {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
return checkConfigEtcd(ctx, globalEtcdClient, configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
|
||||||
|
// Treat object not found as config not found.
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+89
-59
@@ -20,16 +20,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Steps to move from version N to version N+1
|
// Steps to move from version N to version N+1
|
||||||
@@ -41,9 +43,9 @@ import (
|
|||||||
// 6. Make changes in config-current_test.go for any test change
|
// 6. Make changes in config-current_test.go for any test change
|
||||||
|
|
||||||
// Config version
|
// Config version
|
||||||
const serverConfigVersion = "28"
|
const serverConfigVersion = "31"
|
||||||
|
|
||||||
type serverConfig = serverConfigV28
|
type serverConfig = serverConfigV31
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// globalServerConfig server config.
|
// globalServerConfig server config.
|
||||||
@@ -97,12 +99,6 @@ func (s *serverConfig) GetCredential() auth.Credentials {
|
|||||||
return s.Credential
|
return s.Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBrowser set if browser is enabled.
|
|
||||||
func (s *serverConfig) SetBrowser(b bool) {
|
|
||||||
// Set the new value.
|
|
||||||
s.Browser = BoolFlag(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWorm set if worm is enabled.
|
// SetWorm set if worm is enabled.
|
||||||
func (s *serverConfig) SetWorm(b bool) {
|
func (s *serverConfig) SetWorm(b bool) {
|
||||||
// Set the new value.
|
// Set the new value.
|
||||||
@@ -126,17 +122,6 @@ func (s *serverConfig) GetStorageClass() (storageClass, storageClass) {
|
|||||||
return s.StorageClass.Standard, s.StorageClass.RRS
|
return s.StorageClass.Standard, s.StorageClass.RRS
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBrowser get current credentials.
|
|
||||||
func (s *serverConfig) GetBrowser() bool {
|
|
||||||
if globalIsEnvBrowser {
|
|
||||||
return globalIsBrowserEnabled
|
|
||||||
}
|
|
||||||
if s == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return bool(s.Browser)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWorm get current credentials.
|
// GetWorm get current credentials.
|
||||||
func (s *serverConfig) GetWorm() bool {
|
func (s *serverConfig) GetWorm() bool {
|
||||||
if globalIsEnvWORM {
|
if globalIsEnvWORM {
|
||||||
@@ -188,81 +173,82 @@ func (s *serverConfig) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Region: nothing to validate
|
// Region: nothing to validate
|
||||||
// Browser, Worm, Cache and StorageClass values are already validated during json unmarshal
|
// Worm, Cache and StorageClass values are already validated during json unmarshal
|
||||||
|
|
||||||
if s.Domain != "" {
|
|
||||||
if _, ok := dns.IsDomainName(s.Domain); !ok {
|
|
||||||
return errors.New("invalid domain name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range s.Notify.AMQP {
|
for _, v := range s.Notify.AMQP {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("amqp: %s", err.Error())
|
return fmt.Errorf("amqp: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Elasticsearch {
|
for _, v := range s.Notify.Elasticsearch {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("elasticsearch: %s", err.Error())
|
return fmt.Errorf("elasticsearch: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Kafka {
|
for _, v := range s.Notify.Kafka {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("kafka: %s", err.Error())
|
return fmt.Errorf("kafka: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.MQTT {
|
for _, v := range s.Notify.MQTT {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("mqtt: %s", err.Error())
|
return fmt.Errorf("mqtt: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.MySQL {
|
for _, v := range s.Notify.MySQL {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("mysql: %s", err.Error())
|
return fmt.Errorf("mysql: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.NATS {
|
for _, v := range s.Notify.NATS {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("nats: %s", err.Error())
|
return fmt.Errorf("nats: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.PostgreSQL {
|
for _, v := range s.Notify.PostgreSQL {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("postgreSQL: %s", err.Error())
|
return fmt.Errorf("postgreSQL: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Redis {
|
for _, v := range s.Notify.Redis {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("redis: %s", err.Error())
|
return fmt.Errorf("redis: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Webhook {
|
for _, v := range s.Notify.Webhook {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("webhook: %s", err.Error())
|
return fmt.Errorf("webhook: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCompressionConfig sets the current compression config
|
||||||
|
func (s *serverConfig) SetCompressionConfig(extensions []string, mimeTypes []string) {
|
||||||
|
s.Compression.Extensions = extensions
|
||||||
|
s.Compression.MimeTypes = mimeTypes
|
||||||
|
s.Compression.Enabled = globalIsCompressionEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompressionConfig gets the current compression config
|
||||||
|
func (s *serverConfig) GetCompressionConfig() compressionConfig {
|
||||||
|
return s.Compression
|
||||||
|
}
|
||||||
|
|
||||||
func (s *serverConfig) loadFromEnvs() {
|
func (s *serverConfig) loadFromEnvs() {
|
||||||
// If env is set override the credentials from config file.
|
// If env is set override the credentials from config file.
|
||||||
if globalIsEnvCreds {
|
if globalIsEnvCreds {
|
||||||
s.SetCredential(globalActiveCred)
|
s.SetCredential(globalActiveCred)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsEnvBrowser {
|
|
||||||
s.SetBrowser(globalIsBrowserEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvWORM {
|
if globalIsEnvWORM {
|
||||||
s.SetWorm(globalWORMEnabled)
|
s.SetWorm(globalWORMEnabled)
|
||||||
}
|
}
|
||||||
@@ -271,10 +257,6 @@ func (s *serverConfig) loadFromEnvs() {
|
|||||||
s.SetRegion(globalServerRegion)
|
s.SetRegion(globalServerRegion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsEnvDomainName {
|
|
||||||
s.Domain = globalDomainName
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsStorageClass {
|
if globalIsStorageClass {
|
||||||
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
||||||
}
|
}
|
||||||
@@ -286,6 +268,24 @@ func (s *serverConfig) loadFromEnvs() {
|
|||||||
if globalKMS != nil {
|
if globalKMS != nil {
|
||||||
s.KMS = globalKMSConfig
|
s.KMS = globalKMSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if globalIsEnvCompression {
|
||||||
|
s.SetCompressionConfig(globalCompressExtensions, globalCompressMimeTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwksURL, ok := os.LookupEnv("MINIO_IAM_JWKS_URL"); ok {
|
||||||
|
if u, err := xnet.ParseURL(jwksURL); err == nil {
|
||||||
|
s.OpenID.JWKS.URL = u
|
||||||
|
s.OpenID.JWKS.PopulatePublicKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opaURL, ok := os.LookupEnv("MINIO_IAM_OPA_URL"); ok {
|
||||||
|
if u, err := xnet.ParseURL(opaURL); err == nil {
|
||||||
|
s.Policy.OPA.URL = u
|
||||||
|
s.Policy.OPA.AuthToken = os.Getenv("MINIO_IAM_OPA_AUTHTOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNotificationTargets tries to establish connections to all notification
|
// TestNotificationTargets tries to establish connections to all notification
|
||||||
@@ -395,14 +395,12 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
|
|||||||
return "Credential configuration differs"
|
return "Credential configuration differs"
|
||||||
case s.Region != t.Region:
|
case s.Region != t.Region:
|
||||||
return "Region configuration differs"
|
return "Region configuration differs"
|
||||||
case s.Browser != t.Browser:
|
|
||||||
return "Browser configuration differs"
|
|
||||||
case s.Domain != t.Domain:
|
|
||||||
return "Domain configuration differs"
|
|
||||||
case s.StorageClass != t.StorageClass:
|
case s.StorageClass != t.StorageClass:
|
||||||
return "StorageClass configuration differs"
|
return "StorageClass configuration differs"
|
||||||
case !reflect.DeepEqual(s.Cache, t.Cache):
|
case !reflect.DeepEqual(s.Cache, t.Cache):
|
||||||
return "Cache configuration differs"
|
return "Cache configuration differs"
|
||||||
|
case !reflect.DeepEqual(s.Compression, t.Compression):
|
||||||
|
return "Compression configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.AMQP, t.Notify.AMQP):
|
case !reflect.DeepEqual(s.Notify.AMQP, t.Notify.AMQP):
|
||||||
return "AMQP Notification configuration differs"
|
return "AMQP Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.NATS, t.Notify.NATS):
|
case !reflect.DeepEqual(s.Notify.NATS, t.Notify.NATS):
|
||||||
@@ -442,7 +440,6 @@ func newServerConfig() *serverConfig {
|
|||||||
Version: serverConfigVersion,
|
Version: serverConfigVersion,
|
||||||
Credential: cred,
|
Credential: cred,
|
||||||
Region: globalMinioDefaultRegion,
|
Region: globalMinioDefaultRegion,
|
||||||
Browser: true,
|
|
||||||
StorageClass: storageClassConfig{
|
StorageClass: storageClassConfig{
|
||||||
Standard: storageClass{},
|
Standard: storageClass{},
|
||||||
RRS: storageClass{},
|
RRS: storageClass{},
|
||||||
@@ -455,6 +452,11 @@ func newServerConfig() *serverConfig {
|
|||||||
},
|
},
|
||||||
KMS: crypto.KMSConfig{},
|
KMS: crypto.KMSConfig{},
|
||||||
Notify: notifier{},
|
Notify: notifier{},
|
||||||
|
Compression: compressionConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Extensions: globalCompressExtensions,
|
||||||
|
MimeTypes: globalCompressMimeTypes,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to initialize notification configs.
|
// Make sure to initialize notification configs.
|
||||||
@@ -495,18 +497,12 @@ func (s *serverConfig) loadToCachedConfigs() {
|
|||||||
if !globalIsEnvCreds {
|
if !globalIsEnvCreds {
|
||||||
globalActiveCred = s.GetCredential()
|
globalActiveCred = s.GetCredential()
|
||||||
}
|
}
|
||||||
if !globalIsEnvBrowser {
|
|
||||||
globalIsBrowserEnabled = s.GetBrowser()
|
|
||||||
}
|
|
||||||
if !globalIsEnvWORM {
|
if !globalIsEnvWORM {
|
||||||
globalWORMEnabled = s.GetWorm()
|
globalWORMEnabled = s.GetWorm()
|
||||||
}
|
}
|
||||||
if !globalIsEnvRegion {
|
if !globalIsEnvRegion {
|
||||||
globalServerRegion = s.GetRegion()
|
globalServerRegion = s.GetRegion()
|
||||||
}
|
}
|
||||||
if !globalIsEnvDomainName {
|
|
||||||
globalDomainName = s.Domain
|
|
||||||
}
|
|
||||||
if !globalIsStorageClass {
|
if !globalIsStorageClass {
|
||||||
globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
|
globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
|
||||||
}
|
}
|
||||||
@@ -524,11 +520,31 @@ func (s *serverConfig) loadToCachedConfigs() {
|
|||||||
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !globalIsCompressionEnabled {
|
||||||
|
compressionConf := s.GetCompressionConfig()
|
||||||
|
globalCompressExtensions = compressionConf.Extensions
|
||||||
|
globalCompressMimeTypes = compressionConf.MimeTypes
|
||||||
|
globalIsCompressionEnabled = compressionConf.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMValidators == nil {
|
||||||
|
globalIAMValidators = getAuthValidators(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalPolicyOPA == nil {
|
||||||
|
if s.Policy.OPA.URL != nil && s.Policy.OPA.URL.String() != "" {
|
||||||
|
globalPolicyOPA = iampolicy.NewOpa(iampolicy.OpaArgs{
|
||||||
|
URL: s.Policy.OPA.URL,
|
||||||
|
AuthToken: s.Policy.OPA.AuthToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newConfig - initialize a new server config, saves env parameters if
|
// newSrvConfig - initialize a new server config, saves env parameters if
|
||||||
// found, otherwise use default parameters
|
// found, otherwise use default parameters
|
||||||
func newConfig(objAPI ObjectLayer) error {
|
func newSrvConfig(objAPI ObjectLayer) error {
|
||||||
// Initialize server config.
|
// Initialize server config.
|
||||||
srvCfg := newServerConfig()
|
srvCfg := newServerConfig()
|
||||||
|
|
||||||
@@ -579,6 +595,20 @@ func loadConfig(objAPI ObjectLayer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAuthValidators - returns ValidatorList which contains
|
||||||
|
// enabled providers in server config.
|
||||||
|
// A new authentication provider is added like below
|
||||||
|
// * Add a new provider in pkg/iam/validator package.
|
||||||
|
func getAuthValidators(config *serverConfig) *validator.Validators {
|
||||||
|
validators := validator.NewValidators()
|
||||||
|
|
||||||
|
if config.OpenID.JWKS.URL != nil {
|
||||||
|
validators.Add(validator.NewJWT(config.OpenID.JWKS))
|
||||||
|
}
|
||||||
|
|
||||||
|
return validators
|
||||||
|
}
|
||||||
|
|
||||||
// getNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
// getNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
||||||
// A new notification target is added like below
|
// A new notification target is added like below
|
||||||
// * Add a new target in pkg/event/target package.
|
// * Add a new target in pkg/event/target package.
|
||||||
|
|||||||
+24
-20
@@ -104,7 +104,7 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
|||||||
initConfig(objLayer)
|
initConfig(objLayer)
|
||||||
|
|
||||||
// Check if serverConfig has browser disabled
|
// Check if serverConfig has browser disabled
|
||||||
if globalServerConfig.GetBrowser() {
|
if globalIsBrowserEnabled {
|
||||||
t.Error("Expected browser to be disabled but it is not")
|
t.Error("Expected browser to be disabled but it is not")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,8 +130,8 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if serverConfig has the correct domain
|
// Check if serverConfig has the correct domain
|
||||||
if globalServerConfig.Domain != "domain.com" {
|
if globalDomainName != "domain.com" {
|
||||||
t.Errorf("Expected Domain to be `domain.com`, found " + globalServerConfig.Domain)
|
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
if err = saveConfig(objLayer, configPath, []byte(testCase.configData)); err != nil {
|
if err = saveConfig(context.Background(), objLayer, configPath, []byte(testCase.configData)); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err = getValidConfig(objLayer)
|
_, err = getValidConfig(objLayer)
|
||||||
@@ -260,77 +260,81 @@ func TestConfigDiff(t *testing.T) {
|
|||||||
{&serverConfig{}, nil, "Given configuration is empty"},
|
{&serverConfig{}, nil, "Given configuration is empty"},
|
||||||
// 2
|
// 2
|
||||||
{
|
{
|
||||||
&serverConfig{Credential: auth.Credentials{"u1", "p1"}},
|
&serverConfig{Credential: auth.Credentials{
|
||||||
&serverConfig{Credential: auth.Credentials{"u1", "p2"}},
|
AccessKey: "u1",
|
||||||
|
SecretKey: "p1",
|
||||||
|
Expiration: timeSentinel,
|
||||||
|
}},
|
||||||
|
&serverConfig{Credential: auth.Credentials{
|
||||||
|
AccessKey: "u1",
|
||||||
|
SecretKey: "p2",
|
||||||
|
Expiration: timeSentinel,
|
||||||
|
}},
|
||||||
"Credential configuration differs",
|
"Credential configuration differs",
|
||||||
},
|
},
|
||||||
// 3
|
// 3
|
||||||
{&serverConfig{Region: "us-east-1"}, &serverConfig{Region: "us-west-1"}, "Region configuration differs"},
|
{&serverConfig{Region: "us-east-1"}, &serverConfig{Region: "us-west-1"}, "Region configuration differs"},
|
||||||
// 4
|
// 4
|
||||||
{&serverConfig{Browser: false}, &serverConfig{Browser: true}, "Browser configuration differs"},
|
|
||||||
// 5
|
|
||||||
{&serverConfig{Domain: "domain1"}, &serverConfig{Domain: "domain2"}, "Domain configuration differs"},
|
|
||||||
// 6
|
|
||||||
{
|
{
|
||||||
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 6}}},
|
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 6}}},
|
||||||
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 4}}},
|
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 4}}},
|
||||||
"StorageClass configuration differs",
|
"StorageClass configuration differs",
|
||||||
},
|
},
|
||||||
// 7
|
// 5
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: false}}}},
|
||||||
"AMQP Notification configuration differs",
|
"AMQP Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 8
|
// 6
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: false}}}},
|
||||||
"NATS Notification configuration differs",
|
"NATS Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 9
|
// 7
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: false}}}},
|
||||||
"ElasticSearch Notification configuration differs",
|
"ElasticSearch Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 10
|
// 8
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: false}}}},
|
||||||
"Redis Notification configuration differs",
|
"Redis Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 11
|
// 9
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: false}}}},
|
||||||
"PostgreSQL Notification configuration differs",
|
"PostgreSQL Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 12
|
// 10
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: false}}}},
|
||||||
"Kafka Notification configuration differs",
|
"Kafka Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 13
|
// 11
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: false}}}},
|
||||||
"Webhook Notification configuration differs",
|
"Webhook Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 14
|
// 12
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: false}}}},
|
||||||
"MySQL Notification configuration differs",
|
"MySQL Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 15
|
// 13
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
||||||
"MQTT Notification configuration differs",
|
"MQTT Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 16
|
// 14
|
||||||
{
|
{
|
||||||
&serverConfig{Logger: loggerConfig{
|
&serverConfig{Logger: loggerConfig{
|
||||||
Console: loggerConsole{Enabled: true},
|
Console: loggerConsole{Enabled: true},
|
||||||
|
|||||||
+157
-7
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -29,6 +30,8 @@ import (
|
|||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
@@ -2387,6 +2390,7 @@ func migrateV27ToV28() error {
|
|||||||
|
|
||||||
// config V28 is backward compatible with V27, load the old
|
// config V28 is backward compatible with V27, load the old
|
||||||
// config file in serverConfigV28 struct and initialize KMSConfig
|
// config file in serverConfigV28 struct and initialize KMSConfig
|
||||||
|
|
||||||
srvConfig := &serverConfigV28{}
|
srvConfig := &serverConfigV28{}
|
||||||
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -2409,29 +2413,175 @@ func migrateV27ToV28() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrates '.minio.sys/config.json' v27 to v28.
|
// Migrates '.minio.sys/config.json' to v31.
|
||||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||||
return migrateV27ToV28MinioSys(objAPI)
|
if err := migrateV27ToV28MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateV28ToV29MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateV29ToV30MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return migrateV30ToV31MinioSys(objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
|
||||||
|
data, err := readConfig(context.Background(), objAPI, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionConfig struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
vcfg := &versionConfig
|
||||||
|
if err = json.Unmarshal(data, vcfg); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return vcfg.Version == version, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
ok, data, err := checkConfigVersion(objAPI, configFile, "27")
|
||||||
if err == errConfigNotFound {
|
if err == errConfigNotFound {
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("Unable to load config file. %v", err)
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
}
|
}
|
||||||
if srvConfig.Version != "27" {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srvConfig.Version = "28"
|
cfg := &serverConfigV28{}
|
||||||
srvConfig.KMS = crypto.KMSConfig{}
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
if err = saveServerConfig(context.Background(), objAPI, srvConfig); err != nil {
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "28"
|
||||||
|
cfg.KMS = crypto.KMSConfig{}
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateV28ToV29MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "28")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV29{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "29"
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "28", "29")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "29")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV30{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "30"
|
||||||
|
// Init compression config.For future migration, Compression config needs to be copied over from previous version.
|
||||||
|
cfg.Compression.Enabled = false
|
||||||
|
cfg.Compression.Extensions = globalCompressExtensions
|
||||||
|
cfg.Compression.MimeTypes = globalCompressMimeTypes
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "29", "30")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "30")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV31{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "31"
|
||||||
|
cfg.OpenID.JWKS = validator.JWKSArgs{
|
||||||
|
URL: &xnet.URL{},
|
||||||
|
}
|
||||||
|
cfg.Policy.OPA = iampolicy.OpaArgs{
|
||||||
|
URL: &xnet.URL{},
|
||||||
|
AuthToken: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "30", "31")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if a config migration from v2 to v28 is successfully done
|
// Test if a config migration from v2 to v30 is successfully done
|
||||||
func TestServerConfigMigrateV2toV28(t *testing.T) {
|
func TestServerConfigMigrateV2toV30(t *testing.T) {
|
||||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -203,6 +203,10 @@ func TestServerConfigMigrateV2toV28(t *testing.T) {
|
|||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := migrateMinioSysConfig(objLayer); err != nil {
|
||||||
|
t.Fatal("Unexpected error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize server config and check again if everything is fine
|
// Initialize server config and check again if everything is fine
|
||||||
if err := loadConfig(objLayer); err != nil {
|
if err := loadConfig(objLayer); err != nil {
|
||||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||||
@@ -218,6 +222,7 @@ func TestServerConfigMigrateV2toV28(t *testing.T) {
|
|||||||
if globalServerConfig.Credential.AccessKey != accessKey {
|
if globalServerConfig.Credential.AccessKey != accessKey {
|
||||||
t.Fatalf("Access key lost during migration, expected: %v, found:%v", accessKey, globalServerConfig.Credential.AccessKey)
|
t.Fatalf("Access key lost during migration, expected: %v, found:%v", accessKey, globalServerConfig.Credential.AccessKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalServerConfig.Credential.SecretKey != secretKey {
|
if globalServerConfig.Credential.SecretKey != secretKey {
|
||||||
t.Fatalf("Secret key lost during migration, expected: %v, found: %v", secretKey, globalServerConfig.Credential.SecretKey)
|
t.Fatalf("Secret key lost during migration, expected: %v, found: %v", secretKey, globalServerConfig.Credential.SecretKey)
|
||||||
}
|
}
|
||||||
@@ -314,7 +319,6 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
|||||||
if err := migrateV26ToV27(); err == nil {
|
if err := migrateV26ToV27(); err == nil {
|
||||||
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
|
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrateV27ToV28(); err == nil {
|
if err := migrateV27ToV28(); err == nil {
|
||||||
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
|
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-2
@@ -22,6 +22,8 @@ import (
|
|||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -737,9 +739,7 @@ type serverConfigV28 struct {
|
|||||||
// S3 API configuration.
|
// S3 API configuration.
|
||||||
Credential auth.Credentials `json:"credential"`
|
Credential auth.Credentials `json:"credential"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Browser BoolFlag `json:"browser"`
|
|
||||||
Worm BoolFlag `json:"worm"`
|
Worm BoolFlag `json:"worm"`
|
||||||
Domain string `json:"domain"`
|
|
||||||
|
|
||||||
// Storage class configuration
|
// Storage class configuration
|
||||||
StorageClass storageClassConfig `json:"storageclass"`
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
@@ -756,3 +756,84 @@ type serverConfigV28 struct {
|
|||||||
// Logger configuration
|
// Logger configuration
|
||||||
Logger loggerConfig `json:"logger"`
|
Logger loggerConfig `json:"logger"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serverConfigV29 is just like version '28'.
|
||||||
|
type serverConfigV29 serverConfigV28
|
||||||
|
|
||||||
|
// compressionConfig represents the compression settings.
|
||||||
|
type compressionConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Extensions []string `json:"extensions"`
|
||||||
|
MimeTypes []string `json:"mime-types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV30 is just like version '29', stores additionally
|
||||||
|
// extensions and mimetypes fields for compression.
|
||||||
|
type serverConfigV30 struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifier `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
|
||||||
|
// Compression configuration
|
||||||
|
Compression compressionConfig `json:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV31 is just like version '30', with OPA and OpenID configuration.
|
||||||
|
type serverConfigV31 struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifier `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
|
||||||
|
// Compression configuration
|
||||||
|
Compression compressionConfig `json:"compress"`
|
||||||
|
|
||||||
|
// OpenID configuration
|
||||||
|
OpenID struct {
|
||||||
|
// JWKS validator config.
|
||||||
|
JWKS validator.JWKSArgs `json:"jwks"`
|
||||||
|
} `json:"openid"`
|
||||||
|
|
||||||
|
// External policy enforcements.
|
||||||
|
Policy struct {
|
||||||
|
// OPA configuration.
|
||||||
|
OPA iampolicy.OpaArgs `json:"opa"`
|
||||||
|
|
||||||
|
// Add new external policy enforcements here.
|
||||||
|
} `json:"policy"`
|
||||||
|
}
|
||||||
|
|||||||
+53
-110
@@ -20,16 +20,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,16 +59,10 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a backup of the current config
|
// Create a backup of the current config
|
||||||
reader, err := readConfig(ctx, objAPI, configFile)
|
oldData, err := readConfig(ctx, objAPI, configFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var oldData []byte
|
|
||||||
oldData, err = ioutil.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
backupConfigFile := path.Join(minioConfigPrefix, minioConfigBackupFile)
|
backupConfigFile := path.Join(minioConfigPrefix, minioConfigBackupFile)
|
||||||
err = saveConfig(objAPI, backupConfigFile, oldData)
|
if err = saveConfig(ctx, objAPI, backupConfigFile, oldData); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -81,40 +72,18 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the new config in the std config path
|
// Save the new config in the std config path
|
||||||
return saveConfig(objAPI, configFile, data)
|
return saveConfig(ctx, objAPI, configFile, data)
|
||||||
}
|
|
||||||
|
|
||||||
func readConfigEtcd(configFile string) ([]byte, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
||||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
|
||||||
defer cancel()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.Count == 0 {
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
|
||||||
for _, ev := range resp.Kvs {
|
|
||||||
if string(ev.Key) == configFile {
|
|
||||||
return ev.Value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
||||||
var configData []byte
|
var configData []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
if globalEtcdClient != nil {
|
if globalEtcdClient != nil {
|
||||||
configData, err = readConfigEtcd(configFile)
|
configData, err = readConfigEtcd(ctx, globalEtcdClient, configFile)
|
||||||
} else {
|
} else {
|
||||||
var reader io.Reader
|
configData, err = readConfig(ctx, objAPI, configFile)
|
||||||
reader, err = readConfig(ctx, objAPI, configFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
configData, err = ioutil.ReadAll(reader)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -129,82 +98,17 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = &serverConfig{}
|
var config = &serverConfig{}
|
||||||
if err := json.Unmarshal(configData, config); err != nil {
|
if err = json.Unmarshal(configData, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := quick.CheckData(config); err != nil {
|
if err = quick.CheckData(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkServerConfigEtcd(configFile string) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
||||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
|
||||||
defer cancel()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.Count == 0 {
|
|
||||||
return errConfigNotFound
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkServerConfig(ctx context.Context, objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
if globalEtcdClient != nil {
|
|
||||||
return checkServerConfigEtcd(configFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
|
|
||||||
// Convert ObjectNotFound, Quorum errors into errConfigNotFound
|
|
||||||
if isErrObjectNotFound(err) || isInsufficientReadQuorum(err) {
|
|
||||||
return errConfigNotFound
|
|
||||||
}
|
|
||||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
|
|
||||||
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = objAPI.PutObject(context.Background(), minioMetaBucket, configFile, hashReader, nil, ObjectOptions{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var errConfigNotFound = errors.New("config file not found")
|
|
||||||
|
|
||||||
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
// Read entire content by setting size to -1
|
|
||||||
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, "", ObjectOptions{}); err != nil {
|
|
||||||
// Convert ObjectNotFound, IncompleteBody and Quorum errors into errConfigNotFound
|
|
||||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) || isInsufficientReadQuorum(err) {
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return config not found on empty content.
|
|
||||||
if buffer.Len() == 0 {
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return &buffer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigSys - config system.
|
// ConfigSys - config system.
|
||||||
type ConfigSys struct{}
|
type ConfigSys struct{}
|
||||||
|
|
||||||
@@ -218,7 +122,33 @@ func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
|
|||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
return initConfig(objAPI)
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
// Initializing configuration needs a retry mechanism for
|
||||||
|
// the following reasons:
|
||||||
|
// - Read quorum is lost just after the initialization
|
||||||
|
// of the object layer.
|
||||||
|
// - Write quorum not met when upgrading configuration
|
||||||
|
// version is needed.
|
||||||
|
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _ = <-retryTimerCh:
|
||||||
|
err := initConfig(objAPI)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||||
|
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||||
|
logger.Info("Waiting for configuration to be initialized..")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigSys - creates new config system object.
|
// NewConfigSys - creates new config system object.
|
||||||
@@ -230,8 +160,9 @@ func NewConfigSys() *ConfigSys {
|
|||||||
func migrateConfigToMinioSys(objAPI ObjectLayer) error {
|
func migrateConfigToMinioSys(objAPI ObjectLayer) error {
|
||||||
defer os.Rename(getConfigFile(), getConfigFile()+".deprecated")
|
defer os.Rename(getConfigFile(), getConfigFile()+".deprecated")
|
||||||
|
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
// Verify if backend already has the file.
|
// Verify if backend already has the file.
|
||||||
if err := checkServerConfig(context.Background(), objAPI); err != errConfigNotFound {
|
if err := checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||||
return err
|
return err
|
||||||
} // if errConfigNotFound proceed to migrate..
|
} // if errConfigNotFound proceed to migrate..
|
||||||
|
|
||||||
@@ -260,7 +191,14 @@ func initConfig(objAPI ObjectLayer) error {
|
|||||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||||
cancel()
|
cancel()
|
||||||
if err == nil && resp.Count > 0 {
|
if err == nil && resp.Count > 0 {
|
||||||
return migrateConfig()
|
if err = migrateConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates etcd ${HOME}/.minio/config.json to '/config/config.json'
|
||||||
|
if err := migrateConfigToMinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if isFile(getConfigFile()) {
|
if isFile(getConfigFile()) {
|
||||||
@@ -276,10 +214,15 @@ func initConfig(objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkServerConfig(context.Background(), objAPI); err != nil {
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
// Watch config for changes and reloads them in-memory.
|
||||||
|
go watchConfig(objAPI, configFile, loadConfig)
|
||||||
|
|
||||||
|
if err := checkConfig(context.Background(), objAPI, configFile); err != nil {
|
||||||
if err == errConfigNotFound {
|
if err == errConfigNotFound {
|
||||||
// Config file does not exist, we create it fresh and return upon success.
|
// Config file does not exist, we create it fresh and return upon success.
|
||||||
if err = newConfig(objAPI); err != nil {
|
if err = newSrvConfig(objAPI); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+25
-63
@@ -17,11 +17,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Writes S3 compatible copy part range error.
|
// Writes S3 compatible copy part range error.
|
||||||
@@ -39,68 +36,33 @@ func writeCopyPartErr(w http.ResponseWriter, err error, url *url.URL) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses x-amz-copy-source-range for CopyObjectPart API. Specifically written to
|
// Parses x-amz-copy-source-range for CopyObjectPart API. Its behavior
|
||||||
// differentiate the behavior between regular httpRange header v/s x-amz-copy-source-range.
|
// is different from regular HTTP range header. It only supports the
|
||||||
// The range of bytes to copy from the source object. The range value must use the form
|
// form `bytes=first-last` where first and last are zero-based byte
|
||||||
// bytes=first-last, where the first and last are the zero-based byte offsets to copy.
|
// offsets. See
|
||||||
// For example, bytes=0-9 indicates that you want to copy the first ten bytes of the source.
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
|
||||||
func parseCopyPartRange(rangeString string, resourceSize int64) (hrange *httpRange, err error) {
|
// for full details. This function treats an empty rangeString as
|
||||||
// Return error if given range string doesn't start with byte range prefix.
|
// referring to the whole resource.
|
||||||
if !strings.HasPrefix(rangeString, byteRangePrefix) {
|
func parseCopyPartRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
|
||||||
return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
|
hrange, err = parseRequestRangeSpec(rangeString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if hrange.IsSuffixLength || hrange.Start < 0 || hrange.End < 0 {
|
||||||
// Trim byte range prefix.
|
|
||||||
byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix)
|
|
||||||
|
|
||||||
// Check if range string contains delimiter '-', else return error. eg. "bytes=8"
|
|
||||||
sepIndex := strings.Index(byteRangeString, "-")
|
|
||||||
if sepIndex == -1 {
|
|
||||||
return nil, errInvalidRange
|
return nil, errInvalidRange
|
||||||
}
|
}
|
||||||
|
return hrange, nil
|
||||||
offsetBeginString := byteRangeString[:sepIndex]
|
}
|
||||||
offsetBegin := int64(-1)
|
|
||||||
// Convert offsetBeginString only if its not empty.
|
// checkCopyPartRangeWithSize adds more check to the range string in case of
|
||||||
if len(offsetBeginString) > 0 {
|
// copy object part. This API requires having specific start and end range values
|
||||||
if !validBytePos.MatchString(offsetBeginString) {
|
// e.g. 'bytes=3-10'. Other use cases will be rejected.
|
||||||
return nil, errInvalidRange
|
func checkCopyPartRangeWithSize(rs *HTTPRangeSpec, resourceSize int64) (err error) {
|
||||||
}
|
if rs == nil {
|
||||||
if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
|
return nil
|
||||||
return nil, errInvalidRange
|
}
|
||||||
}
|
if rs.IsSuffixLength || rs.Start >= resourceSize || rs.End >= resourceSize {
|
||||||
}
|
return errInvalidRangeSource
|
||||||
|
}
|
||||||
offsetEndString := byteRangeString[sepIndex+1:]
|
return nil
|
||||||
offsetEnd := int64(-1)
|
|
||||||
// Convert offsetEndString only if its not empty.
|
|
||||||
if len(offsetEndString) > 0 {
|
|
||||||
if !validBytePos.MatchString(offsetEndString) {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rangeString contains first byte positions. eg. "bytes=2-" or
|
|
||||||
// rangeString contains last bye positions. eg. "bytes=-2"
|
|
||||||
if offsetBegin == -1 || offsetEnd == -1 {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last byte position should not be greater than first byte
|
|
||||||
// position. eg. "bytes=5-2"
|
|
||||||
if offsetBegin > offsetEnd {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// First and last byte positions should not be >= resourceSize.
|
|
||||||
if offsetBegin >= resourceSize || offsetEnd >= resourceSize {
|
|
||||||
return nil, errInvalidRangeSource
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success..
|
|
||||||
return &httpRange{offsetBegin, offsetEnd, resourceSize}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-17
@@ -19,35 +19,37 @@ package cmd
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
// Test parseCopyPartRange()
|
// Test parseCopyPartRange()
|
||||||
func TestParseCopyPartRange(t *testing.T) {
|
func TestParseCopyPartRangeSpec(t *testing.T) {
|
||||||
// Test success cases.
|
// Test success cases.
|
||||||
successCases := []struct {
|
successCases := []struct {
|
||||||
rangeString string
|
rangeString string
|
||||||
offsetBegin int64
|
offsetBegin int64
|
||||||
offsetEnd int64
|
offsetEnd int64
|
||||||
length int64
|
|
||||||
}{
|
}{
|
||||||
{"bytes=2-5", 2, 5, 4},
|
{"bytes=2-5", 2, 5},
|
||||||
{"bytes=2-9", 2, 9, 8},
|
{"bytes=2-9", 2, 9},
|
||||||
{"bytes=2-2", 2, 2, 1},
|
{"bytes=2-2", 2, 2},
|
||||||
{"bytes=0000-0006", 0, 6, 7},
|
{"bytes=0000-0006", 0, 6},
|
||||||
}
|
}
|
||||||
|
objectSize := int64(10)
|
||||||
|
|
||||||
for _, successCase := range successCases {
|
for _, successCase := range successCases {
|
||||||
hrange, err := parseCopyPartRange(successCase.rangeString, 10)
|
rs, err := parseCopyPartRangeSpec(successCase.rangeString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected: <nil>, got: %s", err)
|
t.Fatalf("expected: <nil>, got: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hrange.offsetBegin != successCase.offsetBegin {
|
start, length, err1 := rs.GetOffsetLength(objectSize)
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.offsetBegin, hrange.offsetBegin)
|
if err1 != nil {
|
||||||
|
t.Fatalf("expected: <nil>, got: %s", err1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hrange.offsetEnd != successCase.offsetEnd {
|
if start != successCase.offsetBegin {
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.offsetEnd, hrange.offsetEnd)
|
t.Fatalf("expected: %d, got: %d", successCase.offsetBegin, start)
|
||||||
}
|
}
|
||||||
if hrange.getLength() != successCase.length {
|
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.length, hrange.getLength())
|
if start+length-1 != successCase.offsetEnd {
|
||||||
|
t.Fatalf("expected: %d, got: %d", successCase.offsetEnd, start+length-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +61,16 @@ func TestParseCopyPartRange(t *testing.T) {
|
|||||||
"bytes=2-+5",
|
"bytes=2-+5",
|
||||||
"bytes=2--5",
|
"bytes=2--5",
|
||||||
"bytes=-",
|
"bytes=-",
|
||||||
"",
|
|
||||||
"2-5",
|
"2-5",
|
||||||
"bytes = 2-5",
|
"bytes = 2-5",
|
||||||
"bytes=2 - 5",
|
"bytes=2 - 5",
|
||||||
"bytes=0-0,-1",
|
"bytes=0-0,-1",
|
||||||
"bytes=2-5 ",
|
"bytes=2-5 ",
|
||||||
|
"bytes=-1",
|
||||||
|
"bytes=1-",
|
||||||
}
|
}
|
||||||
for _, rangeString := range invalidRangeStrings {
|
for _, rangeString := range invalidRangeStrings {
|
||||||
if _, err := parseCopyPartRange(rangeString, 10); err == nil {
|
if _, err := parseCopyPartRangeSpec(rangeString); err == nil {
|
||||||
t.Fatalf("expected: an error, got: <nil> for range %s", rangeString)
|
t.Fatalf("expected: an error, got: <nil> for range %s", rangeString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,8 +81,14 @@ func TestParseCopyPartRange(t *testing.T) {
|
|||||||
"bytes=20-30",
|
"bytes=20-30",
|
||||||
}
|
}
|
||||||
for _, rangeString := range errorRangeString {
|
for _, rangeString := range errorRangeString {
|
||||||
if _, err := parseCopyPartRange(rangeString, 10); err != errInvalidRangeSource {
|
rs, err := parseCopyPartRangeSpec(rangeString)
|
||||||
t.Fatalf("expected: %s, got: %s", errInvalidRangeSource, err)
|
if err == nil {
|
||||||
|
err1 := checkCopyPartRangeWithSize(rs, objectSize)
|
||||||
|
if err1 != errInvalidRangeSource {
|
||||||
|
t.Fatalf("expected: %s, got: %s", errInvalidRangeSource, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected: %s, got: <nil>", errInvalidRangeSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ const (
|
|||||||
SSEAlgorithmKMS = "aws:kms"
|
SSEAlgorithmKMS = "aws:kms"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RemoveSensitiveHeaders removes confidential encryption
|
||||||
|
// information - e.g. the SSE-C key - from the HTTP headers.
|
||||||
|
// It has the same semantics as RemoveSensitiveEntires.
|
||||||
|
func RemoveSensitiveHeaders(h http.Header) {
|
||||||
|
h.Del(SSECKey)
|
||||||
|
h.Del(SSECopyKey)
|
||||||
|
}
|
||||||
|
|
||||||
// S3 represents AWS SSE-S3. It provides functionality to handle
|
// S3 represents AWS SSE-S3. It provides functionality to handle
|
||||||
// SSE-S3 requests.
|
// SSE-S3 requests.
|
||||||
var S3 = s3{}
|
var S3 = s3{}
|
||||||
@@ -164,7 +172,6 @@ func (ssecCopy) IsRequested(h http.Header) bool {
|
|||||||
// ParseHTTP parses the SSE-C headers and returns the SSE-C client key
|
// ParseHTTP parses the SSE-C headers and returns the SSE-C client key
|
||||||
// on success. SSE-C copy headers are ignored.
|
// on success. SSE-C copy headers are ignored.
|
||||||
func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
||||||
defer h.Del(SSECKey) // remove SSE-C key from headers after parsing
|
|
||||||
if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 {
|
if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 {
|
||||||
return key, ErrInvalidCustomerAlgorithm
|
return key, ErrInvalidCustomerAlgorithm
|
||||||
}
|
}
|
||||||
@@ -190,7 +197,6 @@ func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
|||||||
// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key
|
// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key
|
||||||
// on success. Regular SSE-C headers are ignored.
|
// on success. Regular SSE-C headers are ignored.
|
||||||
func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
||||||
defer h.Del(SSECopyKey) // remove SSE-C copy key of source object from headers after parsing
|
|
||||||
if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 {
|
if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 {
|
||||||
return key, ErrInvalidCustomerAlgorithm
|
return key, ErrInvalidCustomerAlgorithm
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-3
@@ -16,6 +16,7 @@ package crypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -250,9 +251,6 @@ func TestSSECParse(t *testing.T) {
|
|||||||
if err == nil && key == zeroKey {
|
if err == nil && key == zeroKey {
|
||||||
t.Errorf("Test %d: parsed client key is zero key", i)
|
t.Errorf("Test %d: parsed client key is zero key", i)
|
||||||
}
|
}
|
||||||
if _, ok := test.Header[SSECKey]; ok {
|
|
||||||
t.Errorf("Test %d: client key is not removed from HTTP headers after parsing", i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,3 +335,102 @@ func TestSSECopyParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var removeSensitiveHeadersTests = []struct {
|
||||||
|
Header, ExpectedHeader http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
SSECKey: []string{""},
|
||||||
|
SSECopyKey: []string{""},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{},
|
||||||
|
},
|
||||||
|
{ // Standard SSE-C request headers
|
||||||
|
Header: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // Standard SSE-C + SSE-C-copy request headers
|
||||||
|
Header: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
SSECopyKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // Standard SSE-C + metadata request headers
|
||||||
|
Header: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
"X-Amz-Meta-Test-1": []string{"Test-1"},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
"X-Amz-Meta-Test-1": []string{"Test-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveSensitiveHeaders(t *testing.T) {
|
||||||
|
isEqual := func(x, y http.Header) bool {
|
||||||
|
if len(x) != len(y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range x {
|
||||||
|
u, ok := y[k]
|
||||||
|
if !ok || len(v) != len(u) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sort.Strings(v)
|
||||||
|
sort.Strings(u)
|
||||||
|
for j := range v {
|
||||||
|
if v[j] != u[j] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
areKeysEqual := func(h http.Header, metadata map[string]string) bool {
|
||||||
|
if len(h) != len(metadata) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k := range h {
|
||||||
|
if _, ok := metadata[k]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range removeSensitiveHeadersTests {
|
||||||
|
metadata := make(map[string]string, len(test.Header))
|
||||||
|
for k := range test.Header {
|
||||||
|
metadata[k] = "" // set metadata key - we don't care about the value
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveSensitiveHeaders(test.Header)
|
||||||
|
if !isEqual(test.ExpectedHeader, test.Header) {
|
||||||
|
t.Errorf("Test %d: filtered headers do not match expected headers - got: %v , want: %v", i, test.Header, test.ExpectedHeader)
|
||||||
|
}
|
||||||
|
RemoveSensitiveEntries(metadata)
|
||||||
|
if !areKeysEqual(test.ExpectedHeader, metadata) {
|
||||||
|
t.Errorf("Test %d: filtered headers do not match expected headers - got: %v , want: %v", i, test.Header, test.ExpectedHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,3 +140,37 @@ func (key ObjectKey) DerivePartKey(id uint32) (partKey [32]byte) {
|
|||||||
mac.Sum(partKey[:0])
|
mac.Sum(partKey[:0])
|
||||||
return partKey
|
return partKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SealETag seals the etag using the object key.
|
||||||
|
// It does not encrypt empty ETags because such ETags indicate
|
||||||
|
// that the S3 client hasn't sent an ETag = MD5(object) and
|
||||||
|
// the backend can pick an ETag value.
|
||||||
|
func (key ObjectKey) SealETag(etag []byte) []byte {
|
||||||
|
if len(etag) == 0 { // don't encrypt empty ETag - only if client sent ETag = MD5(object)
|
||||||
|
return etag
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
mac := hmac.New(sha256.New, key[:])
|
||||||
|
mac.Write([]byte("SSE-etag"))
|
||||||
|
if _, err := sio.Encrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil {
|
||||||
|
logger.CriticalIf(context.Background(), errors.New("Unable to encrypt ETag using object key"))
|
||||||
|
}
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsealETag unseals the etag using the provided object key.
|
||||||
|
// It does not try to decrypt the ETag if len(etag) == 16
|
||||||
|
// because such ETags indicate that the S3 client hasn't sent
|
||||||
|
// an ETag = MD5(object) and the backend has picked an ETag value.
|
||||||
|
func (key ObjectKey) UnsealETag(etag []byte) ([]byte, error) {
|
||||||
|
if !IsETagSealed(etag) {
|
||||||
|
return etag, nil
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
mac := hmac.New(sha256.New, key[:])
|
||||||
|
mac.Write([]byte("SSE-etag"))
|
||||||
|
if _, err := sio.Decrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,3 +166,31 @@ func TestDerivePartKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sealUnsealETagTests = []string{
|
||||||
|
"",
|
||||||
|
"90682b8e8cc7609c",
|
||||||
|
"90682b8e8cc7609c4671e1d64c73fc30",
|
||||||
|
"90682b8e8cc7609c4671e1d64c73fc307fb3104f",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSealETag(t *testing.T) {
|
||||||
|
var key ObjectKey
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i)
|
||||||
|
}
|
||||||
|
for i, etag := range sealUnsealETagTests {
|
||||||
|
tag, err := hex.DecodeString(etag)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to decode etag: %s", i, err)
|
||||||
|
}
|
||||||
|
sealedETag := key.SealETag(tag)
|
||||||
|
unsealedETag, err := key.UnsealETag(sealedETag)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to decrypt etag: %s", i, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(unsealedETag, tag) {
|
||||||
|
t.Errorf("Test %d: unsealed etag does not match: got %s - want %s", i, hex.EncodeToString(unsealedETag), etag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,26 @@ func IsMultiPart(metadata map[string]string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveSensitiveEntries removes confidential encryption
|
||||||
|
// information - e.g. the SSE-C key - from the metadata map.
|
||||||
|
// It has the same semantics as RemoveSensitiveHeaders.
|
||||||
|
func RemoveSensitiveEntries(metadata map[string]string) { // The functions is tested in TestRemoveSensitiveHeaders for compatibility reasons
|
||||||
|
delete(metadata, SSECKey)
|
||||||
|
delete(metadata, SSECopyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveInternalEntries removes all crypto-specific internal
|
||||||
|
// metadata entries from the metadata map.
|
||||||
|
func RemoveInternalEntries(metadata map[string]string) {
|
||||||
|
delete(metadata, SSEMultipart)
|
||||||
|
delete(metadata, SSEIV)
|
||||||
|
delete(metadata, SSESealAlgorithm)
|
||||||
|
delete(metadata, SSECSealedKey)
|
||||||
|
delete(metadata, S3SealedKey)
|
||||||
|
delete(metadata, S3KMSKeyID)
|
||||||
|
delete(metadata, S3KMSSealedKey)
|
||||||
|
}
|
||||||
|
|
||||||
// IsEncrypted returns true if the object metadata indicates
|
// IsEncrypted returns true if the object metadata indicates
|
||||||
// that it was uploaded using some form of server-side-encryption.
|
// that it was uploaded using some form of server-side-encryption.
|
||||||
//
|
//
|
||||||
@@ -211,3 +231,6 @@ func (ssec) ParseMetadata(metadata map[string]string) (sealedKey SealedKey, err
|
|||||||
copy(sealedKey.Key[:], encryptedKey)
|
copy(sealedKey.Key[:], encryptedKey)
|
||||||
return sealedKey, nil
|
return sealedKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsETagSealed returns true if the etag seems to be encrypted.
|
||||||
|
func IsETagSealed(etag []byte) bool { return len(etag) > 16 }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package crypto
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
@@ -326,15 +327,20 @@ func TestS3CreateMetadata(t *testing.T) {
|
|||||||
_ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm})
|
_ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm})
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssecCreateMetadataTests = []SealedKey{
|
var ssecCreateMetadataTests = []struct {
|
||||||
{Algorithm: SealAlgorithm},
|
KeyID string
|
||||||
{IV: [32]byte{0xff}, Key: [64]byte{0x7e}, Algorithm: SealAlgorithm},
|
SealedDataKey []byte
|
||||||
|
SealedKey SealedKey
|
||||||
|
}{
|
||||||
|
{KeyID: "", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}},
|
||||||
|
{KeyID: "cafebabe", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}},
|
||||||
|
{KeyID: "deadbeef", SealedDataKey: make([]byte, 32), SealedKey: SealedKey{IV: [32]byte{0xf7}, Key: [64]byte{0xea}, Algorithm: SealAlgorithm}},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSSECCreateMetadata(t *testing.T) {
|
func TestSSECCreateMetadata(t *testing.T) {
|
||||||
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||||
logger.Disable = true
|
logger.Disable = true
|
||||||
for i, test := range s3CreateMetadataTests {
|
for i, test := range ssecCreateMetadataTests {
|
||||||
metadata := SSEC.CreateMetadata(nil, test.SealedKey)
|
metadata := SSEC.CreateMetadata(nil, test.SealedKey)
|
||||||
sealedKey, err := SSEC.ParseMetadata(metadata)
|
sealedKey, err := SSEC.ParseMetadata(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -359,3 +365,75 @@ func TestSSECCreateMetadata(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
_ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm})
|
_ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: "20000f00fba2ee2ae4845f725964eeb9e092edfabc7ab9f9239e8344341f769a51ce99b4801b0699b92b16a72fa94972", IsSealed: true}, // 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsETagSealed(t *testing.T) {
|
||||||
|
for i, test := range isETagSealedTests {
|
||||||
|
etag, err := hex.DecodeString(test.ETag)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to decode etag: %s", i, err)
|
||||||
|
}
|
||||||
|
if sealed := IsETagSealed(etag); sealed != test.IsSealed {
|
||||||
|
t.Errorf("Test %d: got %v - want %v", i, sealed, test.IsSealed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeInternalEntriesTests = []struct {
|
||||||
|
Metadata, Expected map[string]string
|
||||||
|
}{
|
||||||
|
{ // 0
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEMultipart: "",
|
||||||
|
SSEIV: "",
|
||||||
|
SSESealAlgorithm: "",
|
||||||
|
SSECSealedKey: "",
|
||||||
|
S3SealedKey: "",
|
||||||
|
S3KMSKeyID: "",
|
||||||
|
S3KMSSealedKey: "",
|
||||||
|
},
|
||||||
|
Expected: map[string]string{},
|
||||||
|
},
|
||||||
|
{ // 1
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEMultipart: "",
|
||||||
|
SSEIV: "",
|
||||||
|
"X-Amz-Meta-A": "X",
|
||||||
|
"X-Minio-Internal-B": "Y",
|
||||||
|
},
|
||||||
|
Expected: map[string]string{
|
||||||
|
"X-Amz-Meta-A": "X",
|
||||||
|
"X-Minio-Internal-B": "Y",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveInternalEntries(t *testing.T) {
|
||||||
|
isEqual := func(x, y map[string]string) bool {
|
||||||
|
if len(x) != len(y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range x {
|
||||||
|
if u, ok := y[k]; !ok || v != u {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range removeInternalEntriesTests {
|
||||||
|
RemoveInternalEntries(test.Metadata)
|
||||||
|
if !isEqual(test.Metadata, test.Expected) {
|
||||||
|
t.Errorf("Test %d: got %v - want %v", i, test.Metadata, test.Expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/ioutil"
|
"github.com/minio/minio/pkg/ioutil"
|
||||||
@@ -69,10 +71,59 @@ const (
|
|||||||
// domain is "SSE-S3".
|
// domain is "SSE-S3".
|
||||||
func (s3) String() string { return "SSE-S3" }
|
func (s3) String() string { return "SSE-S3" }
|
||||||
|
|
||||||
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
|
// from the metadata using KMS and returns the decrypted object
|
||||||
|
// key.
|
||||||
|
func (sse s3) UnsealObjectKey(kms KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
keyID, kmsKey, sealedKey, err := sse.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unsealKey, err := kms.UnsealKey(keyID, kmsKey, Context{bucket: path.Join(bucket, object)})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = key.Unseal(unsealKey, sealedKey, sse.String(), bucket, object)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// String returns the SSE domain as string. For SSE-C the
|
// String returns the SSE domain as string. For SSE-C the
|
||||||
// domain is "SSE-C".
|
// domain is "SSE-C".
|
||||||
func (ssec) String() string { return "SSE-C" }
|
func (ssec) String() string { return "SSE-C" }
|
||||||
|
|
||||||
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
|
// from the metadata using the SSE-C client key of the HTTP headers
|
||||||
|
// and returns the decrypted object key.
|
||||||
|
func (sse ssec) UnsealObjectKey(h http.Header, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
clientKey, err := sse.ParseHTTP(h)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return unsealObjectKey(clientKey, metadata, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
|
// from the metadata using the SSE-Copy client key of the HTTP headers
|
||||||
|
// and returns the decrypted object key.
|
||||||
|
func (sse ssecCopy) UnsealObjectKey(h http.Header, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
clientKey, err := sse.ParseHTTP(h)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return unsealObjectKey(clientKey, metadata, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsealObjectKey decrypts and returns the sealed object key
|
||||||
|
// from the metadata using the SSE-C client key.
|
||||||
|
func unsealObjectKey(clientKey [32]byte, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
sealedKey, err := SSEC.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = key.Unseal(clientKey, sealedKey, SSEC.String(), bucket, object)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptSinglePart encrypts an io.Reader which must be the
|
// EncryptSinglePart encrypts an io.Reader which must be the
|
||||||
// the body of a single-part PUT request.
|
// the body of a single-part PUT request.
|
||||||
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
||||||
|
|||||||
+196
-1
@@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestS3String(t *testing.T) {
|
func TestS3String(t *testing.T) {
|
||||||
const Domain = "SSE-S3"
|
const Domain = "SSE-S3"
|
||||||
@@ -29,3 +32,195 @@ func TestSSECString(t *testing.T) {
|
|||||||
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ssecUnsealObjectKeyTests = []struct {
|
||||||
|
Headers http.Header
|
||||||
|
Bucket, Object string
|
||||||
|
Metadata map[string]string
|
||||||
|
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{ // 0 - Valid HTTP headers and valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // 1 - Valid HTTP headers but invalid metadata entries for bucket/object2
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object2",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrSecretKeyMismatch,
|
||||||
|
},
|
||||||
|
{ // 2 - Valid HTTP headers but invalid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm,
|
||||||
|
},
|
||||||
|
{ // 3 - Invalid HTTP headers for valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKeyMD5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECUnsealObjectKey(t *testing.T) {
|
||||||
|
for i, test := range ssecUnsealObjectKeyTests {
|
||||||
|
if _, err := SSEC.UnsealObjectKey(test.Headers, test.Metadata, test.Bucket, test.Object); err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got: %v - want: %v", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sseCopyUnsealObjectKeyTests = []struct {
|
||||||
|
Headers http.Header
|
||||||
|
Bucket, Object string
|
||||||
|
Metadata map[string]string
|
||||||
|
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{ // 0 - Valid HTTP headers and valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // 1 - Valid HTTP headers but invalid metadata entries for bucket/object2
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object2",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrSecretKeyMismatch,
|
||||||
|
},
|
||||||
|
{ // 2 - Valid HTTP headers but invalid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm,
|
||||||
|
},
|
||||||
|
{ // 3 - Invalid HTTP headers for valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKeyMD5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECopyUnsealObjectKey(t *testing.T) {
|
||||||
|
for i, test := range sseCopyUnsealObjectKeyTests {
|
||||||
|
if _, err := SSECopy.UnsealObjectKey(test.Headers, test.Metadata, test.Bucket, test.Object); err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got: %v - want: %v", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var s3UnsealObjectKeyTests = []struct {
|
||||||
|
KMS KMS
|
||||||
|
Bucket, Object string
|
||||||
|
Metadata map[string]string
|
||||||
|
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{ // 0 - Valid KMS key-ID and valid metadata entries for bucket/object
|
||||||
|
KMS: NewKMS([32]byte{}),
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "hhVY0LKR1YtZbzAKxTWUfZt5enDfYX6Fxz1ma8Kiudc=",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": "IAAfALhsOeD5AE3s5Zgq3DZ5VFGsOa3B0ksVC86veDcaj+fXv2U0VadhPaOKYr9Emd5ssOsO0uIhIIrKiOy9rA==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key": "IAAfAMRS2iw45FsfiF3QXajSYVWj1lxMpQm6DxDGPtADCX6fJQQ4atHBtfpgqJFyeQmIHsm0FBI+UlHw1Lv4ug==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id": "test-key-1",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
},
|
||||||
|
ExpectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // 1 - Valid KMS key-ID for invalid metadata entries for bucket/object
|
||||||
|
KMS: NewKMS([32]byte{}),
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "hhVY0LKR1YtZbzAKxTWUfZt5enDfYX6Fxz1ma8Kiudc=",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": "IAAfALhsOeD5AE3s5Zgq3DZ5VFGsOa3B0ksVC86veDcaj+fXv2U0VadhPaOKYr9Emd5ssOsO0uIhIIrKiOy9rA==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key": "IAAfAMRS2iw45FsfiF3QXajSYVWj1lxMpQm6DxDGPtADCX6fJQQ4atHBtfpgqJFyeQmIHsm0FBI+UlHw1Lv4ug==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id": "test-key-1",
|
||||||
|
},
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3UnsealObjectKey(t *testing.T) {
|
||||||
|
for i, test := range s3UnsealObjectKeyTests {
|
||||||
|
if _, err := S3.UnsealObjectKey(test.KMS, test.Metadata, test.Bucket, test.Object); err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got: %v - want: %v", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
-7
@@ -28,8 +28,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// VaultEndpointEnv Vault endpoint environment variable
|
// vaultEndpointEnv Vault endpoint environment variable
|
||||||
VaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
|
vaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
|
||||||
// vaultAuthTypeEnv type of vault auth to be used
|
// vaultAuthTypeEnv type of vault auth to be used
|
||||||
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
|
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
|
||||||
// vaultAppRoleIDEnv Vault AppRole ID environment variable
|
// vaultAppRoleIDEnv Vault AppRole ID environment variable
|
||||||
@@ -40,6 +40,10 @@ const (
|
|||||||
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
|
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
|
||||||
// vaultKeyNameEnv Vault Encryption Key Name environment variable
|
// vaultKeyNameEnv Vault Encryption Key Name environment variable
|
||||||
vaultKeyNameEnv = "MINIO_SSE_VAULT_KEY_NAME"
|
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 (
|
var (
|
||||||
@@ -93,7 +97,7 @@ type VaultConfig struct {
|
|||||||
// been set
|
// been set
|
||||||
func validateVaultConfig(c *VaultConfig) error {
|
func validateVaultConfig(c *VaultConfig) error {
|
||||||
if c.Endpoint == "" {
|
if c.Endpoint == "" {
|
||||||
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", VaultEndpointEnv)
|
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", vaultEndpointEnv)
|
||||||
}
|
}
|
||||||
if strings.ToLower(c.Auth.Type) != "approle" {
|
if strings.ToLower(c.Auth.Type) != "approle" {
|
||||||
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
|
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
|
||||||
@@ -110,7 +114,6 @@ func validateVaultConfig(c *VaultConfig) error {
|
|||||||
if c.Key.Version < 0 {
|
if c.Key.Version < 0 {
|
||||||
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
|
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ func getVaultAccessToken(client *vault.Client, appRoleID, appSecret string) (tok
|
|||||||
// variables and performs validations.
|
// variables and performs validations.
|
||||||
func NewVaultConfig() (KMSConfig, error) {
|
func NewVaultConfig() (KMSConfig, error) {
|
||||||
kc := KMSConfig{}
|
kc := KMSConfig{}
|
||||||
endpoint := os.Getenv(VaultEndpointEnv)
|
endpoint := os.Getenv(vaultEndpointEnv)
|
||||||
roleID := os.Getenv(vaultAppRoleIDEnv)
|
roleID := os.Getenv(vaultAppRoleIDEnv)
|
||||||
roleSecret := os.Getenv(vaultAppSecretIDEnv)
|
roleSecret := os.Getenv(vaultAppSecretIDEnv)
|
||||||
keyName := os.Getenv(vaultKeyNameEnv)
|
keyName := os.Getenv(vaultKeyNameEnv)
|
||||||
@@ -177,9 +180,15 @@ func NewVaultConfig() (KMSConfig, error) {
|
|||||||
// and gets a client token for future api calls.
|
// and gets a client token for future api calls.
|
||||||
func NewVault(kmsConf KMSConfig) (KMS, error) {
|
func NewVault(kmsConf KMSConfig) (KMS, error) {
|
||||||
config := kmsConf.Vault
|
config := kmsConf.Vault
|
||||||
c, err := vault.NewClient(&vault.Config{
|
vconfig := &vault.Config{
|
||||||
Address: config.Endpoint,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate input data size and it can never be less than zero.
|
// Validate input data size and it can never be less than zero.
|
||||||
if data.Size() < 0 {
|
if data.Size() < -1 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return ObjectInfo{}, errInvalidArgument
|
return ObjectInfo{}, errInvalidArgument
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-17
@@ -31,14 +31,14 @@ import (
|
|||||||
|
|
||||||
"github.com/djherbis/atime"
|
"github.com/djherbis/atime"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/wildcard"
|
"github.com/minio/minio/pkg/wildcard"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created.
|
// disk cache needs to have object size space free for a cache entry to be created.
|
||||||
cacheSizeMultiplier = 100
|
|
||||||
cacheTrashDir = "trash"
|
cacheTrashDir = "trash"
|
||||||
cacheCleanupInterval = 10 // in minutes
|
cacheCleanupInterval = 10 // in minutes
|
||||||
)
|
)
|
||||||
@@ -57,6 +57,7 @@ type cacheObjects struct {
|
|||||||
// file path patterns to exclude from cache
|
// file path patterns to exclude from cache
|
||||||
exclude []string
|
exclude []string
|
||||||
// Object functions pointing to the corresponding functions of backend implementation.
|
// Object functions pointing to the corresponding functions of backend implementation.
|
||||||
|
GetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
|
||||||
GetObjectFn func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
|
GetObjectFn func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
|
||||||
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
PutObjectFn func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
PutObjectFn func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
@@ -88,6 +89,7 @@ type CacheObjectLayer interface {
|
|||||||
ListBuckets(ctx context.Context) (buckets []BucketInfo, err error)
|
ListBuckets(ctx context.Context) (buckets []BucketInfo, err error)
|
||||||
DeleteBucket(ctx context.Context, bucket string) error
|
DeleteBucket(ctx context.Context, bucket string) error
|
||||||
// Object operations.
|
// Object operations.
|
||||||
|
GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
|
||||||
GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
|
GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
|
||||||
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
@@ -103,6 +105,11 @@ type CacheObjectLayer interface {
|
|||||||
StorageInfo(ctx context.Context) CacheStorageInfo
|
StorageInfo(ctx context.Context) CacheStorageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCacheable returns if the object should be saved in the cache.
|
||||||
|
func (o ObjectInfo) IsCacheable() bool {
|
||||||
|
return !crypto.IsEncrypted(o.UserDefined)
|
||||||
|
}
|
||||||
|
|
||||||
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
||||||
func backendDownError(err error) bool {
|
func backendDownError(err error) bool {
|
||||||
_, backendDown := err.(BackendDown)
|
_, backendDown := err.(BackendDown)
|
||||||
@@ -175,6 +182,83 @@ func (c cacheObjects) getMetadata(objInfo ObjectInfo) map[string]string {
|
|||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
if c.isCacheExclude(bucket, object) {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch cacheFSObjects if object is currently cached or nearest available cache drive
|
||||||
|
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheReader, cacheErr := dcache.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
|
|
||||||
|
objInfo, err := c.GetObjectInfoFn(ctx, bucket, object, opts)
|
||||||
|
if backendDownError(err) && cacheErr == nil {
|
||||||
|
return cacheReader, nil
|
||||||
|
} else if err != nil {
|
||||||
|
if _, ok := err.(ObjectNotFound); ok {
|
||||||
|
// Delete cached entry if backend object was deleted.
|
||||||
|
dcache.Delete(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !objInfo.IsCacheable() || filterFromCache(objInfo.UserDefined) {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheErr == nil {
|
||||||
|
if cacheReader.ObjInfo.ETag == objInfo.ETag && !isStaleCache(objInfo) {
|
||||||
|
// Object is not stale, so serve from cache
|
||||||
|
return cacheReader, nil
|
||||||
|
}
|
||||||
|
cacheReader.Close()
|
||||||
|
// Object is stale, so delete from cache
|
||||||
|
dcache.Delete(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we got here, we are serving the request from backend,
|
||||||
|
// and also adding the object to the cache.
|
||||||
|
|
||||||
|
if rs != nil {
|
||||||
|
// We don't cache partial objects.
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
if !dcache.diskAvailable(objInfo.Size) {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
if bkErr != nil {
|
||||||
|
return nil, bkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize pipe.
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
teeReader := io.TeeReader(bkReader, pipeWriter)
|
||||||
|
hashReader, herr := hash.NewReader(pipeReader, bkReader.ObjInfo.Size, "", "", bkReader.ObjInfo.Size)
|
||||||
|
if herr != nil {
|
||||||
|
bkReader.Close()
|
||||||
|
return nil, herr
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
opts := ObjectOptions{}
|
||||||
|
putErr := dcache.Put(ctx, bucket, object, hashReader, c.getMetadata(bkReader.ObjInfo), opts)
|
||||||
|
// close the write end of the pipe, so the error gets
|
||||||
|
// propagated to getObjReader
|
||||||
|
pipeWriter.CloseWithError(putErr)
|
||||||
|
}()
|
||||||
|
|
||||||
|
cleanupBackend := func() { bkReader.Close() }
|
||||||
|
cleanupPipe := func() { pipeReader.Close() }
|
||||||
|
gr = NewGetObjectReaderFromReader(teeReader, bkReader.ObjInfo, cleanupBackend, cleanupPipe)
|
||||||
|
return gr, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Uses cached-object to serve the request. If object is not cached it serves the request from the backend and also
|
// Uses cached-object to serve the request. If object is not cached it serves the request from the backend and also
|
||||||
// stores it in the cache for serving subsequent requests.
|
// stores it in the cache for serving subsequent requests.
|
||||||
func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||||
@@ -200,6 +284,10 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !backendDown && !objInfo.IsCacheable() {
|
||||||
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
|
}
|
||||||
|
|
||||||
if !backendDown && filterFromCache(objInfo.UserDefined) {
|
if !backendDown && filterFromCache(objInfo.UserDefined) {
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
@@ -219,22 +307,18 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
|||||||
// We don't cache partial objects.
|
// We don't cache partial objects.
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
if !dcache.diskAvailable(objInfo.Size * cacheSizeMultiplier) {
|
if !dcache.diskAvailable(objInfo.Size) {
|
||||||
// cache only objects < 1/100th of disk capacity
|
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
// Initialize pipe.
|
// Initialize pipe.
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
hashReader, err := hash.NewReader(pipeReader, objInfo.Size, "", "")
|
hashReader, err := hash.NewReader(pipeReader, objInfo.Size, "", "", objInfo.Size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err = GetObjectFn(ctx, bucket, object, 0, objInfo.Size, io.MultiWriter(writer, pipeWriter), etag, opts); err != nil {
|
gerr := GetObjectFn(ctx, bucket, object, 0, objInfo.Size, io.MultiWriter(writer, pipeWriter), etag, opts)
|
||||||
pipeWriter.CloseWithError(err)
|
pipeWriter.CloseWithError(gerr) // Close writer explicitly signaling we wrote all data.
|
||||||
return
|
|
||||||
}
|
|
||||||
pipeWriter.Close() // Close writer explicitly signaling we wrote all data.
|
|
||||||
}()
|
}()
|
||||||
err = dcache.Put(ctx, bucket, object, hashReader, c.getMetadata(objInfo), opts)
|
err = dcache.Put(ctx, bucket, object, hashReader, c.getMetadata(objInfo), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -570,7 +654,7 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
|||||||
size := r.Size()
|
size := r.Size()
|
||||||
|
|
||||||
// fetch from backend if there is no space on cache drive
|
// fetch from backend if there is no space on cache drive
|
||||||
if !dcache.diskAvailable(size * cacheSizeMultiplier) {
|
if !dcache.diskAvailable(size) {
|
||||||
return putObjectFn(ctx, bucket, object, r, metadata, opts)
|
return putObjectFn(ctx, bucket, object, r, metadata, opts)
|
||||||
}
|
}
|
||||||
// fetch from backend if cache exclude pattern or cache-control
|
// fetch from backend if cache exclude pattern or cache-control
|
||||||
@@ -582,13 +666,13 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
|||||||
objInfo = ObjectInfo{}
|
objInfo = ObjectInfo{}
|
||||||
// Initialize pipe to stream data to backend
|
// Initialize pipe to stream data to backend
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
hashReader, err := hash.NewReader(pipeReader, size, r.MD5HexString(), r.SHA256HexString())
|
hashReader, err := hash.NewReader(pipeReader, size, r.MD5HexString(), r.SHA256HexString(), r.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
// Initialize pipe to stream data to cache
|
// Initialize pipe to stream data to cache
|
||||||
rPipe, wPipe := io.Pipe()
|
rPipe, wPipe := io.Pipe()
|
||||||
cHashReader, err := hash.NewReader(rPipe, size, r.MD5HexString(), r.SHA256HexString())
|
cHashReader, err := hash.NewReader(rPipe, size, r.MD5HexString(), r.SHA256HexString(), r.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
@@ -662,9 +746,9 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
|||||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
|
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure cache has at least cacheSizeMultiplier * size available
|
// make sure cache has at least size space available
|
||||||
size := data.Size()
|
size := data.Size()
|
||||||
if !dcache.diskAvailable(size * cacheSizeMultiplier) {
|
if !dcache.diskAvailable(size) {
|
||||||
select {
|
select {
|
||||||
case dcache.purgeChan <- struct{}{}:
|
case dcache.purgeChan <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -675,13 +759,13 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
|||||||
info = PartInfo{}
|
info = PartInfo{}
|
||||||
// Initialize pipe to stream data to backend
|
// Initialize pipe to stream data to backend
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString())
|
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Initialize pipe to stream data to cache
|
// Initialize pipe to stream data to cache
|
||||||
rPipe, wPipe := io.Pipe()
|
rPipe, wPipe := io.Pipe()
|
||||||
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString())
|
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -882,6 +966,9 @@ func newServerCacheObjects(config CacheConfig) (CacheObjectLayer, error) {
|
|||||||
GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
||||||
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
|
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
|
||||||
},
|
},
|
||||||
|
GetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
|
},
|
||||||
PutObjectFn: func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
PutObjectFn: func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
return newObjectLayerFn().PutObject(ctx, bucket, object, data, metadata, opts)
|
return newObjectLayerFn().PutObject(ctx, bucket, object, data, metadata, opts)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func TestDiskCache(t *testing.T) {
|
|||||||
opts := ObjectOptions{}
|
opts := ObjectOptions{}
|
||||||
|
|
||||||
byteReader := bytes.NewReader([]byte(content))
|
byteReader := bytes.NewReader([]byte(content))
|
||||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,7 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
|||||||
opts := ObjectOptions{}
|
opts := ObjectOptions{}
|
||||||
|
|
||||||
byteReader := bytes.NewReader([]byte(content))
|
byteReader := bytes.NewReader([]byte(content))
|
||||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var alphabets = []byte("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||||
|
|
||||||
|
// DummyDataGen returns a reader that repeats the bytes in `alphabets`
|
||||||
|
// upto the desired length.
|
||||||
|
type DummyDataGen struct {
|
||||||
|
b []byte
|
||||||
|
idx, length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDummyDataGen returns a ReadSeeker over the first `totalLength`
|
||||||
|
// bytes from the infinite stream consisting of repeated
|
||||||
|
// concatenations of `alphabets`.
|
||||||
|
//
|
||||||
|
// The skipOffset (generally = 0) can be used to skip a given number
|
||||||
|
// of bytes from the beginning of the infinite stream. This is useful
|
||||||
|
// to compare such streams of bytes that may be split up, because:
|
||||||
|
//
|
||||||
|
// Given the function:
|
||||||
|
//
|
||||||
|
// f := func(r io.Reader) string {
|
||||||
|
// b, _ := ioutil.ReadAll(r)
|
||||||
|
// return string(b)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for example, the following is true:
|
||||||
|
//
|
||||||
|
// f(NewDummyDataGen(100, 0)) == f(NewDummyDataGen(50, 0)) + f(NewDummyDataGen(50, 50))
|
||||||
|
func NewDummyDataGen(totalLength, skipOffset int64) io.ReadSeeker {
|
||||||
|
if totalLength < 0 {
|
||||||
|
panic("Negative length passed to DummyDataGen!")
|
||||||
|
}
|
||||||
|
if skipOffset < 0 {
|
||||||
|
panic("Negative rotations are not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
skipOffset = skipOffset % int64(len(alphabets))
|
||||||
|
as := make([]byte, 2*len(alphabets))
|
||||||
|
copy(as, alphabets)
|
||||||
|
copy(as[len(alphabets):], alphabets)
|
||||||
|
b := as[skipOffset : skipOffset+int64(len(alphabets))]
|
||||||
|
return &DummyDataGen{
|
||||||
|
length: totalLength,
|
||||||
|
b: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DummyDataGen) Read(b []byte) (n int, err error) {
|
||||||
|
k := len(b)
|
||||||
|
numLetters := int64(len(d.b))
|
||||||
|
for k > 0 && d.idx < d.length {
|
||||||
|
w := copy(b[len(b)-k:], d.b[d.idx%numLetters:])
|
||||||
|
k -= w
|
||||||
|
d.idx += int64(w)
|
||||||
|
n += w
|
||||||
|
}
|
||||||
|
if d.idx >= d.length {
|
||||||
|
extraBytes := d.idx - d.length
|
||||||
|
n -= int(extraBytes)
|
||||||
|
if n < 0 {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DummyDataGen) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errors.New("Invalid offset")
|
||||||
|
}
|
||||||
|
d.idx = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
if d.idx+offset < 0 {
|
||||||
|
return 0, errors.New("Invalid offset")
|
||||||
|
}
|
||||||
|
d.idx += offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
if d.length+offset < 0 {
|
||||||
|
return 0, errors.New("Invalid offset")
|
||||||
|
}
|
||||||
|
d.idx = d.length + offset
|
||||||
|
}
|
||||||
|
return d.idx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDummyDataGenerator(t *testing.T) {
|
||||||
|
readAll := func(r io.Reader) string {
|
||||||
|
b, _ := ioutil.ReadAll(r)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
checkEq := func(a, b string) {
|
||||||
|
if a != b {
|
||||||
|
t.Fatalf("Unexpected equality failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEq(readAll(NewDummyDataGen(0, 0)), "")
|
||||||
|
|
||||||
|
checkEq(readAll(NewDummyDataGen(10, 0)), readAll(NewDummyDataGen(10, int64(len(alphabets)))))
|
||||||
|
|
||||||
|
checkEq(readAll(NewDummyDataGen(100, 0)), readAll(NewDummyDataGen(50, 0))+readAll(NewDummyDataGen(50, 50)))
|
||||||
|
|
||||||
|
r := NewDummyDataGen(100, 0)
|
||||||
|
r.Seek(int64(len(alphabets)), 0)
|
||||||
|
checkEq(readAll(r), readAll(NewDummyDataGen(100-int64(len(alphabets)), 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares all the bytes returned by the given readers. Any Read
|
||||||
|
// errors cause a `false` result. A string describing the error is
|
||||||
|
// also returned.
|
||||||
|
func cmpReaders(r1, r2 io.Reader) (bool, string) {
|
||||||
|
bufLen := 32 * 1024
|
||||||
|
b1, b2 := make([]byte, bufLen), make([]byte, bufLen)
|
||||||
|
for i := 0; true; i++ {
|
||||||
|
n1, e1 := io.ReadFull(r1, b1)
|
||||||
|
n2, e2 := io.ReadFull(r2, b2)
|
||||||
|
if n1 != n2 {
|
||||||
|
return false, fmt.Sprintf("Read %d != %d bytes from the readers", n1, n2)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(b1[:n1], b2[:n2]) {
|
||||||
|
return false, fmt.Sprintf("After reading %d equal buffers (32Kib each), we got the following two strings:\n%v\n%v\n",
|
||||||
|
i, b1, b2)
|
||||||
|
}
|
||||||
|
// Check if stream has ended
|
||||||
|
if (e1 == io.ErrUnexpectedEOF && e2 == io.ErrUnexpectedEOF) || (e1 == io.EOF && e2 == io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if e1 != nil || e2 != nil {
|
||||||
|
return false, fmt.Sprintf("Got unexpected error values: %v == %v", e1, e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmpReaders(t *testing.T) {
|
||||||
|
{
|
||||||
|
r1 := bytes.NewBuffer([]byte("abc"))
|
||||||
|
r2 := bytes.NewBuffer([]byte("abc"))
|
||||||
|
ok, msg := cmpReaders(r1, r2)
|
||||||
|
if !(ok && msg == "") {
|
||||||
|
t.Fatalf("unexpected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
r1 := bytes.NewBuffer([]byte("abc"))
|
||||||
|
r2 := bytes.NewBuffer([]byte("abcd"))
|
||||||
|
ok, _ := cmpReaders(r1, r2)
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("unexpected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
@@ -59,6 +60,10 @@ func (api *DummyObjectLayer) ListObjectsV2(ctx context.Context, bucket, prefix,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *DummyObjectLayer) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lock LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
func (api *DummyObjectLayer) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -154,3 +159,7 @@ func (api *DummyObjectLayer) IsNotificationSupported() (b bool) {
|
|||||||
func (api *DummyObjectLayer) IsEncryptionSupported() (b bool) {
|
func (api *DummyObjectLayer) IsEncryptionSupported() (b bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *DummyObjectLayer) IsCompressionSupported() (b bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+381
-38
@@ -37,7 +37,6 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// AWS errors for invalid SSE-C requests.
|
// AWS errors for invalid SSE-C requests.
|
||||||
errInsecureSSERequest = errors.New("SSE-C requests require TLS connections")
|
|
||||||
errEncryptedObject = errors.New("The object was stored using a form of SSE")
|
errEncryptedObject = errors.New("The object was stored using a form of SSE")
|
||||||
errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
|
errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
|
||||||
errKMSNotConfigured = errors.New("KMS not configured for a server side encrypted object")
|
errKMSNotConfigured = errors.New("KMS not configured for a server side encrypted object")
|
||||||
@@ -80,20 +79,35 @@ func hasServerSideEncryptionHeader(header http.Header) bool {
|
|||||||
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
|
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isEncryptedMultipart returns true if the current object is
|
||||||
|
// uploaded by the user using multipart mechanism:
|
||||||
|
// initiate new multipart, upload part, complete upload
|
||||||
|
func isEncryptedMultipart(objInfo ObjectInfo) bool {
|
||||||
|
if len(objInfo.Parts) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !crypto.IsMultiPart(objInfo.UserDefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, part := range objInfo.Parts {
|
||||||
|
_, err := sio.DecryptedSize(uint64(part.Size))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Further check if this object is uploaded using multipart mechanism
|
||||||
|
// by the user and it is not about XL internally splitting the
|
||||||
|
// object into parts in PutObject()
|
||||||
|
return !(objInfo.backendType == BackendErasure && len(objInfo.ETag) == 32)
|
||||||
|
}
|
||||||
|
|
||||||
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
||||||
// It returns the client provided key on success.
|
// It returns the client provided key on success.
|
||||||
func ParseSSECopyCustomerRequest(r *http.Request, metadata map[string]string) (key []byte, err error) {
|
func ParseSSECopyCustomerRequest(h http.Header, metadata map[string]string) (key []byte, err error) {
|
||||||
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(h) {
|
||||||
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
|
||||||
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
|
||||||
// Minio uses a BufConn (wrapping a tls.Conn) so the type check within the http package
|
|
||||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
|
||||||
return nil, errInsecureSSERequest
|
|
||||||
}
|
|
||||||
if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(r.Header) {
|
|
||||||
return nil, crypto.ErrIncompatibleEncryptionMethod
|
return nil, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
}
|
||||||
k, err := crypto.SSECopy.ParseHTTP(r.Header)
|
k, err := crypto.SSECopy.ParseHTTP(h)
|
||||||
return k[:], err
|
return k[:], err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +120,6 @@ func ParseSSECustomerRequest(r *http.Request) (key []byte, err error) {
|
|||||||
// ParseSSECustomerHeader parses the SSE-C header fields and returns
|
// ParseSSECustomerHeader parses the SSE-C header fields and returns
|
||||||
// the client provided key on success.
|
// the client provided key on success.
|
||||||
func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||||
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
|
||||||
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
|
||||||
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
|
||||||
// Minio uses a BufConn (wrapping a tls.Conn) so the type check within the http package
|
|
||||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
|
||||||
return nil, errInsecureSSERequest
|
|
||||||
}
|
|
||||||
if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
|
if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
|
||||||
return key, crypto.ErrIncompatibleEncryptionMethod
|
return key, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
}
|
||||||
@@ -123,8 +130,6 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
|||||||
|
|
||||||
// This function rotates old to new key.
|
// This function rotates old to new key.
|
||||||
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
||||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
default:
|
default:
|
||||||
return errObjectTampered
|
return errObjectTampered
|
||||||
@@ -155,8 +160,6 @@ 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) {
|
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
||||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
|
||||||
|
|
||||||
var sealedKey crypto.SealedKey
|
var sealedKey crypto.SealedKey
|
||||||
if sseS3 {
|
if sseS3 {
|
||||||
if globalKMS == nil {
|
if globalKMS == nil {
|
||||||
@@ -240,12 +243,11 @@ func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if crypto.SSECopy.IsRequested(r.Header) {
|
if crypto.SSECopy.IsRequested(r.Header) {
|
||||||
key, err = ParseSSECopyCustomerRequest(r, metadata)
|
key, err = ParseSSECopyCustomerRequest(r.Header, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(metadata, crypto.SSECopyKey) // make sure we do not save the key by accident
|
|
||||||
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +314,125 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
|
|||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding support for reader based interface
|
||||||
|
|
||||||
|
// DecryptRequestWithSequenceNumberR - same as
|
||||||
|
// DecryptRequestWithSequenceNumber but with a reader
|
||||||
|
func DecryptRequestWithSequenceNumberR(client io.Reader, h http.Header, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
if crypto.S3.IsEncrypted(metadata) {
|
||||||
|
return newDecryptReader(client, nil, bucket, object, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := ParseSSECustomerHeader(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newDecryptReader(client, key, bucket, object, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptCopyRequestR - same as DecryptCopyRequest, but with a
|
||||||
|
// Reader
|
||||||
|
func DecryptCopyRequestR(client io.Reader, h http.Header, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||||
|
var (
|
||||||
|
key []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if crypto.SSECopy.IsRequested(h) {
|
||||||
|
key, err = ParseSSECopyCustomerRequest(h, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newDecryptReader(client, key, bucket, object, 0, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecryptReader(client io.Reader, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newDecryptReaderWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecryptReaderWithObjectKey(client io.Reader, objectEncryptionKey []byte, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
reader, err := sio.DecryptReader(client, sio.Config{
|
||||||
|
Key: objectEncryptionKey,
|
||||||
|
SequenceNumber: seqNumber,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, crypto.ErrInvalidCustomerKey
|
||||||
|
}
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptedOffsetLength - returns encrypted offset and length
|
||||||
|
// along with sequence number
|
||||||
|
func GetEncryptedOffsetLength(startOffset, length int64, objInfo ObjectInfo) (seqNumber uint32, encStartOffset, encLength int64) {
|
||||||
|
if !isEncryptedMultipart(objInfo) {
|
||||||
|
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seqNumber, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptBlocksRequestR - same as DecryptBlocksRequest but with a
|
||||||
|
// reader
|
||||||
|
func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||||
|
length int64, seqNumber uint32, partStart int, oi ObjectInfo, copySource bool) (
|
||||||
|
io.Reader, error) {
|
||||||
|
|
||||||
|
bucket, object := oi.Bucket, oi.Name
|
||||||
|
|
||||||
|
// Single part case
|
||||||
|
if !isEncryptedMultipart(oi) {
|
||||||
|
var reader io.Reader
|
||||||
|
var err error
|
||||||
|
if copySource {
|
||||||
|
reader, err = DecryptCopyRequestR(inputReader, h, bucket, object, oi.UserDefined)
|
||||||
|
} else {
|
||||||
|
reader, err = DecryptRequestWithSequenceNumberR(inputReader, h, bucket, object, seqNumber, oi.UserDefined)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
partDecRelOffset := int64(seqNumber) * sseDAREPackageBlockSize
|
||||||
|
partEncRelOffset := int64(seqNumber) * (sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||||
|
|
||||||
|
w := &DecryptBlocksReader{
|
||||||
|
reader: inputReader,
|
||||||
|
startSeqNum: seqNumber,
|
||||||
|
partDecRelOffset: partDecRelOffset,
|
||||||
|
partEncRelOffset: partEncRelOffset,
|
||||||
|
parts: oi.Parts,
|
||||||
|
partIndex: partStart,
|
||||||
|
header: h,
|
||||||
|
bucket: bucket,
|
||||||
|
object: object,
|
||||||
|
customerKeyHeader: h.Get(crypto.SSECKey),
|
||||||
|
copySource: copySource,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metadata = map[string]string{}
|
||||||
|
// Copy encryption metadata for internal use.
|
||||||
|
for k, v := range oi.UserDefined {
|
||||||
|
w.metadata[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.copySource {
|
||||||
|
w.customerKeyHeader = h.Get(crypto.SSECopyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
||||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||||
@@ -323,7 +444,6 @@ func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
|
||||||
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +453,124 @@ func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, me
|
|||||||
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
|
// DecryptBlocksReader - decrypts multipart parts, while implementing
|
||||||
|
// a io.Reader compatible interface.
|
||||||
|
type DecryptBlocksReader struct {
|
||||||
|
// Source of the encrypted content that will be decrypted
|
||||||
|
reader io.Reader
|
||||||
|
// Current decrypter for the current encrypted data block
|
||||||
|
decrypter io.Reader
|
||||||
|
// Start sequence number
|
||||||
|
startSeqNum uint32
|
||||||
|
// Current part index
|
||||||
|
partIndex int
|
||||||
|
// Parts information
|
||||||
|
parts []objectPartInfo
|
||||||
|
header http.Header
|
||||||
|
bucket, object string
|
||||||
|
metadata map[string]string
|
||||||
|
|
||||||
|
partDecRelOffset, partEncRelOffset int64
|
||||||
|
|
||||||
|
copySource bool
|
||||||
|
// Customer Key
|
||||||
|
customerKeyHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DecryptBlocksReader) buildDecrypter(partID int) error {
|
||||||
|
m := make(map[string]string)
|
||||||
|
for k, v := range d.metadata {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
// Initialize the first decrypter; new decrypters will be
|
||||||
|
// initialized in Read() operation as needed.
|
||||||
|
var key []byte
|
||||||
|
var err error
|
||||||
|
if d.copySource {
|
||||||
|
if crypto.SSEC.IsEncrypted(d.metadata) {
|
||||||
|
d.header.Set(crypto.SSECopyKey, d.customerKeyHeader)
|
||||||
|
key, err = ParseSSECopyCustomerRequest(d.header, d.metadata)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if crypto.SSEC.IsEncrypted(d.metadata) {
|
||||||
|
d.header.Set(crypto.SSECKey, d.customerKeyHeader)
|
||||||
|
key, err = ParseSSECustomerHeader(d.header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
objectEncryptionKey, err := decryptObjectInfo(key, d.bucket, d.object, m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var partIDbin [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, objectEncryptionKey) // derive part encryption key from part ID and object key
|
||||||
|
mac.Write(partIDbin[:])
|
||||||
|
partEncryptionKey := mac.Sum(nil)
|
||||||
|
|
||||||
|
// Limit the reader, so the decryptor doesnt receive bytes
|
||||||
|
// from the next part (different DARE stream)
|
||||||
|
encLenToRead := d.parts[d.partIndex].Size - d.partEncRelOffset
|
||||||
|
decrypter, err := newDecryptReaderWithObjectKey(io.LimitReader(d.reader, encLenToRead), partEncryptionKey, d.startSeqNum, m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.decrypter = decrypter
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DecryptBlocksReader) Read(p []byte) (int, error) {
|
||||||
|
var err error
|
||||||
|
var n1 int
|
||||||
|
decPartSize, _ := sio.DecryptedSize(uint64(d.parts[d.partIndex].Size))
|
||||||
|
unreadPartLen := int64(decPartSize) - d.partDecRelOffset
|
||||||
|
if int64(len(p)) < unreadPartLen {
|
||||||
|
n1, err = d.decrypter.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
d.partDecRelOffset += int64(n1)
|
||||||
|
} else {
|
||||||
|
n1, err = io.ReadFull(d.decrypter, p[:unreadPartLen])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should now proceed to next part, reset all
|
||||||
|
// values appropriately.
|
||||||
|
d.partEncRelOffset = 0
|
||||||
|
d.partDecRelOffset = 0
|
||||||
|
d.startSeqNum = 0
|
||||||
|
|
||||||
|
d.partIndex++
|
||||||
|
if d.partIndex == len(d.parts) {
|
||||||
|
return n1, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.buildDecrypter(d.parts[d.partIndex].Number)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n1, err = d.decrypter.Read(p[n1:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.partDecRelOffset += int64(n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptBlocksWriter - decrypts multipart parts, while implementing
|
||||||
|
// a io.Writer compatible interface.
|
||||||
type DecryptBlocksWriter struct {
|
type DecryptBlocksWriter struct {
|
||||||
// Original writer where the plain data will be written
|
// Original writer where the plain data will be written
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
@@ -367,7 +604,7 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|||||||
if w.copySource {
|
if w.copySource {
|
||||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||||
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
||||||
key, err = ParseSSECopyCustomerRequest(w.req, w.metadata)
|
key, err = ParseSSECopyCustomerRequest(w.req.Header, w.metadata)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||||
@@ -391,13 +628,6 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|||||||
mac.Write(partIDbin[:])
|
mac.Write(partIDbin[:])
|
||||||
partEncryptionKey := mac.Sum(nil)
|
partEncryptionKey := mac.Sum(nil)
|
||||||
|
|
||||||
// make sure we do not save the key by accident
|
|
||||||
if w.copySource {
|
|
||||||
delete(m, crypto.SSECopyKey)
|
|
||||||
} else {
|
|
||||||
delete(m, crypto.SSECKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to provide a NopCloser such that a Close
|
// make sure to provide a NopCloser such that a Close
|
||||||
// on sio.decryptWriter doesn't close the underlying writer's
|
// on sio.decryptWriter doesn't close the underlying writer's
|
||||||
// close which perhaps can close the stream prematurely.
|
// close which perhaps can close the stream prematurely.
|
||||||
@@ -485,7 +715,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
|||||||
var seqNumber uint32
|
var seqNumber uint32
|
||||||
var encStartOffset, encLength int64
|
var encStartOffset, encLength int64
|
||||||
|
|
||||||
if len(objInfo.Parts) == 0 || !crypto.IsMultiPart(objInfo.UserDefined) {
|
if !isEncryptedMultipart(objInfo) {
|
||||||
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
||||||
|
|
||||||
var writer io.WriteCloser
|
var writer io.WriteCloser
|
||||||
@@ -647,7 +877,7 @@ func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
|||||||
if !crypto.IsEncrypted(o.UserDefined) {
|
if !crypto.IsEncrypted(o.UserDefined) {
|
||||||
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
||||||
}
|
}
|
||||||
if len(o.Parts) == 0 || !crypto.IsMultiPart(o.UserDefined) {
|
if !isEncryptedMultipart(*o) {
|
||||||
size, err := sio.DecryptedSize(uint64(o.Size))
|
size, err := sio.DecryptedSize(uint64(o.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errObjectTampered // assign correct error type
|
err = errObjectTampered // assign correct error type
|
||||||
@@ -666,6 +896,119 @@ func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDecryptedRange - To decrypt the range (off, length) of the
|
||||||
|
// decrypted object stream, we need to read the range (encOff,
|
||||||
|
// encLength) of the encrypted object stream to decrypt it, and
|
||||||
|
// compute skipLen, the number of bytes to skip in the beginning of
|
||||||
|
// the encrypted range.
|
||||||
|
//
|
||||||
|
// In addition we also compute the object part number for where the
|
||||||
|
// requested range starts, along with the DARE sequence number within
|
||||||
|
// that part. For single part objects, the partStart will be 0.
|
||||||
|
func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, skipLen int64, seqNumber uint32, partStart int, err error) {
|
||||||
|
if !crypto.IsEncrypted(o.UserDefined) {
|
||||||
|
err = errors.New("Object is not encrypted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rs == nil {
|
||||||
|
// No range, so offsets refer to the whole object.
|
||||||
|
return 0, int64(o.Size), 0, 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble slice of (decrypted) part sizes in `sizes`
|
||||||
|
var decObjSize int64 // decrypted total object size
|
||||||
|
var partSize uint64
|
||||||
|
partSize, err = sio.DecryptedSize(uint64(o.Size))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sizes := []int64{int64(partSize)}
|
||||||
|
decObjSize = sizes[0]
|
||||||
|
if isEncryptedMultipart(*o) {
|
||||||
|
sizes = make([]int64, len(o.Parts))
|
||||||
|
decObjSize = 0
|
||||||
|
for i, part := range o.Parts {
|
||||||
|
partSize, err = sio.DecryptedSize(uint64(part.Size))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := int64(partSize)
|
||||||
|
sizes[i] = t
|
||||||
|
decObjSize += t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var off, length int64
|
||||||
|
off, length, err = rs.GetOffsetLength(decObjSize)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we have:
|
||||||
|
//
|
||||||
|
// 1. the decrypted part sizes in `sizes` (single element for
|
||||||
|
// single part object) and total decrypted object size `decObjSize`
|
||||||
|
//
|
||||||
|
// 2. the (decrypted) start offset `off` and (decrypted)
|
||||||
|
// length to read `length`
|
||||||
|
//
|
||||||
|
// These are the inputs to the rest of the algorithm below.
|
||||||
|
|
||||||
|
// Locate the part containing the start of the required range
|
||||||
|
var partEnd int
|
||||||
|
var cumulativeSum, encCumulativeSum int64
|
||||||
|
for i, size := range sizes {
|
||||||
|
if off < cumulativeSum+size {
|
||||||
|
partStart = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cumulativeSum += size
|
||||||
|
encPartSize, _ := sio.EncryptedSize(uint64(size))
|
||||||
|
encCumulativeSum += int64(encPartSize)
|
||||||
|
}
|
||||||
|
// partStart is always found in the loop above,
|
||||||
|
// because off is validated.
|
||||||
|
|
||||||
|
sseDAREEncPackageBlockSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||||
|
startPkgNum := (off - cumulativeSum) / sseDAREPackageBlockSize
|
||||||
|
|
||||||
|
// Now we can calculate the number of bytes to skip
|
||||||
|
skipLen = (off - cumulativeSum) % sseDAREPackageBlockSize
|
||||||
|
|
||||||
|
encOff = encCumulativeSum + startPkgNum*sseDAREEncPackageBlockSize
|
||||||
|
// Locate the part containing the end of the required range
|
||||||
|
endOffset := off + length - 1
|
||||||
|
for i1, size := range sizes[partStart:] {
|
||||||
|
i := partStart + i1
|
||||||
|
if endOffset < cumulativeSum+size {
|
||||||
|
partEnd = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cumulativeSum += size
|
||||||
|
encPartSize, _ := sio.EncryptedSize(uint64(size))
|
||||||
|
encCumulativeSum += int64(encPartSize)
|
||||||
|
}
|
||||||
|
// partEnd is always found in the loop above, because off and
|
||||||
|
// length are validated.
|
||||||
|
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
|
||||||
|
// Check if the DARE package containing the end offset is a
|
||||||
|
// full sized package (as the last package in the part may be
|
||||||
|
// smaller)
|
||||||
|
lastPartSize, _ := sio.EncryptedSize(uint64(sizes[partEnd]))
|
||||||
|
if endEncOffset > encCumulativeSum+int64(lastPartSize) {
|
||||||
|
endEncOffset = encCumulativeSum + int64(lastPartSize)
|
||||||
|
}
|
||||||
|
encLength = endEncOffset - encOff
|
||||||
|
// Set the sequence number as the starting package number of
|
||||||
|
// the requested block
|
||||||
|
seqNumber = uint32(startPkgNum)
|
||||||
|
return encOff, encLength, skipLen, seqNumber, partStart, nil
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptedSize returns the size of the object after encryption.
|
// EncryptedSize returns the size of the object after encryption.
|
||||||
// An encrypted object is always larger than a plain object
|
// An encrypted object is always larger than a plain object
|
||||||
// except for zero size objects.
|
// except for zero size objects.
|
||||||
@@ -716,7 +1059,7 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
|
|||||||
// decryption succeeded.
|
// decryption succeeded.
|
||||||
//
|
//
|
||||||
// DecryptObjectInfo also returns whether the object is encrypted or not.
|
// DecryptObjectInfo also returns whether the object is encrypted or not.
|
||||||
func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (encrypted bool, err error) {
|
func DecryptObjectInfo(info ObjectInfo, headers http.Header) (encrypted bool, err error) {
|
||||||
// Directories are never encrypted.
|
// Directories are never encrypted.
|
||||||
if info.IsDir {
|
if info.IsDir {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -734,7 +1077,7 @@ func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (encrypted bool, e
|
|||||||
err = errEncryptedObject
|
err = errEncryptedObject
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info.Size, err = info.DecryptedSize()
|
_, err = info.DecryptedSize()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+285
-236
@@ -18,11 +18,12 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
"github.com/minio/sio"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hasServerSideEncryptionHeaderTests = []struct {
|
var hasServerSideEncryptionHeaderTests = []struct {
|
||||||
@@ -105,237 +106,6 @@ func TesthasSSECustomerHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var parseSSECustomerRequestTests = []struct {
|
|
||||||
headers map[string]string
|
|
||||||
useTLS bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
|
||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
|
||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
useTLS: false, err: errInsecureSSERequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES 256",
|
|
||||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
|
||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
|
||||||
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
|
||||||
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
|
||||||
crypto.SSECKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
|
||||||
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "", // 7
|
|
||||||
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
|
||||||
crypto.SSECKeyMD5: "",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKeyMD5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
|
||||||
crypto.SSECKeyMD5: "",
|
|
||||||
crypto.SSEHeader: "",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrIncompatibleEncryptionMethod,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseSSECustomerRequest(t *testing.T) {
|
|
||||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
|
||||||
for i, test := range parseSSECustomerRequestTests {
|
|
||||||
headers := http.Header{}
|
|
||||||
for k, v := range test.headers {
|
|
||||||
headers.Set(k, v)
|
|
||||||
}
|
|
||||||
request := &http.Request{}
|
|
||||||
request.Header = headers
|
|
||||||
globalIsSSL = test.useTLS
|
|
||||||
|
|
||||||
_, err := ParseSSECustomerRequest(request)
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
|
||||||
}
|
|
||||||
key := request.Header.Get(crypto.SSECKey)
|
|
||||||
if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
|
|
||||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var parseSSECopyCustomerRequestTests = []struct {
|
|
||||||
headers map[string]string
|
|
||||||
metadata map[string]string
|
|
||||||
useTLS bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": base64.StdEncoding.EncodeToString(make([]byte, 64))},
|
|
||||||
useTLS: true, err: crypto.ErrIncompatibleEncryptionMethod,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: false, err: errInsecureSSERequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES 256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
|
||||||
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
|
||||||
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
|
||||||
crypto.SSECopyKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
|
||||||
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "", // 7
|
|
||||||
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
|
||||||
crypto.SSECopyKeyMD5: "",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKeyMD5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseSSECopyCustomerRequest(t *testing.T) {
|
|
||||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
|
||||||
for i, test := range parseSSECopyCustomerRequestTests {
|
|
||||||
headers := http.Header{}
|
|
||||||
for k, v := range test.headers {
|
|
||||||
headers.Set(k, v)
|
|
||||||
}
|
|
||||||
request := &http.Request{}
|
|
||||||
request.Header = headers
|
|
||||||
globalIsSSL = test.useTLS
|
|
||||||
|
|
||||||
_, err := ParseSSECopyCustomerRequest(request, test.metadata)
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
|
||||||
}
|
|
||||||
key := request.Header.Get(crypto.SSECopyKey)
|
|
||||||
if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
|
|
||||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var encryptRequestTests = []struct {
|
var encryptRequestTests = []struct {
|
||||||
header map[string]string
|
header map[string]string
|
||||||
metadata map[string]string
|
metadata map[string]string
|
||||||
@@ -374,9 +144,6 @@ func TestEncryptRequest(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||||
}
|
}
|
||||||
if key, ok := test.metadata[crypto.SSECKey]; ok {
|
|
||||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
|
||||||
}
|
|
||||||
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; !ok {
|
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; !ok {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
||||||
}
|
}
|
||||||
@@ -557,7 +324,7 @@ var decryptObjectInfoTests = []struct {
|
|||||||
|
|
||||||
func TestDecryptObjectInfo(t *testing.T) {
|
func TestDecryptObjectInfo(t *testing.T) {
|
||||||
for i, test := range decryptObjectInfoTests {
|
for i, test := range decryptObjectInfoTests {
|
||||||
if encrypted, err := DecryptObjectInfo(&test.info, test.headers); err != test.expErr {
|
if encrypted, err := DecryptObjectInfo(test.info, test.headers); err != test.expErr {
|
||||||
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
||||||
} else if enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
|
} else if enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
|
||||||
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
||||||
@@ -566,3 +333,285 @@ func TestDecryptObjectInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetDecryptedRange(t *testing.T) {
|
||||||
|
var (
|
||||||
|
pkgSz = int64(64) * humanize.KiByte
|
||||||
|
minPartSz = int64(5) * humanize.MiByte
|
||||||
|
maxPartSz = int64(5) * humanize.GiByte
|
||||||
|
|
||||||
|
getEncSize = func(s int64) int64 {
|
||||||
|
v, _ := sio.EncryptedSize(uint64(s))
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
udMap = func(isMulti bool) map[string]string {
|
||||||
|
m := map[string]string{
|
||||||
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||||
|
crypto.SSEMultipart: "1",
|
||||||
|
}
|
||||||
|
if !isMulti {
|
||||||
|
delete(m, crypto.SSEMultipart)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Single part object tests
|
||||||
|
var (
|
||||||
|
mkSPObj = func(s int64) ObjectInfo {
|
||||||
|
return ObjectInfo{
|
||||||
|
Size: getEncSize(s),
|
||||||
|
UserDefined: udMap(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
testSP := []struct {
|
||||||
|
decSz int64
|
||||||
|
oi ObjectInfo
|
||||||
|
}{
|
||||||
|
{0, mkSPObj(0)},
|
||||||
|
{1, mkSPObj(1)},
|
||||||
|
{pkgSz - 1, mkSPObj(pkgSz - 1)},
|
||||||
|
{pkgSz, mkSPObj(pkgSz)},
|
||||||
|
{2*pkgSz - 1, mkSPObj(2*pkgSz - 1)},
|
||||||
|
{minPartSz, mkSPObj(minPartSz)},
|
||||||
|
{maxPartSz, mkSPObj(maxPartSz)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range testSP {
|
||||||
|
{
|
||||||
|
// nil range
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
if skip != 0 || sn != 0 || ps != 0 || o != 0 || l != getEncSize(test.decSz) {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.decSz >= 10 {
|
||||||
|
// first 10 bytes
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, 0, 9})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
var rLen = pkgSz + 32
|
||||||
|
if test.decSz < pkgSz {
|
||||||
|
rLen = test.decSz + 32
|
||||||
|
}
|
||||||
|
if skip != 0 || sn != 0 || ps != 0 || o != 0 || l != rLen {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kb32 := int64(32) * humanize.KiByte
|
||||||
|
if test.decSz >= (64+32)*humanize.KiByte {
|
||||||
|
// Skip the first 32Kib, and read the next 64Kib
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, kb32, 3*kb32 - 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
var rLen = (pkgSz + 32) * 2
|
||||||
|
if test.decSz < 2*pkgSz {
|
||||||
|
rLen = (pkgSz + 32) + (test.decSz - pkgSz + 32)
|
||||||
|
}
|
||||||
|
if skip != kb32 || sn != 0 || ps != 0 || o != 0 || l != rLen {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.decSz >= (64*2+32)*humanize.KiByte {
|
||||||
|
// Skip the first 96Kib and read the next 64Kib
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, 3 * kb32, 5*kb32 - 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
var rLen = (pkgSz + 32) * 2
|
||||||
|
if test.decSz-pkgSz < 2*pkgSz {
|
||||||
|
rLen = (pkgSz + 32) + (test.decSz - pkgSz + 32*2)
|
||||||
|
}
|
||||||
|
if skip != kb32 || sn != 1 || ps != 0 || o != pkgSz+32 || l != rLen {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multipart object tests
|
||||||
|
var (
|
||||||
|
// make a multipart object-info given part sizes
|
||||||
|
mkMPObj = func(sizes []int64) ObjectInfo {
|
||||||
|
r := make([]objectPartInfo, len(sizes))
|
||||||
|
sum := int64(0)
|
||||||
|
for i, s := range sizes {
|
||||||
|
r[i].Number = i
|
||||||
|
r[i].Size = int64(getEncSize(s))
|
||||||
|
sum += r[i].Size
|
||||||
|
}
|
||||||
|
return ObjectInfo{
|
||||||
|
Size: sum,
|
||||||
|
UserDefined: udMap(true),
|
||||||
|
Parts: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Simple useful utilities
|
||||||
|
repeat = func(k int64, n int) []int64 {
|
||||||
|
a := []int64{}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
a = append(a, k)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
lsum = func(s []int64) int64 {
|
||||||
|
sum := int64(0)
|
||||||
|
for _, i := range s {
|
||||||
|
if i < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
sum += i
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
esum = func(oi ObjectInfo) int64 {
|
||||||
|
sum := int64(0)
|
||||||
|
for _, i := range oi.Parts {
|
||||||
|
sum += i.Size
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
s1 := []int64{5487701, 5487799, 3}
|
||||||
|
s2 := repeat(5487701, 5)
|
||||||
|
s3 := repeat(maxPartSz, 10000)
|
||||||
|
testMPs := []struct {
|
||||||
|
decSizes []int64
|
||||||
|
oi ObjectInfo
|
||||||
|
}{
|
||||||
|
{s1, mkMPObj(s1)},
|
||||||
|
{s2, mkMPObj(s2)},
|
||||||
|
{s3, mkMPObj(s3)},
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is a reference (re-)implementation of
|
||||||
|
// decrypted range computation, written solely for the purpose
|
||||||
|
// of the unit tests.
|
||||||
|
//
|
||||||
|
// `s` gives the decrypted part sizes, and the other
|
||||||
|
// parameters describe the desired read segment. When
|
||||||
|
// `isFromEnd` is true, `skipLen` argument is ignored.
|
||||||
|
decryptedRangeRef := func(s []int64, skipLen, readLen int64, isFromEnd bool) (o, l, skip int64, sn uint32, ps int) {
|
||||||
|
oSize := lsum(s)
|
||||||
|
if isFromEnd {
|
||||||
|
skipLen = oSize - readLen
|
||||||
|
}
|
||||||
|
if skipLen < 0 || readLen < 0 || oSize < 0 || skipLen+readLen > oSize {
|
||||||
|
t.Fatalf("Impossible read specified: %d %d %d", skipLen, readLen, oSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cumulativeSum, cumulativeEncSum int64
|
||||||
|
toRead := readLen
|
||||||
|
readStart := false
|
||||||
|
for i, v := range s {
|
||||||
|
partOffset := int64(0)
|
||||||
|
partDarePkgOffset := int64(0)
|
||||||
|
if !readStart && cumulativeSum+v > skipLen {
|
||||||
|
// Read starts at the current part
|
||||||
|
readStart = true
|
||||||
|
|
||||||
|
partOffset = skipLen - cumulativeSum
|
||||||
|
|
||||||
|
// All return values except `l` are
|
||||||
|
// calculated here.
|
||||||
|
sn = uint32(partOffset / pkgSz)
|
||||||
|
skip = partOffset % pkgSz
|
||||||
|
ps = i
|
||||||
|
o = cumulativeEncSum + int64(sn)*(pkgSz+32)
|
||||||
|
|
||||||
|
partDarePkgOffset = partOffset - skip
|
||||||
|
}
|
||||||
|
if readStart {
|
||||||
|
currentPartBytes := v - partOffset
|
||||||
|
currentPartDareBytes := v - partDarePkgOffset
|
||||||
|
if currentPartBytes < toRead {
|
||||||
|
toRead -= currentPartBytes
|
||||||
|
l += getEncSize(currentPartDareBytes)
|
||||||
|
} else {
|
||||||
|
// current part has the last
|
||||||
|
// byte required
|
||||||
|
lbPartOffset := partOffset + toRead - 1
|
||||||
|
|
||||||
|
// round up the lbPartOffset
|
||||||
|
// to the end of the
|
||||||
|
// corresponding DARE package
|
||||||
|
lbPkgEndOffset := lbPartOffset - (lbPartOffset % pkgSz) + pkgSz
|
||||||
|
if lbPkgEndOffset > v {
|
||||||
|
lbPkgEndOffset = v
|
||||||
|
}
|
||||||
|
bytesToDrop := v - lbPkgEndOffset
|
||||||
|
|
||||||
|
// Last segment to update `l`
|
||||||
|
l += getEncSize(currentPartDareBytes - bytesToDrop)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cumulativeSum += v
|
||||||
|
cumulativeEncSum += getEncSize(v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range testMPs {
|
||||||
|
{
|
||||||
|
// nil range
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
if o != 0 || l != esum(test.oi) || skip != 0 || sn != 0 || ps != 0 {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip 1Mib and read 1Mib (in the decrypted object)
|
||||||
|
//
|
||||||
|
// The check below ensures the object is large enough
|
||||||
|
// for the read.
|
||||||
|
if lsum(test.decSizes) >= 2*humanize.MiByte {
|
||||||
|
skipLen, readLen := int64(1)*humanize.MiByte, int64(1)*humanize.MiByte
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, skipLen, skipLen + readLen - 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oRef, lRef, skipRef, snRef, psRef := decryptedRangeRef(test.decSizes, skipLen, readLen, false)
|
||||||
|
if o != oRef || l != lRef || skip != skipRef || sn != snRef || ps != psRef {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d (Ref: %d %d %d %d %d)",
|
||||||
|
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the last 6Mib+1 bytes of the (decrypted)
|
||||||
|
// object
|
||||||
|
//
|
||||||
|
// The check below ensures the object is large enough
|
||||||
|
// for the read.
|
||||||
|
readLen := int64(6)*humanize.MiByte + 1
|
||||||
|
if lsum(test.decSizes) >= readLen {
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{true, -readLen, -1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oRef, lRef, skipRef, snRef, psRef := decryptedRangeRef(test.decSizes, 0, readLen, true)
|
||||||
|
if o != oRef || l != lRef || skip != skipRef || sn != snRef || ps != psRef {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d (Ref: %d %d %d %d %d)",
|
||||||
|
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -168,6 +168,18 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
|||||||
// EndpointList - list of same type of endpoint.
|
// EndpointList - list of same type of endpoint.
|
||||||
type EndpointList []Endpoint
|
type EndpointList []Endpoint
|
||||||
|
|
||||||
|
// Nodes - returns number of unique servers.
|
||||||
|
func (endpoints EndpointList) Nodes() int {
|
||||||
|
uniqueNodes := set.NewStringSet()
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if uniqueNodes.Contains(endpoint.Host) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uniqueNodes.Add(endpoint.Host)
|
||||||
|
}
|
||||||
|
return len(uniqueNodes)
|
||||||
|
}
|
||||||
|
|
||||||
// IsHTTPS - returns true if secure for URLEndpointType.
|
// IsHTTPS - returns true if secure for URLEndpointType.
|
||||||
func (endpoints EndpointList) IsHTTPS() bool {
|
func (endpoints EndpointList) IsHTTPS() bool {
|
||||||
return endpoints[0].IsHTTPS()
|
return endpoints[0].IsHTTPS()
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import (
|
|||||||
|
|
||||||
type badDisk struct{ StorageAPI }
|
type badDisk struct{ StorageAPI }
|
||||||
|
|
||||||
|
func (a badDisk) String() string {
|
||||||
|
return "bad-disk"
|
||||||
|
}
|
||||||
|
|
||||||
func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
||||||
return errFaultyDisk
|
return errFaultyDisk
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
|||||||
if write < int64(len(block)) {
|
if write < int64(len(block)) {
|
||||||
n, err := io.Copy(dst, bytes.NewReader(block[:write]))
|
n, err := io.Copy(dst, bytes.NewReader(block[:write]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
if err != io.ErrClosedPipe {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
totalWritten += n
|
totalWritten += n
|
||||||
@@ -90,7 +92,10 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
|||||||
// Copy the block.
|
// Copy the block.
|
||||||
n, err := io.Copy(dst, bytes.NewReader(block))
|
n, err := io.Copy(dst, bytes.NewReader(block))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
// The writer will be closed incase of range queries, which will emit ErrClosedPipe.
|
||||||
|
if err != io.ErrClosedPipe {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-10
@@ -24,7 +24,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
pathutil "path"
|
pathutil "path"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
@@ -137,11 +136,7 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
|||||||
|
|
||||||
// Guess content-type from the extension if possible.
|
// Guess content-type from the extension if possible.
|
||||||
if m.Meta["content-type"] == "" {
|
if m.Meta["content-type"] == "" {
|
||||||
if objectExt := pathutil.Ext(object); objectExt != "" {
|
m.Meta["content-type"] = mimedb.TypeByExtension(pathutil.Ext(object))
|
||||||
if content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]; ok {
|
|
||||||
m.Meta["content-type"] = content.ContentType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasSuffix(object, slashSeparator) {
|
if hasSuffix(object, slashSeparator) {
|
||||||
@@ -223,11 +218,13 @@ func parseFSPartsArray(fsMetaBuf []byte) []objectPartInfo {
|
|||||||
name := gjson.Get(partJSON, "name").String()
|
name := gjson.Get(partJSON, "name").String()
|
||||||
etag := gjson.Get(partJSON, "etag").String()
|
etag := gjson.Get(partJSON, "etag").String()
|
||||||
size := gjson.Get(partJSON, "size").Int()
|
size := gjson.Get(partJSON, "size").Int()
|
||||||
|
actualSize := gjson.Get(partJSON, "actualSize").Int()
|
||||||
partsArray = append(partsArray, objectPartInfo{
|
partsArray = append(partsArray, objectPartInfo{
|
||||||
Number: int(number),
|
Number: int(number),
|
||||||
Name: name,
|
Name: name,
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
Size: size,
|
Size: size,
|
||||||
|
ActualSize: int64(actualSize),
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
+93
-47
@@ -46,21 +46,25 @@ func (fs *FSObjects) getMultipartSHADir(bucket, object string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns partNumber.etag
|
// Returns partNumber.etag
|
||||||
func (fs *FSObjects) encodePartFile(partNumber int, etag string) string {
|
func (fs *FSObjects) encodePartFile(partNumber int, etag string, actualSize int64) string {
|
||||||
return fmt.Sprintf("%.5d.%s", partNumber, etag)
|
return fmt.Sprintf("%.5d.%s.%d", partNumber, etag, actualSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns partNumber and etag
|
// Returns partNumber and etag
|
||||||
func (fs *FSObjects) decodePartFile(name string) (partNumber int, etag string, err error) {
|
func (fs *FSObjects) decodePartFile(name string) (partNumber int, etag string, actualSize int64, err error) {
|
||||||
result := strings.Split(name, ".")
|
result := strings.Split(name, ".")
|
||||||
if len(result) != 2 {
|
if len(result) != 3 {
|
||||||
return 0, "", errUnexpected
|
return 0, "", 0, errUnexpected
|
||||||
}
|
}
|
||||||
partNumber, err = strconv.Atoi(result[0])
|
partNumber, err = strconv.Atoi(result[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", errUnexpected
|
return 0, "", 0, errUnexpected
|
||||||
}
|
}
|
||||||
return partNumber, result[1], nil
|
actualSize, err = strconv.ParseInt(result[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", 0, errUnexpected
|
||||||
|
}
|
||||||
|
return partNumber, result[1], actualSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appends parts to an appendFile sequentially.
|
// Appends parts to an appendFile sequentially.
|
||||||
@@ -95,7 +99,7 @@ func (fs *FSObjects) backgroundAppend(ctx context.Context, bucket, object, uploa
|
|||||||
if entry == fs.metaJSONFile {
|
if entry == fs.metaJSONFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
partNumber, etag, err := fs.decodePartFile(entry)
|
partNumber, etag, actualSize, err := fs.decodePartFile(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.GetReqInfo(ctx).AppendTags("entry", entry)
|
logger.GetReqInfo(ctx).AppendTags("entry", entry)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -119,7 +123,7 @@ func (fs *FSObjects) backgroundAppend(ctx context.Context, bucket, object, uploa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file.parts = append(file.parts, PartInfo{PartNumber: nextPartNumber, ETag: etag})
|
file.parts = append(file.parts, PartInfo{PartNumber: partNumber, ETag: etag, ActualSize: actualSize})
|
||||||
nextPartNumber++
|
nextPartNumber++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,22 +259,6 @@ func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
|||||||
return pi, toObjectErr(err)
|
return pi, toObjectErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize pipe.
|
|
||||||
go func() {
|
|
||||||
if gerr := fs.GetObject(ctx, srcBucket, srcObject, startOffset, length, srcInfo.Writer, srcInfo.ETag, srcOpts); gerr != nil {
|
|
||||||
if gerr = srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Close writer explicitly signaling we wrote all data.
|
|
||||||
if gerr := srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
partInfo, err := fs.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.Reader, dstOpts)
|
partInfo, err := fs.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.Reader, dstOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pi, toObjectErr(err, dstBucket, dstObject)
|
return pi, toObjectErr(err, dstBucket, dstObject)
|
||||||
@@ -292,8 +280,8 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
|||||||
return pi, toObjectErr(err, bucket)
|
return pi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate input data size and it can never be less than zero.
|
// Validate input data size and it can never be less than -1.
|
||||||
if data.Size() < 0 {
|
if data.Size() < -1 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return pi, toObjectErr(errInvalidArgument)
|
return pi, toObjectErr(errInvalidArgument)
|
||||||
}
|
}
|
||||||
@@ -338,7 +326,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
|||||||
if etag == "" {
|
if etag == "" {
|
||||||
etag = GenETag()
|
etag = GenETag()
|
||||||
}
|
}
|
||||||
partPath := pathJoin(uploadIDDir, fs.encodePartFile(partID, etag))
|
partPath := pathJoin(uploadIDDir, fs.encodePartFile(partID, etag, data.ActualSize()))
|
||||||
|
|
||||||
if err = fsRenameFile(ctx, tmpPartPath, partPath); err != nil {
|
if err = fsRenameFile(ctx, tmpPartPath, partPath); err != nil {
|
||||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||||
@@ -355,6 +343,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
|||||||
LastModified: fi.ModTime(),
|
LastModified: fi.ModTime(),
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
Size: fi.Size(),
|
Size: fi.Size(),
|
||||||
|
ActualSize: data.ActualSize(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +370,7 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
}
|
}
|
||||||
|
|
||||||
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
|
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
|
||||||
_, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile))
|
if _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)); err != nil {
|
||||||
if err != nil {
|
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
return result, InvalidUploadID{UploadID: uploadID}
|
return result, InvalidUploadID{UploadID: uploadID}
|
||||||
}
|
}
|
||||||
@@ -400,7 +388,7 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
if entry == fs.metaJSONFile {
|
if entry == fs.metaJSONFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
partNumber, etag1, derr := fs.decodePartFile(entry)
|
partNumber, etag1, _, derr := fs.decodePartFile(entry)
|
||||||
if derr != nil {
|
if derr != nil {
|
||||||
logger.LogIf(ctx, derr)
|
logger.LogIf(ctx, derr)
|
||||||
return result, toObjectErr(derr)
|
return result, toObjectErr(derr)
|
||||||
@@ -410,11 +398,11 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
partsMap[partNumber] = etag1
|
partsMap[partNumber] = etag1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stat1, serr := fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(partNumber, etag1)))
|
stat1, serr := fsStatFile(ctx, pathJoin(uploadIDDir, getPartFile(entries, partNumber, etag1)))
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
return result, toObjectErr(serr)
|
return result, toObjectErr(serr)
|
||||||
}
|
}
|
||||||
stat2, serr := fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(partNumber, etag2)))
|
stat2, serr := fsStatFile(ctx, pathJoin(uploadIDDir, getPartFile(entries, partNumber, etag2)))
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
return result, toObjectErr(serr)
|
return result, toObjectErr(serr)
|
||||||
}
|
}
|
||||||
@@ -424,8 +412,19 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parts []PartInfo
|
var parts []PartInfo
|
||||||
|
var actualSize int64
|
||||||
for partNumber, etag := range partsMap {
|
for partNumber, etag := range partsMap {
|
||||||
parts = append(parts, PartInfo{PartNumber: partNumber, ETag: etag})
|
partFile := getPartFile(entries, partNumber, etag)
|
||||||
|
if partFile == "" {
|
||||||
|
return result, InvalidPart{}
|
||||||
|
}
|
||||||
|
// Read the actualSize from the pathFileName.
|
||||||
|
subParts := strings.Split(partFile, ".")
|
||||||
|
actualSize, err = strconv.ParseInt(subParts[len(subParts)-1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return result, InvalidPart{}
|
||||||
|
}
|
||||||
|
parts = append(parts, PartInfo{PartNumber: partNumber, ETag: etag, ActualSize: actualSize})
|
||||||
}
|
}
|
||||||
sort.Slice(parts, func(i int, j int) bool {
|
sort.Slice(parts, func(i int, j int) bool {
|
||||||
return parts[i].PartNumber < parts[j].PartNumber
|
return parts[i].PartNumber < parts[j].PartNumber
|
||||||
@@ -455,12 +454,12 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
}
|
}
|
||||||
for i, part := range result.Parts {
|
for i, part := range result.Parts {
|
||||||
var stat os.FileInfo
|
var stat os.FileInfo
|
||||||
stat, err = fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag)))
|
stat, err = fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag, part.ActualSize)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, toObjectErr(err)
|
return result, toObjectErr(err)
|
||||||
}
|
}
|
||||||
result.Parts[i].LastModified = stat.ModTime()
|
result.Parts[i].LastModified = stat.ModTime()
|
||||||
result.Parts[i].Size = stat.Size()
|
result.Parts[i].Size = part.ActualSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fsMetaBytes, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile))
|
fsMetaBytes, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile))
|
||||||
@@ -480,6 +479,9 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
//
|
//
|
||||||
// Implements S3 compatible Complete multipart API.
|
// Implements S3 compatible Complete multipart API.
|
||||||
func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart) (oi ObjectInfo, e error) {
|
||||||
|
|
||||||
|
var actualSize int64
|
||||||
|
|
||||||
if err := checkCompleteMultipartArgs(ctx, bucket, object, fs); err != nil {
|
if err := checkCompleteMultipartArgs(ctx, bucket, object, fs); err != nil {
|
||||||
return oi, toObjectErr(err)
|
return oi, toObjectErr(err)
|
||||||
}
|
}
|
||||||
@@ -516,9 +518,37 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
// Allocate parts similar to incoming slice.
|
// 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 {
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("uploadIDDir", uploadIDDir)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return oi, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save consolidated actual size.
|
||||||
|
var objectActualSize int64
|
||||||
// Validate all parts and then commit to disk.
|
// Validate all parts and then commit to disk.
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
partPath := pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag))
|
partFile := getPartFile(entries, part.PartNumber, part.ETag)
|
||||||
|
if partFile == "" {
|
||||||
|
return oi, InvalidPart{
|
||||||
|
PartNumber: part.PartNumber,
|
||||||
|
GotETag: part.ETag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the actualSize from the pathFileName.
|
||||||
|
subParts := strings.Split(partFile, ".")
|
||||||
|
actualSize, err = strconv.ParseInt(subParts[len(subParts)-1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return oi, InvalidPart{
|
||||||
|
PartNumber: part.PartNumber,
|
||||||
|
GotETag: part.ETag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partPath := pathJoin(uploadIDDir, partFile)
|
||||||
|
|
||||||
var fi os.FileInfo
|
var fi os.FileInfo
|
||||||
fi, err = fsStatFile(ctx, partPath)
|
fi, err = fsStatFile(ctx, partPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -528,24 +558,28 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
if partSize == -1 {
|
if partSize == -1 {
|
||||||
partSize = fi.Size()
|
partSize = actualSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fsMeta.Parts[i] = objectPartInfo{
|
fsMeta.Parts[i] = objectPartInfo{
|
||||||
Number: part.PartNumber,
|
Number: part.PartNumber,
|
||||||
ETag: part.ETag,
|
ETag: part.ETag,
|
||||||
Size: fi.Size(),
|
Size: fi.Size(),
|
||||||
|
ActualSize: actualSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consolidate the actual size.
|
||||||
|
objectActualSize += actualSize
|
||||||
|
|
||||||
if i == len(parts)-1 {
|
if i == len(parts)-1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// All parts except the last part has to be atleast 5MB.
|
// All parts except the last part has to be atleast 5MB.
|
||||||
if !isMinAllowedPartSize(fi.Size()) {
|
if !isMinAllowedPartSize(actualSize) {
|
||||||
return oi, PartTooSmall{
|
return oi, PartTooSmall{
|
||||||
PartNumber: part.PartNumber,
|
PartNumber: part.PartNumber,
|
||||||
PartSize: fi.Size(),
|
PartSize: actualSize,
|
||||||
PartETag: part.ETag,
|
PartETag: part.ETag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,11 +621,12 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if appendFallback {
|
if appendFallback {
|
||||||
fsRemoveFile(ctx, file.filePath)
|
if file != nil {
|
||||||
|
fsRemoveFile(ctx, file.filePath)
|
||||||
|
}
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
partPath := pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag))
|
partPath := getPartFile(entries, part.PartNumber, part.ETag)
|
||||||
err = mioutil.AppendFile(appendFilePath, partPath)
|
if err = mioutil.AppendFile(appendFilePath, pathJoin(uploadIDDir, partPath)); err != nil {
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return oi, toObjectErr(err)
|
return oi, toObjectErr(err)
|
||||||
}
|
}
|
||||||
@@ -628,6 +663,8 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
fsMeta.Meta = make(map[string]string)
|
fsMeta.Meta = make(map[string]string)
|
||||||
}
|
}
|
||||||
fsMeta.Meta["etag"] = s3MD5
|
fsMeta.Meta["etag"] = s3MD5
|
||||||
|
// Save consolidated actual size.
|
||||||
|
fsMeta.Meta[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(objectActualSize, 10)
|
||||||
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return oi, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
@@ -676,6 +713,11 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.appendFileMapMu.Lock()
|
fs.appendFileMapMu.Lock()
|
||||||
|
// Remove file in tmp folder
|
||||||
|
file := fs.appendFileMap[uploadID]
|
||||||
|
if file != nil {
|
||||||
|
fsRemoveFile(ctx, file.filePath)
|
||||||
|
}
|
||||||
delete(fs.appendFileMap, uploadID)
|
delete(fs.appendFileMap, uploadID)
|
||||||
fs.appendFileMapMu.Unlock()
|
fs.appendFileMapMu.Unlock()
|
||||||
|
|
||||||
@@ -688,10 +730,14 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
|
|||||||
}
|
}
|
||||||
return toObjectErr(err, bucket, object)
|
return toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore the error returned as Windows fails to remove directory if a file in it
|
// Ignore the error returned as Windows fails to remove directory if a file in it
|
||||||
// is Open()ed by the backgroundAppend()
|
// is Open()ed by the backgroundAppend()
|
||||||
fsRemoveAll(ctx, uploadIDDir)
|
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))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+116
-52
@@ -17,10 +17,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -416,35 +418,19 @@ func (fs *FSObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
|||||||
// update metadata.
|
// update metadata.
|
||||||
func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, e error) {
|
||||||
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
||||||
// Hold write lock on destination since in both cases
|
|
||||||
// - if source and destination are same
|
|
||||||
// - if source and destination are different
|
|
||||||
// it is the sole mutating state.
|
|
||||||
objectDWLock := fs.nsMutex.NewNSLock(dstBucket, dstObject)
|
|
||||||
if err := objectDWLock.GetLock(globalObjectTimeout); err != nil {
|
|
||||||
return oi, err
|
|
||||||
}
|
|
||||||
defer objectDWLock.Unlock()
|
|
||||||
// if source and destination are different, we have to hold
|
|
||||||
// additional read lock as well to protect against writes on
|
|
||||||
// source.
|
|
||||||
if !cpSrcDstSame {
|
if !cpSrcDstSame {
|
||||||
// Hold read locks on source object only if we are
|
objectDWLock := fs.nsMutex.NewNSLock(dstBucket, dstObject)
|
||||||
// going to read data from source object.
|
if err := objectDWLock.GetLock(globalObjectTimeout); err != nil {
|
||||||
objectSRLock := fs.nsMutex.NewNSLock(srcBucket, srcObject)
|
|
||||||
if err := objectSRLock.GetRLock(globalObjectTimeout); err != nil {
|
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
defer objectSRLock.RUnlock()
|
defer objectDWLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(ctx, srcBucket); err != nil {
|
if _, err := fs.statBucketDir(ctx, srcBucket); err != nil {
|
||||||
return oi, toObjectErr(err, srcBucket)
|
return oi, toObjectErr(err, srcBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cpSrcDstSame && srcInfo.metadataOnly {
|
if cpSrcDstSame && srcInfo.metadataOnly {
|
||||||
// Close any writer which was initialized.
|
|
||||||
defer srcInfo.Writer.Close()
|
|
||||||
|
|
||||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, srcBucket, srcObject, fs.metaJSONFile)
|
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, srcBucket, srcObject, fs.metaJSONFile)
|
||||||
wlk, err := fs.rwPool.Write(fsMetaPath)
|
wlk, err := fs.rwPool.Write(fsMetaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -476,20 +462,6 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
|||||||
return fsMeta.ToObjectInfo(srcBucket, srcObject, fi), nil
|
return fsMeta.ToObjectInfo(srcBucket, srcObject, fi), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
if gerr := fs.getObject(ctx, srcBucket, srcObject, 0, srcInfo.Size, srcInfo.Writer, srcInfo.ETag, !cpSrcDstSame); gerr != nil {
|
|
||||||
if gerr = srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Close writer explicitly signaling we wrote all data.
|
|
||||||
if gerr := srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
objInfo, err := fs.putObject(ctx, dstBucket, dstObject, srcInfo.Reader, srcInfo.UserDefined)
|
objInfo, err := fs.putObject(ctx, dstBucket, dstObject, srcInfo.Reader, srcInfo.UserDefined)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oi, toObjectErr(err, dstBucket, dstObject)
|
return oi, toObjectErr(err, dstBucket, dstObject)
|
||||||
@@ -498,6 +470,100 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
|||||||
return objInfo, nil
|
return objInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and a reader for object
|
||||||
|
// content.
|
||||||
|
func (fs *FSObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
|
||||||
|
if err = checkGetObjArgs(ctx, bucket, object); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fs.statBucketDir(ctx, bucket); err != nil {
|
||||||
|
return nil, toObjectErr(err, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nsUnlocker = func() {}
|
||||||
|
|
||||||
|
if lockType != noLock {
|
||||||
|
// Lock the object before reading.
|
||||||
|
lock := fs.nsMutex.NewNSLock(bucket, object)
|
||||||
|
switch lockType {
|
||||||
|
case writeLock:
|
||||||
|
if err = lock.GetLock(globalObjectTimeout); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nsUnlocker = lock.Unlock
|
||||||
|
case readLock:
|
||||||
|
if err = lock.GetRLock(globalObjectTimeout); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nsUnlocker = lock.RUnlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a directory, we need to send an reader that returns no bytes.
|
||||||
|
if hasSuffix(object, slashSeparator) {
|
||||||
|
// The lock taken above is released when
|
||||||
|
// objReader.Close() is called by the caller.
|
||||||
|
return NewGetObjectReaderFromReader(bytes.NewBuffer(nil), ObjectInfo{}, nsUnlocker), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we get the object info
|
||||||
|
var objInfo ObjectInfo
|
||||||
|
if objInfo, err = fs.getObjectInfo(ctx, bucket, object); err != nil {
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a rwPool lock for NFS gateway type deployment
|
||||||
|
rwPoolUnlocker := func() {}
|
||||||
|
if bucket != minioMetaBucket && lockType != noLock {
|
||||||
|
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fs.metaJSONFile)
|
||||||
|
_, err = fs.rwPool.Open(fsMetaPath)
|
||||||
|
if err != nil && err != errFileNotFound {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
// Need to clean up lock after getObject is
|
||||||
|
// completed.
|
||||||
|
rwPoolUnlocker = func() { fs.rwPool.Close(fsMetaPath) }
|
||||||
|
}
|
||||||
|
|
||||||
|
objReaderFn, off, length, rErr := NewGetObjectReader(rs, objInfo, nsUnlocker, rwPoolUnlocker)
|
||||||
|
if rErr != nil {
|
||||||
|
return nil, rErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the object, doesn't exist returns an s3 compatible error.
|
||||||
|
fsObjPath := pathJoin(fs.fsPath, bucket, object)
|
||||||
|
readCloser, size, err := fsOpenFile(ctx, fsObjPath, off)
|
||||||
|
if err != nil {
|
||||||
|
rwPoolUnlocker()
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
var reader io.Reader
|
||||||
|
reader = io.LimitReader(readCloser, length)
|
||||||
|
closeFn := func() {
|
||||||
|
readCloser.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if range is valid
|
||||||
|
if off > size || off+length > size {
|
||||||
|
err = InvalidRange{off, length, size}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
closeFn()
|
||||||
|
rwPoolUnlocker()
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objReaderFn(reader, h, closeFn)
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject - reads an object from the disk.
|
// GetObject - reads an object from the disk.
|
||||||
// Supports additional parameters like offset and length
|
// Supports additional parameters like offset and length
|
||||||
// which are synonymous with HTTP Range requests.
|
// which are synonymous with HTTP Range requests.
|
||||||
@@ -596,7 +662,10 @@ func (fs *FSObjects) getObject(ctx context.Context, bucket, object string, offse
|
|||||||
buf := make([]byte, int(bufSize))
|
buf := make([]byte, int(bufSize))
|
||||||
|
|
||||||
_, err = io.CopyBuffer(writer, io.LimitReader(reader, length), buf)
|
_, err = io.CopyBuffer(writer, io.LimitReader(reader, length), buf)
|
||||||
logger.LogIf(ctx, err)
|
// The writer will be closed incase of range queries, which will emit ErrClosedPipe.
|
||||||
|
if err == io.ErrClosedPipe {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
return toObjectErr(err, bucket, object)
|
return toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,13 +674,8 @@ func (fs *FSObjects) createFsJSON(object, fsMetaPath string) error {
|
|||||||
fsMeta := newFSMetaV1()
|
fsMeta := newFSMetaV1()
|
||||||
fsMeta.Meta = make(map[string]string)
|
fsMeta.Meta = make(map[string]string)
|
||||||
fsMeta.Meta["etag"] = GenETag()
|
fsMeta.Meta["etag"] = GenETag()
|
||||||
if objectExt := path.Ext(object); objectExt != "" {
|
contentType := mimedb.TypeByExtension(path.Ext(object))
|
||||||
if content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]; ok {
|
fsMeta.Meta["content-type"] = contentType
|
||||||
fsMeta.Meta["content-type"] = content.ContentType
|
|
||||||
} else {
|
|
||||||
fsMeta.Meta["content-type"] = "application/octet-stream"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wlk, werr := fs.rwPool.Create(fsMetaPath)
|
wlk, werr := fs.rwPool.Create(fsMetaPath)
|
||||||
if werr == nil {
|
if werr == nil {
|
||||||
_, err := fsMeta.WriteTo(wlk)
|
_, err := fsMeta.WriteTo(wlk)
|
||||||
@@ -626,13 +690,8 @@ func (fs *FSObjects) defaultFsJSON(object string) fsMetaV1 {
|
|||||||
fsMeta := newFSMetaV1()
|
fsMeta := newFSMetaV1()
|
||||||
fsMeta.Meta = make(map[string]string)
|
fsMeta.Meta = make(map[string]string)
|
||||||
fsMeta.Meta["etag"] = defaultEtag
|
fsMeta.Meta["etag"] = defaultEtag
|
||||||
if objectExt := path.Ext(object); objectExt != "" {
|
contentType := mimedb.TypeByExtension(path.Ext(object))
|
||||||
if content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]; ok {
|
fsMeta.Meta["content-type"] = contentType
|
||||||
fsMeta.Meta["content-type"] = content.ContentType
|
|
||||||
} else {
|
|
||||||
fsMeta.Meta["content-type"] = "application/octet-stream"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fsMeta
|
return fsMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +875,7 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate input data size and it can never be less than zero.
|
// Validate input data size and it can never be less than zero.
|
||||||
if data.Size() < 0 {
|
if data.Size() < -1 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return ObjectInfo{}, errInvalidArgument
|
return ObjectInfo{}, errInvalidArgument
|
||||||
}
|
}
|
||||||
@@ -959,7 +1018,7 @@ func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
|
|||||||
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
|
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
|
||||||
var err error
|
var err error
|
||||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||||
if err != nil {
|
if err != nil && err != errFileNotFound {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(context.Background(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1215,7 +1274,7 @@ func (fs *FSObjects) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error)
|
|||||||
|
|
||||||
// SetBucketPolicy sets policy on bucket
|
// SetBucketPolicy sets policy on bucket
|
||||||
func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
||||||
return savePolicyConfig(fs, bucket, policy)
|
return savePolicyConfig(ctx, fs, bucket, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketPolicy will get policy on bucket
|
// GetBucketPolicy will get policy on bucket
|
||||||
@@ -1259,3 +1318,8 @@ func (fs *FSObjects) IsNotificationSupported() bool {
|
|||||||
func (fs *FSObjects) IsEncryptionSupported() bool {
|
func (fs *FSObjects) IsEncryptionSupported() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (fs *FSObjects) IsCompressionSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
+12
-1
@@ -224,7 +224,16 @@ func FromMinioClientListBucketResultToV2Info(bucket string, result minio.ListBuc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMinioClientMetadata converts metadata to map[string][]string
|
// ToMinioClientObjectInfoMetadata convertes metadata to map[string][]string
|
||||||
|
func ToMinioClientObjectInfoMetadata(metadata map[string]string) map[string][]string {
|
||||||
|
mm := make(map[string][]string, len(metadata))
|
||||||
|
for k, v := range metadata {
|
||||||
|
mm[http.CanonicalHeaderKey(k)] = []string{v}
|
||||||
|
}
|
||||||
|
return mm
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMinioClientMetadata converts metadata to map[string]string
|
||||||
func ToMinioClientMetadata(metadata map[string]string) map[string]string {
|
func ToMinioClientMetadata(metadata map[string]string) map[string]string {
|
||||||
mm := make(map[string]string)
|
mm := make(map[string]string)
|
||||||
for k, v := range metadata {
|
for k, v := range metadata {
|
||||||
@@ -285,6 +294,8 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
|||||||
err = BucketPolicyNotFound{}
|
err = BucketPolicyNotFound{}
|
||||||
case "InvalidBucketName":
|
case "InvalidBucketName":
|
||||||
err = BucketNameInvalid{Bucket: bucket}
|
err = BucketNameInvalid{Bucket: bucket}
|
||||||
|
case "InvalidPart":
|
||||||
|
err = InvalidPart{}
|
||||||
case "NoSuchBucket":
|
case "NoSuchBucket":
|
||||||
err = BucketNotFound{Bucket: bucket}
|
err = BucketNotFound{Bucket: bucket}
|
||||||
case "NoSuchKey":
|
case "NoSuchKey":
|
||||||
|
|||||||
+59
-31
@@ -157,28 +157,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
// Create certs path.
|
// Create certs path.
|
||||||
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
|
||||||
if len(cacheConfig.Drives) > 0 {
|
|
||||||
var err error
|
|
||||||
// initialize the new disk cache objects.
|
|
||||||
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
|
||||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check and load SSL certificates.
|
// Check and load SSL certificates.
|
||||||
var err error
|
var err error
|
||||||
globalPublicCerts, globalRootCAs, globalTLSCerts, globalIsSSL, err = getSSLConfig()
|
globalPublicCerts, globalRootCAs, globalTLSCerts, globalIsSSL, err = getSSLConfig()
|
||||||
@@ -189,14 +167,16 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
|
|
||||||
initNSLock(false) // Enable local namespace lock.
|
initNSLock(false) // Enable local namespace lock.
|
||||||
|
|
||||||
// Create new notification system.
|
|
||||||
globalNotificationSys = NewNotificationSys(globalServerConfig, EndpointList{})
|
|
||||||
|
|
||||||
// Create new policy system.
|
|
||||||
globalPolicySys = NewPolicySys()
|
|
||||||
|
|
||||||
router := mux.NewRouter().SkipClean(true)
|
router := mux.NewRouter().SkipClean(true)
|
||||||
|
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
// Enable STS router if etcd is enabled.
|
||||||
|
registerSTSRouter(router)
|
||||||
|
|
||||||
|
// Enable admin router if etcd is enabled.
|
||||||
|
registerAdminRouter(router)
|
||||||
|
}
|
||||||
|
|
||||||
// Add healthcheck router
|
// Add healthcheck router
|
||||||
registerHealthCheckRouter(router)
|
registerHealthCheckRouter(router)
|
||||||
|
|
||||||
@@ -234,8 +214,59 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new config system.
|
||||||
|
globalConfigSys = NewConfigSys()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
loadLoggers()
|
||||||
|
|
||||||
|
// This is only to uniquely identify each gateway deployments.
|
||||||
|
logger.SetDeploymentID(os.Getenv("MINIO_GATEWAY_DEPLOYMENT_ID"))
|
||||||
|
|
||||||
|
var cacheConfig = globalServerConfig.GetCacheConfig()
|
||||||
|
if len(cacheConfig.Drives) > 0 {
|
||||||
|
var err error
|
||||||
|
// initialize the new disk cache objects.
|
||||||
|
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
||||||
|
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load logger subsystem
|
||||||
|
loadLoggers()
|
||||||
|
|
||||||
|
// Re-enable logging
|
||||||
|
logger.Disable = false
|
||||||
|
|
||||||
|
// Create new IAM system.
|
||||||
|
globalIAMSys = NewIAMSys()
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
// Initialize IAM sys.
|
||||||
|
go globalIAMSys.Init(newObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new policy system.
|
||||||
|
globalPolicySys = NewPolicySys()
|
||||||
|
|
||||||
|
// Initialize policy system.
|
||||||
go globalPolicySys.Init(newObject)
|
go globalPolicySys.Init(newObject)
|
||||||
|
|
||||||
|
// Create new notification system.
|
||||||
|
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||||
|
|
||||||
// Once endpoints are finalized, initialize the new object api.
|
// Once endpoints are finalized, initialize the new object api.
|
||||||
globalObjLayerMutex.Lock()
|
globalObjLayerMutex.Lock()
|
||||||
globalObjectAPI = newObject
|
globalObjectAPI = newObject
|
||||||
@@ -256,8 +287,5 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
printGatewayStartupMessage(getAPIEndpoints(gatewayAddr), gatewayName)
|
printGatewayStartupMessage(getAPIEndpoints(gatewayAddr), gatewayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reenable logging
|
|
||||||
logger.Disable = false
|
|
||||||
|
|
||||||
handleSignals()
|
handleSignals()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,10 @@ func printGatewayCommonMsg(apiEndpoints []string) {
|
|||||||
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
||||||
// Colorize the message and print.
|
// Colorize the message and print.
|
||||||
logger.StartupMessage(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
logger.StartupMessage(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
||||||
logger.StartupMessage(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
if isTerminal() {
|
||||||
logger.StartupMessage(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
logger.StartupMessage(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
||||||
|
logger.StartupMessage(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
||||||
|
}
|
||||||
|
|
||||||
if globalIsBrowserEnabled {
|
if globalIsBrowserEnabled {
|
||||||
logger.StartupMessage(colorBlue("\nBrowser Access:"))
|
logger.StartupMessage(colorBlue("\nBrowser Access:"))
|
||||||
|
|||||||
@@ -151,3 +151,8 @@ func (a GatewayUnsupported) IsNotificationSupported() bool {
|
|||||||
func (a GatewayUnsupported) IsEncryptionSupported() bool {
|
func (a GatewayUnsupported) IsEncryptionSupported() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (a GatewayUnsupported) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -615,6 +615,31 @@ func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, contin
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (a *azureObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = a.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := a.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject - reads an object from azure. Supports additional
|
// GetObject - reads an object from azure. Supports additional
|
||||||
// parameters like offset and length which are synonymous with
|
// parameters like offset and length which are synonymous with
|
||||||
// HTTP Range requests.
|
// HTTP Range requests.
|
||||||
@@ -660,10 +685,33 @@ func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
|||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate correct ETag's if possible, this code primarily exists
|
||||||
|
// because AWS S3 indicates that
|
||||||
|
//
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
|
||||||
|
//
|
||||||
|
// Objects created by the PUT Object, POST Object, or Copy operation,
|
||||||
|
// or through the AWS Management Console, and are encrypted by SSE-S3
|
||||||
|
// or plaintext, have ETags that are an MD5 digest of their object data.
|
||||||
|
//
|
||||||
|
// Some applications depend on this behavior refer https://github.com/minio/minio/issues/6550
|
||||||
|
// So we handle it here and make this consistent.
|
||||||
|
etag := minio.ToS3ETag(blob.Properties.Etag)
|
||||||
|
switch {
|
||||||
|
case blob.Properties.ContentMD5 != "":
|
||||||
|
b, err := base64.StdEncoding.DecodeString(blob.Properties.ContentMD5)
|
||||||
|
if err == nil {
|
||||||
|
etag = hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
case blob.Metadata["md5sum"] != "":
|
||||||
|
etag = blob.Metadata["md5sum"]
|
||||||
|
delete(blob.Metadata, "md5sum")
|
||||||
|
}
|
||||||
|
|
||||||
return minio.ObjectInfo{
|
return minio.ObjectInfo{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
UserDefined: azurePropertiesToS3Meta(blob.Metadata, blob.Properties),
|
UserDefined: azurePropertiesToS3Meta(blob.Metadata, blob.Properties),
|
||||||
ETag: minio.ToS3ETag(blob.Properties.Etag),
|
ETag: etag,
|
||||||
ModTime: time.Time(blob.Properties.LastModified),
|
ModTime: time.Time(blob.Properties.LastModified),
|
||||||
Name: object,
|
Name: object,
|
||||||
Size: blob.Properties.ContentLength,
|
Size: blob.Properties.ContentLength,
|
||||||
@@ -675,15 +723,103 @@ func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
|||||||
// PutObject - Create a new blob with the incoming data,
|
// PutObject - Create a new blob with the incoming data,
|
||||||
// uses Azure equivalent CreateBlockBlobFromReader.
|
// uses Azure equivalent CreateBlockBlobFromReader.
|
||||||
func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||||
|
if data.Size() < azureBlockSize/10 {
|
||||||
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
|
blob.Metadata, blob.Properties, err = s3MetaToAzureProperties(ctx, metadata)
|
||||||
|
if err = blob.CreateBlockBlobFromReader(data, nil); err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
|
return a.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := getAzureUploadID()
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, err
|
||||||
|
}
|
||||||
|
etag := data.MD5HexString()
|
||||||
|
if etag == "" {
|
||||||
|
etag = minio.GenETag()
|
||||||
|
}
|
||||||
|
|
||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
blob.Metadata, blob.Properties, err = s3MetaToAzureProperties(ctx, metadata)
|
subPartSize, subPartNumber := int64(azureBlockSize), 1
|
||||||
|
for remainingSize := data.Size(); remainingSize >= 0; remainingSize -= subPartSize {
|
||||||
|
// Allow to create zero sized part.
|
||||||
|
if remainingSize == 0 && subPartNumber > 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if remainingSize < subPartSize {
|
||||||
|
subPartSize = remainingSize
|
||||||
|
}
|
||||||
|
|
||||||
|
id := azureGetBlockID(1, subPartNumber, uuid, etag)
|
||||||
|
if err = blob.PutBlockWithLength(id, uint64(subPartSize), io.LimitReader(data, subPartSize), nil); err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
|
subPartNumber++
|
||||||
|
}
|
||||||
|
|
||||||
|
objBlob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
|
resp, err := objBlob.GetBlockList(storage.BlockListTypeUncommitted, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
err = blob.CreateBlockBlobFromReader(data, nil)
|
|
||||||
|
getBlocks := func(partNumber int, etag string) (blocks []storage.Block, size int64, aerr error) {
|
||||||
|
for _, part := range resp.UncommittedBlocks {
|
||||||
|
var partID int
|
||||||
|
var readUploadID string
|
||||||
|
var md5Hex string
|
||||||
|
if partID, _, readUploadID, md5Hex, aerr = azureParseBlockID(part.Name); aerr != nil {
|
||||||
|
return nil, 0, aerr
|
||||||
|
}
|
||||||
|
|
||||||
|
if partNumber == partID && uuid == readUploadID && etag == md5Hex {
|
||||||
|
blocks = append(blocks, storage.Block{
|
||||||
|
ID: part.Name,
|
||||||
|
Status: storage.BlockStatusUncommitted,
|
||||||
|
})
|
||||||
|
|
||||||
|
size += part.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(blocks) == 0 {
|
||||||
|
return nil, 0, minio.InvalidPart{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks, size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocks []storage.Block
|
||||||
|
blocks, _, err = getBlocks(1, etag)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return objInfo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = objBlob.PutBlockList(blocks, nil); err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save md5sum for future processing on the object.
|
||||||
|
metadata["x-amz-meta-md5sum"] = hex.EncodeToString(data.MD5Current())
|
||||||
|
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
if err = objBlob.SetProperties(nil); err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
|
if err = objBlob.SetMetadata(nil); err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
return a.GetObjectInfo(ctx, bucket, object, opts)
|
return a.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,10 +1085,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
||||||
derr := blob.Delete(nil)
|
derr := blob.Delete(nil)
|
||||||
logger.GetReqInfo(ctx).AppendTags("uploadID", uploadID)
|
logger.GetReqInfo(ctx).AppendTags("uploadID", uploadID)
|
||||||
@@ -1123,3 +1255,8 @@ func (a *azureObjects) DeleteBucketPolicy(ctx context.Context, bucket string) er
|
|||||||
err := container.SetPermissions(perm, nil)
|
err := container.SetPermissions(perm, nil)
|
||||||
return azureToObjectError(err)
|
return azureToObjectError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (a *azureObjects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -394,6 +395,31 @@ func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
|
|||||||
return loi, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (l *b2Objects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = l.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := l.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject reads an object from B2. Supports additional
|
// GetObject reads an object from B2. Supports additional
|
||||||
// parameters like offset and length which are synonymous with
|
// parameters like offset and length which are synonymous with
|
||||||
// HTTP Range requests.
|
// HTTP Range requests.
|
||||||
@@ -814,3 +840,8 @@ func (l *b2Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error
|
|||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return b2ToObjectError(err)
|
return b2ToObjectError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (l *b2Objects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -736,6 +736,31 @@ func (l *gcsGateway) ListObjectsV2(ctx context.Context, bucket, prefix, continua
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (l *gcsGateway) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = l.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := l.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject - reads an object from GCS. Supports additional
|
// GetObject - reads an object from GCS. Supports additional
|
||||||
// parameters like offset and length which are synonymous with
|
// parameters like offset and length which are synonymous with
|
||||||
// HTTP Range requests.
|
// HTTP Range requests.
|
||||||
@@ -750,7 +775,13 @@ func (l *gcsGateway) GetObject(ctx context.Context, bucket string, key string, s
|
|||||||
return gcsToObjectError(err, bucket)
|
return gcsToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
object := l.client.Bucket(bucket).Object(key)
|
// GCS storage decompresses a gzipped object by default and returns the data.
|
||||||
|
// Refer to https://cloud.google.com/storage/docs/transcoding#decompressive_transcoding
|
||||||
|
// Need to set `Accept-Encoding` header to `gzip` when issuing a GetObject call, to be able
|
||||||
|
// to download the object in compressed state.
|
||||||
|
// Calling ReadCompressed with true accomplishes that.
|
||||||
|
object := l.client.Bucket(bucket).Object(key).ReadCompressed(true)
|
||||||
|
|
||||||
r, err := object.NewRangeReader(l.ctx, startOffset, length)
|
r, err := object.NewRangeReader(l.ctx, startOffset, length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -1422,3 +1453,8 @@ func (l *gcsGateway) DeleteBucketPolicy(ctx context.Context, bucket string) erro
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (l *gcsGateway) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -506,6 +506,31 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (t *tritonObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = t.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := t.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject - Reads an object from Manta. Supports additional parameters like
|
// GetObject - Reads an object from Manta. Supports additional parameters like
|
||||||
// offset and length which are synonymous with HTTP Range requests.
|
// offset and length which are synonymous with HTTP Range requests.
|
||||||
//
|
//
|
||||||
@@ -634,3 +659,8 @@ func (t *tritonObjects) DeleteObject(ctx context.Context, bucket, object string)
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (t *tritonObjects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,3 +125,8 @@ type nasObjects struct {
|
|||||||
func (l *nasObjects) IsNotificationSupported() bool {
|
func (l *nasObjects) IsNotificationSupported() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (l *nasObjects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ ENVIRONMENT VARIABLES:
|
|||||||
|
|
||||||
DOMAIN:
|
DOMAIN:
|
||||||
MINIO_DOMAIN: To enable virtual-host-style requests, set this value to Minio host domain name.
|
MINIO_DOMAIN: To enable virtual-host-style requests, set this value to Minio host domain name.
|
||||||
|
|
||||||
CACHE:
|
CACHE:
|
||||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||||
@@ -546,6 +546,31 @@ func ossGetObject(ctx context.Context, client *oss.Client, bucket, key string, s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (l *ossObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = l.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := l.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject reads an object on OSS. Supports additional
|
// GetObject reads an object on OSS. Supports additional
|
||||||
// parameters like offset and length which are synonymous with
|
// parameters like offset and length which are synonymous with
|
||||||
// HTTP Range requests.
|
// HTTP Range requests.
|
||||||
@@ -1070,3 +1095,8 @@ func (l *ossObjects) DeleteBucketPolicy(ctx context.Context, bucket string) erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (l *ossObjects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
miniogo "github.com/minio/minio-go"
|
miniogo "github.com/minio/minio-go"
|
||||||
|
"github.com/minio/minio-go/pkg/credentials"
|
||||||
"github.com/minio/minio-go/pkg/s3utils"
|
"github.com/minio/minio-go/pkg/s3utils"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
@@ -69,6 +71,9 @@ ENVIRONMENT VARIABLES:
|
|||||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||||
|
|
||||||
|
LOGGER:
|
||||||
|
MINIO_LOGGER_HTTP_ENDPOINT: HTTP endpoint URL to log all incoming requests.
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
1. Start minio gateway server for AWS S3 backend.
|
1. Start minio gateway server for AWS S3 backend.
|
||||||
$ export MINIO_ACCESS_KEY=accesskey
|
$ export MINIO_ACCESS_KEY=accesskey
|
||||||
@@ -80,7 +85,13 @@ EXAMPLES:
|
|||||||
$ export MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
$ export MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||||
$ {{.HelpName}} https://play.minio.io:9000
|
$ {{.HelpName}} https://play.minio.io:9000
|
||||||
|
|
||||||
3. Start minio gateway server for AWS S3 backend with edge caching enabled.
|
3. Start minio gateway server for AWS S3 backend logging all requests to http endpoint.
|
||||||
|
$ export MINIO_ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||||
|
$ export MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||||
|
$ export MINIO_LOGGER_HTTP_ENDPOINT="http://localhost:8000/"
|
||||||
|
$ {{.HelpName}} https://play.minio.io:9000
|
||||||
|
|
||||||
|
4. Start minio gateway server for AWS S3 backend with edge caching enabled.
|
||||||
$ export MINIO_ACCESS_KEY=accesskey
|
$ export MINIO_ACCESS_KEY=accesskey
|
||||||
$ export MINIO_SECRET_KEY=secretkey
|
$ export MINIO_SECRET_KEY=secretkey
|
||||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||||
@@ -88,6 +99,15 @@ EXAMPLES:
|
|||||||
$ export MINIO_CACHE_EXPIRY=40
|
$ export MINIO_CACHE_EXPIRY=40
|
||||||
$ export MINIO_CACHE_MAXUSE=80
|
$ export MINIO_CACHE_MAXUSE=80
|
||||||
$ {{.HelpName}}
|
$ {{.HelpName}}
|
||||||
|
|
||||||
|
4. Start minio gateway server for AWS S3 backend using AWS environment variables.
|
||||||
|
NOTE: The access and secret key in this case will authenticate with Minio instead
|
||||||
|
of AWS and AWS envs will be used to authenticate to AWS S3.
|
||||||
|
$ export AWS_ACCESS_KEY_ID=aws_access_key
|
||||||
|
$ export AWS_SECRET_ACCESS_KEY=aws_secret_key
|
||||||
|
$ export MINIO_ACCESS_KEY=accesskey
|
||||||
|
$ export MINIO_SECRET_KEY=secretkey
|
||||||
|
$ {{.HelpName}}
|
||||||
`
|
`
|
||||||
|
|
||||||
minio.RegisterGatewayCommand(cli.Command{
|
minio.RegisterGatewayCommand(cli.Command{
|
||||||
@@ -149,7 +169,7 @@ func randString(n int, src rand.Source, prefix string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newS3 - Initializes a new client by auto probing S3 server signature.
|
// newS3 - Initializes a new client by auto probing S3 server signature.
|
||||||
func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
func newS3(url string) (*miniogo.Core, error) {
|
||||||
if url == "" {
|
if url == "" {
|
||||||
url = "https://s3.amazonaws.com"
|
url = "https://s3.amazonaws.com"
|
||||||
}
|
}
|
||||||
@@ -160,19 +180,33 @@ func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clnt, err := miniogo.NewV4(endpoint, accessKey, secretKey, secure)
|
// Chains all credential types, in the following order:
|
||||||
|
// - AWS env vars (i.e. AWS_ACCESS_KEY_ID)
|
||||||
|
// - IAM profile based credentials. (performs an HTTP
|
||||||
|
// call to a pre-defined endpoint, only valid inside
|
||||||
|
// configured ec2 instances)
|
||||||
|
// - AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials)
|
||||||
|
// - Static credentials provided by user (i.e. MINIO_ACCESS_KEY)
|
||||||
|
creds := credentials.NewChainCredentials([]credentials.Provider{
|
||||||
|
&credentials.EnvAWS{},
|
||||||
|
&credentials.IAM{
|
||||||
|
Client: &http.Client{
|
||||||
|
Transport: minio.NewCustomHTTPTransport(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&credentials.FileAWSCredentials{},
|
||||||
|
&credentials.EnvMinio{},
|
||||||
|
})
|
||||||
|
|
||||||
|
clnt, err := miniogo.NewWithCredentials(endpoint, creds, secure, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bucket-sign-")
|
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bucket-sign-")
|
||||||
|
// Check if the provided keys are valid.
|
||||||
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
||||||
clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure)
|
return nil, err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &miniogo.Core{Client: clnt}, nil
|
return &miniogo.Core{Client: clnt}, nil
|
||||||
@@ -180,8 +214,9 @@ func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
|||||||
|
|
||||||
// NewGatewayLayer returns s3 ObjectLayer.
|
// NewGatewayLayer returns s3 ObjectLayer.
|
||||||
func (g *S3) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
func (g *S3) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||||
// Probe S3 signature with input credentials.
|
// creds are ignored here, since S3 gateway implements chaining
|
||||||
clnt, err := newS3(g.host, creds.AccessKey, creds.SecretKey)
|
// all credentials.
|
||||||
|
clnt, err := newS3(g.host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -301,6 +336,31 @@ func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
|
|||||||
return minio.FromMinioClientListBucketV2Result(bucket, result), nil
|
return minio.FromMinioClientListBucketV2Result(bucket, result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (l *s3Objects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = l.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := l.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject reads an object from S3. Supports additional
|
// GetObject reads an object from S3. Supports additional
|
||||||
// parameters like offset and length which are synonymous with
|
// parameters like offset and length which are synonymous with
|
||||||
// HTTP Range requests.
|
// HTTP Range requests.
|
||||||
@@ -350,6 +410,9 @@ func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
// On success, populate the key & metadata so they are present in the notification
|
||||||
|
oi.Key = object
|
||||||
|
oi.Metadata = minio.ToMinioClientObjectInfoMetadata(metadata)
|
||||||
|
|
||||||
return minio.FromMinioClientObjectInfo(bucket, oi), nil
|
return minio.FromMinioClientObjectInfo(bucket, oi), nil
|
||||||
}
|
}
|
||||||
@@ -487,3 +550,8 @@ func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (l *s3Objects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ func TestS3ToObjectError(t *testing.T) {
|
|||||||
inputErr: errResponse("InvalidBucketName"),
|
inputErr: errResponse("InvalidBucketName"),
|
||||||
expectedErr: minio.BucketNameInvalid{},
|
expectedErr: minio.BucketNameInvalid{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
inputErr: errResponse("InvalidPart"),
|
||||||
|
expectedErr: minio.InvalidPart{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
inputErr: errResponse("NoSuchBucketPolicy"),
|
inputErr: errResponse("NoSuchBucketPolicy"),
|
||||||
expectedErr: minio.BucketPolicyNotFound{},
|
expectedErr: minio.BucketPolicyNotFound{},
|
||||||
|
|||||||
@@ -431,6 +431,31 @@ func (s *siaObjects) ListObjects(ctx context.Context, bucket string, prefix stri
|
|||||||
return loi, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
|
func (s *siaObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||||
|
var objInfo minio.ObjectInfo
|
||||||
|
objInfo, err = s.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset, length int64
|
||||||
|
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
err := s.GetObject(ctx, bucket, object, startOffset, 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 minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *siaObjects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
func (s *siaObjects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||||
dstFile := path.Join(s.TempDir, minio.MustGetUUID())
|
dstFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||||
defer os.Remove(dstFile)
|
defer os.Remove(dstFile)
|
||||||
@@ -626,3 +651,8 @@ func (s *siaObjects) deleteTempFileWhenUploadCompletes(ctx context.Context, temp
|
|||||||
|
|
||||||
os.Remove(tempFile)
|
os.Remove(tempFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (s *siaObjects) IsCompressionSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
+16
-1
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/minio/minio-go/pkg/set"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
@@ -689,7 +690,7 @@ func setBucketForwardingHandler(h http.Handler) http.Handler {
|
|||||||
// canceled immediately.
|
// canceled immediately.
|
||||||
func setRateLimitHandler(h http.Handler) http.Handler {
|
func setRateLimitHandler(h http.Handler) http.Handler {
|
||||||
_, maxLimit, err := sys.GetMaxOpenFileLimit()
|
_, maxLimit, err := sys.GetMaxOpenFileLimit()
|
||||||
logger.FatalIf(err, "Unable to get maximum open file limit", context.Background())
|
logger.FatalIf(err, "Unable to get maximum open file limit")
|
||||||
// Burst value is set to 1 to allow only maxOpenFileLimit
|
// Burst value is set to 1 to allow only maxOpenFileLimit
|
||||||
// requests to happen at once.
|
// requests to happen at once.
|
||||||
l := rate.NewLimiter(rate.Limit(maxLimit), 1)
|
l := rate.NewLimiter(rate.Limit(maxLimit), 1)
|
||||||
@@ -773,3 +774,17 @@ func (h criticalErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|||||||
}()
|
}()
|
||||||
h.handler.ServeHTTP(w, r)
|
h.handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSSETLSHandler(h http.Handler) http.Handler { return sseTLSHandler{h} }
|
||||||
|
|
||||||
|
// sseTLSHandler enforces certain rules for SSE requests which are made / must be made over TLS.
|
||||||
|
type sseTLSHandler struct{ handler http.Handler }
|
||||||
|
|
||||||
|
func (h sseTLSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Deny SSE-C requests if not made over TLS
|
||||||
|
if !globalIsSSL && (crypto.SSEC.IsRequested(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||||
|
writeErrorResponseHeadersOnly(w, ErrInsecureSSECustomerRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -181,3 +182,39 @@ func TestContainsReservedMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sseTLSHandlerTests = []struct {
|
||||||
|
Header http.Header
|
||||||
|
IsTLS, ShouldFail bool
|
||||||
|
}{
|
||||||
|
{Header: http.Header{}, IsTLS: false, ShouldFail: false}, // 0
|
||||||
|
{Header: http.Header{crypto.SSECAlgorithm: []string{"AES256"}}, IsTLS: false, ShouldFail: true}, // 1
|
||||||
|
{Header: http.Header{crypto.SSECAlgorithm: []string{"AES256"}}, IsTLS: true, ShouldFail: false}, // 2
|
||||||
|
{Header: http.Header{crypto.SSECKey: []string{""}}, IsTLS: true, ShouldFail: false}, // 3
|
||||||
|
{Header: http.Header{crypto.SSECopyAlgorithm: []string{""}}, IsTLS: false, ShouldFail: true}, // 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSETLSHandler(t *testing.T) {
|
||||||
|
defer func(isSSL bool) { globalIsSSL = isSSL }(globalIsSSL) // reset globalIsSSL after test
|
||||||
|
|
||||||
|
var okHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
for i, test := range sseTLSHandlerTests {
|
||||||
|
globalIsSSL = test.IsTLS
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := new(http.Request)
|
||||||
|
r.Header = test.Header
|
||||||
|
|
||||||
|
h := setSSETLSHandler(okHandler)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case test.ShouldFail && w.Code == http.StatusOK:
|
||||||
|
t.Errorf("Test %d: should fail but status code is HTTP %d", i, w.Code)
|
||||||
|
case !test.ShouldFail && w.Code != http.StatusOK:
|
||||||
|
t.Errorf("Test %d: should not fail but status code is HTTP %d and not 200 OK", i, w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import (
|
|||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/certs"
|
"github.com/minio/minio/pkg/certs"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
// minio configuration related constants.
|
// minio configuration related constants.
|
||||||
@@ -81,6 +83,8 @@ const (
|
|||||||
globalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
globalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||||
// Refresh interval to update in-memory bucket policy cache.
|
// Refresh interval to update in-memory bucket policy cache.
|
||||||
globalRefreshBucketPolicyInterval = 5 * time.Minute
|
globalRefreshBucketPolicyInterval = 5 * time.Minute
|
||||||
|
// Refresh interval to update in-memory iam config cache.
|
||||||
|
globalRefreshIAMInterval = 5 * time.Minute
|
||||||
|
|
||||||
// Limit of location constraint XML for unauthenticted PUT bucket operations.
|
// Limit of location constraint XML for unauthenticted PUT bucket operations.
|
||||||
maxLocationConstraintSize = 3 * humanize.MiByte
|
maxLocationConstraintSize = 3 * humanize.MiByte
|
||||||
@@ -132,6 +136,7 @@ var (
|
|||||||
|
|
||||||
globalNotificationSys *NotificationSys
|
globalNotificationSys *NotificationSys
|
||||||
globalPolicySys *PolicySys
|
globalPolicySys *PolicySys
|
||||||
|
globalIAMSys *IAMSys
|
||||||
|
|
||||||
// CA root certificates, a nil value means system certs pool will be used
|
// CA root certificates, a nil value means system certs pool will be used
|
||||||
globalRootCAs *x509.CertPool
|
globalRootCAs *x509.CertPool
|
||||||
@@ -227,6 +232,29 @@ var (
|
|||||||
globalKMS crypto.KMS
|
globalKMS crypto.KMS
|
||||||
// KMS config
|
// KMS config
|
||||||
globalKMSConfig crypto.KMSConfig
|
globalKMSConfig crypto.KMSConfig
|
||||||
|
|
||||||
|
// Is compression include extensions/content-types set.
|
||||||
|
globalIsEnvCompression bool
|
||||||
|
|
||||||
|
// Is compression enabeld.
|
||||||
|
globalIsCompressionEnabled = false
|
||||||
|
|
||||||
|
// Include-list for compression.
|
||||||
|
globalCompressExtensions = []string{".txt", ".log", ".csv", ".json"}
|
||||||
|
globalCompressMimeTypes = []string{"text/csv", "text/plain", "application/json"}
|
||||||
|
|
||||||
|
// Some standard object extensions which we strictly dis-allow for compression.
|
||||||
|
standardExcludeCompressExtensions = []string{".gz", ".bz2", ".rar", ".zip", ".7z"}
|
||||||
|
|
||||||
|
// Some standard content-types which we strictly dis-allow for compression.
|
||||||
|
standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
|
||||||
|
|
||||||
|
// Authorization validators list.
|
||||||
|
globalIAMValidators *validator.Validators
|
||||||
|
|
||||||
|
// OPA policy system.
|
||||||
|
globalPolicyOPA *iampolicy.Opa
|
||||||
|
|
||||||
// Add new variable global values here.
|
// Add new variable global values here.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ func TestBuffConnReadTimeout(t *testing.T) {
|
|||||||
|
|
||||||
tcpConn, terr := tcpListener.AcceptTCP()
|
tcpConn, terr := tcpListener.AcceptTCP()
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
t.Fatalf("failed to accept new connection. %v", terr)
|
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)
|
||||||
defer bufconn.Close()
|
defer bufconn.Close()
|
||||||
@@ -55,11 +56,13 @@ func TestBuffConnReadTimeout(t *testing.T) {
|
|||||||
var b = make([]byte, 12)
|
var b = make([]byte, 12)
|
||||||
_, terr = bufconn.Read(b)
|
_, terr = bufconn.Read(b)
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
t.Fatalf("failed to read from client. %v", terr)
|
t.Errorf("failed to read from client. %v", terr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
received := string(b)
|
received := string(b)
|
||||||
if received != "message one\n" {
|
if received != "message one\n" {
|
||||||
t.Fatalf(`server: expected: "message one\n", got: %v`, received)
|
t.Errorf(`server: expected: "message one\n", got: %v`, received)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for more than read timeout to simulate processing.
|
// Wait for more than read timeout to simulate processing.
|
||||||
@@ -67,17 +70,20 @@ func TestBuffConnReadTimeout(t *testing.T) {
|
|||||||
|
|
||||||
_, terr = bufconn.Read(b)
|
_, terr = bufconn.Read(b)
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
t.Fatalf("failed to read from client. %v", terr)
|
t.Errorf("failed to read from client. %v", terr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
received = string(b)
|
received = string(b)
|
||||||
if received != "message two\n" {
|
if received != "message two\n" {
|
||||||
t.Fatalf(`server: expected: "message two\n", got: %v`, received)
|
t.Errorf(`server: expected: "message two\n", got: %v`, received)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a response.
|
// Send a response.
|
||||||
_, terr = io.WriteString(bufconn, "messages received\n")
|
_, terr = io.WriteString(bufconn, "messages received\n")
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
t.Fatalf("failed to write to client. %v", terr)
|
t.Errorf("failed to write to client. %v", terr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes all deadlines if any.
|
// Removes all deadlines if any.
|
||||||
|
|||||||
+105
-113
@@ -35,6 +35,8 @@ import (
|
|||||||
|
|
||||||
var sslRequiredErrMsg = []byte("HTTP/1.0 403 Forbidden\r\n\r\nSSL required")
|
var sslRequiredErrMsg = []byte("HTTP/1.0 403 Forbidden\r\n\r\nSSL required")
|
||||||
|
|
||||||
|
var badRequestMsg = []byte("HTTP/1.0 400 Bad Request\r\n\r\n")
|
||||||
|
|
||||||
// HTTP methods.
|
// HTTP methods.
|
||||||
var methods = []string{
|
var methods = []string{
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
@@ -73,11 +75,40 @@ func isHTTPMethod(s string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getResourceHost(bufConn *BufConn, maxHeaderBytes, methodLen int) (resource string, host string, err error) {
|
func getPlainText(bufConn *BufConn) (bool, error) {
|
||||||
|
defer bufConn.setReadTimeout()
|
||||||
|
|
||||||
|
if bufConn.canSetReadDeadline() {
|
||||||
|
// Set deadline such that we close the connection quickly
|
||||||
|
// of no data was received from the Peek()
|
||||||
|
bufConn.SetReadDeadline(time.Now().UTC().Add(time.Second * 3))
|
||||||
|
}
|
||||||
|
b, err := bufConn.Peek(1)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, method := range methods {
|
||||||
|
if b[0] == method[0] {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMethodResourceHost(bufConn *BufConn, maxHeaderBytes int) (method string, resource string, host string, err error) {
|
||||||
|
defer bufConn.setReadTimeout()
|
||||||
|
|
||||||
var data []byte
|
var data []byte
|
||||||
for dataLen := 0; methodLen+dataLen < maxHeaderBytes; dataLen += 8 {
|
for dataLen := 1; dataLen < maxHeaderBytes; dataLen++ {
|
||||||
if data, err = bufConn.Peek(methodLen + dataLen); err != nil {
|
if bufConn.canSetReadDeadline() {
|
||||||
return "", "", err
|
// Set deadline such that we close the connection quickly
|
||||||
|
// of no data was received from the Peek()
|
||||||
|
bufConn.SetReadDeadline(time.Now().UTC().Add(time.Second * 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = bufConn.bufReader.Peek(dataLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := strings.Split(string(data), "\n")
|
tokens := strings.Split(string(data), "\n")
|
||||||
@@ -85,8 +116,21 @@ func getResourceHost(bufConn *BufConn, maxHeaderBytes, methodLen int) (resource
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if resource == "" {
|
if method == "" && resource == "" {
|
||||||
resource = strings.SplitN(tokens[0][methodLen:], " ", 2)[0]
|
if i := strings.IndexByte(tokens[0], ' '); i == -1 {
|
||||||
|
return "", "", "", fmt.Errorf("malformed HTTP request from '%s'", bufConn.LocalAddr())
|
||||||
|
}
|
||||||
|
httpTokens := strings.SplitN(tokens[0], " ", 3)
|
||||||
|
if len(httpTokens) < 3 {
|
||||||
|
return "", "", "", fmt.Errorf("malformed HTTP request from '%s'", bufConn.LocalAddr())
|
||||||
|
}
|
||||||
|
if !isHTTPMethod(httpTokens[0]) {
|
||||||
|
return "", "", "", fmt.Errorf("malformed HTTP request, invalid HTTP method '%s' from '%s'",
|
||||||
|
httpTokens[0], bufConn.LocalAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
method = httpTokens[0]
|
||||||
|
resource = httpTokens[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, token := range tokens[1:] {
|
for _, token := range tokens[1:] {
|
||||||
@@ -99,7 +143,7 @@ func getResourceHost(bufConn *BufConn, maxHeaderBytes, methodLen int) (resource
|
|||||||
token = strings.ToLower(token)
|
token = strings.ToLower(token)
|
||||||
if strings.HasPrefix(token, "host: ") {
|
if strings.HasPrefix(token, "host: ") {
|
||||||
host = strings.TrimPrefix(strings.TrimSuffix(token, "\r"), "host: ")
|
host = strings.TrimPrefix(strings.TrimSuffix(token, "\r"), "host: ")
|
||||||
return resource, host, nil
|
return method, resource, host, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +152,7 @@ func getResourceHost(bufConn *BufConn, maxHeaderBytes, methodLen int) (resource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resource, host, nil
|
return "", "", "", fmt.Errorf("malformed HTTP request from %s", bufConn.LocalAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
type acceptResult struct {
|
type acceptResult struct {
|
||||||
@@ -175,9 +219,48 @@ func (listener *httpListener) start() {
|
|||||||
tcpConn.SetKeepAlivePeriod(listener.tcpKeepAliveTimeout)
|
tcpConn.SetKeepAlivePeriod(listener.tcpKeepAliveTimeout)
|
||||||
|
|
||||||
bufconn := newBufConn(tcpConn, listener.readTimeout, listener.writeTimeout)
|
bufconn := newBufConn(tcpConn, listener.readTimeout, listener.writeTimeout)
|
||||||
|
if listener.tlsConfig != nil {
|
||||||
|
ok, err := getPlainText(bufconn)
|
||||||
|
if err != nil {
|
||||||
|
// Peek could fail legitimately when clients abruptly close
|
||||||
|
// connection. E.g. Chrome browser opens connections speculatively to
|
||||||
|
// speed up loading of a web page. Peek may also fail due to network
|
||||||
|
// saturation on a transport with read timeout set. All other kind of
|
||||||
|
// errors should be logged for further investigation. Thanks @brendanashworth.
|
||||||
|
if !isRoutineNetErr(err) {
|
||||||
|
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
||||||
|
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
||||||
|
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
bufconn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Peek bytes of maximum length of all HTTP methods.
|
if ok {
|
||||||
data, err := bufconn.Peek(methodMaxLen)
|
// As TLS is configured and we got plain text HTTP request,
|
||||||
|
// return 403 (forbidden) error.
|
||||||
|
bufconn.Write(sslRequiredErrMsg)
|
||||||
|
bufconn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// As the listener is configured with TLS, try to do TLS handshake, drop the connection if it fails.
|
||||||
|
tlsConn := tls.Server(bufconn, listener.tlsConfig)
|
||||||
|
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
||||||
|
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
||||||
|
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
bufconn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bufconn = newBufConn(tlsConn, listener.readTimeout, listener.writeTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
method, resource, host, err := getMethodResourceHost(bufconn, listener.maxHeaderBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Peek could fail legitimately when clients abruptly close
|
// Peek could fail legitimately when clients abruptly close
|
||||||
// connection. E.g. Chrome browser opens connections speculatively to
|
// connection. E.g. Chrome browser opens connections speculatively to
|
||||||
@@ -189,116 +272,25 @@ func (listener *httpListener) start() {
|
|||||||
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
|
bufconn.Write(badRequestMsg)
|
||||||
}
|
}
|
||||||
bufconn.Close()
|
bufconn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return bufconn if read data is a valid HTTP method.
|
header := make(http.Header)
|
||||||
tokens := strings.SplitN(string(data), " ", 2)
|
if host != "" {
|
||||||
if isHTTPMethod(tokens[0]) {
|
header.Add("Host", host)
|
||||||
if listener.tlsConfig != nil {
|
|
||||||
// As TLS is configured and we got plain text HTTP request,
|
|
||||||
// return 403 (forbidden) error.
|
|
||||||
bufconn.Write(sslRequiredErrMsg)
|
|
||||||
bufconn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource, host string
|
|
||||||
if resource, host, err = getResourceHost(bufconn, listener.maxHeaderBytes, len(tokens[0])+1); err != nil {
|
|
||||||
if !isRoutineNetErr(err) {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
|
||||||
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
bufconn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
if host != "" {
|
|
||||||
header.Add("Host", host)
|
|
||||||
}
|
|
||||||
bufconn.setRequest(&http.Request{
|
|
||||||
Method: tokens[0],
|
|
||||||
URL: &url.URL{Path: resource},
|
|
||||||
Host: bufconn.LocalAddr().String(),
|
|
||||||
Header: header,
|
|
||||||
})
|
|
||||||
bufconn.setUpdateFuncs(listener.updateBytesReadFunc, listener.updateBytesWrittenFunc)
|
|
||||||
|
|
||||||
send(acceptResult{bufconn, nil}, doneCh)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
bufconn.setRequest(&http.Request{
|
||||||
|
Method: method,
|
||||||
|
URL: &url.URL{Path: resource},
|
||||||
|
Host: bufconn.LocalAddr().String(),
|
||||||
|
Header: header,
|
||||||
|
})
|
||||||
|
bufconn.setUpdateFuncs(listener.updateBytesReadFunc, listener.updateBytesWrittenFunc)
|
||||||
|
|
||||||
if listener.tlsConfig != nil {
|
send(acceptResult{bufconn, nil}, doneCh)
|
||||||
// As the listener is configured with TLS, try to do TLS handshake, drop the connection if it fails.
|
|
||||||
tlsConn := tls.Server(bufconn, listener.tlsConfig)
|
|
||||||
if err = tlsConn.Handshake(); err != nil {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
|
||||||
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
bufconn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the connection contains HTTP request or not.
|
|
||||||
bufconn = newBufConn(tlsConn, listener.readTimeout, listener.writeTimeout)
|
|
||||||
|
|
||||||
// Peek bytes of maximum length of all HTTP methods.
|
|
||||||
data, err = bufconn.Peek(methodMaxLen)
|
|
||||||
if err != nil {
|
|
||||||
if !isRoutineNetErr(err) {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
|
||||||
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
bufconn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return bufconn if read data is a valid HTTP method.
|
|
||||||
tokens := strings.SplitN(string(data), " ", 2)
|
|
||||||
if isHTTPMethod(tokens[0]) {
|
|
||||||
var resource, host string
|
|
||||||
if resource, host, err = getResourceHost(bufconn, listener.maxHeaderBytes, len(tokens[0])+1); err != nil {
|
|
||||||
if !isRoutineNetErr(err) {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
|
||||||
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
bufconn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
if host != "" {
|
|
||||||
header.Add("Host", host)
|
|
||||||
}
|
|
||||||
bufconn.setRequest(&http.Request{
|
|
||||||
Method: tokens[0],
|
|
||||||
URL: &url.URL{Path: resource},
|
|
||||||
Host: bufconn.LocalAddr().String(),
|
|
||||||
Header: header,
|
|
||||||
})
|
|
||||||
bufconn.setUpdateFuncs(listener.updateBytesReadFunc, listener.updateBytesWrittenFunc)
|
|
||||||
|
|
||||||
send(acceptResult{bufconn, nil}, doneCh)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", bufconn.RemoteAddr().String())
|
|
||||||
reqInfo.AppendTags("localAddr", bufconn.LocalAddr().String())
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
|
|
||||||
bufconn.Close()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closure to handle TCPListener until done channel is closed.
|
// Closure to handle TCPListener until done channel is closed.
|
||||||
|
|||||||
@@ -393,11 +393,11 @@ func TestHTTPListenerAccept(t *testing.T) {
|
|||||||
{[]string{"localhost:0"}, nil, "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "GET / HTTP/1.0\r\n"},
|
{[]string{"localhost:0"}, nil, "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "GET / HTTP/1.0\r\n"},
|
||||||
{[]string{nonLoopBackIP + ":0"}, nil, "POST / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "POST / HTTP/1.0\r\n"},
|
{[]string{nonLoopBackIP + ":0"}, nil, "POST / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "POST / HTTP/1.0\r\n"},
|
||||||
{[]string{nonLoopBackIP + ":0"}, nil, "HEAD / HTTP/1.0\r\nhost: example.org\r\n\r\n", "200 OK\r\n", "HEAD / HTTP/1.0\r\n"},
|
{[]string{nonLoopBackIP + ":0"}, nil, "HEAD / HTTP/1.0\r\nhost: example.org\r\n\r\n", "200 OK\r\n", "HEAD / HTTP/1.0\r\n"},
|
||||||
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, nil, "CONNECT \r\nHost: www.example.org\r\n\r\n", "200 OK\r\n", "CONNECT \r\n"},
|
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, nil, "CONNECT / HTTP/1.0\r\nHost: www.example.org\r\n\r\n", "200 OK\r\n", "CONNECT / HTTP/1.0\r\n"},
|
||||||
{[]string{"localhost:0"}, tlsConfig, "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "GET / HTTP/1.0\r\n"},
|
{[]string{"localhost:0"}, tlsConfig, "GET / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "GET / HTTP/1.0\r\n"},
|
||||||
{[]string{nonLoopBackIP + ":0"}, tlsConfig, "POST / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "POST / HTTP/1.0\r\n"},
|
{[]string{nonLoopBackIP + ":0"}, tlsConfig, "POST / HTTP/1.0\r\nHost: example.org\r\n\r\n", "200 OK\r\n", "POST / HTTP/1.0\r\n"},
|
||||||
{[]string{nonLoopBackIP + ":0"}, tlsConfig, "HEAD / HTTP/1.0\r\nhost: example.org\r\n\r\n", "200 OK\r\n", "HEAD / HTTP/1.0\r\n"},
|
{[]string{nonLoopBackIP + ":0"}, tlsConfig, "HEAD / HTTP/1.0\r\nhost: example.org\r\n\r\n", "200 OK\r\n", "HEAD / HTTP/1.0\r\n"},
|
||||||
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, "CONNECT \r\nHost: www.example.org\r\n\r\n", "200 OK\r\n", "CONNECT \r\n"},
|
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}, tlsConfig, "CONNECT / HTTP/1.0\r\nHost: www.example.org\r\n\r\n", "200 OK\r\n", "CONNECT / HTTP/1.0\r\n"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
@@ -643,11 +643,22 @@ func TestHTTPListenerAcceptError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err = bufio.NewReader(conn).ReadString('\n')
|
if !testCase.secureClient && testCase.tlsConfig != nil {
|
||||||
if err == nil {
|
buf := make([]byte, len(sslRequiredErrMsg))
|
||||||
t.Fatalf("Test %d: reply read: expected = EOF got = <nil>", i+1)
|
var n int
|
||||||
} else if err.Error() != "EOF" {
|
n, err = io.ReadFull(conn, buf)
|
||||||
t.Fatalf("Test %d: reply read: expected = EOF got = %v", i+1, err)
|
if err != nil {
|
||||||
|
t.Fatalf("Test %d: reply read: expected = <nil> got = %v", i+1, err)
|
||||||
|
} else if n != len(buf) {
|
||||||
|
t.Fatalf("Test %d: reply length: expected = %v got = %v", i+1, len(buf), n)
|
||||||
|
} else if !bytes.Equal(buf, sslRequiredErrMsg) {
|
||||||
|
t.Fatalf("Test %d: reply: expected = %v got = %v", i+1, string(sslRequiredErrMsg), string(buf))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = bufio.NewReader(conn).ReadString('\n'); err != io.EOF {
|
||||||
|
t.Errorf("Test %d: reply read: expected = io.EOF, got = %s", i+1, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|||||||
@@ -17,12 +17,10 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/certs"
|
"github.com/minio/minio/pkg/certs"
|
||||||
)
|
)
|
||||||
@@ -96,78 +94,3 @@ func TestNewServer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerTLSCiphers(t *testing.T) {
|
|
||||||
var unsupportedCipherSuites = []uint16{
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13)
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // No countermeasures against timing attacks
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13)
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, // Broken cipher
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, // Sweet32
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13)
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // No countermeasures against timing attacks
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13)
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, // Broken cipher
|
|
||||||
|
|
||||||
// all RSA-PKCS1-v1.5 ciphers are disabled - danger of Bleichenbacher attack variants
|
|
||||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // Sweet32
|
|
||||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13)
|
|
||||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA256, // No countermeasures against timing attacks
|
|
||||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13)
|
|
||||||
tls.TLS_RSA_WITH_RC4_128_SHA, // Broken cipher
|
|
||||||
|
|
||||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // Disabled because of RSA-PKCS1-v1.5 - AES-GCM is considered secure.
|
|
||||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // Disabled because of RSA-PKCS1-v1.5 - AES-GCM is considered secure.
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
ciphers []uint16
|
|
||||||
resetServerCiphers bool
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{nil, false, false},
|
|
||||||
{defaultCipherSuites, false, false},
|
|
||||||
{unsupportedCipherSuites, false, true},
|
|
||||||
{nil, true, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
func() {
|
|
||||||
addr := "127.0.0.1:" + getNextPort()
|
|
||||||
|
|
||||||
server := NewServer([]string{addr},
|
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintf(w, "Hello, world")
|
|
||||||
}), getCert)
|
|
||||||
if testCase.resetServerCiphers {
|
|
||||||
// Use Go default ciphers.
|
|
||||||
server.TLSConfig.CipherSuites = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
server.Start()
|
|
||||||
}()
|
|
||||||
defer server.Shutdown()
|
|
||||||
|
|
||||||
client := http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
CipherSuites: testCase.ciphers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is no guaranteed way to know whether the HTTP server is started successfully.
|
|
||||||
// The only option is to connect and check. Hence below sleep is used as workaround.
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
_, err := client.Get("https://" + addr)
|
|
||||||
expectErr := (err != nil)
|
|
||||||
|
|
||||||
if expectErr != testCase.expectErr {
|
|
||||||
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+83
-55
@@ -17,8 +17,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -27,27 +27,82 @@ const (
|
|||||||
byteRangePrefix = "bytes="
|
byteRangePrefix = "bytes="
|
||||||
)
|
)
|
||||||
|
|
||||||
// Valid byte position regexp
|
// HTTPRangeSpec represents a range specification as supported by S3 GET
|
||||||
var validBytePos = regexp.MustCompile(`^[0-9]+$`)
|
// object request.
|
||||||
|
//
|
||||||
|
// Case 1: Not present -> represented by a nil RangeSpec
|
||||||
|
// Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10}
|
||||||
|
// Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1}
|
||||||
|
// Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1}
|
||||||
|
type HTTPRangeSpec struct {
|
||||||
|
// Does the range spec refer to a suffix of the object?
|
||||||
|
IsSuffixLength bool
|
||||||
|
|
||||||
// HttpRange specifies the byte range to be sent to the client.
|
// Start and end offset specified in range spec
|
||||||
type httpRange struct {
|
Start, End int64
|
||||||
offsetBegin int64
|
|
||||||
offsetEnd int64
|
|
||||||
resourceSize int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String populate range stringer interface
|
// GetLength - get length of range
|
||||||
func (hrange httpRange) String() string {
|
func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) {
|
||||||
return fmt.Sprintf("bytes %d-%d/%d", hrange.offsetBegin, hrange.offsetEnd, hrange.resourceSize)
|
switch {
|
||||||
|
case resourceSize < 0:
|
||||||
|
return 0, errors.New("Resource size cannot be negative")
|
||||||
|
|
||||||
|
case h == nil:
|
||||||
|
rangeLength = resourceSize
|
||||||
|
|
||||||
|
case h.IsSuffixLength:
|
||||||
|
specifiedLen := -h.Start
|
||||||
|
rangeLength = specifiedLen
|
||||||
|
if specifiedLen > resourceSize {
|
||||||
|
rangeLength = resourceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
case h.Start >= resourceSize:
|
||||||
|
return 0, errInvalidRange
|
||||||
|
|
||||||
|
case h.End > -1:
|
||||||
|
end := h.End
|
||||||
|
if resourceSize <= end {
|
||||||
|
end = resourceSize - 1
|
||||||
|
}
|
||||||
|
rangeLength = end - h.Start + 1
|
||||||
|
|
||||||
|
case h.End == -1:
|
||||||
|
rangeLength = resourceSize - h.Start
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, errors.New("Unexpected range specification case")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rangeLength, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getlength - get length from the range.
|
// GetOffsetLength computes the start offset and length of the range
|
||||||
func (hrange httpRange) getLength() int64 {
|
// given the size of the resource
|
||||||
return 1 + hrange.offsetEnd - hrange.offsetBegin
|
func (h *HTTPRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) {
|
||||||
|
if h == nil {
|
||||||
|
// No range specified, implies whole object.
|
||||||
|
return 0, resourceSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
length, err = h.GetLength(resourceSize)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
start = h.Start
|
||||||
|
if h.IsSuffixLength {
|
||||||
|
start = resourceSize + h.Start
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return start, length, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRequestRange(rangeString string, resourceSize int64) (hrange *httpRange, err error) {
|
// Parse a HTTP range header value into a HTTPRangeSpec
|
||||||
|
func parseRequestRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
|
||||||
// Return error if given range string doesn't start with byte range prefix.
|
// Return error if given range string doesn't start with byte range prefix.
|
||||||
if !strings.HasPrefix(rangeString, byteRangePrefix) {
|
if !strings.HasPrefix(rangeString, byteRangePrefix) {
|
||||||
return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
|
return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
|
||||||
@@ -66,12 +121,12 @@ func parseRequestRange(rangeString string, resourceSize int64) (hrange *httpRang
|
|||||||
offsetBegin := int64(-1)
|
offsetBegin := int64(-1)
|
||||||
// Convert offsetBeginString only if its not empty.
|
// Convert offsetBeginString only if its not empty.
|
||||||
if len(offsetBeginString) > 0 {
|
if len(offsetBeginString) > 0 {
|
||||||
if !validBytePos.MatchString(offsetBeginString) {
|
if offsetBeginString[0] == '+' {
|
||||||
return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString)
|
return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetBeginString)
|
||||||
}
|
} else if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
|
||||||
|
|
||||||
if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
|
|
||||||
return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString)
|
return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString)
|
||||||
|
} else if offsetBegin < 0 {
|
||||||
|
return nil, fmt.Errorf("First byte position is negative ('%d')", offsetBegin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,57 +134,30 @@ func parseRequestRange(rangeString string, resourceSize int64) (hrange *httpRang
|
|||||||
offsetEnd := int64(-1)
|
offsetEnd := int64(-1)
|
||||||
// Convert offsetEndString only if its not empty.
|
// Convert offsetEndString only if its not empty.
|
||||||
if len(offsetEndString) > 0 {
|
if len(offsetEndString) > 0 {
|
||||||
if !validBytePos.MatchString(offsetEndString) {
|
if offsetEndString[0] == '+' {
|
||||||
return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString)
|
return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetEndString)
|
||||||
}
|
} else if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
|
||||||
|
|
||||||
if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
|
|
||||||
return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString)
|
return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString)
|
||||||
|
} else if offsetEnd < 0 {
|
||||||
|
return nil, fmt.Errorf("Last byte position is negative ('%d')", offsetEnd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rangeString contains first and last byte positions. eg. "bytes=2-5"
|
|
||||||
switch {
|
switch {
|
||||||
case offsetBegin > -1 && offsetEnd > -1:
|
case offsetBegin > -1 && offsetEnd > -1:
|
||||||
if offsetBegin > offsetEnd {
|
if offsetBegin > offsetEnd {
|
||||||
// Last byte position is not greater than first byte position. eg. "bytes=5-2"
|
|
||||||
return nil, fmt.Errorf("'%s' does not have valid range value", rangeString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First and last byte positions should not be >= resourceSize.
|
|
||||||
if offsetBegin >= resourceSize {
|
|
||||||
return nil, errInvalidRange
|
return nil, errInvalidRange
|
||||||
}
|
}
|
||||||
|
return &HTTPRangeSpec{false, offsetBegin, offsetEnd}, nil
|
||||||
if offsetEnd >= resourceSize {
|
|
||||||
offsetEnd = resourceSize - 1
|
|
||||||
}
|
|
||||||
case offsetBegin > -1:
|
case offsetBegin > -1:
|
||||||
// rangeString contains only first byte position. eg. "bytes=8-"
|
return &HTTPRangeSpec{false, offsetBegin, -1}, nil
|
||||||
if offsetBegin >= resourceSize {
|
|
||||||
// First byte position should not be >= resourceSize.
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
offsetEnd = resourceSize - 1
|
|
||||||
case offsetEnd > -1:
|
case offsetEnd > -1:
|
||||||
// rangeString contains only last byte position. eg. "bytes=-3"
|
|
||||||
if offsetEnd == 0 {
|
if offsetEnd == 0 {
|
||||||
// Last byte position should not be zero eg. "bytes=-0"
|
|
||||||
return nil, errInvalidRange
|
return nil, errInvalidRange
|
||||||
}
|
}
|
||||||
|
return &HTTPRangeSpec{true, -offsetEnd, -1}, nil
|
||||||
if offsetEnd >= resourceSize {
|
|
||||||
offsetBegin = 0
|
|
||||||
} else {
|
|
||||||
offsetBegin = resourceSize - offsetEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
offsetEnd = resourceSize - 1
|
|
||||||
default:
|
default:
|
||||||
// rangeString contains first and last byte positions missing. eg. "bytes=-"
|
// rangeString contains first and last byte positions missing. eg. "bytes=-"
|
||||||
return nil, fmt.Errorf("'%s' does not have valid range value", rangeString)
|
return nil, fmt.Errorf("'%s' does not have valid range value", rangeString)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &httpRange{offsetBegin, offsetEnd, resourceSize}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-51
@@ -16,75 +16,87 @@
|
|||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
// Test parseRequestRange()
|
func TestHTTPRequestRangeSpec(t *testing.T) {
|
||||||
func TestParseRequestRange(t *testing.T) {
|
resourceSize := int64(10)
|
||||||
// Test success cases.
|
validRangeSpecs := []struct {
|
||||||
successCases := []struct {
|
spec string
|
||||||
rangeString string
|
expOffset, expLength int64
|
||||||
offsetBegin int64
|
|
||||||
offsetEnd int64
|
|
||||||
length int64
|
|
||||||
}{
|
}{
|
||||||
{"bytes=2-5", 2, 5, 4},
|
{"bytes=0-", 0, 10},
|
||||||
{"bytes=2-20", 2, 9, 8},
|
{"bytes=1-", 1, 9},
|
||||||
{"bytes=2-2", 2, 2, 1},
|
{"bytes=0-9", 0, 10},
|
||||||
{"bytes=0000-0006", 0, 6, 7},
|
{"bytes=1-10", 1, 9},
|
||||||
{"bytes=2-", 2, 9, 8},
|
{"bytes=1-1", 1, 1},
|
||||||
{"bytes=-4", 6, 9, 4},
|
{"bytes=2-5", 2, 4},
|
||||||
{"bytes=-20", 0, 9, 10},
|
{"bytes=-5", 5, 5},
|
||||||
|
{"bytes=-1", 9, 1},
|
||||||
|
{"bytes=-1000", 0, 10},
|
||||||
}
|
}
|
||||||
|
for i, testCase := range validRangeSpecs {
|
||||||
for _, successCase := range successCases {
|
rs, err := parseRequestRangeSpec(testCase.spec)
|
||||||
hrange, err := parseRequestRange(successCase.rangeString, 10)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected: <nil>, got: %s", err)
|
t.Errorf("unexpected err: %v", err)
|
||||||
}
|
}
|
||||||
|
o, l, err := rs.GetOffsetLength(resourceSize)
|
||||||
if hrange.offsetBegin != successCase.offsetBegin {
|
if err != nil {
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.offsetBegin, hrange.offsetBegin)
|
t.Errorf("unexpected err: %v", err)
|
||||||
}
|
}
|
||||||
|
if o != testCase.expOffset || l != testCase.expLength {
|
||||||
if hrange.offsetEnd != successCase.offsetEnd {
|
t.Errorf("Case %d: got bad offset/length: %d,%d expected: %d,%d",
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.offsetEnd, hrange.offsetEnd)
|
i, o, l, testCase.expOffset, testCase.expLength)
|
||||||
}
|
|
||||||
if hrange.getLength() != successCase.length {
|
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.length, hrange.getLength())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test invalid range strings.
|
unparsableRangeSpecs := []string{
|
||||||
invalidRangeStrings := []string{
|
|
||||||
"bytes=8",
|
|
||||||
"bytes=5-2",
|
|
||||||
"bytes=+2-5",
|
|
||||||
"bytes=2-+5",
|
|
||||||
"bytes=2--5",
|
|
||||||
"bytes=-",
|
"bytes=-",
|
||||||
|
"bytes==",
|
||||||
|
"bytes==1-10",
|
||||||
|
"bytes=",
|
||||||
|
"bytes=aa",
|
||||||
|
"aa",
|
||||||
"",
|
"",
|
||||||
"2-5",
|
"bytes=1-10-",
|
||||||
"bytes = 2-5",
|
"bytes=1--10",
|
||||||
"bytes=2 - 5",
|
"bytes=-1-10",
|
||||||
"bytes=0-0,-1",
|
"bytes=0-+3",
|
||||||
"bytes=2-5 ",
|
"bytes=+3-+5",
|
||||||
|
"bytes=10-11,12-10", // Unsupported by S3/Minio (valid in RFC)
|
||||||
}
|
}
|
||||||
for _, rangeString := range invalidRangeStrings {
|
for i, urs := range unparsableRangeSpecs {
|
||||||
if _, err := parseRequestRange(rangeString, 10); err == nil {
|
rs, err := parseRequestRangeSpec(urs)
|
||||||
t.Fatalf("expected: an error, got: <nil>")
|
if err == nil {
|
||||||
|
t.Errorf("Case %d: Did not get an expected error - got %v", i, rs)
|
||||||
|
}
|
||||||
|
if err == errInvalidRange {
|
||||||
|
t.Errorf("Case %d: Got invalid range error instead of a parse error", i)
|
||||||
|
}
|
||||||
|
if rs != nil {
|
||||||
|
t.Errorf("Case %d: Got non-nil rs though err != nil: %v", i, rs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test error range strings.
|
invalidRangeSpecs := []string{
|
||||||
errorRangeString := []string{
|
"bytes=5-3",
|
||||||
"bytes=10-10",
|
"bytes=10-10",
|
||||||
"bytes=20-30",
|
"bytes=10-",
|
||||||
"bytes=20-",
|
"bytes=100-",
|
||||||
"bytes=-0",
|
"bytes=-0",
|
||||||
}
|
}
|
||||||
for _, rangeString := range errorRangeString {
|
for i, irs := range invalidRangeSpecs {
|
||||||
if _, err := parseRequestRange(rangeString, 10); err != errInvalidRange {
|
var err1, err2 error
|
||||||
t.Fatalf("expected: %s, got: %s", errInvalidRange, err)
|
var rs *HTTPRangeSpec
|
||||||
|
var o, l int64
|
||||||
|
rs, err1 = parseRequestRangeSpec(irs)
|
||||||
|
if err1 == nil {
|
||||||
|
o, l, err2 = rs.GetOffsetLength(resourceSize)
|
||||||
}
|
}
|
||||||
|
if err1 == errInvalidRange || (err1 == nil && err2 == errInvalidRange) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("Case %d: Expected errInvalidRange but: %v %v %d %d %v", i, rs, err1, o, l, err2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+699
@@ -0,0 +1,699 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IAM configuration directory.
|
||||||
|
iamConfigPrefix = minioConfigPrefix + "/iam"
|
||||||
|
|
||||||
|
// IAM users directory.
|
||||||
|
iamConfigUsersPrefix = iamConfigPrefix + "/users/"
|
||||||
|
|
||||||
|
// IAM policies directory.
|
||||||
|
iamConfigPoliciesPrefix = iamConfigPrefix + "/policies/"
|
||||||
|
|
||||||
|
// IAM sts directory.
|
||||||
|
iamConfigSTSPrefix = iamConfigPrefix + "/sts/"
|
||||||
|
|
||||||
|
// IAM identity file which captures identity credentials.
|
||||||
|
iamIdentityFile = "identity.json"
|
||||||
|
|
||||||
|
// IAM policy file which provides policies for each users.
|
||||||
|
iamPolicyFile = "policy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IAMSys - config system.
|
||||||
|
type IAMSys struct {
|
||||||
|
sync.RWMutex
|
||||||
|
iamUsersMap map[string]auth.Credentials
|
||||||
|
iamPolicyMap map[string]string
|
||||||
|
iamCannedPolicyMap map[string]iampolicy.Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load - load iam.json
|
||||||
|
func (sys *IAMSys) Load(objAPI ObjectLayer) error {
|
||||||
|
return sys.Init(objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init - initializes config system from iam.json
|
||||||
|
func (sys *IAMSys) Init(objAPI ObjectLayer) error {
|
||||||
|
if objAPI == nil {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Refresh IAMSys in background.
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(globalRefreshIAMInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-globalServiceDoneCh:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
sys.refresh(objAPI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
// Initializing IAM needs a retry mechanism for
|
||||||
|
// the following reasons:
|
||||||
|
// - Read quorum is lost just after the initialization
|
||||||
|
// of the object layer.
|
||||||
|
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _ = <-retryTimerCh:
|
||||||
|
// Load IAMSys once during boot.
|
||||||
|
if err := sys.refresh(objAPI); err != nil {
|
||||||
|
if err == errDiskNotFound ||
|
||||||
|
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||||
|
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||||
|
logger.Info("Waiting for IAM subsystem to be initialized..")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCannedPolicy - deletes a canned policy.
|
||||||
|
func (sys *IAMSys) DeleteCannedPolicy(policyName string) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyName == "" {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
configFile := pathJoin(iamConfigPoliciesPrefix, policyName, iamPolicyFile)
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
err = deleteConfigEtcd(context.Background(), globalEtcdClient, configFile)
|
||||||
|
} else {
|
||||||
|
err = deleteConfig(context.Background(), objectAPI, configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
delete(sys.iamCannedPolicyMap, policyName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCannedPolicies - lists all canned policies.
|
||||||
|
func (sys *IAMSys) ListCannedPolicies() (map[string][]byte, error) {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
var cannedPolicyMap = make(map[string][]byte)
|
||||||
|
|
||||||
|
sys.RLock()
|
||||||
|
defer sys.RUnlock()
|
||||||
|
|
||||||
|
for k, v := range sys.iamCannedPolicyMap {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cannedPolicyMap[k] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
return cannedPolicyMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCannedPolicy - sets a new canned policy.
|
||||||
|
func (sys *IAMSys) SetCannedPolicy(policyName string, p iampolicy.Policy) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.IsEmpty() || policyName == "" {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := pathJoin(iamConfigPoliciesPrefix, policyName, iamPolicyFile)
|
||||||
|
data, err := json.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
|
||||||
|
} else {
|
||||||
|
err = saveConfig(context.Background(), objectAPI, configFile, data)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
sys.iamCannedPolicyMap[policyName] = p
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserPolicy - sets policy to given user name.
|
||||||
|
func (sys *IAMSys) SetUserPolicy(accessKey, policyName string) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
if _, ok := sys.iamUsersMap[accessKey]; !ok {
|
||||||
|
return errNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := sys.iamCannedPolicyMap[policyName]; !ok {
|
||||||
|
return errNoSuchPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(policyName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile)
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
|
||||||
|
} else {
|
||||||
|
err = saveConfig(context.Background(), objectAPI, configFile, data)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.iamPolicyMap[accessKey] = policyName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser - set user credentials.
|
||||||
|
func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
pFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile)
|
||||||
|
iFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
// It is okay to ingnore errors when deleting policy.json for the user.
|
||||||
|
_ = deleteConfigEtcd(context.Background(), globalEtcdClient, pFile)
|
||||||
|
err = deleteConfigEtcd(context.Background(), globalEtcdClient, iFile)
|
||||||
|
} else {
|
||||||
|
// It is okay to ingnore errors when deleting policy.json for the user.
|
||||||
|
_ = deleteConfig(context.Background(), objectAPI, pFile)
|
||||||
|
err = deleteConfig(context.Background(), objectAPI, iFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
switch err.(type) {
|
||||||
|
case ObjectNotFound:
|
||||||
|
err = errNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
delete(sys.iamUsersMap, accessKey)
|
||||||
|
delete(sys.iamPolicyMap, accessKey)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTempUser - set temporary user credentials, these credentials have an expiry.
|
||||||
|
func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := pathJoin(iamConfigSTSPrefix, accessKey, iamIdentityFile)
|
||||||
|
data, err := json.Marshal(cred)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
|
||||||
|
} else {
|
||||||
|
err = saveConfig(context.Background(), objectAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
sys.iamUsersMap[accessKey] = cred
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers - list all users.
|
||||||
|
func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
var users = make(map[string]madmin.UserInfo)
|
||||||
|
|
||||||
|
sys.RLock()
|
||||||
|
defer sys.RUnlock()
|
||||||
|
|
||||||
|
for k, v := range sys.iamUsersMap {
|
||||||
|
users[k] = madmin.UserInfo{
|
||||||
|
PolicyName: sys.iamPolicyMap[k],
|
||||||
|
Status: madmin.AccountStatus(v.Status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserStatus - sets current user status, supports disabled or enabled.
|
||||||
|
func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != madmin.AccountEnabled && status != madmin.AccountDisabled {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
cred, ok := sys.iamUsersMap[accessKey]
|
||||||
|
if !ok {
|
||||||
|
return errNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
uinfo := madmin.UserInfo{
|
||||||
|
SecretKey: cred.SecretKey,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
|
||||||
|
data, err := json.Marshal(uinfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
|
||||||
|
} else {
|
||||||
|
err = saveConfig(context.Background(), objectAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.iamUsersMap[accessKey] = auth.Credentials{
|
||||||
|
AccessKey: accessKey,
|
||||||
|
SecretKey: uinfo.SecretKey,
|
||||||
|
Status: string(uinfo.Status),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser - set user credentials.
|
||||||
|
func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
|
||||||
|
data, err := json.Marshal(uinfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
|
||||||
|
} else {
|
||||||
|
err = saveConfig(context.Background(), objectAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
sys.iamUsersMap[accessKey] = auth.Credentials{
|
||||||
|
AccessKey: accessKey,
|
||||||
|
SecretKey: uinfo.SecretKey,
|
||||||
|
Status: string(uinfo.Status),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser - get user credentials
|
||||||
|
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
|
||||||
|
sys.RLock()
|
||||||
|
defer sys.RUnlock()
|
||||||
|
|
||||||
|
cred, ok = sys.iamUsersMap[accessKey]
|
||||||
|
return cred, ok && cred.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||||
|
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||||
|
sys.RLock()
|
||||||
|
defer sys.RUnlock()
|
||||||
|
|
||||||
|
// If opa is configured, use OPA always.
|
||||||
|
if globalPolicyOPA != nil {
|
||||||
|
return globalPolicyOPA.IsAllowed(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If policy is available for given user, check the policy.
|
||||||
|
if name, found := sys.iamPolicyMap[args.AccountName]; found {
|
||||||
|
p, ok := sys.iamCannedPolicyMap[name]
|
||||||
|
return ok && p.IsAllowed(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As policy is not available and OPA is not configured, return the owner value.
|
||||||
|
return args.IsOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultContextTimeout = 5 * time.Minute
|
||||||
|
|
||||||
|
// Similar to reloadUsers but updates users, policies maps from etcd server,
|
||||||
|
func reloadEtcdUsers(prefix string, usersMap map[string]auth.Credentials, policyMap map[string]string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||||
|
r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||||
|
defer cancel()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// No users are created yet.
|
||||||
|
if r.Count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
users := set.NewStringSet()
|
||||||
|
for _, kv := range r.Kvs {
|
||||||
|
// Extract user by stripping off the `prefix` value as suffix,
|
||||||
|
// then strip off the remaining basename to obtain the prefix
|
||||||
|
// value, usually in the following form.
|
||||||
|
//
|
||||||
|
// key := "config/iam/users/newuser/identity.json"
|
||||||
|
// prefix := "config/iam/users/"
|
||||||
|
// v := trim(trim(key, prefix), base(key)) == "newuser"
|
||||||
|
//
|
||||||
|
user := path.Clean(strings.TrimSuffix(strings.TrimPrefix(string(kv.Key), prefix), path.Base(string(kv.Key))))
|
||||||
|
if !users.Contains(user) {
|
||||||
|
users.Add(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload config and policies for all users.
|
||||||
|
for _, user := range users.ToSlice() {
|
||||||
|
idFile := pathJoin(prefix, user, iamIdentityFile)
|
||||||
|
pFile := pathJoin(prefix, user, iamPolicyFile)
|
||||||
|
cdata, cerr := readConfigEtcd(ctx, globalEtcdClient, idFile)
|
||||||
|
pdata, perr := readConfigEtcd(ctx, globalEtcdClient, pFile)
|
||||||
|
if cerr != nil && cerr != errConfigNotFound {
|
||||||
|
return cerr
|
||||||
|
}
|
||||||
|
if perr != nil && perr != errConfigNotFound {
|
||||||
|
return perr
|
||||||
|
}
|
||||||
|
if cerr == errConfigNotFound && perr == errConfigNotFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cerr == nil {
|
||||||
|
var cred auth.Credentials
|
||||||
|
if err = json.Unmarshal(cdata, &cred); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cred.AccessKey = user
|
||||||
|
if cred.IsExpired() {
|
||||||
|
deleteConfigEtcd(ctx, globalEtcdClient, idFile)
|
||||||
|
deleteConfigEtcd(ctx, globalEtcdClient, pFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usersMap[cred.AccessKey] = cred
|
||||||
|
}
|
||||||
|
if perr == nil {
|
||||||
|
var policyName string
|
||||||
|
if err = json.Unmarshal(pdata, &policyName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
policyMap[user] = policyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadEtcdPolicies(prefix string, cannedPolicyMap map[string]iampolicy.Policy) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||||
|
r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||||
|
defer cancel()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// No users are created yet.
|
||||||
|
if r.Count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
policies := set.NewStringSet()
|
||||||
|
for _, kv := range r.Kvs {
|
||||||
|
// Extract policy by stripping off the `prefix` value as suffix,
|
||||||
|
// then strip off the remaining basename to obtain the prefix
|
||||||
|
// value, usually in the following form.
|
||||||
|
//
|
||||||
|
// key := "config/iam/policies/newpolicy/identity.json"
|
||||||
|
// prefix := "config/iam/policies/"
|
||||||
|
// v := trim(trim(key, prefix), base(key)) == "newpolicy"
|
||||||
|
//
|
||||||
|
policyName := path.Clean(strings.TrimSuffix(strings.TrimPrefix(string(kv.Key), prefix), path.Base(string(kv.Key))))
|
||||||
|
if !policies.Contains(policyName) {
|
||||||
|
policies.Add(policyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload config and policies for all policys.
|
||||||
|
for _, policyName := range policies.ToSlice() {
|
||||||
|
pFile := pathJoin(prefix, policyName, iamPolicyFile)
|
||||||
|
pdata, perr := readConfigEtcd(ctx, globalEtcdClient, pFile)
|
||||||
|
if perr != nil {
|
||||||
|
return perr
|
||||||
|
}
|
||||||
|
var p iampolicy.Policy
|
||||||
|
if err = json.Unmarshal(pdata, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cannedPolicyMap[policyName] = p
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadPolicies(objectAPI ObjectLayer, prefix string, cannedPolicyMap map[string]iampolicy.Policy) error {
|
||||||
|
marker := ""
|
||||||
|
for {
|
||||||
|
var lo ListObjectsInfo
|
||||||
|
var err error
|
||||||
|
lo, err = objectAPI.ListObjects(context.Background(), minioMetaBucket, prefix, marker, "/", 1000)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
marker = lo.NextMarker
|
||||||
|
for _, prefix := range lo.Prefixes {
|
||||||
|
pFile := pathJoin(prefix, iamPolicyFile)
|
||||||
|
pdata, perr := readConfig(context.Background(), objectAPI, pFile)
|
||||||
|
if perr != nil {
|
||||||
|
return perr
|
||||||
|
}
|
||||||
|
var p iampolicy.Policy
|
||||||
|
if err = json.Unmarshal(pdata, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cannedPolicyMap[path.Base(prefix)] = p
|
||||||
|
}
|
||||||
|
if !lo.IsTruncated {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadUsers reads an updates users, policies from object layer into user and policy maps.
|
||||||
|
func reloadUsers(objectAPI ObjectLayer, prefix string, usersMap map[string]auth.Credentials, policyMap map[string]string) error {
|
||||||
|
marker := ""
|
||||||
|
for {
|
||||||
|
var lo ListObjectsInfo
|
||||||
|
var err error
|
||||||
|
lo, err = objectAPI.ListObjects(context.Background(), minioMetaBucket, prefix, marker, "/", 1000)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
marker = lo.NextMarker
|
||||||
|
for _, prefix := range lo.Prefixes {
|
||||||
|
idFile := pathJoin(prefix, iamIdentityFile)
|
||||||
|
pFile := pathJoin(prefix, iamPolicyFile)
|
||||||
|
cdata, cerr := readConfig(context.Background(), objectAPI, idFile)
|
||||||
|
pdata, perr := readConfig(context.Background(), objectAPI, pFile)
|
||||||
|
if cerr != nil && cerr != errConfigNotFound {
|
||||||
|
return cerr
|
||||||
|
}
|
||||||
|
if perr != nil && perr != errConfigNotFound {
|
||||||
|
return perr
|
||||||
|
}
|
||||||
|
if cerr == errConfigNotFound && perr == errConfigNotFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cerr == nil {
|
||||||
|
var cred auth.Credentials
|
||||||
|
if err = json.Unmarshal(cdata, &cred); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cred.AccessKey = path.Base(prefix)
|
||||||
|
if cred.IsExpired() {
|
||||||
|
// Delete expired identity.
|
||||||
|
objectAPI.DeleteObject(context.Background(), minioMetaBucket, idFile)
|
||||||
|
// Delete expired identity policy.
|
||||||
|
objectAPI.DeleteObject(context.Background(), minioMetaBucket, pFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usersMap[cred.AccessKey] = cred
|
||||||
|
}
|
||||||
|
if perr == nil {
|
||||||
|
var policyName string
|
||||||
|
if err = json.Unmarshal(pdata, &policyName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
policyMap[path.Base(prefix)] = policyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !lo.IsTruncated {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default canned policies only if not already overridden by users.
|
||||||
|
func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) {
|
||||||
|
_, ok := policies["writeonly"]
|
||||||
|
if !ok {
|
||||||
|
policies["writeonly"] = iampolicy.WriteOnly
|
||||||
|
}
|
||||||
|
_, ok = policies["readonly"]
|
||||||
|
if !ok {
|
||||||
|
policies["readonly"] = iampolicy.ReadOnly
|
||||||
|
}
|
||||||
|
_, ok = policies["readwrite"]
|
||||||
|
if !ok {
|
||||||
|
policies["readwrite"] = iampolicy.ReadWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh IAMSys.
|
||||||
|
func (sys *IAMSys) refresh(objAPI ObjectLayer) error {
|
||||||
|
iamUsersMap := make(map[string]auth.Credentials)
|
||||||
|
iamPolicyMap := make(map[string]string)
|
||||||
|
iamCannedPolicyMap := make(map[string]iampolicy.Policy)
|
||||||
|
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
if err := reloadEtcdPolicies(iamConfigPoliciesPrefix, iamCannedPolicyMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := reloadEtcdUsers(iamConfigUsersPrefix, iamUsersMap, iamPolicyMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := reloadEtcdUsers(iamConfigSTSPrefix, iamUsersMap, iamPolicyMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := reloadPolicies(objAPI, iamConfigPoliciesPrefix, iamCannedPolicyMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := reloadUsers(objAPI, iamConfigUsersPrefix, iamUsersMap, iamPolicyMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := reloadUsers(objAPI, iamConfigSTSPrefix, iamUsersMap, iamPolicyMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets default canned policies, if none set.
|
||||||
|
setDefaultCannedPolicies(iamCannedPolicyMap)
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
sys.iamUsersMap = iamUsersMap
|
||||||
|
sys.iamPolicyMap = iamPolicyMap
|
||||||
|
sys.iamCannedPolicyMap = iamCannedPolicyMap
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIAMSys - creates new config system object.
|
||||||
|
func NewIAMSys() *IAMSys {
|
||||||
|
return &IAMSys{
|
||||||
|
iamUsersMap: make(map[string]auth.Credentials),
|
||||||
|
iamPolicyMap: make(map[string]string),
|
||||||
|
iamCannedPolicyMap: make(map[string]iampolicy.Policy),
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
-32
@@ -49,7 +49,33 @@ var (
|
|||||||
errNoAuthToken = errors.New("JWT token missing")
|
errNoAuthToken = errors.New("JWT token missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
func authenticateJWT(accessKey, secretKey string, expiry time.Duration) (string, error) {
|
func authenticateJWTUsers(accessKey, secretKey string, expiry time.Duration) (string, error) {
|
||||||
|
passedCredential, err := auth.CreateCredentials(accessKey, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCred := globalServerConfig.GetCredential()
|
||||||
|
if serverCred.AccessKey != passedCredential.AccessKey {
|
||||||
|
var ok bool
|
||||||
|
serverCred, ok = globalIAMSys.GetUser(accessKey)
|
||||||
|
if !ok {
|
||||||
|
return "", errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !serverCred.Equal(passedCredential) {
|
||||||
|
return "", errAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
|
||||||
|
ExpiresAt: UTCNow().Add(expiry).Unix(),
|
||||||
|
Subject: accessKey,
|
||||||
|
})
|
||||||
|
return jwt.SignedString([]byte(serverCred.SecretKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticateJWTAdmin(accessKey, secretKey string, expiry time.Duration) (string, error) {
|
||||||
passedCredential, err := auth.CreateCredentials(accessKey, secretKey)
|
passedCredential, err := auth.CreateCredentials(accessKey, secretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -73,68 +99,133 @@ func authenticateJWT(accessKey, secretKey string, expiry time.Duration) (string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func authenticateNode(accessKey, secretKey string) (string, error) {
|
func authenticateNode(accessKey, secretKey string) (string, error) {
|
||||||
return authenticateJWT(accessKey, secretKey, defaultInterNodeJWTExpiry)
|
return authenticateJWTAdmin(accessKey, secretKey, defaultInterNodeJWTExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateWeb(accessKey, secretKey string) (string, error) {
|
func authenticateWeb(accessKey, secretKey string) (string, error) {
|
||||||
return authenticateJWT(accessKey, secretKey, defaultJWTExpiry)
|
return authenticateJWTUsers(accessKey, secretKey, defaultJWTExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateURL(accessKey, secretKey string) (string, error) {
|
func authenticateURL(accessKey, secretKey string) (string, error) {
|
||||||
return authenticateJWT(accessKey, secretKey, defaultURLJWTExpiry)
|
return authenticateJWTUsers(accessKey, secretKey, defaultURLJWTExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyFuncCallback(jwtToken *jwtgo.Token) (interface{}, error) {
|
func stsTokenCallback(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||||
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodHMAC); !ok {
|
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
if err := jwtToken.Claims.Valid(); err != nil {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
if claims, ok := jwtToken.Claims.(jwtgo.MapClaims); ok {
|
||||||
|
accessKey, ok := claims["accessKey"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||||
|
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||||
|
}
|
||||||
|
if globalIAMSys == nil {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
_, ok = globalIAMSys.GetUser(accessKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||||
|
}
|
||||||
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAuthTokenValid(tokenString string) bool {
|
// Callback function used for parsing
|
||||||
if tokenString == "" {
|
func webTokenCallback(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||||
return false
|
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
||||||
}
|
}
|
||||||
var claims jwtgo.StandardClaims
|
|
||||||
jwtToken, err := jwtgo.ParseWithClaims(tokenString, &claims, keyFuncCallback)
|
if err := jwtToken.Claims.Valid(); err != nil {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := jwtToken.Claims.(*jwtgo.StandardClaims); ok {
|
||||||
|
if claims.Subject == globalServerConfig.GetCredential().AccessKey {
|
||||||
|
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||||
|
}
|
||||||
|
if globalIAMSys == nil {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
cred, ok := globalIAMSys.GetUser(claims.Subject)
|
||||||
|
if !ok {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
return []byte(cred.SecretKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJWTWithClaims(tokenString string, claims jwtgo.Claims) (*jwtgo.Token, error) {
|
||||||
|
p := &jwtgo.Parser{
|
||||||
|
SkipClaimsValidation: true,
|
||||||
|
}
|
||||||
|
jwtToken, err := p.ParseWithClaims(tokenString, claims, webTokenCallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
switch e := err.(type) {
|
||||||
return false
|
case *jwtgo.ValidationError:
|
||||||
|
if e.Inner == nil {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
return nil, e.Inner
|
||||||
|
}
|
||||||
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
if err = claims.Valid(); err != nil {
|
return jwtToken, nil
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return jwtToken.Valid && claims.Subject == globalServerConfig.GetCredential().AccessKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHTTPRequestValid(req *http.Request) bool {
|
func isAuthTokenValid(token string) bool {
|
||||||
return webRequestAuthenticate(req) == nil
|
_, _, err := webTokenAuthenticate(token)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func webTokenAuthenticate(token string) (jwtgo.StandardClaims, bool, error) {
|
||||||
|
var claims = jwtgo.StandardClaims{}
|
||||||
|
if token == "" {
|
||||||
|
return claims, false, errNoAuthToken
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtToken, err := parseJWTWithClaims(token, &claims)
|
||||||
|
if err != nil {
|
||||||
|
return claims, false, err
|
||||||
|
}
|
||||||
|
if !jwtToken.Valid {
|
||||||
|
return claims, false, errAuthentication
|
||||||
|
}
|
||||||
|
owner := claims.Subject == globalServerConfig.GetCredential().AccessKey
|
||||||
|
return claims, owner, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the request is authenticated.
|
// Check if the request is authenticated.
|
||||||
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
||||||
// Returns errAuthentication for all other errors.
|
// Returns errAuthentication for all other errors.
|
||||||
func webRequestAuthenticate(req *http.Request) error {
|
func webRequestAuthenticate(req *http.Request) (jwtgo.StandardClaims, bool, error) {
|
||||||
var claims jwtgo.StandardClaims
|
var claims = jwtgo.StandardClaims{}
|
||||||
jwtToken, err := jwtreq.ParseFromRequestWithClaims(req, jwtreq.AuthorizationHeaderExtractor, &claims, keyFuncCallback)
|
tokStr, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == jwtreq.ErrNoTokenInRequest {
|
if err == jwtreq.ErrNoTokenInRequest {
|
||||||
return errNoAuthToken
|
return claims, false, errNoAuthToken
|
||||||
}
|
}
|
||||||
return errAuthentication
|
return claims, false, err
|
||||||
}
|
}
|
||||||
if err = claims.Valid(); err != nil {
|
jwtToken, err := parseJWTWithClaims(tokStr, &claims)
|
||||||
return errAuthentication
|
if err != nil {
|
||||||
}
|
return claims, false, err
|
||||||
if claims.Subject != globalServerConfig.GetCredential().AccessKey {
|
|
||||||
return errInvalidAccessKeyID
|
|
||||||
}
|
}
|
||||||
if !jwtToken.Valid {
|
if !jwtToken.Valid {
|
||||||
return errAuthentication
|
return claims, false, errAuthentication
|
||||||
}
|
}
|
||||||
return nil
|
owner := claims.Subject == globalServerConfig.GetCredential().AccessKey
|
||||||
|
return claims, owner, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthToken() string {
|
func newAuthToken() string {
|
||||||
|
|||||||
+1
-1
@@ -142,7 +142,7 @@ func TestWebRequestAuthenticate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
gotErr := webRequestAuthenticate(testCase.req)
|
_, _, gotErr := webRequestAuthenticate(testCase.req)
|
||||||
if testCase.expectedErr != gotErr {
|
if testCase.expectedErr != gotErr {
|
||||||
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
|
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// localAdminClient - represents admin operation to be executed locally.
|
// localAdminClient - represents admin operation to be executed locally.
|
||||||
@@ -80,3 +84,40 @@ func (lc localAdminClient) GetConfig() ([]byte, error) {
|
|||||||
|
|
||||||
return json.Marshal(globalServerConfig)
|
return json.Marshal(globalServerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartProfiling - starts profiling on the local server.
|
||||||
|
func (lc localAdminClient) StartProfiling(profiler string) error {
|
||||||
|
if globalProfiler != nil {
|
||||||
|
globalProfiler.Stop()
|
||||||
|
}
|
||||||
|
prof, err := startProfiler(profiler, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalProfiler = prof
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadProfilingData - stops and returns profiling data of the local server.
|
||||||
|
func (lc localAdminClient) DownloadProfilingData() ([]byte, error) {
|
||||||
|
if globalProfiler == nil {
|
||||||
|
return nil, errors.New("profiler not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
profilerPath := globalProfiler.Path()
|
||||||
|
|
||||||
|
// Stop the profiler
|
||||||
|
globalProfiler.Stop()
|
||||||
|
|
||||||
|
profilerFile, err := os.Open(profilerPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(profilerFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func NewLockRPCServer() (*xrpc.Server, error) {
|
|||||||
// Register distributed NS lock handlers.
|
// Register distributed NS lock handlers.
|
||||||
func registerDistNSLockRouter(router *mux.Router) {
|
func registerDistNSLockRouter(router *mux.Router) {
|
||||||
rpcServer, err := NewLockRPCServer()
|
rpcServer, err := NewLockRPCServer()
|
||||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
logger.FatalIf(err, "Unable to initialize Lock RPC Server")
|
||||||
|
|
||||||
// Start lock maintenance from all lock servers.
|
// Start lock maintenance from all lock servers.
|
||||||
go startLockMaintenance(globalLockServer)
|
go startLockMaintenance(globalLockServer)
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ import (
|
|||||||
// in plain or json format to the standard output.
|
// in plain or json format to the standard output.
|
||||||
type ConsoleTarget struct{}
|
type ConsoleTarget struct{}
|
||||||
|
|
||||||
func (c *ConsoleTarget) send(entry logEntry) error {
|
func (c *ConsoleTarget) send(e interface{}) error {
|
||||||
|
entry, ok := e.(logEntry)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Uexpected log entry structure %#v", e)
|
||||||
|
}
|
||||||
if jsonFlag {
|
if jsonFlag {
|
||||||
logJSON, err := json.Marshal(&entry)
|
logJSON, err := json.Marshal(&entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+4
-3
@@ -32,7 +32,8 @@ import (
|
|||||||
// is returned to the caller.
|
// is returned to the caller.
|
||||||
type HTTPTarget struct {
|
type HTTPTarget struct {
|
||||||
// Channel of log entries
|
// Channel of log entries
|
||||||
logCh chan logEntry
|
logCh chan interface{}
|
||||||
|
|
||||||
// HTTP(s) endpoint
|
// HTTP(s) endpoint
|
||||||
endpoint string
|
endpoint string
|
||||||
client http.Client
|
client http.Client
|
||||||
@@ -72,14 +73,14 @@ func NewHTTP(endpoint string, transport *http.Transport) LoggingTarget {
|
|||||||
client: http.Client{
|
client: http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
},
|
},
|
||||||
logCh: make(chan logEntry, 10000),
|
logCh: make(chan interface{}, 10000),
|
||||||
}
|
}
|
||||||
|
|
||||||
h.startHTTPLogger()
|
h.startHTTPLogger()
|
||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPTarget) send(entry logEntry) error {
|
func (h *HTTPTarget) send(entry interface{}) error {
|
||||||
select {
|
select {
|
||||||
case h.logCh <- entry:
|
case h.logCh <- entry:
|
||||||
default:
|
default:
|
||||||
|
|||||||
+52
-3
@@ -21,6 +21,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/build"
|
"go/build"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -341,6 +342,55 @@ func logIf(ctx context.Context, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type auditEntry struct {
|
||||||
|
DeploymentID string `json:"deploymentid,omitempty"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
API *api `json:"api,omitempty"`
|
||||||
|
RemoteHost string `json:"remotehost,omitempty"`
|
||||||
|
RequestID string `json:"requestID,omitempty"`
|
||||||
|
UserAgent string `json:"userAgent,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditLog - logs audit logs to all targets.
|
||||||
|
func AuditLog(ctx context.Context, r *http.Request) {
|
||||||
|
if Disable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := GetReqInfo(ctx)
|
||||||
|
if req == nil {
|
||||||
|
req = &ReqInfo{API: "SYSTEM"}
|
||||||
|
}
|
||||||
|
|
||||||
|
API := "SYSTEM"
|
||||||
|
if req.API != "" {
|
||||||
|
API = req.API
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make(map[string]string)
|
||||||
|
for _, entry := range req.GetTags() {
|
||||||
|
tags[entry.Key] = entry.Val
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := auditEntry{
|
||||||
|
DeploymentID: deploymentID,
|
||||||
|
RemoteHost: req.RemoteHost,
|
||||||
|
RequestID: req.RequestID,
|
||||||
|
UserAgent: req.UserAgent,
|
||||||
|
Time: time.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
API: &api{Name: API, Args: &args{Bucket: req.BucketName, Object: req.ObjectName}},
|
||||||
|
Metadata: tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send audit logs only to http targets.
|
||||||
|
for _, t := range Targets {
|
||||||
|
if _, ok := t.(*HTTPTarget); ok {
|
||||||
|
t.send(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ErrCritical is the value panic'd whenever CriticalIf is called.
|
// ErrCritical is the value panic'd whenever CriticalIf is called.
|
||||||
var ErrCritical struct{}
|
var ErrCritical struct{}
|
||||||
|
|
||||||
@@ -450,10 +500,9 @@ func (f fatalMsg) pretty(msg string, args ...interface{}) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var info infoMsg
|
type infoMsg struct{}
|
||||||
|
|
||||||
type infoMsg struct {
|
var info infoMsg
|
||||||
}
|
|
||||||
|
|
||||||
func (i infoMsg) json(msg string, args ...interface{}) {
|
func (i infoMsg) json(msg string, args ...interface{}) {
|
||||||
logJSON, err := json.Marshal(&logEntry{
|
logJSON, err := json.Marshal(&logEntry{
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ func GetReqInfo(ctx context.Context) *ReqInfo {
|
|||||||
if ok {
|
if ok {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
r = &ReqInfo{}
|
||||||
|
SetReqInfo(ctx, r)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ package logger
|
|||||||
// a single log entry and send it to the log target
|
// a single log entry and send it to the log target
|
||||||
// e.g. send the log to a http server
|
// e.g. send the log to a http server
|
||||||
type LoggingTarget interface {
|
type LoggingTarget interface {
|
||||||
send(entry logEntry) error
|
send(entry interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Targets is the set of enabled loggers
|
// Targets is the set of enabled loggers
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ func newDsyncNodes(endpoints EndpointList) (clnts []dsync.NetLocker, myNode int)
|
|||||||
locker = &(receiver.ll)
|
locker = &(receiver.ll)
|
||||||
} else {
|
} else {
|
||||||
host, err := xnet.ParseHost(endpoint.Host)
|
host, err := xnet.ParseHost(endpoint.Host)
|
||||||
logger.FatalIf(err, "Unable to parse Lock RPC Host", context.Background())
|
logger.FatalIf(err, "Unable to parse Lock RPC Host")
|
||||||
locker, err = NewLockRPCClient(host)
|
locker, err = NewLockRPCClient(host)
|
||||||
logger.FatalIf(err, "Unable to initialize Lock RPC Client", context.Background())
|
logger.FatalIf(err, "Unable to initialize Lock RPC Client")
|
||||||
}
|
}
|
||||||
|
|
||||||
clnts = append(clnts, locker)
|
clnts = append(clnts, locker)
|
||||||
|
|||||||
@@ -218,7 +218,8 @@ func TestNamespaceForceUnlockTest(t *testing.T) {
|
|||||||
// Try to claim lock again.
|
// Try to claim lock again.
|
||||||
anotherLock := globalNSMutex.NewNSLock("bucket", "object")
|
anotherLock := globalNSMutex.NewNSLock("bucket", "object")
|
||||||
if anotherLock.GetLock(newDynamicTimeout(60*time.Second, time.Second)) != nil {
|
if anotherLock.GetLock(newDynamicTimeout(60*time.Second, time.Second)) != nil {
|
||||||
t.Fatalf("Failed to get lock")
|
t.Errorf("Failed to get lock")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// And signal success.
|
// And signal success.
|
||||||
ch <- struct{}{}
|
ch <- struct{}{}
|
||||||
|
|||||||
+57
-41
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@@ -241,20 +242,19 @@ func (sys *NotificationSys) initListeners(ctx context.Context, objAPI ObjectLaye
|
|||||||
// and configFile, take a transaction lock to avoid data race between readConfig()
|
// and configFile, take a transaction lock to avoid data race between readConfig()
|
||||||
// and saveConfig().
|
// and saveConfig().
|
||||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
if err := objLock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer objLock.Unlock()
|
defer objLock.RUnlock()
|
||||||
|
|
||||||
reader, e := readConfig(ctx, objAPI, configFile)
|
configData, e := readConfig(ctx, objAPI, configFile)
|
||||||
if e != nil && !IsErrIgnored(e, errDiskNotFound, errConfigNotFound) {
|
if e != nil && !IsErrIgnored(e, errDiskNotFound, errConfigNotFound) {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
listenerList := []ListenBucketNotificationArgs{}
|
listenerList := []ListenBucketNotificationArgs{}
|
||||||
if reader != nil {
|
if configData != nil {
|
||||||
err := json.NewDecoder(reader).Decode(&listenerList)
|
if err := json.Unmarshal(configData, &listenerList); err != nil {
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,6 @@ func (sys *NotificationSys) initListeners(ctx context.Context, objAPI ObjectLaye
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
activeListenerList := []ListenBucketNotificationArgs{}
|
|
||||||
for _, args := range listenerList {
|
for _, args := range listenerList {
|
||||||
found, err := isLocalHost(args.Addr.Name)
|
found, err := isLocalHost(args.Addr.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -301,16 +300,31 @@ func (sys *NotificationSys) initListeners(ctx context.Context, objAPI ObjectLaye
|
|||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
activeListenerList = append(activeListenerList, args)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(activeListenerList)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *NotificationSys) refresh(objAPI ObjectLayer) error {
|
||||||
|
buckets, err := objAPI.ListBuckets(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for _, bucket := range buckets {
|
||||||
return saveConfig(objAPI, configFile, data)
|
ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{BucketName: bucket.Name})
|
||||||
|
config, err := readNotificationConfig(ctx, objAPI, bucket.Name)
|
||||||
|
if err != nil && err != errNoSuchNotifications {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == errNoSuchNotifications {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sys.AddRulesMap(bucket.Name, config.ToRulesMap())
|
||||||
|
if err = sys.initListeners(ctx, objAPI, bucket.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init - initializes notification system from notification.xml and listener.json of all buckets.
|
// Init - initializes notification system from notification.xml and listener.json of all buckets.
|
||||||
@@ -319,28 +333,29 @@ func (sys *NotificationSys) Init(objAPI ObjectLayer) error {
|
|||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
buckets, err := objAPI.ListBuckets(context.Background())
|
doneCh := make(chan struct{})
|
||||||
if err != nil {
|
defer close(doneCh)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bucket := range buckets {
|
// Initializing notification needs a retry mechanism for
|
||||||
ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{BucketName: bucket.Name})
|
// the following reasons:
|
||||||
config, err := readNotificationConfig(ctx, objAPI, bucket.Name)
|
// - Read quorum is lost just after the initialization
|
||||||
if err != nil {
|
// of the object layer.
|
||||||
if !IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _ = <-retryTimerCh:
|
||||||
|
if err := sys.refresh(objAPI); err != nil {
|
||||||
|
if err == errDiskNotFound ||
|
||||||
|
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||||
|
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||||
|
logger.Info("Waiting for notification subsystem to be initialized..")
|
||||||
|
continue
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
return nil
|
||||||
sys.AddRulesMap(bucket.Name, config.ToRulesMap())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = sys.initListeners(ctx, objAPI, bucket.Name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRulesMap - adds rules map for bucket name.
|
// AddRulesMap - adds rules map for bucket name.
|
||||||
@@ -433,6 +448,7 @@ func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
|
|||||||
sys.RLock()
|
sys.RLock()
|
||||||
targetIDSet := sys.bucketRulesMap[args.BucketName].Match(args.EventName, args.Object.Name)
|
targetIDSet := sys.bucketRulesMap[args.BucketName].Match(args.EventName, args.Object.Name)
|
||||||
sys.RUnlock()
|
sys.RUnlock()
|
||||||
|
|
||||||
if len(targetIDSet) == 0 {
|
if len(targetIDSet) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -550,7 +566,7 @@ func sendEvent(args eventArgs) {
|
|||||||
func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) (*event.Config, error) {
|
func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) (*event.Config, error) {
|
||||||
// Construct path to notification.xml for the given bucket.
|
// Construct path to notification.xml for the given bucket.
|
||||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
||||||
reader, err := readConfig(ctx, objAPI, configFile)
|
configData, err := readConfig(ctx, objAPI, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errConfigNotFound {
|
if err == errConfigNotFound {
|
||||||
err = errNoSuchNotifications
|
err = errNoSuchNotifications
|
||||||
@@ -559,19 +575,19 @@ func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := event.ParseConfig(reader, globalServerConfig.GetRegion(), globalNotificationSys.targetList)
|
config, err := event.ParseConfig(bytes.NewReader(configData), globalServerConfig.GetRegion(), globalNotificationSys.targetList)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveNotificationConfig(objAPI ObjectLayer, bucketName string, config *event.Config) error {
|
func saveNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName string, config *event.Config) error {
|
||||||
data, err := xml.Marshal(config)
|
data, err := xml.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
||||||
return saveConfig(objAPI, configFile, data)
|
return saveConfig(ctx, objAPI, configFile, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveListener - saves HTTP client currently listening for events to listener.json.
|
// SaveListener - saves HTTP client currently listening for events to listener.json.
|
||||||
@@ -596,14 +612,14 @@ func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name
|
|||||||
}
|
}
|
||||||
defer objLock.Unlock()
|
defer objLock.Unlock()
|
||||||
|
|
||||||
reader, err := readConfig(ctx, objAPI, configFile)
|
configData, err := readConfig(ctx, objAPI, configFile)
|
||||||
if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
|
if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
listenerList := []ListenBucketNotificationArgs{}
|
listenerList := []ListenBucketNotificationArgs{}
|
||||||
if reader != nil {
|
if configData != nil {
|
||||||
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
if err = json.Unmarshal(configData, &listenerList); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -622,7 +638,7 @@ func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveConfig(objAPI, configFile, data)
|
return saveConfig(ctx, objAPI, configFile, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveListener - removes HTTP client currently listening for events from listener.json.
|
// RemoveListener - removes HTTP client currently listening for events from listener.json.
|
||||||
@@ -647,14 +663,14 @@ func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.Target
|
|||||||
}
|
}
|
||||||
defer objLock.Unlock()
|
defer objLock.Unlock()
|
||||||
|
|
||||||
reader, err := readConfig(ctx, objAPI, configFile)
|
configData, err := readConfig(ctx, objAPI, configFile)
|
||||||
if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
|
if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
listenerList := []ListenBucketNotificationArgs{}
|
listenerList := []ListenBucketNotificationArgs{}
|
||||||
if reader != nil {
|
if configData != nil {
|
||||||
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
if err = json.Unmarshal(configData, &listenerList); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -681,5 +697,5 @@ func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.Target
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveConfig(objAPI, configFile, data)
|
return saveConfig(ctx, objAPI, configFile, data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func newStorageAPI(endpoint Endpoint) (storage StorageAPI, err error) {
|
|||||||
return newPosix(endpoint.Path)
|
return newPosix(endpoint.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newStorageRPC(endpoint), nil
|
return newStorageRESTClient(endpoint), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup a directory recursively.
|
// Cleanup a directory recursively.
|
||||||
|
|||||||
@@ -109,8 +109,12 @@ type ObjectInfo struct {
|
|||||||
Writer io.WriteCloser `json:"-"`
|
Writer io.WriteCloser `json:"-"`
|
||||||
Reader *hash.Reader `json:"-"`
|
Reader *hash.Reader `json:"-"`
|
||||||
metadataOnly bool
|
metadataOnly bool
|
||||||
|
|
||||||
// Date and time when the object was last accessed.
|
// Date and time when the object was last accessed.
|
||||||
AccTime time.Time
|
AccTime time.Time
|
||||||
|
|
||||||
|
// backendType indicates which backend filled this structure
|
||||||
|
backendType BackendType
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPartsInfo - represents list of all parts.
|
// ListPartsInfo - represents list of all parts.
|
||||||
@@ -258,6 +262,9 @@ type PartInfo struct {
|
|||||||
|
|
||||||
// Size in bytes of the part.
|
// Size in bytes of the part.
|
||||||
Size int64
|
Size int64
|
||||||
|
|
||||||
|
// Decompressed Size.
|
||||||
|
ActualSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultipartInfo - represents metadata in progress multipart upload.
|
// MultipartInfo - represents metadata in progress multipart upload.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user