Compare commits

...

108 Commits

Author SHA1 Message Date
Harshavardhana 9f14433cbd Ensure that setConfig uses latest functionality (#6302) 2018-08-17 18:51:34 -07:00
Harshavardhana 50a817e3d3 Use new listener which implements enhanced tcp features (#6289)
This package provide customizable TCP net.Listener with various
performance-related options:

 * SO_REUSEPORT. This option allows linear scaling server performance
   on multi-CPU servers.
   See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for details.
 * TCP_DEFER_ACCEPT. This option expects the server reads from the accepted
   connection before writing to them.
 * TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details.
2018-08-17 18:44:02 -07:00
Harshavardhana 5a4a57700b Add select docs and fix return values for Select API (#6300) 2018-08-17 17:11:39 -07:00
Harshavardhana 3de5a3157f Enhance picking valid xlMeta based on quorum (#6297)
This PR borrows the idea from getFormatXLQuorum()
2018-08-17 14:42:04 -07:00
Kaan Kabalak 50dec08002 Correct link paths in Chinese documentation (#6299) 2018-08-17 13:16:17 -07:00
poornas e71ef905f9 Add support for SSE-S3 server side encryption with vault (#6192)
Add support for sse-s3 encryption with vault as KMS.

Also refactoring code to make use of headers and functions defined in
crypto package and clean up duplicated code.
2018-08-17 12:52:14 -07:00
junpeng liu 3d197c1449 Modify several translation errors (#6038) 2018-08-17 12:04:09 +05:30
Harshavardhana 65de2d68c0 Allow for proper garbage collection pooling bytes.Buffer (#6266) 2018-08-16 18:37:43 -07:00
Harshavardhana 1103ad2d08 Watch for symlinked certs in container envs (#6282)
Fixes #6278
2018-08-16 18:37:21 -07:00
Harshavardhana eab947cf42 Make sure to update modTime in erasure metadata (#6296)
This is to ensure that when we update xl.json with new
parts have the latest modtime, in-turn avoids consistency
issues when the disk is offline.
2018-08-16 17:55:01 -07:00
Harshavardhana 0fe9e95250 Validate prefixes on all sets (#6294)
This PR fixes a regression introduced in 8eb838bf91
where hashing technique was used on prefixes to get the right set
to perform the operation, this is not correct since prefixes and
their corresponding keys might hash to a different value depending
on the key length.

For prefixes/directories we should look everywhere to support proper
quorum based listing.

Fixes #6293
2018-08-16 16:49:38 -07:00
kannappanr c7946ab9ab Remove unnecessary error log messages (#6186) 2018-08-16 12:57:49 -07:00
Harshavardhana f5df3b4795 Remove select docs (#6287)
Select API is sufficiently documented, this doc is also incomplete.

- https://aws.amazon.com/blogs/aws/s3-glacier-select/
- https://aws.amazon.com/blogs/developer/introducing-support-for-amazon-s3-select-in-the-aws-sdk-for-ruby/
- https://aws.amazon.com/blogs/developer/introducing-support-for-amazon-s3-select-in-the-aws-sdk-for-javascript/
- https://aws.amazon.com/blogs/developer/category/storage/s3-select/
2018-08-15 19:47:22 -07:00
Harshavardhana f26325c988 Support supplying custom drives per set count (#6261) 2018-08-15 16:35:21 -07:00
Arjun Mishra 7c14cdb60e S3 Select API Support for CSV (#6127)
Add support for trivial where clause cases
2018-08-15 03:30:19 -07:00
Harshavardhana 0e02328c98 Migrate config.json from config-dir to backend (#6195)
This PR is the first set of changes to move the config
to the backend, the changes use the existing `config.json`
allows it to be migrated such that we can save it in on
backend disks.

In future releases, we will slowly migrate out of the
current architecture.

Fixes #6182
2018-08-15 10:11:47 +05:30
Harshavardhana 380524ae27 Unlock read lock on uploadID upon errors (#6283) 2018-08-14 18:35:30 -07:00
kannappanr 0286e61aee Log disk not found error just once (#6059)
Modified the LogIf function to log only if the error passed
is not on the ignored errors list.

Currently, only disk not found error is added to the list.
Added a new function in logger package called LogAlwaysIf, 
which will print on any error.

Fixes #5997
2018-08-14 13:58:48 -07:00
wd256 ff29aed05d gcs: Translate S3 user-defined metadata prefix to/from GCS custom metadata prefix (#6270) 2018-08-14 11:53:39 -07:00
Harshavardhana 64f2c61813 Implement memory efficient readdir for windows (#6247)
Fixes #6164
2018-08-09 14:52:29 -07:00
Andreas Auernhammer 525c04fd07 crypto: add SSE-KMS HTTP header detection (#6228)
This commit adds support for detecting SSE-KMS headers.
The server should be able to detect SSE-KMS headers to
at least fail such S3 requests with not implemented.
2018-08-09 13:02:57 -07:00
Sanat Mouli efac90461a Fix bug in Share Object Modal (#6257)
Fixes #6249
2018-08-09 09:45:43 -07:00
Durgadas Kamath 71979376b5 Add s390x support (#6263) 2018-08-08 15:49:12 -07:00
Anis Elleuch 5a1ae862a7 Avoid sending an error after 206 HTTP code (#6264)
When a S3 client sends a GET Object with a range header, 206 http
code is returned indicating success, however the call of the object
layer's GetObject() inside the handler can return an error and will lead
to writing an XML error message, which is obviously wrong since
we already sent 206 http code. So in the case, we just stop sending
data to the S3 client, this latter can still detect if there is no
error when comparing received data with Content-Length header
in the Get Object response.
2018-08-08 15:39:47 -07:00
Anis Elleuch 6df20734f9 Avoid logging the body of the http 206 response (#6258)
When an S3 client issues a GET request with range specified, Minio
server returns some partial data with 206 http code. The latter
is sent in MINIO_HTTP_TRACE output which is incorrect. This PR
fixes the issue.
2018-08-08 12:34:42 -07:00
Sanat Mouli 98bce72295 Fix output path for go-bindata-assetfs (#6253) 2018-08-08 10:46:02 +05:30
Harshavardhana 2f1756489e Add tracing capabilities for internode rpc Servers (#6254) 2018-08-07 15:21:30 +05:30
Harshavardhana 9719640e34 Use sha256-simd instead of crypto/sha256 (#6252) 2018-08-06 18:00:10 -07:00
Krishna Srinivas ce02ab613d Simplify erasure code by separating bitrot from erasure code (#5959) 2018-08-06 15:14:08 -07:00
Oleg Kovalov 37de2dbd3b simplifying if-else chains to switches (#6208) 2018-08-06 10:26:40 -07:00
Harshavardhana a82500f162 Support dumb terminals by turning off color (#6246)
ANSI colors do not work on dumb terminals, in situations
when minio is running as a service under systemd.

This PR ensures we turn off color in those situations.
2018-08-06 18:16:49 +05:30
Harshavardhana 2dede2fdc2 Add reliable RemoveAll to handle racy situations (#6227) 2018-08-06 09:45:28 +05:30
Harshavardhana 13fbb96736 Hold locks granularly in nslockMap (#6242)
With benchmarks increases the performance for small files
by almost 4x times the previous releases.
2018-08-06 08:55:25 +05:30
Harshavardhana eabfcea34e Add granular locking in retryTicker (#6236)
This is to avoid serializing RPC contention on ongoing
parallel operations, the blocking profile indicating
all calls were being serialized through setRetryTicker.
2018-08-03 18:57:00 -07:00
Andreas Auernhammer a078703214 catch crypto.* errors and add SSE-S3 invalid algorithm err (#6229)
This commit adds the crypto.* errors to the
`toAPIErrorCode` switch. Further this commit adds an S3
API error code returned whenever the client specifes a
SSE-S3 request with an invalid algorithm parameter.
2018-08-03 16:55:45 -07:00
Harshavardhana bd2b22572f Increase max idle connections from 100 to 4096 (#6244)
This is to be inline with our benchmarking results
2018-08-03 16:29:28 -07:00
poornas 5f69f04909 nas gateway: fix regression in global bucket policy initialization (#6243)
Fixes #6238
globalPolicySys used to be initialized in fs/xl layer. The referenced
commit moved this logic to server/gateway initialization,but a check
to avoid double initialization prevented globalPolicySys to be loaded
from disk for NAS.

fixes regression from commit be1700f595
2018-08-03 15:12:18 -07:00
Minio Trusted a1a426e523 Update yaml files to latest version RELEASE.2018-08-02T23-11-36Z 2018-08-02 23:17:30 +00:00
Harshavardhana a091b1a3ee Fix admin API doc formatting (#6235) 2018-08-02 14:21:38 -07:00
Harshavardhana 556a51120c Deprecate ListLocks and ClearLocks (#6233)
No locks are ever left in memory, we also
have a periodic interval of clearing stale locks
anyways. The lock instrumentation was not complete
and was seldom used.

Deprecate this for now and bring it back later if
it is really needed. This also in-turn seems to improve
performance slightly.
2018-08-02 23:09:42 +05:30
poornas eb391a53c1 check for syscall errors in posix-errors helper functions (#6232) 2018-08-02 10:38:51 +05:30
Harshavardhana e17e09ea3c Handle POST object upload without filename param (#6221)
POST mime/multipart upload style can have filename value optional
which leads to implementation issues in Go releases in their
standard mime/multipart library.

When `filename` doesn't exist Go doesn't update `form.File` which
we rely on to extract the incoming file data, strangely when `filename`
is not specified this data is buffered in memory and is now part of
`form.Value` instead of `form.File` which creates an inconsistent
behavior.

This PR tries to fix this in our code for the time being, but ideal PR
would be to fix the upstream mime/multipart library to handle the
above situation consistently.
2018-08-01 14:19:11 -07:00
Andreas Auernhammer 76c423392a crypto: add GenerateIV from random IV generation (#6215)
This commit adds a `GenerateIV` function to simplify
the generation of random IVs.

It adds some unit tests for `GenerateIV` in key_test.go
2018-08-01 01:02:07 -07:00
Aarushi Arya 8e6d756e3a Appropriate error message on unsuccessful update. (#6203) 2018-08-01 01:01:37 -07:00
Andreas Auernhammer a7c9058375 crypto: implement Stringer for S3 and SSEC (#6216)
This commit adds a `fmt.Stringer` implementation for
SSE-S3 and SSE-C. The string representation is the
domain used for object key sealing.
See: `ObjectKey.Seal(...)` and `ObjectKey.Unseal(...)`
2018-07-31 11:15:12 -07:00
Nitish Tiwari b16e33bcf5 Fix Kubernetes TLS doc to avoid creating CAs dir on read only mount (#6214) 2018-07-31 10:58:34 -07:00
Nitish Tiwari 197af49c99 Fix healthcheck handler to verify gateway backend liveness (#6218)
Fixes #6217
2018-07-31 10:55:34 -07:00
kannappanr 264cc4020f Return 503 instead of 404 if more than half of disks are not found (#6207)
Fixes #6163
2018-07-31 00:23:29 -07:00
Minio Trusted df88421087 Update yaml files to latest version RELEASE.2018-07-31T02-11-47Z 2018-07-31 02:17:27 +00:00
Harshavardhana dbd89bbae3 Remove double RLocks for GetBucketInfo (#6209) 2018-07-30 17:38:52 -07:00
Aditya Manthramurthy 224a272cf2 Fix type of bitrot mismatch error (#6205)
The error type `hashMismatchError` is lost when the error is received
from a remote disk.

Fixes #6201
2018-07-29 15:00:37 +05:30
Harshavardhana ad86454580 Make sure to handle FaultyDisks in listing ops (#6204)
Continuing from PR 157ed65c35

Our posix.go implementation did not handle  I/O errors
properly on the disks, this led to situations where
top-level callers such as ListObjects might return early
without even verifying all the available disks.

This commit tries to address this in Kubernetes, drbd/nbd based
persistent volumes which can disconnect under load and
result in the situations with disks return I/O errors.

This commit also simplifies listing operation, listing
never returns any error. We can avoid this since we pretty
much ignore most of the errors anyways. When objects are
accessed directly we return proper errors.
2018-07-27 15:32:19 -07:00
Andreas Auernhammer 644c2ce326 crypto: add support for parsing/creating SSE-C/SSE-S3 metadata (#6169)
* crypto: add support for parsing SSE-C/SSE-S3 metadata

This commit adds support for detecting and parsing
SSE-C/SSE-S3 object metadata. With the `IsEncrypted`
functions it is possible to determine whether an object
seems to be encrypted. With the `ParseMetadata` functions
it is possible to validate such metadata and extract the
SSE-C/SSE-S3 related values.

It also fixes some naming issues.

* crypto: add functions for creating SSE object metadata

This commit adds functions for creating SSE-S3 and
SSE-C metadata. It also adds a `CreateMultipartMetadata`
for creating multipart metadata.

For all functions unit tests are included.
2018-07-25 13:35:54 -07:00
Harshavardhana 2debe77586 Remove error returned when part sizes are un-equal (#6183)
Since implementing `pwrite` like implementation would
require a more complex code than background append
implementation, it is better to keep the current code
as is and not implement `pwrite` based functionality.

Closes #4881
2018-07-24 21:31:03 -07:00
Harshavardhana 20480ba3f7 Remove references to MINIO_ENDPOINTS (#6200)
MINIO_ENDPOINTS is a special case scenario
we don't need to document it for now.
2018-07-24 17:17:25 -07:00
kannappanr 4f52d22c36 Fix make test failure (#6185)
Fix shadowing errors.
2018-07-24 14:17:58 -07:00
Kaan Kabalak cbe8df198e Replace pointer with default cursor on filename hover (#6193) 2018-07-24 13:34:20 +05:30
Harshavardhana 157ed65c35 Fix healthcheck handler to check errors in local disks only (#6184)
Healthcheck handler in current implementation was
performing ListBuckets() to check for liveness of Minio
service. ListBuckets() implementation on the other hand
doesn't do quorum based listing and if one of the disks
returned error, an I/O error it would be lead to kubernetes
taking the minio pod down prematurely even if the disk
is not local to that minio server.

The reason is ListBuckets() call cannot be trusted to
provide us the valid information that we need, Minio is a
clustered application which is designed to handle disk
failures. Error on one of the disks doesn't mean the pod
should become fully non-operational.

This PR attempts to fix this by only checking for alive
disks which are local to each setup and also by simply
performing a Stat() operation, if the Stat() returned
error on all disks local to a particular server then
we can let kubernetes safely take it down, until then
we should be operational.
2018-07-23 12:21:25 -07:00
Minio Trusted 869018ad14 Update yaml files to latest version RELEASE.2018-07-23T18-34-49Z 2018-07-23 18:39:08 +00:00
Harshavardhana 5acc2a6db1 S3 gateway signature probe use a unique bucket (#6190)
This fixes an issue because someone is
using `probe-bucket-sign` bucket name in
region 'eu-central-1'
2018-07-23 10:16:56 -07:00
kannappanr 2cd14f567c Do not set Key and BucketName in ErrorResponse, if empty (#6174) 2018-07-23 14:09:09 +05:30
Harshavardhana f1be356cc6 Do not use parallel deletes to avoid random I/O (#6178)
The current code for deleting 1000 objects simultaneously
causes significant random I/O, which on slower drives
leads to servers disconnecting in a distributed setup.

Simplify this by serially deleting and reducing the
chattiness of this operation.
2018-07-20 21:21:01 -07:00
kannappanr 76ddf4d32f Log x-amz-request-id as log and XML error response (#6173)
Currently, requestid field in logEntry is not populated, as the
requestid field gets set at the very end.
It is now set before regular handler functions. This is also
useful in setting it as part of the XML error response.

Travis build for ppc64le has been quite inconsistent and stays queued
for most of the time. Removing this build as part of Travis.yml for
the time being.
2018-07-20 18:46:32 -07:00
kannappanr 7b91bd71fe Remove ppc64le support from .travis.yml (#6180)
Travis CI for ppc64le stays in queued state.
Removing the support for now.
2018-07-20 18:39:55 -07:00
Harshavardhana 36ab615518 Remove unused functions and constants (#6175) 2018-07-20 23:37:43 +05:30
kannappanr 963a70053b Do not trace HTTP body of PostPolicyBuckethandler (#6177) 2018-07-20 07:37:46 -07:00
Anis Elleuch 9c5e971a58 Add new console/http loggers (#6066)
- Add console target logging, enabled by default.
- Add http target logging, which supports an endpoint
  with basic authentication (username/password are passed
  in the endpoint url itself)
- HTTP target logging is asynchronous and some logs can be
  dropped if channel buffer (10000) is full
2018-07-19 15:55:06 -07:00
Anis Elleuch b1c9eb0e01 Disable splitting lines in pretty error messages (#6171)
In a small window, UI error tries to split lines for an eye candy
error message. However, since we show some docs.minio.io links in some
error messages, these links are actually broken and not easily selected
in a X terminal. This PR changes the behavior and won't split lines
anymore.
2018-07-19 15:49:02 -07:00
Nitish Tiwari b8f4f26cf6 Add S3 gateway documentation (#6165)
Fixes #4830
2018-07-19 11:54:38 -07:00
kannappanr 43cc0096fa Add support for deployment ID (#6144)
deployment ID helps in identifying a minio deployment in the case of remote
logging targets.
2018-07-18 20:17:35 -07:00
Anis Elleuch e8a008f5b5 Better validation of all config file fields (#6090)
Add Validate() to serverConfig to call it at server
startup and in Admin SetConfig handler to minimize
errors scenario after server restart.
2018-07-18 11:22:29 -07:00
Andreas Auernhammer 758a80e39b crypto: add basic functionality for parsing SSE-C headers (#6148)
This commit adds basic support for SSE-C / SSE-C copy.
This includes functions for determining whether SSE-C
is requested by the S3 client and functions for parsing
such HTTP headers.

All S3 SSE-C parsing errors are exported such that callers
can pattern-match to forward the correct error to S3
clients.

Further the SSE-C related internal metadata entry-keys
are added by this commit.
2018-07-18 10:49:26 -07:00
wd256 3ec4738955 gcs: Use Pager to iterate results in ListObjectsV1/V2 (#6162)
Fixes #6052
2018-07-18 21:49:16 +05:30
Andreas Auernhammer 6c93c60424 crypto: add a basic KMS implementation (#6161)
This commit adds a basic KMS implementation for an
operator-specified SSE-S3 master key. The master key
is wrapped as KMS such that using SSE-S3 with master key
and SSE-S3 with KMS can use the same code.

Bindings for a remote / true KMS (like hashicorp vault)
will be added later on.
2018-07-17 22:40:34 -07:00
Krishna Srinivas 0c9f4c9092 formatMetaV1 should be "inherited" by disk format structs (#6134) 2018-07-16 20:26:42 -07:00
Andreas Auernhammer 289d6ce1d7 crypto: update SSE-S3 and SSE-C key derivation (#6152)
This commit updates the key derivation to reflect the
latest change of crypto/doc.go. This includes handling
the insecure legacy KDF.

Since #6064 is fixed, the 3. test case for object key
generation is enabled again.
2018-07-16 07:49:50 -07:00
Rob Girard 2a12e694f3 Changed command line examples (#6149)
Order for server:disk originally provided wouldn't stripe 
wide and may lead to availability issues.

Also added Short-form examples using {1...n} and a 
warning about {1..2} vs {1...3}
2018-07-14 20:48:38 +05:30
Harshavardhana db26d3c9e2 Fix handling files at leaf attempting disk.ListDir() (#6155)
Return an ignorable error upon readDir() failure on
a file.
2018-07-14 12:11:48 +05:30
Stefan Husch 914c76a801 Implement lock.Open() to fix #5642 (#6150) 2018-07-13 18:37:02 -07:00
Krishnan Parthasarathi a1ef90be52 gcs: Limit number of objects listed to max-keys (#6133) 2018-07-13 10:27:26 -07:00
Minio Trusted 7c4a41b933 Update yaml files to latest version RELEASE.2018-07-13T00-09-07Z 2018-07-13 00:43:51 +00:00
Nitish Tiwari 2aa18cafc6 Update federation target to etcd/clientv3 (#6119)
With CoreDNS now supporting etcdv3 as the DNS backend, we
can update our federation target to etcdv3. Users will now be
able to use etcdv3 server as the federation backbone.

Minio will update bucket data to etcdv3 and CoreDNS can pick
that data up and serve it as bucket style DNS path.
2018-07-12 14:12:40 -07:00
Andreas Auernhammer adf7340394 fix size computation for en/decrypted objects (#6147)
This PR fixes the size calculation for encrypted multipart
objects.
2018-07-12 11:23:32 -07:00
Harshavardhana b11a8eb3f4 Support multiple values for x-amz-meta header (#6145)
Fixes #5595
2018-07-12 09:40:14 -07:00
Andreas Auernhammer 15771ebe8d Fix decrypted object size and key derivation in CopyObjectPart (#6141)
This commit fixes the size calculation for multipart
objects. The decrypted size of an encrypted multipart
object is the sum of the decrypted part sizes.

Also fixes the key derivation in CopyObjectPart.
Instead of using the same object-encryption-key for each
part now an unique per-part key is derived.

Updates #6139
2018-07-12 21:59:56 +05:30
Praveen raj Mani 44865596db SignatureV4 validation with Metadata in the presignedUrl (#5894)
The `X-Amz-Meta-`/`X-Minio-Meta-` will now be recognized in query string also.

Fixes #5857 #5950
2018-07-10 20:27:10 -07:00
Nitish Tiwari c9bc7e47b9 Update distributed docs (#6123)
We need to clarify that distributed Minio doesn't strictly need a
fresh drive, instead it just needs a fresh directory on the drive.
2018-07-10 07:32:24 +05:30
Anis Elleuch be1700f595 Avoid startup abort when a notify target is down (#6126)
Minio server was preventing itself to start when any notification
target is down and not running. The PR changes the behavior by
avoiding startup abort in that case, so the user will still
be able to access Minio server using mc admin commands after
a restart or set config commands.
2018-07-10 07:20:31 +05:30
Minio Trusted 42c5b64e4e Update yaml files to latest version RELEASE.2018-07-10T01-42-11Z 2018-07-10 01:46:03 +00:00
Krishna Srinivas 40ed0d1f5d Support 1GB disk size (#6137)
Pivotal CF by default has 1GB disk option which causes minio to not start
2018-07-09 18:23:49 -07:00
Andreas Auernhammer b181a693fb fix object rebinding SSE-C security guarantee violation (#6121)
This commit fixes a weakness of the key-encryption-key
derivation for SSE-C encrypted objects. Before this
change the key-encryption-key was not bound to / didn't
depend on the object path. This allows an attacker to
repalce objects - encrypted with the same
client-key - with each other.

This change fixes this issue by updating the
key-encryption-key derivation to include:
 - the domain (in this case SSE-C)
 - a canonical object path representation
 - the encryption & key derivation algorithm

Changing the object path now causes the KDF to derive a
different key-encryption-key such that the object-key
unsealing fails.
Including the domain (SSE-C) and encryption & key
derivation algorithm is not directly neccessary for this
fix. However, both will be included for the SSE-S3 KDF.
So they are included here to avoid updating the KDF
again when we add SSE-S3.

The leagcy KDF 'DARE-SHA256' is only used for existing
objects and never for new objects / key rotation.
2018-07-09 17:18:28 -07:00
Anis Elleuch 4ddc222f46 fix: Propagate bucket policy update in a distributed setup (#6135)
Commit 0d52126023 caused a regression in setting
a new bucket policy in a distributed setup. The reason is that gob is not able
to encode fields declared as interfaces unless we provide GobEncode() and GobDecode()
This PR adds them by using json marshaller and unmarshaller that are already
implemented for Functions interface.
2018-07-09 02:18:48 -07:00
Mike Scarlett c310cbbe89 Update comments regarding GCS component count (#6131) 2018-07-06 17:07:11 -07:00
Harshavardhana 0ef0d7e685 pkg/certs: On windows watch for directory changes to load certs (#6128)
This PR fixes an issue when configuring Minio TLS on windows
2018-07-05 16:33:37 -07:00
wksw c62813c887 completed sample config and fix wrong json format (#6096)
missing worm configuration and wrong json format
2018-07-05 13:40:07 +05:30
poornas 1da362538b cache: allow ellipse style entries for MINIO_CACHE_DRIVES (#6088)
Fixes #5863
2018-07-03 16:54:10 -07:00
Krishna Srinivas e40a5e05e1 Do notification in background to not block S3 client REST calls (#6005) 2018-07-03 11:09:36 -07:00
ebozduman b0b0fb4c8d Validate Minio config.json file on the client side (#6067) 2018-07-03 11:07:46 -07:00
kannappanr 726e75611e Do not log BucketNotFound errors on minio console (#6114) 2018-07-03 11:04:55 -07:00
Rafael Peria de Sene 317e648c0d Add support for ppc64le (#6116)
Signed-off-by: Rafael Peria de Sene <rpsene@br.ibm.com>
2018-07-03 09:42:19 -07:00
Harshavardhana 80b3e9cb03 use appropriate HTTP status for storage is full (#6117) 2018-07-03 09:40:14 -07:00
Harshavardhana 6c85706c24 Use GetSourceIP for source ip as request params (#6109)
Fixes #6108
2018-07-02 14:40:18 -07:00
Harshavardhana d7ced9a8b5 Upgrade gjson to bring in new performance improvements (#6106) 2018-07-02 13:31:11 -07:00
Praveen raj Mani 360f3f9335 Checking the existence of the bucket in DeleteObjectHandler (#6085)
Fixes #6077
2018-06-30 22:35:43 -07:00
wd256 25f9b0bc3b Handle ListObjectsV2 start-after parameter in ObjectLayer (#6078) 2018-07-01 09:52:45 +05:30
Harshavardhana a5453c307f Fix kernel tuning script to ignore write failures (#6107)
Certain SCSI drivers do not allow certain tuning parameters
like nr_requests, max_sectors_kb to be changed, ignore these
errors silently as this script is simply a best effort.

Fixes #6103
2018-06-30 14:55:21 -07:00
Harshavardhana 92a6676a2f Avoid unnecessary logging for policy not found errors (#6104) 2018-06-29 06:30:10 -07:00
Minio Trusted f53d511798 Update yaml files to latest version RELEASE.2018-06-29T02-11-29Z 2018-06-29 02:14:58 +00:00
844 changed files with 134785 additions and 75799 deletions
+1
View File
@@ -22,3 +22,4 @@ parts/
prime/ prime/
stage/ stage/
.sia_temp/ .sia_temp/
config.json
+14 -18
View File
@@ -8,27 +8,23 @@ dist: trusty
language: go language: go
os: matrix:
- linux include:
- os: linux
env: env:
- ARCH=x86_64 - ARCH=x86_64
go: 1.10.1
script:
- make
- diff -au <(gofmt -s -d cmd) <(printf "")
- diff -au <(gofmt -s -d pkg) <(printf "")
- make test GOFLAGS="-timeout 15m -race -v"
- make coverage
- node --version
- cd browser && yarn && yarn test && cd ..
before_install: before_install:
- nvm install stable - nvm install stable
script:
## Run all the tests
- make
- diff -au <(gofmt -s -d cmd) <(printf "")
- diff -au <(gofmt -s -d pkg) <(printf "")
- make test GOFLAGS="-timeout 15m -race -v"
- make coverage
- node --version
- cd browser && yarn && yarn test && cd ..
after_success: after_success:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash)
go:
- '1.10.1'
+2 -2
View File
@@ -42,8 +42,8 @@ ineffassign:
cyclo: cyclo:
@echo "Running $@" @echo "Running $@"
@${GOPATH}/bin/gocyclo -over 100 cmd @${GOPATH}/bin/gocyclo -over 200 cmd
@${GOPATH}/bin/gocyclo -over 100 pkg @${GOPATH}/bin/gocyclo -over 200 pkg
deadcode: deadcode:
@echo "Running $@" @echo "Running $@"
+5 -3
View File
@@ -57,7 +57,7 @@ export class ObjectActions extends React.Component {
}) })
} }
render() { render() {
const { object, showShareObjectModal } = this.props const { object, showShareObjectModal, shareObjectName } = this.props
return ( return (
<Dropdown id={`obj-actions-${object.name}`}> <Dropdown id={`obj-actions-${object.name}`}>
<Dropdown.Toggle noCaret className="fia-toggle" /> <Dropdown.Toggle noCaret className="fia-toggle" />
@@ -77,7 +77,8 @@ export class ObjectActions extends React.Component {
<i className="fa fa-trash" /> <i className="fa fa-trash" />
</a> </a>
</Dropdown.Menu> </Dropdown.Menu>
{showShareObjectModal && <ShareObjectModal object={object} />} {(showShareObjectModal && shareObjectName === object.name) &&
<ShareObjectModal object={object} />}
{this.state.showDeleteConfirmation && ( {this.state.showDeleteConfirmation && (
<DeleteObjectConfirmModal <DeleteObjectConfirmModal
deleteObject={this.deleteObject.bind(this)} deleteObject={this.deleteObject.bind(this)}
@@ -92,7 +93,8 @@ export class ObjectActions extends React.Component {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
return { return {
object: ownProps.object, object: ownProps.object,
showShareObjectModal: state.objects.shareObject.show showShareObjectModal: state.objects.shareObject.show,
shareObjectName: state.objects.shareObject.object
} }
} }
@@ -88,8 +88,21 @@ describe("ObjectActions", () => {
object={{ name: "obj1" }} object={{ name: "obj1" }}
currentPrefix={"pre1/"} currentPrefix={"pre1/"}
showShareObjectModal={true} showShareObjectModal={true}
shareObjectName={"obj1"}
/> />
) )
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1) expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1)
}) })
it("shouldn't render ShareObjectModal when the names of the objects don't match", () => {
const wrapper = shallow(
<ObjectActions
object={{ name: "obj1" }}
currentPrefix={"pre1/"}
showShareObjectModal={true}
shareObjectName={"obj2"}
/>
)
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(0)
})
}) })
+6
View File
@@ -100,6 +100,12 @@ div.fesl-row {
} }
} }
.fesl-item-name {
a {
cursor: default;
}
}
/*-------------------------- /*--------------------------
Icons Icons
----------------------------*/ ----------------------------*/
+2 -2
View File
@@ -70,9 +70,9 @@ async.waterfall([
commitId = stdout.replace('\n', '') commitId = stdout.replace('\n', '')
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId) if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
assetsFileName = 'ui-assets.go'; assetsFileName = 'ui-assets.go';
var cmd = 'go-bindata-assetfs -pkg browser -nocompress=true production/...' var cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true production/...'
if (!isProduction) { if (!isProduction) {
cmd = 'go-bindata-assetfs -pkg browser -nocompress=true dev/...' cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true dev/...'
} }
console.log('Running', cmd) console.log('Running', cmd)
exec(cmd, cb) exec(cmd, cb)
+24 -24
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -89,11 +89,11 @@ check_minimum_version() {
assert_is_supported_arch() { assert_is_supported_arch() {
case "${ARCH}" in case "${ARCH}" in
x86_64 | amd64 | aarch64 | arm* ) x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x )
return return
;; ;;
*) *)
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, arm*]" echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x]"
exit 1 exit 1
esac esac
} }
+4
View File
@@ -71,6 +71,8 @@ function start_minio_erasure_sets()
function start_minio_dist_erasure_sets() function start_minio_dist_erasure_sets()
{ {
declare -a minio_pids declare -a minio_pids
export MINIO_ACCESS_KEY=$ACCESS_KEY
export MINIO_SECRET_KEY=$SECRET_KEY
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9000.log" 2>&1 & "${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
minio_pids[0]=$! minio_pids[0]=$!
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9001.log" 2>&1 & "${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
@@ -99,6 +101,8 @@ function start_minio_dist_erasure_sets()
function start_minio_dist_erasure() function start_minio_dist_erasure()
{ {
declare -a minio_pids declare -a minio_pids
export MINIO_ACCESS_KEY=$ACCESS_KEY
export MINIO_SECRET_KEY=$SECRET_KEY
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 & "${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
minio_pids[0]=$! minio_pids[0]=$!
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 & "${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
+2 -2
View File
@@ -54,7 +54,7 @@ type accessControlPolicy struct {
// This operation uses the ACL // This operation uses the ACL
// subresource to return the ACL of a specified bucket. // subresource to return the ACL of a specified bucket.
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetBucketACL") ctx := newContext(r, w, "GetBucketACL")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -101,7 +101,7 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
// This operation uses the ACL // This operation uses the ACL
// subresource to return the ACL of a specified object. // subresource to return the ACL of a specified object.
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetObjectACL") ctx := newContext(r, w, "GetObjectACL")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
+93 -266
View File
@@ -21,10 +21,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"sync" "sync"
"time" "time"
@@ -37,8 +35,6 @@ import (
) )
const ( const (
minioConfigTmpFormat = "config-%s.json"
maxConfigJSONSize = 256 * 1024 // 256KiB maxConfigJSONSize = 256 * 1024 // 256KiB
) )
@@ -47,11 +43,10 @@ type mgmtQueryKey string
// Only valid query params for mgmt admin APIs. // Only valid query params for mgmt admin APIs.
const ( const (
mgmtBucket mgmtQueryKey = "bucket" mgmtBucket mgmtQueryKey = "bucket"
mgmtPrefix mgmtQueryKey = "prefix" mgmtPrefix mgmtQueryKey = "prefix"
mgmtLockOlderThan mgmtQueryKey = "older-than" mgmtClientToken mgmtQueryKey = "clientToken"
mgmtClientToken mgmtQueryKey = "clientToken" mgmtForceStart mgmtQueryKey = "forceStart"
mgmtForceStart mgmtQueryKey = "forceStart"
) )
var ( var (
@@ -275,133 +270,6 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
writeSuccessResponseJSON(w, jsonBytes) writeSuccessResponseJSON(w, jsonBytes)
} }
// validateLockQueryParams - Validates query params for list/clear
// locks management APIs.
func validateLockQueryParams(vars url.Values) (string, string, time.Duration,
APIErrorCode) {
bucket := vars.Get(string(mgmtBucket))
prefix := vars.Get(string(mgmtPrefix))
olderThanStr := vars.Get(string(mgmtLockOlderThan))
// N B empty bucket name is invalid
if !IsValidBucketName(bucket) {
return "", "", time.Duration(0), ErrInvalidBucketName
}
// empty prefix is valid.
if !IsValidObjectPrefix(prefix) {
return "", "", time.Duration(0), ErrInvalidObjectName
}
// If older-than parameter was empty then set it to 0s to list
// all locks older than now.
if olderThanStr == "" {
olderThanStr = "0s"
}
duration, err := time.ParseDuration(olderThanStr)
if err != nil {
logger.LogIf(context.Background(), err)
return "", "", time.Duration(0), ErrInvalidDuration
}
return bucket, prefix, duration, ErrNone
}
// ListLocksHandler - GET /minio/admin/v1/locks?bucket=mybucket&prefix=myprefix&older-than=10s
// - bucket is a mandatory query parameter
// - prefix and older-than are optional query parameters
// ---------
// Lists locks held on a given bucket, prefix and duration it was held for.
func (a adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkAdminRequestAuthType(r, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(w, adminAPIErr, r.URL)
return
}
vars := r.URL.Query()
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponseJSON(w, adminAPIErr, r.URL)
return
}
// Fetch lock information of locks matching bucket/prefix that
// are available for longer than duration.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix,
duration)
if err != nil {
writeErrorResponseJSON(w, ErrInternalError, r.URL)
logger.LogIf(context.Background(), err)
return
}
// Marshal list of locks as json.
jsonBytes, err := json.Marshal(volLocks)
if err != nil {
writeErrorResponseJSON(w, ErrInternalError, r.URL)
logger.LogIf(context.Background(), err)
return
}
// Reply with list of locks held on bucket, matching prefix
// held longer than duration supplied, as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// ClearLocksHandler - DELETE /minio/admin/v1/locks?bucket=mybucket&prefix=myprefix&duration=duration
// - bucket is a mandatory query parameter
// - prefix and older-than are optional query parameters
// ---------
// Clear locks held on a given bucket, prefix and duration it was held for.
func (a adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "ClearLocks")
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
return
}
adminAPIErr := checkAdminRequestAuthType(r, "")
if adminAPIErr != ErrNone {
writeErrorResponseJSON(w, adminAPIErr, r.URL)
return
}
vars := r.URL.Query()
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponseJSON(w, adminAPIErr, r.URL)
return
}
// Fetch lock information of locks matching bucket/prefix that
// are held for longer than duration.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix,
duration)
if err != nil {
writeErrorResponseJSON(w, ErrInternalError, r.URL)
logger.LogIf(ctx, err)
return
}
// Marshal list of locks as json.
jsonBytes, err := json.Marshal(volLocks)
if err != nil {
writeErrorResponseJSON(w, ErrInternalError, r.URL)
logger.LogIf(ctx, err)
return
}
objLayer.ClearLocks(ctx, volLocks)
// Reply with list of locks cleared, as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// 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,
@@ -464,7 +332,7 @@ func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
// sequence. However, if the force-start flag is provided, the server // sequence. However, if the force-start flag is provided, the server
// aborts the running heal sequence and starts a new one. // aborts the running heal sequence and starts a new one.
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "Heal") ctx := newContext(r, w, "Heal")
// Get object layer instance. // Get object layer instance.
objLayer := newObjectLayerFn() objLayer := newObjectLayerFn()
@@ -570,6 +438,15 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
// GetConfigHandler - GET /minio/admin/v1/config // GetConfigHandler - GET /minio/admin/v1/config
// Get config.json of this minio setup. // Get config.json of this minio setup.
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetConfigHandler")
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature. // Validate request signature.
adminAPIErr := checkAdminRequestAuthType(r, "") adminAPIErr := checkAdminRequestAuthType(r, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
@@ -577,26 +454,28 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
return return
} }
// Take a read lock on minio/config.json. NB minio is a config, err := readServerConfig(ctx, objectAPI)
// reserved bucket name and wouldn't conflict with normal
// object operations.
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
if configLock.GetRLock(globalObjectTimeout) != nil {
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
return
}
defer configLock.RUnlock()
// Get config.json - in distributed mode, the configuration
// occurring on a quorum of the servers is returned.
configBytes, err := getPeerConfig(globalAdminPeers)
if err != nil { if err != nil {
logger.LogIf(context.Background(), err)
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
return return
} }
writeSuccessResponseJSON(w, configBytes) configData, err := json.Marshal(config)
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
return
}
password := config.GetCredential().SecretKey
econfigData, err := madmin.EncryptServerConfigData(password, configData)
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
return
}
writeSuccessResponseJSON(w, econfigData)
} }
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API // toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
@@ -605,65 +484,15 @@ func toAdminAPIErrCode(err error) APIErrorCode {
switch err { switch err {
case errXLWriteQuorum: case errXLWriteQuorum:
return ErrAdminConfigNoQuorum return ErrAdminConfigNoQuorum
default:
return toAPIErrorCode(err)
} }
return toAPIErrorCode(err)
}
// SetConfigResult - represents detailed results of a set-config
// operation.
type nodeSummary struct {
Name string `json:"name"`
ErrSet bool `json:"errSet"`
ErrMsg string `json:"errMsg"`
}
type setConfigResult struct {
NodeResults []nodeSummary `json:"nodeResults"`
Status bool `json:"status"`
}
// writeSetConfigResponse - writes setConfigResult value as json
// depending on the status.
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers,
errs []error, status bool, reqURL *url.URL) {
var nodeResults []nodeSummary
// Build nodeResults based on error values received during
// set-config operation.
for i := range errs {
nodeResults = append(nodeResults, nodeSummary{
Name: peers[i].addr,
ErrSet: errs[i] != nil,
ErrMsg: fmt.Sprintf("%v", errs[i]),
})
}
result := setConfigResult{
Status: status,
NodeResults: nodeResults,
}
// The following elaborate json encoding is to avoid escaping
// '<', '>' in <nil>. Note: json.Encoder.Encode() adds a
// gratuitous "\n".
var resultBuf bytes.Buffer
enc := json.NewEncoder(&resultBuf)
enc.SetEscapeHTML(false)
jsonErr := enc.Encode(result)
if jsonErr != nil {
writeErrorResponseJSON(w, toAPIErrorCode(jsonErr), reqURL)
return
}
writeSuccessResponseJSON(w, resultBuf.Bytes())
return
} }
// 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 := context.Background()
// Get current object layer instance. // Get current object layer instance.
objectAPI := newObjectLayerFn() objectAPI := newObjectLayerFn()
if objectAPI == nil { if objectAPI == nil {
@@ -671,12 +500,6 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
return return
} }
// Deny if WORM is enabled
if globalWORMEnabled {
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
return
}
// Validate request signature. // Validate request signature.
adminAPIErr := checkAdminRequestAuthType(r, "") adminAPIErr := checkAdminRequestAuthType(r, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
@@ -698,7 +521,13 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
return return
} }
configBytes := configBuf[:n] password := globalServerConfig.GetCredential().SecretKey
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
return
}
// Validate JSON provided in the request body: check the // Validate JSON provided in the request body: check the
// client has not sent JSON objects with duplicate keys. // client has not sent JSON objects with duplicate keys.
@@ -712,7 +541,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
err = json.Unmarshal(configBytes, &config) err = json.Unmarshal(configBytes, &config)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL) writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
return return
} }
@@ -727,53 +556,45 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
} }
} }
// Write config received from request onto a temporary file on if err = config.Validate(); err != nil {
// all nodes. writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
errs := writeTmpConfigPeers(globalAdminPeers, tmpFileName, configBytes)
// Check if the operation succeeded in quorum or more nodes.
rErr := reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
if rErr != nil {
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
return return
} }
// Take a lock on minio/config.json. NB minio is a reserved if err = saveServerConfig(objectAPI, &config); err != nil {
// bucket name and wouldn't conflict with normal object writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
// operations.
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
if configLock.GetLock(globalObjectTimeout) != nil {
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
return
}
defer configLock.Unlock()
// Rename the temporary config file to config.json
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
rErr = reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
if rErr != nil {
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
return return
} }
// serverMux (cmd/server-mux.go) implements graceful shutdown, // Reply to the client before restarting minio server.
// where all listeners are closed and process restart/shutdown writeSuccessResponseHeadersOnly(w)
// happens after 5s or completion of all ongoing http
// requests, whichever is earlier.
writeSetConfigResponse(w, globalAdminPeers, errs, true, r.URL)
// Restart all node for the modified config to take effect.
sendServiceCmd(globalAdminPeers, serviceRestart) sendServiceCmd(globalAdminPeers, serviceRestart)
} }
// ConfigCredsHandler - POST /minio/admin/v1/config/credential // UpdateCredsHandler - POST /minio/admin/v1/config/credential
// ---------- // ----------
// Update credentials in a minio server. In a distributed setup, // Update credentials in a minio server. In a distributed setup,
// update all the servers in the cluster. // update all the servers in the cluster.
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter, func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
r *http.Request) { r *http.Request) {
ctx := newContext(r, w, "UpdateCredentialsHandler")
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
return
}
// Avoid setting new credentials when they are already passed
// by the environment. Deny if WORM is enabled.
if globalIsEnvCreds {
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
return
}
// Authenticate request // Authenticate request
adminAPIErr := checkAdminRequestAuthType(r, "") adminAPIErr := checkAdminRequestAuthType(r, "")
if adminAPIErr != ErrNone { if adminAPIErr != ErrNone {
@@ -781,18 +602,32 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
return return
} }
// Avoid setting new credentials when they are already passed // Read configuration bytes from request body.
// by the environment. Deny if WORM is enabled. configBuf := make([]byte, maxConfigJSONSize+1)
if globalIsEnvCreds || globalWORMEnabled { n, err := io.ReadFull(r.Body, configBuf)
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) if err == nil {
// More than maxConfigSize bytes were available
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
return
}
if err != io.ErrUnexpectedEOF {
logger.LogIf(ctx, err)
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
return
}
password := globalServerConfig.GetCredential().SecretKey
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
if err != nil {
logger.LogIf(ctx, err)
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
return return
} }
// Decode request body // Decode request body
var req madmin.SetCredsReq var req madmin.SetCredsReq
err := json.NewDecoder(r.Body).Decode(&req) if err = json.Unmarshal(configBytes, &req); err != nil {
if err != nil { logger.LogIf(ctx, err)
logger.LogIf(context.Background(), err)
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL) writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
return return
} }
@@ -803,33 +638,25 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
return return
} }
// Take a lock on minio/config.json. Prevents concurrent
// config file updates.
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
if configLock.GetLock(globalObjectTimeout) != nil {
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
return
}
defer configLock.Unlock()
// Acquire lock before updating global configuration. // Acquire lock before updating global configuration.
globalServerConfigMu.Lock() globalServerConfigMu.Lock()
defer globalServerConfigMu.Unlock() defer globalServerConfigMu.Unlock()
// Update local credentials in memory. // Update local credentials in memory.
globalServerConfig.SetCredential(creds) globalServerConfig.SetCredential(creds)
if err = globalServerConfig.Save(getConfigFile()); err != nil {
writeErrorResponseJSON(w, ErrInternalError, r.URL) if err = saveServerConfig(objectAPI, globalServerConfig); err != nil {
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
return return
} }
// Notify all other Minio peers to update credentials // Notify all other Minio peers to update credentials
for host, err := range globalNotificationSys.SetCredentials(creds) { for host, err := range globalNotificationSys.LoadCredentials() {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String()) reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String())
ctx := logger.SetReqInfo(context.Background(), reqInfo) ctx := logger.SetReqInfo(ctx, reqInfo)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
// At this stage, the operation is successful, return 200 OK // Reply to the client before restarting minio server.
w.WriteHeader(http.StatusOK) writeSuccessResponseHeadersOnly(w)
} }
+115 -321
View File
@@ -26,7 +26,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -38,21 +37,39 @@ import (
var ( var (
configJSON = []byte(`{ configJSON = []byte(`{
"version": "13", "version": "28",
"credential": { "credential": {
"accessKey": "minio", "accessKey": "minio",
"secretKey": "minio123" "secretKey": "minio123"
}, },
"region": "us-west-1", "region": "",
"logger": { "browser": "on",
"console": { "worm": "off",
"enable": true, "domain": "",
"level": "fatal" "storageclass": {
}, "standard": "",
"file": { "rrs": ""
"enable": false, },
"fileName": "", "cache": {
"level": "" "drives": [],
"expiry": 90,
"maxuse": 80,
"exclude": []
},
"kms": {
"vault": {
"endpoint": "",
"auth": {
"type": "",
"approle": {
"id": "",
"secret": ""
}
},
"key-id": {
"name": "",
"version": 0
}
} }
}, },
"notify": { "notify": {
@@ -63,6 +80,7 @@ var (
"exchange": "", "exchange": "",
"routingKey": "", "routingKey": "",
"exchangeType": "", "exchangeType": "",
"deliveryMode": 0,
"mandatory": false, "mandatory": false,
"immediate": false, "immediate": false,
"durable": false, "durable": false,
@@ -71,6 +89,47 @@ var (
"autoDeleted": false "autoDeleted": false
} }
}, },
"elasticsearch": {
"1": {
"enable": false,
"format": "",
"url": "",
"index": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"mqtt": {
"1": {
"enable": false,
"broker": "",
"topic": "",
"qos": 0,
"clientId": "",
"username": "",
"password": "",
"reconnectInterval": 0,
"keepAliveInterval": 0
}
},
"mysql": {
"1": {
"enable": false,
"format": "",
"dsnString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"nats": { "nats": {
"1": { "1": {
"enable": false, "enable": false,
@@ -90,24 +149,10 @@ var (
} }
} }
}, },
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": { "postgresql": {
"1": { "1": {
"enable": false, "enable": false,
"format": "",
"connectionString": "", "connectionString": "",
"table": "", "table": "",
"host": "", "host": "",
@@ -117,11 +162,13 @@ var (
"database": "" "database": ""
} }
}, },
"kafka": { "redis": {
"1": { "1": {
"enable": false, "enable": false,
"brokers": null, "format": "",
"topic": "" "address": "",
"password": "",
"key": ""
} }
}, },
"webhook": { "webhook": {
@@ -130,8 +177,20 @@ var (
"endpoint": "" "endpoint": ""
} }
} }
} },
}`) "logger": {
"console": {
"enabled": true
},
"http": {
"1": {
"enabled": false,
"endpoint": "http://user:example@localhost:9001/api/endpoint"
}
}
}
}`)
) )
// adminXLTestBed - encapsulates subsystems that need to be setup for // adminXLTestBed - encapsulates subsystems that need to be setup for
@@ -149,17 +208,17 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
// reset global variables to start afresh. // reset global variables to start afresh.
resetTestGlobals() resetTestGlobals()
// Initialize minio server config.
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
return nil, err
}
// Initializing objectLayer for HealFormatHandler. // Initializing objectLayer for HealFormatHandler.
objLayer, xlDirs, xlErr := initTestXLObjLayer() objLayer, xlDirs, xlErr := initTestXLObjLayer()
if xlErr != nil { if xlErr != nil {
return nil, xlErr return nil, xlErr
} }
// Initialize minio server config.
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
return nil, err
}
// Initialize boot time // Initialize boot time
globalBootTime = UTCNow() globalBootTime = UTCNow()
@@ -176,10 +235,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
// Init global heal state // Init global heal state
initAllHealState(globalIsXL) initAllHealState(globalIsXL)
globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints) globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
if err != nil {
return nil, err
}
// Create new policy system. // Create new policy system.
globalPolicySys = NewPolicySys() globalPolicySys = NewPolicySys()
@@ -189,17 +245,15 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
registerAdminRouter(adminRouter) registerAdminRouter(adminRouter)
return &adminXLTestBed{ return &adminXLTestBed{
configPath: rootPath, xlDirs: xlDirs,
xlDirs: xlDirs, objLayer: objLayer,
objLayer: objLayer, router: adminRouter,
router: adminRouter,
}, nil }, nil
} }
// TearDown - method that resets the test bed for subsequent unit // TearDown - method that resets the test bed for subsequent unit
// tests to start afresh. // tests to start afresh.
func (atb *adminXLTestBed) TearDown() { func (atb *adminXLTestBed) TearDown() {
os.RemoveAll(atb.configPath)
removeRoots(atb.xlDirs) removeRoots(atb.xlDirs)
resetTestGlobals() resetTestGlobals()
} }
@@ -538,8 +592,14 @@ func TestServiceSetCreds(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("JSONify err: %v", err) t.Fatalf("JSONify err: %v", err)
} }
ebody, err := madmin.EncryptServerConfigData(credentials.SecretKey, body)
if err != nil {
t.Fatal(err)
}
// Construct setCreds request // Construct setCreds request
req, err := getServiceCmdRequest(setCreds, credentials, body) req, err := getServiceCmdRequest(setCreds, credentials, ebody)
if err != nil { if err != nil {
t.Fatalf("Failed to build service status request %v", err) t.Fatalf("Failed to build service status request %v", err)
} }
@@ -570,193 +630,6 @@ func TestServiceSetCreds(t *testing.T) {
} }
} }
// mkLockQueryVal - helper function to build lock query param.
func mkLockQueryVal(bucket, prefix, durationStr string) url.Values {
qVal := url.Values{}
qVal.Set(string(mgmtBucket), bucket)
qVal.Set(string(mgmtPrefix), prefix)
qVal.Set(string(mgmtLockOlderThan), durationStr)
return qVal
}
// Test for locks list management REST API.
func TestListLocksHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
testCases := []struct {
bucket string
prefix string
duration string
expectedStatus int
}{
// Test 1 - valid testcase
{
bucket: "mybucket",
prefix: "myobject",
duration: "1s",
expectedStatus: http.StatusOK,
},
// Test 2 - invalid duration
{
bucket: "mybucket",
prefix: "myprefix",
duration: "invalidDuration",
expectedStatus: http.StatusBadRequest,
},
// Test 3 - invalid bucket name
{
bucket: `invalid\\Bucket`,
prefix: "myprefix",
duration: "1h",
expectedStatus: http.StatusBadRequest,
},
// Test 4 - invalid prefix
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
duration: "1h",
expectedStatus: http.StatusBadRequest,
},
}
for i, test := range testCases {
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
req, err := newTestRequest("GET", "/minio/admin/v1/locks?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct list locks request - %v", i+1, err)
}
cred := globalServerConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign list locks request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.router.ServeHTTP(rec, req)
if test.expectedStatus != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
}
}
}
// Test for locks clear management REST API.
func TestClearLocksHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
testCases := []struct {
bucket string
prefix string
duration string
expectedStatus int
}{
// Test 1 - valid testcase
{
bucket: "mybucket",
prefix: "myobject",
duration: "1s",
expectedStatus: http.StatusOK,
},
// Test 2 - invalid duration
{
bucket: "mybucket",
prefix: "myprefix",
duration: "invalidDuration",
expectedStatus: http.StatusBadRequest,
},
// Test 3 - invalid bucket name
{
bucket: `invalid\\Bucket`,
prefix: "myprefix",
duration: "1h",
expectedStatus: http.StatusBadRequest,
},
// Test 4 - invalid prefix
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
duration: "1h",
expectedStatus: http.StatusBadRequest,
},
}
for i, test := range testCases {
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
req, err := newTestRequest("DELETE", "/minio/admin/v1/locks?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct clear locks request - %v", i+1, err)
}
cred := globalServerConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign clear locks request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.router.ServeHTTP(rec, req)
if test.expectedStatus != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
}
}
}
// Test for lock query param validation helper function.
func TestValidateLockQueryParams(t *testing.T) {
// reset globals.
// this is to make sure that the tests are not affected by modified globals.
resetTestGlobals()
// initialize NSLock.
initNSLock(false)
// Sample query values for test cases.
allValidVal := mkLockQueryVal("bucket", "prefix", "1s")
invalidBucketVal := mkLockQueryVal(`invalid\\Bucket`, "prefix", "1s")
invalidPrefixVal := mkLockQueryVal("bucket", `invalid\\Prefix`, "1s")
invalidOlderThanVal := mkLockQueryVal("bucket", "prefix", "invalidDuration")
testCases := []struct {
qVals url.Values
apiErr APIErrorCode
}{
{
qVals: invalidBucketVal,
apiErr: ErrInvalidBucketName,
},
{
qVals: invalidPrefixVal,
apiErr: ErrInvalidObjectName,
},
{
qVals: invalidOlderThanVal,
apiErr: ErrInvalidDuration,
},
{
qVals: allValidVal,
apiErr: ErrNone,
},
}
for i, test := range testCases {
_, _, _, apiErr := validateLockQueryParams(test.qVals)
if apiErr != test.apiErr {
t.Errorf("Test %d - Expected error %v but received %v", i+1, test.apiErr, apiErr)
}
}
}
// buildAdminRequest - helper function to build an admin API request. // buildAdminRequest - helper function to build an admin API request.
func buildAdminRequest(queryVal url.Values, method, path string, func buildAdminRequest(queryVal url.Values, method, path string,
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) { contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
@@ -826,8 +699,14 @@ func TestSetConfigHandler(t *testing.T) {
queryVal := url.Values{} queryVal := url.Values{}
queryVal.Set("config", "") queryVal.Set("config", "")
password := globalServerConfig.GetCredential().SecretKey
econfigJSON, err := madmin.EncryptServerConfigData(password, configJSON)
if err != nil {
t.Fatal(err)
}
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config", req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
int64(len(configJSON)), bytes.NewReader(configJSON)) int64(len(econfigJSON)), bytes.NewReader(econfigJSON))
if err != nil { if err != nil {
t.Fatalf("Failed to construct set-config object request - %v", err) t.Fatalf("Failed to construct set-config object request - %v", err)
} }
@@ -838,16 +717,6 @@ func TestSetConfigHandler(t *testing.T) {
t.Errorf("Expected to succeed but failed with %d", rec.Code) t.Errorf("Expected to succeed but failed with %d", rec.Code)
} }
result := setConfigResult{}
err = json.NewDecoder(rec.Body).Decode(&result)
if err != nil {
t.Fatalf("Failed to decode set config result json %v", err)
}
if !result.Status {
t.Error("Expected set-config to succeed, but failed")
}
// 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
@@ -870,7 +739,7 @@ func TestSetConfigHandler(t *testing.T) {
// Check that a config with duplicate keys in an object return // Check that a config with duplicate keys in an object return
// error. // error.
{ {
invalidCfg := append(configJSON[:len(configJSON)-1], []byte(`, "version": "15"}`)...) invalidCfg := append(econfigJSON[:len(econfigJSON)-1], []byte(`, "version": "15"}`)...)
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 {
@@ -968,81 +837,6 @@ func TestToAdminAPIErr(t *testing.T) {
} }
} }
func TestWriteSetConfigResponse(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootPath)
testCases := []struct {
status bool
errs []error
}{
// 1. all nodes returned success.
{
status: true,
errs: []error{nil, nil, nil, nil},
},
// 2. some nodes returned errors.
{
status: false,
errs: []error{errDiskNotFound, nil, errDiskAccessDenied, errFaultyDisk},
},
}
testPeers := []adminPeer{
{
addr: "localhost:9001",
},
{
addr: "localhost:9002",
},
{
addr: "localhost:9003",
},
{
addr: "localhost:9004",
},
}
testURL, err := url.Parse("http://dummy.com")
if err != nil {
t.Fatalf("Failed to parse a place-holder url")
}
var actualResult setConfigResult
for i, test := range testCases {
rec := httptest.NewRecorder()
writeSetConfigResponse(rec, testPeers, test.errs, test.status, testURL)
resp := rec.Result()
jsonBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Test %d: Failed to read response %v", i+1, err)
}
err = json.Unmarshal(jsonBytes, &actualResult)
if err != nil {
t.Fatalf("Test %d: Failed to unmarshal json %v", i+1, err)
}
if actualResult.Status != test.status {
t.Errorf("Test %d: Expected status %v but received %v", i+1, test.status, actualResult.Status)
}
for p, res := range actualResult.NodeResults {
if res.Name != testPeers[p].addr {
t.Errorf("Test %d: Expected node name %s but received %s", i+1, testPeers[p].addr, res.Name)
}
expectedErrMsg := fmt.Sprintf("%v", test.errs[p])
if res.ErrMsg != expectedErrMsg {
t.Errorf("Test %d: Expected error %s but received %s", i+1, expectedErrMsg, res.ErrMsg)
}
expectedErrSet := test.errs[p] != nil
if res.ErrSet != expectedErrSet {
t.Errorf("Test %d: Expected ErrSet %v but received %v", i+1, expectedErrSet, res.ErrSet)
}
}
}
}
func mkHealStartReq(t *testing.T, bucket, prefix string, func mkHealStartReq(t *testing.T, bucket, prefix string,
opts madmin.HealOpts) *http.Request { opts madmin.HealOpts) *http.Request {
+3 -10
View File
@@ -53,13 +53,6 @@ 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))
/// Lock operations
// List Locks
adminV1Router.Methods(http.MethodGet).Path("/locks").HandlerFunc(httpTraceAll(adminAPI.ListLocksHandler))
// Clear locks
adminV1Router.Methods(http.MethodDelete).Path("/locks").HandlerFunc(httpTraceAll(adminAPI.ClearLocksHandler))
/// Heal operations /// Heal operations
// Heal processing endpoint. // Heal processing endpoint.
@@ -70,9 +63,9 @@ func registerAdminRouter(router *mux.Router) {
/// Config operations /// Config operations
// Update credentials // Update credentials
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceAll(adminAPI.UpdateCredentialsHandler)) adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateCredentialsHandler))
// Get config // Get config
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceAll(adminAPI.GetConfigHandler)) adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
// Set config // Set config
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceAll(adminAPI.SetConfigHandler)) adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
} }
-274
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"net" "net"
"sort" "sort"
@@ -54,19 +53,6 @@ func (rpcClient *AdminRPCClient) ReInitFormat(dryRun bool) error {
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply) return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
} }
// ListLocks - Sends list locks command to remote server via RPC.
func (rpcClient *AdminRPCClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
args := ListLocksQuery{
Bucket: bucket,
Prefix: prefix,
Duration: duration,
}
var reply []VolumeLockInfo
err := rpcClient.Call(adminServiceName+".ListLocks", &args, &reply)
return reply, err
}
// ServerInfo - returns the server info of the server to which the RPC call is made. // ServerInfo - returns the server info of the server to which the RPC call is made.
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) { func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid) err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
@@ -82,29 +68,6 @@ func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
return reply, err return reply, err
} }
// WriteTmpConfig - writes config file content to a temporary file on a remote node.
func (rpcClient *AdminRPCClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
args := WriteConfigArgs{
TmpFileName: tmpFileName,
Buf: configBytes,
}
reply := VoidReply{}
err := rpcClient.Call(adminServiceName+".WriteTmpConfig", &args, &reply)
logger.LogIf(context.Background(), err)
return err
}
// CommitConfig - Move the new config in tmpFileName onto config.json on a remote node.
func (rpcClient *AdminRPCClient) CommitConfig(tmpFileName string) error {
args := CommitConfigArgs{FileName: tmpFileName}
reply := VoidReply{}
err := rpcClient.Call(adminServiceName+".CommitConfig", &args, &reply)
logger.LogIf(context.Background(), err)
return 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"
@@ -147,11 +110,8 @@ func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
type adminCmdRunner interface { type adminCmdRunner interface {
SignalService(s serviceSignal) error SignalService(s serviceSignal) error
ReInitFormat(dryRun bool) error ReInitFormat(dryRun bool) error
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
ServerInfo() (ServerInfoData, error) ServerInfo() (ServerInfoData, error)
GetConfig() ([]byte, error) GetConfig() ([]byte, error)
WriteTmpConfig(tmpFileName string, configBytes []byte) error
CommitConfig(tmpFileName string) error
} }
// adminPeer - represents an entity that implements admin API RPCs. // adminPeer - represents an entity that implements admin API RPCs.
@@ -244,56 +204,6 @@ func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
errs[0] = invokeServiceCmd(cps[0], cmd) errs[0] = invokeServiceCmd(cps[0], cmd)
} }
// listPeerLocksInfo - fetch list of locks held on the given bucket,
// matching prefix held longer than duration from all peer servers.
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
// Used to aggregate volume lock information from all nodes.
allLocks := make([][]VolumeLockInfo, len(peers))
errs := make([]error, len(peers))
var wg sync.WaitGroup
localPeer := peers[0]
remotePeers := peers[1:]
for i, remotePeer := range remotePeers {
wg.Add(1)
go func(idx int, remotePeer adminPeer) {
defer wg.Done()
// `remotePeers` is right-shifted by one position relative to `peers`
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, duration)
}(i+1, remotePeer)
}
wg.Wait()
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, duration)
// Summarizing errors received for ListLocks RPC across all
// nodes. N B the possible unavailability of quorum in errors
// applies only to distributed setup.
errCount, err := reduceErrs(errs, []error{})
if err != nil {
if errCount >= (len(peers)/2 + 1) {
return nil, err
}
return nil, InsufficientReadQuorum{}
}
// Group lock information across nodes by (bucket, object)
// pair. For readability only.
paramLockMap := make(map[nsParam][]VolumeLockInfo)
for _, nodeLocks := range allLocks {
for _, lockInfo := range nodeLocks {
param := nsParam{
volume: lockInfo.Bucket,
path: lockInfo.Object,
}
paramLockMap[param] = append(paramLockMap[param], lockInfo)
}
}
groupedLockInfos := []VolumeLockInfo{}
for _, volLocks := range paramLockMap {
groupedLockInfos = append(groupedLockInfos, volLocks...)
}
return groupedLockInfos, nil
}
// uptimeSlice - used to sort uptimes in chronological order. // uptimeSlice - used to sort uptimes in chronological order.
type uptimeSlice []struct { type uptimeSlice []struct {
err error err error
@@ -365,187 +275,3 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
return latestUptime, nil return latestUptime, nil
} }
// getPeerConfig - Fetches config.json from all nodes in the setup and
// returns the one that occurs in a majority of them.
func getPeerConfig(peers adminPeers) ([]byte, error) {
if !globalIsDistXL {
return peers[0].cmdRunner.GetConfig()
}
errs := make([]error, len(peers))
configs := make([][]byte, len(peers))
// Get config from all servers.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
}(i, peer)
}
wg.Wait()
// Find the maximally occurring config among peers in a
// distributed setup.
serverConfigs := make([]serverConfig, len(peers))
for i, configBytes := range configs {
if errs[i] != nil {
continue
}
// Unmarshal the received config files.
err := json.Unmarshal(configBytes, &serverConfigs[i])
if err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peers[i].addr)
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
return nil, err
}
}
configJSON, err := getValidServerConfig(serverConfigs, errs)
if err != nil {
logger.LogIf(context.Background(), err)
return nil, err
}
// Return the config.json that was present quorum or more
// number of disks.
return json.Marshal(configJSON)
}
// getValidServerConfig - finds the server config that is present in
// quorum or more number of servers.
func getValidServerConfig(serverConfigs []serverConfig, errs []error) (scv serverConfig, e error) {
// majority-based quorum
quorum := len(serverConfigs)/2 + 1
// Count the number of disks a config.json was found in.
configCounter := make([]int, len(serverConfigs))
// We group equal serverConfigs by the lowest index of the
// same value; e.g, let us take the following serverConfigs
// in a 4-node setup,
// serverConfigs == [c1, c2, c1, c1]
// configCounter == [3, 1, 0, 0]
// c1, c2 are the only distinct values that appear. c1 is
// identified by 0, the lowest index it appears in and c2 is
// identified by 1. So, we need to find the number of times
// each of these distinct values occur.
// Invariants:
// 1. At the beginning of the i-th iteration, the number of
// unique configurations seen so far is equal to the number of
// non-zero counter values in config[:i].
// 2. At the beginning of the i-th iteration, the sum of
// elements of configCounter[:i] is equal to the number of
// non-error configurations seen so far.
// For each of the serverConfig ...
for i := range serverConfigs {
// Skip nodes where getConfig failed.
if errs[i] != nil {
continue
}
// Check if it is equal to any of the configurations
// seen so far. If j == i is reached then we have an
// unseen configuration.
for j := 0; j <= i; j++ {
if j < i && configCounter[j] == 0 {
// serverConfigs[j] is known to be
// equal to a value that was already
// seen. See example above for
// clarity.
continue
} else if j < i && serverConfigs[i].ConfigDiff(&serverConfigs[j]) == "" {
// serverConfigs[i] is equal to
// serverConfigs[j], update
// serverConfigs[j]'s counter since it
// is the lower index.
configCounter[j]++
break
} else if j == i {
// serverConfigs[i] is equal to no
// other value seen before. It is
// unique so far.
configCounter[i] = 1
break
} // else invariants specified above are violated.
}
}
// We find the maximally occurring server config and check if
// there is quorum.
var configJSON serverConfig
maxOccurrence := 0
for i, count := range configCounter {
if maxOccurrence < count {
maxOccurrence = count
configJSON = serverConfigs[i]
}
}
// If quorum nodes don't agree.
if maxOccurrence < quorum {
return scv, errXLWriteQuorum
}
return configJSON, nil
}
// Write config contents into a temporary file on all nodes.
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
return []error{err}
}
errs := make([]error, len(peers))
// Write config into temporary file on all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
}(i, peer)
}
wg.Wait()
// Return bytes written and errors (if any) during writing
// temporary config file.
return errs
}
// Move config contents from the given temporary file onto config.json
// on all nodes.
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
}
errs := make([]error, len(peers))
// Rename temporary config file into configDir/config.json on
// all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
}(i, peer)
}
wg.Wait()
// Return errors (if any) received during rename.
return errs
}
-650
View File
@@ -1,650 +0,0 @@
/*
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"path"
"path/filepath"
"sort"
"sync"
"time"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/cmd/logger"
)
const (
// Admin service names
signalServiceRPC = "Admin.SignalService"
reInitFormatRPC = "Admin.ReInitFormat"
listLocksRPC = "Admin.ListLocks"
serverInfoDataRPC = "Admin.ServerInfoData"
getConfigRPC = "Admin.GetConfig"
writeTmpConfigRPC = "Admin.WriteTmpConfig"
commitConfigRPC = "Admin.CommitConfig"
)
// localAdminClient - represents admin operation to be executed locally.
type localAdminClient struct {
}
// remoteAdminClient - represents admin operation to be executed
// remotely, via RPC.
type remoteAdminClient struct {
*AuthRPCClient
}
// adminCmdRunner - abstracts local and remote execution of admin
// commands like service stop and service restart.
type adminCmdRunner interface {
SignalService(s serviceSignal) error
ReInitFormat(dryRun bool) error
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
ServerInfoData() (ServerInfoData, error)
GetConfig() ([]byte, error)
WriteTmpConfig(tmpFileName string, configBytes []byte) error
CommitConfig(tmpFileName string) error
}
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
// SignalService - sends a restart or stop signal to the local server
func (lc localAdminClient) SignalService(s serviceSignal) error {
switch s {
case serviceRestart, serviceStop:
globalServiceSignalCh <- s
default:
return errUnsupportedSignal
}
return nil
}
// ReInitFormat - re-initialize disk format.
func (lc localAdminClient) ReInitFormat(dryRun bool) error {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return errServerNotInitialized
}
return objectAPI.ReloadFormat(context.Background(), dryRun)
}
// ListLocks - Fetches lock information from local lock instrumentation.
func (lc localAdminClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
// check if objectLayer is initialized, if not return.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return nil, errServerNotInitialized
}
return objectAPI.ListLocks(context.Background(), bucket, prefix, duration)
}
func (rc remoteAdminClient) SignalService(s serviceSignal) (err error) {
switch s {
case serviceRestart, serviceStop:
reply := AuthRPCReply{}
err = rc.Call(signalServiceRPC, &SignalServiceArgs{Sig: s},
&reply)
default:
err = errUnsupportedSignal
}
return err
}
// ReInitFormat - re-initialize disk format, remotely.
func (rc remoteAdminClient) ReInitFormat(dryRun bool) error {
reply := AuthRPCReply{}
return rc.Call(reInitFormatRPC, &ReInitFormatArgs{
DryRun: dryRun,
}, &reply)
}
// ListLocks - Sends list locks command to remote server via RPC.
func (rc remoteAdminClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
listArgs := ListLocksQuery{
Bucket: bucket,
Prefix: prefix,
Duration: duration,
}
var reply ListLocksReply
if err := rc.Call(listLocksRPC, &listArgs, &reply); err != nil {
return nil, err
}
return reply.VolLocks, nil
}
// ServerInfoData - Returns the server info of this server.
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
if globalBootTime.IsZero() {
return sid, errServerNotInitialized
}
// Build storage info
objLayer := newObjectLayerFn()
if objLayer == nil {
return sid, errServerNotInitialized
}
storage := objLayer.StorageInfo(context.Background())
return ServerInfoData{
StorageInfo: storage,
ConnStats: globalConnStats.toServerConnStats(),
HTTPStats: globalHTTPStats.toServerHTTPStats(),
Properties: ServerProperties{
Uptime: UTCNow().Sub(globalBootTime),
Version: Version,
CommitID: CommitID,
SQSARN: globalNotificationSys.GetARNList(),
Region: globalServerConfig.GetRegion(),
},
}, nil
}
// ServerInfo - returns the server info of the server to which the RPC call is made.
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
args := AuthRPCArgs{}
reply := ServerInfoDataReply{}
err := rc.Call(serverInfoDataRPC, &args, &reply)
if err != nil {
return sid, err
}
return reply.ServerInfoData, nil
}
// GetConfig - returns config.json of the local server.
func (lc localAdminClient) GetConfig() ([]byte, error) {
if globalServerConfig == nil {
return nil, fmt.Errorf("config not present")
}
return json.Marshal(globalServerConfig)
}
// GetConfig - returns config.json of the remote server.
func (rc remoteAdminClient) GetConfig() ([]byte, error) {
args := AuthRPCArgs{}
reply := ConfigReply{}
if err := rc.Call(getConfigRPC, &args, &reply); err != nil {
return nil, err
}
return reply.Config, nil
}
// WriteTmpConfig - writes config file content to a temporary file on
// the local server.
func (lc localAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
return writeTmpConfigCommon(tmpFileName, configBytes)
}
// WriteTmpConfig - writes config file content to a temporary file on
// a remote node.
func (rc remoteAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
wArgs := WriteConfigArgs{
TmpFileName: tmpFileName,
Buf: configBytes,
}
err := rc.Call(writeTmpConfigRPC, &wArgs, &WriteConfigReply{})
if err != nil {
logger.LogIf(context.Background(), err)
return err
}
return nil
}
// CommitConfig - Move the new config in tmpFileName onto config.json
// on a local node.
func (lc localAdminClient) CommitConfig(tmpFileName string) error {
configFile := getConfigFile()
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
err := os.Rename(tmpConfigFile, configFile)
reqInfo := (&logger.ReqInfo{}).AppendTags("tmpConfigFile", tmpConfigFile)
reqInfo.AppendTags("configFile", configFile)
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
return err
}
// CommitConfig - Move the new config in tmpFileName onto config.json
// on a remote node.
func (rc remoteAdminClient) CommitConfig(tmpFileName string) error {
cArgs := CommitConfigArgs{
FileName: tmpFileName,
}
cReply := CommitConfigReply{}
err := rc.Call(commitConfigRPC, &cArgs, &cReply)
if err != nil {
logger.LogIf(context.Background(), err)
return err
}
return nil
}
// adminPeer - represents an entity that implements admin API RPCs.
type adminPeer struct {
addr string
cmdRunner adminCmdRunner
isLocal bool
}
// type alias for a collection of adminPeer.
type adminPeers []adminPeer
// makeAdminPeers - helper function to construct a collection of adminPeer.
func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
thisPeer := globalMinioAddr
if globalMinioHost == "" {
// When host is not explicitly provided simply
// use the first IPv4.
thisPeer = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
}
adminPeerList = append(adminPeerList, adminPeer{
thisPeer,
localAdminClient{},
true,
})
hostSet := set.CreateStringSet(globalMinioAddr)
cred := globalServerConfig.GetCredential()
serviceEndpoint := path.Join(minioReservedBucketPath, adminPath)
for _, host := range GetRemotePeers(endpoints) {
if hostSet.Contains(host) {
continue
}
hostSet.Add(host)
adminPeerList = append(adminPeerList, adminPeer{
addr: host,
cmdRunner: &remoteAdminClient{newAuthRPCClient(authConfig{
accessKey: cred.AccessKey,
secretKey: cred.SecretKey,
serverAddr: host,
serviceEndpoint: serviceEndpoint,
secureConn: globalIsSSL,
serviceName: "Admin",
})},
})
}
return adminPeerList
}
// peersReInitFormat - reinitialize remote object layers to new format.
func peersReInitFormat(peers adminPeers, dryRun bool) error {
errs := make([]error, len(peers))
// Send ReInitFormat RPC call to all nodes.
// for local adminPeer this is a no-op.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
if !peer.isLocal {
errs[idx] = peer.cmdRunner.ReInitFormat(dryRun)
}
}(i, peer)
}
wg.Wait()
return nil
}
// Initialize global adminPeer collection.
func initGlobalAdminPeers(endpoints EndpointList) {
globalAdminPeers = makeAdminPeers(endpoints)
}
// invokeServiceCmd - Invoke Restart/Stop command.
func invokeServiceCmd(cp adminPeer, cmd serviceSignal) (err error) {
switch cmd {
case serviceRestart, serviceStop:
err = cp.cmdRunner.SignalService(cmd)
}
return err
}
// sendServiceCmd - Invoke Restart command on remote peers
// adminPeer followed by on the local peer.
func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
// Send service command like stop or restart to all remote nodes and finally run on local node.
errs := make([]error, len(cps))
var wg sync.WaitGroup
remotePeers := cps[1:]
for i := range remotePeers {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// we use idx+1 because remotePeers slice is 1 position shifted w.r.t cps
errs[idx+1] = invokeServiceCmd(remotePeers[idx], cmd)
}(i)
}
wg.Wait()
errs[0] = invokeServiceCmd(cps[0], cmd)
}
// listPeerLocksInfo - fetch list of locks held on the given bucket,
// matching prefix held longer than duration from all peer servers.
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
// Used to aggregate volume lock information from all nodes.
allLocks := make([][]VolumeLockInfo, len(peers))
errs := make([]error, len(peers))
var wg sync.WaitGroup
localPeer := peers[0]
remotePeers := peers[1:]
for i, remotePeer := range remotePeers {
wg.Add(1)
go func(idx int, remotePeer adminPeer) {
defer wg.Done()
// `remotePeers` is right-shifted by one position relative to `peers`
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, duration)
}(i+1, remotePeer)
}
wg.Wait()
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, duration)
// Summarizing errors received for ListLocks RPC across all
// nodes. N B the possible unavailability of quorum in errors
// applies only to distributed setup.
errCount, err := reduceErrs(errs, []error{})
if err != nil {
if errCount >= (len(peers)/2 + 1) {
return nil, err
}
return nil, InsufficientReadQuorum{}
}
// Group lock information across nodes by (bucket, object)
// pair. For readability only.
paramLockMap := make(map[nsParam][]VolumeLockInfo)
for _, nodeLocks := range allLocks {
for _, lockInfo := range nodeLocks {
param := nsParam{
volume: lockInfo.Bucket,
path: lockInfo.Object,
}
paramLockMap[param] = append(paramLockMap[param], lockInfo)
}
}
groupedLockInfos := []VolumeLockInfo{}
for _, volLocks := range paramLockMap {
groupedLockInfos = append(groupedLockInfos, volLocks...)
}
return groupedLockInfos, nil
}
// uptimeSlice - used to sort uptimes in chronological order.
type uptimeSlice []struct {
err error
uptime time.Duration
}
func (ts uptimeSlice) Len() int {
return len(ts)
}
func (ts uptimeSlice) Less(i, j int) bool {
return ts[i].uptime < ts[j].uptime
}
func (ts uptimeSlice) Swap(i, j int) {
ts[i], ts[j] = ts[j], ts[i]
}
// getPeerUptimes - returns the uptime since the last time read quorum
// was established on success. Otherwise returns errXLReadQuorum.
func getPeerUptimes(peers adminPeers) (time.Duration, error) {
// In a single node Erasure or FS backend setup the uptime of
// the setup is the uptime of the single minio server
// instance.
if !globalIsDistXL {
return UTCNow().Sub(globalBootTime), nil
}
uptimes := make(uptimeSlice, len(peers))
// Get up time of all servers.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
serverInfoData, rpcErr := peer.cmdRunner.ServerInfoData()
uptimes[idx].uptime, uptimes[idx].err = serverInfoData.Properties.Uptime, rpcErr
}(i, peer)
}
wg.Wait()
// Sort uptimes in chronological order.
sort.Sort(uptimes)
// Pick the readQuorum'th uptime in chronological order. i.e,
// the time at which read quorum was (re-)established.
readQuorum := len(uptimes) / 2
validCount := 0
latestUptime := time.Duration(0)
for _, uptime := range uptimes {
if uptime.err != nil {
logger.LogIf(context.Background(), uptime.err)
continue
}
validCount++
if validCount >= readQuorum {
latestUptime = uptime.uptime
break
}
}
// Less than readQuorum "Admin.Uptime" RPC call returned
// successfully, so read-quorum unavailable.
if validCount < readQuorum {
return time.Duration(0), InsufficientReadQuorum{}
}
return latestUptime, nil
}
// getPeerConfig - Fetches config.json from all nodes in the setup and
// returns the one that occurs in a majority of them.
func getPeerConfig(peers adminPeers) ([]byte, error) {
if !globalIsDistXL {
return peers[0].cmdRunner.GetConfig()
}
errs := make([]error, len(peers))
configs := make([][]byte, len(peers))
// Get config from all servers.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
}(i, peer)
}
wg.Wait()
// Find the maximally occurring config among peers in a
// distributed setup.
serverConfigs := make([]serverConfig, len(peers))
for i, configBytes := range configs {
if errs[i] != nil {
continue
}
// Unmarshal the received config files.
err := json.Unmarshal(configBytes, &serverConfigs[i])
if err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peers[i].addr)
ctx := logger.SetReqInfo(context.Background(), reqInfo)
logger.LogIf(ctx, err)
return nil, err
}
}
configJSON, err := getValidServerConfig(serverConfigs, errs)
if err != nil {
logger.LogIf(context.Background(), err)
return nil, err
}
// Return the config.json that was present quorum or more
// number of disks.
return json.Marshal(configJSON)
}
// getValidServerConfig - finds the server config that is present in
// quorum or more number of servers.
func getValidServerConfig(serverConfigs []serverConfig, errs []error) (scv serverConfig, e error) {
// majority-based quorum
quorum := len(serverConfigs)/2 + 1
// Count the number of disks a config.json was found in.
configCounter := make([]int, len(serverConfigs))
// We group equal serverConfigs by the lowest index of the
// same value; e.g, let us take the following serverConfigs
// in a 4-node setup,
// serverConfigs == [c1, c2, c1, c1]
// configCounter == [3, 1, 0, 0]
// c1, c2 are the only distinct values that appear. c1 is
// identified by 0, the lowest index it appears in and c2 is
// identified by 1. So, we need to find the number of times
// each of these distinct values occur.
// Invariants:
// 1. At the beginning of the i-th iteration, the number of
// unique configurations seen so far is equal to the number of
// non-zero counter values in config[:i].
// 2. At the beginning of the i-th iteration, the sum of
// elements of configCounter[:i] is equal to the number of
// non-error configurations seen so far.
// For each of the serverConfig ...
for i := range serverConfigs {
// Skip nodes where getConfig failed.
if errs[i] != nil {
continue
}
// Check if it is equal to any of the configurations
// seen so far. If j == i is reached then we have an
// unseen configuration.
for j := 0; j <= i; j++ {
if j < i && configCounter[j] == 0 {
// serverConfigs[j] is known to be
// equal to a value that was already
// seen. See example above for
// clarity.
continue
} else if j < i && serverConfigs[i].ConfigDiff(&serverConfigs[j]) == "" {
// serverConfigs[i] is equal to
// serverConfigs[j], update
// serverConfigs[j]'s counter since it
// is the lower index.
configCounter[j]++
break
} else if j == i {
// serverConfigs[i] is equal to no
// other value seen before. It is
// unique so far.
configCounter[i] = 1
break
} // else invariants specified above are violated.
}
}
// We find the maximally occurring server config and check if
// there is quorum.
var configJSON serverConfig
maxOccurrence := 0
for i, count := range configCounter {
if maxOccurrence < count {
maxOccurrence = count
configJSON = serverConfigs[i]
}
}
// If quorum nodes don't agree.
if maxOccurrence < quorum {
return scv, errXLWriteQuorum
}
return configJSON, nil
}
// Write config contents into a temporary file on all nodes.
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
return []error{err}
}
errs := make([]error, len(peers))
// Write config into temporary file on all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
}(i, peer)
}
wg.Wait()
// Return bytes written and errors (if any) during writing
// temporary config file.
return errs
}
// Move config contents from the given temporary file onto config.json
// on all nodes.
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
}
errs := make([]error, len(peers))
// Rename temporary config file into configDir/config.json on
// all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
}(i, peer)
}
wg.Wait()
// Return errors (if any) received during rename.
return errs
}
+1 -41
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"context" "context"
"path" "path"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
@@ -47,20 +46,6 @@ func (receiver *adminRPCReceiver) SignalService(args *SignalServiceArgs, reply *
return receiver.local.SignalService(args.Sig) return receiver.local.SignalService(args.Sig)
} }
// ListLocksQuery - wraps ListLocks API's query values to send over RPC.
type ListLocksQuery struct {
AuthArgs
Bucket string
Prefix string
Duration time.Duration
}
// ListLocks - lists locks held by requests handled by this server instance.
func (receiver *adminRPCReceiver) ListLocks(args *ListLocksQuery, reply *[]VolumeLockInfo) (err error) {
*reply, err = receiver.local.ListLocks(args.Bucket, args.Prefix, args.Duration)
return err
}
// ServerInfo - returns the server info when object layer was initialized on this server. // ServerInfo - returns the server info when object layer was initialized on this server.
func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) { func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) {
*reply, err = receiver.local.ServerInfo() *reply, err = receiver.local.ServerInfo()
@@ -84,31 +69,6 @@ func (receiver *adminRPCReceiver) ReInitFormat(args *ReInitFormatArgs, reply *Vo
return receiver.local.ReInitFormat(args.DryRun) return receiver.local.ReInitFormat(args.DryRun)
} }
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
type WriteConfigArgs struct {
AuthArgs
TmpFileName string
Buf []byte
}
// WriteTmpConfig - writes the supplied config contents onto the
// supplied temporary file.
func (receiver *adminRPCReceiver) WriteTmpConfig(args *WriteConfigArgs, reply *VoidReply) error {
return receiver.local.WriteTmpConfig(args.TmpFileName, args.Buf)
}
// CommitConfigArgs - wraps the config file name that needs to be
// committed into config.json on this node.
type CommitConfigArgs struct {
AuthArgs
FileName string
}
// CommitConfig - Renames the temporary file into config.json on this node.
func (receiver *adminRPCReceiver) CommitConfig(args *CommitConfigArgs, reply *VoidReply) error {
return receiver.local.CommitConfig(args.FileName)
}
// NewAdminRPCServer - returns new admin RPC server. // NewAdminRPCServer - returns new admin RPC server.
func NewAdminRPCServer() (*xrpc.Server, error) { func NewAdminRPCServer() (*xrpc.Server, error) {
rpcServer := xrpc.NewServer() rpcServer := xrpc.NewServer()
@@ -123,5 +83,5 @@ 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", context.Background())
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter() subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
subrouter.Path(adminServiceSubPath).Handler(rpcServer) subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
} }
+2 -376
View File
@@ -17,13 +17,8 @@
package cmd package cmd
import ( import (
"encoding/json"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"path/filepath"
"reflect"
"testing" "testing"
"time" "time"
@@ -99,31 +94,6 @@ func testAdminCmdRunnerReInitFormat(t *testing.T, client adminCmdRunner) {
} }
} }
func testAdminCmdRunnerListLocks(t *testing.T, client adminCmdRunner) {
tmpGlobalObjectAPI := globalObjectAPI
defer func() {
globalObjectAPI = tmpGlobalObjectAPI
}()
testCases := []struct {
objectAPI ObjectLayer
expectErr bool
}{
{&DummyObjectLayer{}, false},
{nil, true},
}
for i, testCase := range testCases {
globalObjectAPI = testCase.objectAPI
_, err := client.ListLocks("", "", time.Duration(0))
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) { func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
tmpGlobalBootTime := globalBootTime tmpGlobalBootTime := globalBootTime
tmpGlobalObjectAPI := globalObjectAPI tmpGlobalObjectAPI := globalObjectAPI
@@ -139,10 +109,8 @@ func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
}() }()
endpoints := new(EndpointList) endpoints := new(EndpointList)
notificationSys, err := NewNotificationSys(globalServerConfig, *endpoints)
if err != nil { notificationSys := NewNotificationSys(globalServerConfig, *endpoints)
t.Fatalf("unexpected error %v", err)
}
testCases := []struct { testCases := []struct {
bootTime time.Time bootTime time.Time
@@ -199,81 +167,6 @@ func testAdminCmdRunnerGetConfig(t *testing.T, client adminCmdRunner) {
} }
} }
func testAdminCmdRunnerWriteTmpConfig(t *testing.T, client adminCmdRunner) {
tmpConfigDir := configDir
defer func() {
configDir = tmpConfigDir
}()
tempDir, err := ioutil.TempDir("", ".AdminCmdRunnerWriteTmpConfig.")
if err != nil {
t.Fatalf("unexpected error %v", err)
}
defer os.RemoveAll(tempDir)
configDir = &ConfigDir{dir: tempDir}
testCases := []struct {
tmpFilename string
configBytes []byte
expectErr bool
}{
{"config1.json", []byte(`{"version":"23","region":"us-west-1a"}`), false},
// Overwrite test.
{"config1.json", []byte(`{"version":"23","region":"us-west-1a","browser":"on"}`), false},
{"config2.json", []byte{}, false},
{"config3.json", nil, false},
}
for i, testCase := range testCases {
err := client.WriteTmpConfig(testCase.tmpFilename, testCase.configBytes)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func testAdminCmdRunnerCommitConfig(t *testing.T, client adminCmdRunner) {
tmpConfigDir := configDir
defer func() {
configDir = tmpConfigDir
}()
tempDir, err := ioutil.TempDir("", ".AdminCmdRunnerCommitConfig.")
if err != nil {
t.Fatalf("unexpected error %v", err)
}
defer os.RemoveAll(tempDir)
configDir = &ConfigDir{dir: tempDir}
err = ioutil.WriteFile(filepath.Join(tempDir, "config.json"), []byte{}, os.ModePerm)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
err = client.WriteTmpConfig("config1.json", []byte(`{"version":"23","region":"us-west-1a"}`))
if err != nil {
t.Fatalf("unexpected error %v", err)
}
testCases := []struct {
tmpFilename string
expectErr bool
}{
{"config1.json", false},
{"config2.json", true},
}
for i, testCase := range testCases {
err := client.CommitConfig(testCase.tmpFilename)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) { func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
rpcServer, err := NewAdminRPCServer() rpcServer, err := NewAdminRPCServer()
if err != nil { if err != nil {
@@ -325,16 +218,6 @@ func TestAdminRPCClientReInitFormat(t *testing.T) {
testAdminCmdRunnerReInitFormat(t, rpcClient) testAdminCmdRunnerReInitFormat(t, rpcClient)
} }
func TestAdminRPCClientListLocks(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close()
defer func() {
globalServerConfig = prevGlobalServerConfig
}()
testAdminCmdRunnerListLocks(t, rpcClient)
}
func TestAdminRPCClientServerInfo(t *testing.T) { func TestAdminRPCClientServerInfo(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t) httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close() defer httpServer.Close()
@@ -354,260 +237,3 @@ func TestAdminRPCClientGetConfig(t *testing.T) {
testAdminCmdRunnerGetConfig(t, rpcClient) testAdminCmdRunnerGetConfig(t, rpcClient)
} }
func TestAdminRPCClientWriteTmpConfig(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close()
defer func() {
globalServerConfig = prevGlobalServerConfig
}()
testAdminCmdRunnerWriteTmpConfig(t, rpcClient)
}
func TestAdminRPCClientCommitConfig(t *testing.T) {
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
defer httpServer.Close()
defer func() {
globalServerConfig = prevGlobalServerConfig
}()
testAdminCmdRunnerCommitConfig(t, rpcClient)
}
var (
config1 = []byte(`{
"version": "13",
"credential": {
"accessKey": "minio",
"secretKey": "minio123"
},
"region": "us-east-1",
"logger": {
"console": {
"enable": true,
"level": "debug"
},
"file": {
"enable": false,
"fileName": "",
"level": ""
}
},
"notify": {
"amqp": {
"1": {
"enable": false,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"nats": {
"1": {
"enable": false,
"address": "",
"subject": "",
"username": "",
"password": "",
"token": "",
"secure": false,
"pingInterval": 0,
"streaming": {
"enable": false,
"clusterID": "",
"clientID": "",
"async": false,
"maxPubAcksInflight": 0
}
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": {
"1": {
"enable": false,
"connectionString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"webhook": {
"1": {
"enable": false,
"endpoint": ""
}
}
}
}
`)
// diff from config1 - amqp.Enable is True
config2 = []byte(`{
"version": "13",
"credential": {
"accessKey": "minio",
"secretKey": "minio123"
},
"region": "us-east-1",
"logger": {
"console": {
"enable": true,
"level": "debug"
},
"file": {
"enable": false,
"fileName": "",
"level": ""
}
},
"notify": {
"amqp": {
"1": {
"enable": true,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"nats": {
"1": {
"enable": false,
"address": "",
"subject": "",
"username": "",
"password": "",
"token": "",
"secure": false,
"pingInterval": 0,
"streaming": {
"enable": false,
"clusterID": "",
"clientID": "",
"async": false,
"maxPubAcksInflight": 0
}
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": {
"1": {
"enable": false,
"connectionString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"webhook": {
"1": {
"enable": false,
"endpoint": ""
}
}
}
}
`)
)
// TestGetValidServerConfig - test for getValidServerConfig.
func TestGetValidServerConfig(t *testing.T) {
var c1, c2 serverConfig
err := json.Unmarshal(config1, &c1)
if err != nil {
t.Fatalf("json unmarshal of %s failed: %v", string(config1), err)
}
err = json.Unmarshal(config2, &c2)
if err != nil {
t.Fatalf("json unmarshal of %s failed: %v", string(config2), err)
}
// Valid config.
noErrs := []error{nil, nil, nil, nil}
serverConfigs := []serverConfig{c1, c2, c1, c1}
validConfig, err := getValidServerConfig(serverConfigs, noErrs)
if err != nil {
t.Errorf("Expected a valid config but received %v instead", err)
}
if !reflect.DeepEqual(validConfig, c1) {
t.Errorf("Expected valid config to be %v but received %v", config1, validConfig)
}
// Invalid config - no quorum.
serverConfigs = []serverConfig{c1, c2, c2, c1}
_, err = getValidServerConfig(serverConfigs, noErrs)
if err != errXLWriteQuorum {
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
}
// All errors
allErrs := []error{errDiskNotFound, errDiskNotFound, errDiskNotFound, errDiskNotFound}
serverConfigs = []serverConfig{{}, {}, {}, {}}
_, err = getValidServerConfig(serverConfigs, allErrs)
if err != errXLWriteQuorum {
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
}
}
+81
View File
@@ -25,6 +25,87 @@ 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"`
+730 -24
View File
@@ -22,10 +22,12 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/coreos/etcd/client" "github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/dns"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/s3select"
) )
// APIError structure // APIError structure
@@ -40,8 +42,8 @@ type APIErrorResponse struct {
XMLName xml.Name `xml:"Error" json:"-"` XMLName xml.Name `xml:"Error" json:"-"`
Code string Code string
Message string Message string
Key string Key string `xml:"Key,omitempty" json:"Key,omitempty"`
BucketName string BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"`
Resource string Resource string
RequestID string `xml:"RequestId" json:"RequestId"` RequestID string `xml:"RequestId" json:"RequestId"`
HostID string `xml:"HostId" json:"HostId"` HostID string `xml:"HostId" json:"HostId"`
@@ -129,6 +131,9 @@ const (
ErrInvalidPrefixMarker ErrInvalidPrefixMarker
// Add new error codes here. // Add new error codes here.
// SSE-S3 related API errors
ErrInvalidEncryptionMethod
// Server-Side-Encryption (with Customer provided key) related API errors. // Server-Side-Encryption (with Customer provided key) related API errors.
ErrInsecureSSECustomerRequest ErrInsecureSSECustomerRequest
ErrSSEMultipartEncrypted ErrSSEMultipartEncrypted
@@ -140,6 +145,9 @@ const (
ErrMissingSSECustomerKeyMD5 ErrMissingSSECustomerKeyMD5
ErrSSECustomerKeyMD5Mismatch ErrSSECustomerKeyMD5Mismatch
ErrInvalidSSECustomerParameters ErrInvalidSSECustomerParameters
ErrIncompatibleEncryptionMethod
ErrKMSNotConfigured
ErrKMSAuthFailure
// Bucket notification related errors. // Bucket notification related errors.
ErrEventNotification ErrEventNotification
@@ -169,7 +177,6 @@ const (
ErrInvalidResourceName ErrInvalidResourceName
ErrServerNotInitialized ErrServerNotInitialized
ErrOperationTimedOut ErrOperationTimedOut
ErrPartsSizeUnequal
ErrInvalidRequest ErrInvalidRequest
// Minio storage class error codes // Minio storage class error codes
ErrInvalidStorageClass ErrInvalidStorageClass
@@ -187,6 +194,7 @@ const (
ErrAdminCredentialsMismatch ErrAdminCredentialsMismatch
ErrInsecureClientRequest ErrInsecureClientRequest
ErrObjectTampered ErrObjectTampered
ErrHealNotImplemented ErrHealNotImplemented
ErrHealNoSuchProcess ErrHealNoSuchProcess
ErrHealInvalidClientToken ErrHealInvalidClientToken
@@ -194,6 +202,93 @@ const (
ErrHealAlreadyRunning ErrHealAlreadyRunning
ErrHealOverlappingPaths ErrHealOverlappingPaths
ErrIncorrectContinuationToken ErrIncorrectContinuationToken
//S3 Select Errors
ErrEmptyRequestBody
ErrUnsupportedFunction
ErrInvalidExpressionType
ErrBusy
ErrUnauthorizedAccess
ErrExpressionTooLong
ErrIllegalSQLFunctionArgument
ErrInvalidKeyPath
ErrInvalidCompressionFormat
ErrInvalidFileHeaderInfo
ErrInvalidJSONType
ErrInvalidQuoteFields
ErrInvalidRequestParameter
ErrInvalidDataType
ErrInvalidTextEncoding
ErrInvalidDataSource
ErrInvalidTableAlias
ErrMissingRequiredParameter
ErrObjectSerializationConflict
ErrUnsupportedSQLOperation
ErrUnsupportedSQLStructure
ErrUnsupportedSyntax
ErrUnsupportedRangeHeader
ErrLexerInvalidChar
ErrLexerInvalidOperator
ErrLexerInvalidLiteral
ErrLexerInvalidIONLiteral
ErrParseExpectedDatePart
ErrParseExpectedKeyword
ErrParseExpectedTokenType
ErrParseExpected2TokenTypes
ErrParseExpectedNumber
ErrParseExpectedRightParenBuiltinFunctionCall
ErrParseExpectedTypeName
ErrParseExpectedWhenClause
ErrParseUnsupportedToken
ErrParseUnsupportedLiteralsGroupBy
ErrParseExpectedMember
ErrParseUnsupportedSelect
ErrParseUnsupportedCase
ErrParseUnsupportedCaseClause
ErrParseUnsupportedAlias
ErrParseUnsupportedSyntax
ErrParseUnknownOperator
ErrParseInvalidPathComponent
ErrParseMissingIdentAfterAt
ErrParseUnexpectedOperator
ErrParseUnexpectedTerm
ErrParseUnexpectedToken
ErrParseUnexpectedKeyword
ErrParseExpectedExpression
ErrParseExpectedLeftParenAfterCast
ErrParseExpectedLeftParenValueConstructor
ErrParseExpectedLeftParenBuiltinFunctionCall
ErrParseExpectedArgumentDelimiter
ErrParseCastArity
ErrParseInvalidTypeParam
ErrParseEmptySelect
ErrParseSelectMissingFrom
ErrParseExpectedIdentForGroupName
ErrParseExpectedIdentForAlias
ErrParseUnsupportedCallWithStar
ErrParseNonUnaryAgregateFunctionCall
ErrParseMalformedJoin
ErrParseExpectedIdentForAt
ErrParseAsteriskIsNotAloneInSelectList
ErrParseCannotMixSqbAndWildcardInSelectList
ErrParseInvalidContextForWildcardInSelectList
ErrIncorrectSQLFunctionArgumentType
ErrValueParseFailure
ErrEvaluatorInvalidArguments
ErrIntegerOverflow
ErrLikeInvalidInputs
ErrCastFailed
ErrInvalidCast
ErrEvaluatorInvalidTimestampFormatPattern
ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
ErrEvaluatorTimestampFormatPatternDuplicateFields
ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
ErrEvaluatorUnterminatedTimestampFormatPatternToken
ErrEvaluatorInvalidTimestampFormatPatternToken
ErrEvaluatorInvalidTimestampFormatPatternSymbol
ErrEvaluatorBindingDoesNotExist
ErrInvalidColumnIndex
ErrMissingHeaders
) )
// error code to APIError structure, these fields carry respective // error code to APIError structure, these fields carry respective
@@ -630,6 +725,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Your metadata headers exceed the maximum allowed metadata size.", Description: "Your metadata headers exceed the maximum allowed metadata size.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidEncryptionMethod: {
Code: "InvalidRequest",
Description: "The encryption method specified is not supported",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInsecureSSECustomerRequest: { ErrInsecureSSECustomerRequest: {
Code: "InvalidRequest", Code: "InvalidRequest",
Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.", Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.",
@@ -680,6 +780,21 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "The provided encryption parameters did not match the ones used originally.", Description: "The provided encryption parameters did not match the ones used originally.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrIncompatibleEncryptionMethod: {
Code: "InvalidArgument",
Description: "Server side encryption specified with both SSE-C and SSE-S3 headers",
HTTPStatusCode: http.StatusBadRequest,
},
ErrKMSNotConfigured: {
Code: "InvalidArgument",
Description: "Server side encryption specified but KMS is not configured",
HTTPStatusCode: http.StatusBadRequest,
},
ErrKMSAuthFailure: {
Code: "InvalidArgument",
Description: "Server side encryption specified but KMS authorization failed",
HTTPStatusCode: http.StatusBadRequest,
},
/// S3 extensions. /// S3 extensions.
ErrContentSHA256Mismatch: { ErrContentSHA256Mismatch: {
@@ -692,7 +807,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
ErrStorageFull: { ErrStorageFull: {
Code: "XMinioStorageFull", Code: "XMinioStorageFull",
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.", Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
HTTPStatusCode: http.StatusInternalServerError, HTTPStatusCode: http.StatusInsufficientStorage,
}, },
ErrRequestBodyParse: { ErrRequestBodyParse: {
Code: "XMinioRequestBodyParse", Code: "XMinioRequestBodyParse",
@@ -785,11 +900,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Your metadata headers are not supported.", Description: "Your metadata headers are not supported.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrPartsSizeUnequal: {
Code: "XMinioPartsSizeUnequal",
Description: "All parts except the last part should be of the same size.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectTampered: { ErrObjectTampered: {
Code: "XMinioObjectTampered", Code: "XMinioObjectTampered",
Description: errObjectTampered.Error(), Description: errObjectTampered.Error(),
@@ -800,6 +910,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds", Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
// Generic Invalid-Request error. Should be used for response errors only for unlikely // Generic Invalid-Request error. Should be used for response errors only for unlikely
// corner case errors for which introducing new APIErrorCode is not worth it. LogIf() // corner case errors for which introducing new APIErrorCode is not worth it. LogIf()
// should be used to log the error at the source of the error for debugging purposes. // should be used to log the error at the source of the error for debugging purposes.
@@ -848,6 +959,432 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "The continuation token provided is incorrect", Description: "The continuation token provided is incorrect",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
//S3 Select API Errors
ErrEmptyRequestBody: {
Code: "EmptyRequestBody",
Description: "Request body cannot be empty.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedFunction: {
Code: "UnsupportedFunction",
Description: "Encountered an unsupported SQL function.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidDataSource: {
Code: "InvalidDataSource",
Description: "Invalid data source type. Only CSV and JSON are supported at this time.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidExpressionType: {
Code: "InvalidExpressionType",
Description: "The ExpressionType is invalid. Only SQL expressions are supported at this time.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrBusy: {
Code: "Busy",
Description: "The service is unavailable. Please retry.",
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrUnauthorizedAccess: {
Code: "UnauthorizedAccess",
Description: "You are not authorized to perform this operation",
HTTPStatusCode: http.StatusUnauthorized,
},
ErrExpressionTooLong: {
Code: "ExpressionTooLong",
Description: "The SQL expression is too long: The maximum byte-length for the SQL expression is 256 KB.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrIllegalSQLFunctionArgument: {
Code: "IllegalSqlFunctionArgument",
Description: "Illegal argument was used in the SQL function.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidKeyPath: {
Code: "InvalidKeyPath",
Description: "Key path in the SQL expression is invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCompressionFormat: {
Code: "InvalidCompressionFormat",
Description: "The file is not in a supported compression format. Only GZIP is supported at this time.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidFileHeaderInfo: {
Code: "InvalidFileHeaderInfo",
Description: "The FileHeaderInfo is invalid. Only NONE, USE, and IGNORE are supported.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidJSONType: {
Code: "InvalidJsonType",
Description: "The JsonType is invalid. Only DOCUMENT and LINES are supported at this time.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidQuoteFields: {
Code: "InvalidQuoteFields",
Description: "The QuoteFields is invalid. Only ALWAYS and ASNEEDED are supported.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidRequestParameter: {
Code: "InvalidRequestParameter",
Description: "The value of a parameter in SelectRequest element is invalid. Check the service API documentation and try again.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidDataType: {
Code: "InvalidDataType",
Description: "The SQL expression contains an invalid data type.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidTextEncoding: {
Code: "InvalidTextEncoding",
Description: "Invalid encoding type. Only UTF-8 encoding is supported at this time.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidTableAlias: {
Code: "InvalidTableAlias",
Description: "The SQL expression contains an invalid table alias.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingRequiredParameter: {
Code: "MissingRequiredParameter",
Description: "The SelectRequest entity is missing a required parameter. Check the service documentation and try again.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectSerializationConflict: {
Code: "ObjectSerializationConflict",
Description: "The SelectRequest entity can only contain one of CSV or JSON. Check the service documentation and try again.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedSQLOperation: {
Code: "UnsupportedSqlOperation",
Description: "Encountered an unsupported SQL operation.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedSQLStructure: {
Code: "UnsupportedSqlStructure",
Description: "Encountered an unsupported SQL structure. Check the SQL Reference.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedSyntax: {
Code: "UnsupportedSyntax",
Description: "Encountered invalid syntax.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedRangeHeader: {
Code: "UnsupportedRangeHeader",
Description: "Range header is not supported for this operation.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrLexerInvalidChar: {
Code: "LexerInvalidChar",
Description: "The SQL expression contains an invalid character.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrLexerInvalidOperator: {
Code: "LexerInvalidOperator",
Description: "The SQL expression contains an invalid literal.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrLexerInvalidLiteral: {
Code: "LexerInvalidLiteral",
Description: "The SQL expression contains an invalid operator.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrLexerInvalidIONLiteral: {
Code: "LexerInvalidIONLiteral",
Description: "The SQL expression contains an invalid operator.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedDatePart: {
Code: "ParseExpectedDatePart",
Description: "Did not find the expected date part in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedKeyword: {
Code: "ParseExpectedKeyword",
Description: "Did not find the expected keyword in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedTokenType: {
Code: "ParseExpectedTokenType",
Description: "Did not find the expected token in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpected2TokenTypes: {
Code: "ParseExpected2TokenTypes",
Description: "Did not find the expected token in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedNumber: {
Code: "ParseExpectedNumber",
Description: "Did not find the expected number in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedRightParenBuiltinFunctionCall: {
Code: "ParseExpectedRightParenBuiltinFunctionCall",
Description: "Did not find the expected right parenthesis character in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedTypeName: {
Code: "ParseExpectedTypeName",
Description: "Did not find the expected type name in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedWhenClause: {
Code: "ParseExpectedWhenClause",
Description: "Did not find the expected WHEN clause in the SQL expression. CASE is not supported.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedToken: {
Code: "ParseUnsupportedToken",
Description: "The SQL expression contains an unsupported token.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedLiteralsGroupBy: {
Code: "ParseUnsupportedLiteralsGroupBy",
Description: "The SQL expression contains an unsupported use of GROUP BY.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedMember: {
Code: "ParseExpectedMember",
Description: "The SQL expression contains an unsupported use of MEMBER.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedSelect: {
Code: "ParseUnsupportedSelect",
Description: "The SQL expression contains an unsupported use of SELECT.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedCase: {
Code: "ParseUnsupportedCase",
Description: "The SQL expression contains an unsupported use of CASE.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedCaseClause: {
Code: "ParseUnsupportedCaseClause",
Description: "The SQL expression contains an unsupported use of CASE.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedAlias: {
Code: "ParseUnsupportedAlias",
Description: "The SQL expression contains an unsupported use of ALIAS.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedSyntax: {
Code: "ParseUnsupportedSyntax",
Description: "The SQL expression contains unsupported syntax.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnknownOperator: {
Code: "ParseUnknownOperator",
Description: "The SQL expression contains an invalid operator.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseInvalidPathComponent: {
Code: "ParseInvalidPathComponent",
Description: "The SQL expression contains an invalid path component.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseMissingIdentAfterAt: {
Code: "ParseMissingIdentAfterAt",
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnexpectedOperator: {
Code: "ParseUnexpectedOperator",
Description: "The SQL expression contains an unexpected operator.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnexpectedTerm: {
Code: "ParseUnexpectedTerm",
Description: "The SQL expression contains an unexpected term.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnexpectedToken: {
Code: "ParseUnexpectedToken",
Description: "The SQL expression contains an unexpected token.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnexpectedKeyword: {
Code: "ParseUnexpectedKeyword",
Description: "The SQL expression contains an unexpected keyword.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedExpression: {
Code: "ParseExpectedExpression",
Description: "Did not find the expected SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedLeftParenAfterCast: {
Code: "ParseExpectedLeftParenAfterCast",
Description: "Did not find expected the left parenthesis in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedLeftParenValueConstructor: {
Code: "ParseExpectedLeftParenValueConstructor",
Description: "Did not find expected the left parenthesis in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedLeftParenBuiltinFunctionCall: {
Code: "ParseExpectedLeftParenBuiltinFunctionCall",
Description: "Did not find the expected left parenthesis in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedArgumentDelimiter: {
Code: "ParseExpectedArgumentDelimiter",
Description: "Did not find the expected argument delimiter in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseCastArity: {
Code: "ParseCastArity",
Description: "The SQL expression CAST has incorrect arity.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseInvalidTypeParam: {
Code: "ParseInvalidTypeParam",
Description: "The SQL expression contains an invalid parameter value.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseEmptySelect: {
Code: "ParseEmptySelect",
Description: "The SQL expression contains an empty SELECT.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseSelectMissingFrom: {
Code: "ParseSelectMissingFrom",
Description: "GROUP is not supported in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedIdentForGroupName: {
Code: "ParseExpectedIdentForGroupName",
Description: "GROUP is not supported in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedIdentForAlias: {
Code: "ParseExpectedIdentForAlias",
Description: "Did not find the expected identifier for the alias in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseUnsupportedCallWithStar: {
Code: "ParseUnsupportedCallWithStar",
Description: "Only COUNT with (*) as a parameter is supported in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseNonUnaryAgregateFunctionCall: {
Code: "ParseNonUnaryAgregateFunctionCall",
Description: "Only one argument is supported for aggregate functions in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseMalformedJoin: {
Code: "ParseMalformedJoin",
Description: "JOIN is not supported in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseExpectedIdentForAt: {
Code: "ParseExpectedIdentForAt",
Description: "Did not find the expected identifier for AT name in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseAsteriskIsNotAloneInSelectList: {
Code: "ParseAsteriskIsNotAloneInSelectList",
Description: "Other expressions are not allowed in the SELECT list when '*' is used without dot notation in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseCannotMixSqbAndWildcardInSelectList: {
Code: "ParseCannotMixSqbAndWildcardInSelectList",
Description: "Cannot mix [] and * in the same expression in a SELECT list in SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrParseInvalidContextForWildcardInSelectList: {
Code: "ParseInvalidContextForWildcardInSelectList",
Description: "Invalid use of * in SELECT list in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrIncorrectSQLFunctionArgumentType: {
Code: "IncorrectSqlFunctionArgumentType",
Description: "Incorrect type of arguments in function call in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrValueParseFailure: {
Code: "ValueParseFailure",
Description: "Time stamp parse failure in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorInvalidArguments: {
Code: "EvaluatorInvalidArguments",
Description: "Incorrect number of arguments in the function call in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrIntegerOverflow: {
Code: "IntegerOverflow",
Description: "Int overflow or underflow in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrLikeInvalidInputs: {
Code: "LikeInvalidInputs",
Description: "Invalid argument given to the LIKE clause in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrCastFailed: {
Code: "CastFailed",
Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCast: {
Code: "InvalidCast",
Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorInvalidTimestampFormatPattern: {
Code: "EvaluatorInvalidTimestampFormatPattern",
Description: "Time stamp format pattern requires additional fields in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing: {
Code: "EvaluatorInvalidTimestampFormatPatternSymbolForParsing",
Description: "Time stamp format pattern contains a valid format symbol that cannot be applied to time stamp parsing in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorTimestampFormatPatternDuplicateFields: {
Code: "EvaluatorTimestampFormatPatternDuplicateFields",
Description: "Time stamp format pattern contains multiple format specifiers representing the time stamp field in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch: {
Code: "EvaluatorUnterminatedTimestampFormatPatternToken",
Description: "Time stamp format pattern contains unterminated token in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorUnterminatedTimestampFormatPatternToken: {
Code: "EvaluatorInvalidTimestampFormatPatternToken",
Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorInvalidTimestampFormatPatternToken: {
Code: "EvaluatorInvalidTimestampFormatPatternToken",
Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorInvalidTimestampFormatPatternSymbol: {
Code: "EvaluatorInvalidTimestampFormatPatternSymbol",
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidColumnIndex: {
Code: "InvalidColumnIndex",
Description: "Column index in the SQL expression is invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrEvaluatorBindingDoesNotExist: {
Code: "ErrEvaluatorBindingDoesNotExist",
Description: "A column name or a path provided does not exist in the SQL expression",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingHeaders: {
Code: "MissingHeaders",
Description: "Some headers in the query are missing from the file. Check the file and try again.",
HTTPStatusCode: http.StatusBadRequest,
},
// Add your error structure here. // Add your error structure here.
} }
@@ -872,17 +1409,19 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
case auth.ErrInvalidSecretKeyLength: case auth.ErrInvalidSecretKeyLength:
apiErr = ErrAdminInvalidSecretKey apiErr = ErrAdminInvalidSecretKey
// SSE errors // SSE errors
case crypto.ErrInvalidEncryptionMethod:
apiErr = ErrInvalidEncryptionMethod
case errInsecureSSERequest: case errInsecureSSERequest:
apiErr = ErrInsecureSSECustomerRequest apiErr = ErrInsecureSSECustomerRequest
case errInvalidSSEAlgorithm: case crypto.ErrInvalidCustomerAlgorithm:
apiErr = ErrInvalidSSECustomerAlgorithm apiErr = ErrInvalidSSECustomerAlgorithm
case errInvalidSSEKey: case crypto.ErrInvalidCustomerKey:
apiErr = ErrInvalidSSECustomerKey apiErr = ErrInvalidSSECustomerKey
case errMissingSSEKey: case crypto.ErrMissingCustomerKey:
apiErr = ErrMissingSSECustomerKey apiErr = ErrMissingSSECustomerKey
case errMissingSSEKeyMD5: case crypto.ErrMissingCustomerKeyMD5:
apiErr = ErrMissingSSECustomerKeyMD5 apiErr = ErrMissingSSECustomerKeyMD5
case errSSEKeyMD5Mismatch: case crypto.ErrCustomerKeyMD5Mismatch:
apiErr = ErrSSECustomerKeyMD5Mismatch apiErr = ErrSSECustomerKeyMD5Mismatch
case errObjectTampered: case errObjectTampered:
apiErr = ErrObjectTampered apiErr = ErrObjectTampered
@@ -890,11 +1429,180 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrSSEEncryptedObject apiErr = ErrSSEEncryptedObject
case errInvalidSSEParameters: case errInvalidSSEParameters:
apiErr = ErrInvalidSSECustomerParameters apiErr = ErrInvalidSSECustomerParameters
case errSSEKeyMismatch: case crypto.ErrInvalidCustomerKey:
apiErr = ErrAccessDenied // no access without correct key apiErr = ErrAccessDenied // no access without correct key
case crypto.ErrIncompatibleEncryptionMethod:
apiErr = ErrIncompatibleEncryptionMethod
case errKMSNotConfigured:
apiErr = ErrKMSNotConfigured
case crypto.ErrKMSAuthLogin:
apiErr = ErrKMSAuthFailure
case context.Canceled, context.DeadlineExceeded: case context.Canceled, context.DeadlineExceeded:
apiErr = ErrOperationTimedOut apiErr = ErrOperationTimedOut
} }
switch err {
case s3select.ErrBusy:
apiErr = ErrBusy
case s3select.ErrUnauthorizedAccess:
apiErr = ErrUnauthorizedAccess
case s3select.ErrExpressionTooLong:
apiErr = ErrExpressionTooLong
case s3select.ErrIllegalSQLFunctionArgument:
apiErr = ErrIllegalSQLFunctionArgument
case s3select.ErrInvalidKeyPath:
apiErr = ErrInvalidKeyPath
case s3select.ErrInvalidCompressionFormat:
apiErr = ErrInvalidCompressionFormat
case s3select.ErrInvalidFileHeaderInfo:
apiErr = ErrInvalidFileHeaderInfo
case s3select.ErrInvalidJSONType:
apiErr = ErrInvalidJSONType
case s3select.ErrInvalidQuoteFields:
apiErr = ErrInvalidQuoteFields
case s3select.ErrInvalidRequestParameter:
apiErr = ErrInvalidRequestParameter
case s3select.ErrInvalidDataType:
apiErr = ErrInvalidDataType
case s3select.ErrInvalidTextEncoding:
apiErr = ErrInvalidTextEncoding
case s3select.ErrInvalidTableAlias:
apiErr = ErrInvalidTableAlias
case s3select.ErrMissingRequiredParameter:
apiErr = ErrMissingRequiredParameter
case s3select.ErrObjectSerializationConflict:
apiErr = ErrObjectSerializationConflict
case s3select.ErrUnsupportedSQLOperation:
apiErr = ErrUnsupportedSQLOperation
case s3select.ErrUnsupportedSQLStructure:
apiErr = ErrUnsupportedSQLStructure
case s3select.ErrUnsupportedSyntax:
apiErr = ErrUnsupportedSyntax
case s3select.ErrUnsupportedRangeHeader:
apiErr = ErrUnsupportedRangeHeader
case s3select.ErrLexerInvalidChar:
apiErr = ErrLexerInvalidChar
case s3select.ErrLexerInvalidOperator:
apiErr = ErrLexerInvalidOperator
case s3select.ErrLexerInvalidLiteral:
apiErr = ErrLexerInvalidLiteral
case s3select.ErrLexerInvalidIONLiteral:
apiErr = ErrLexerInvalidIONLiteral
case s3select.ErrParseExpectedDatePart:
apiErr = ErrParseExpectedDatePart
case s3select.ErrParseExpectedKeyword:
apiErr = ErrParseExpectedKeyword
case s3select.ErrParseExpectedTokenType:
apiErr = ErrParseExpectedTokenType
case s3select.ErrParseExpected2TokenTypes:
apiErr = ErrParseExpected2TokenTypes
case s3select.ErrParseExpectedNumber:
apiErr = ErrParseExpectedNumber
case s3select.ErrParseExpectedRightParenBuiltinFunctionCall:
apiErr = ErrParseExpectedRightParenBuiltinFunctionCall
case s3select.ErrParseExpectedTypeName:
apiErr = ErrParseExpectedTypeName
case s3select.ErrParseExpectedWhenClause:
apiErr = ErrParseExpectedWhenClause
case s3select.ErrParseUnsupportedToken:
apiErr = ErrParseUnsupportedToken
case s3select.ErrParseUnsupportedLiteralsGroupBy:
apiErr = ErrParseUnsupportedLiteralsGroupBy
case s3select.ErrParseExpectedMember:
apiErr = ErrParseExpectedMember
case s3select.ErrParseUnsupportedSelect:
apiErr = ErrParseUnsupportedSelect
case s3select.ErrParseUnsupportedCase:
apiErr = ErrParseUnsupportedCase
case s3select.ErrParseUnsupportedCaseClause:
apiErr = ErrParseUnsupportedCaseClause
case s3select.ErrParseUnsupportedAlias:
apiErr = ErrParseUnsupportedAlias
case s3select.ErrParseUnsupportedSyntax:
apiErr = ErrParseUnsupportedSyntax
case s3select.ErrParseUnknownOperator:
apiErr = ErrParseUnknownOperator
case s3select.ErrParseInvalidPathComponent:
apiErr = ErrParseInvalidPathComponent
case s3select.ErrParseMissingIdentAfterAt:
apiErr = ErrParseMissingIdentAfterAt
case s3select.ErrParseUnexpectedOperator:
apiErr = ErrParseUnexpectedOperator
case s3select.ErrParseUnexpectedTerm:
apiErr = ErrParseUnexpectedTerm
case s3select.ErrParseUnexpectedToken:
apiErr = ErrParseUnexpectedToken
case s3select.ErrParseUnexpectedKeyword:
apiErr = ErrParseUnexpectedKeyword
case s3select.ErrParseExpectedExpression:
apiErr = ErrParseExpectedExpression
case s3select.ErrParseExpectedLeftParenAfterCast:
apiErr = ErrParseExpectedLeftParenAfterCast
case s3select.ErrParseExpectedLeftParenValueConstructor:
apiErr = ErrParseExpectedLeftParenValueConstructor
case s3select.ErrParseExpectedLeftParenBuiltinFunctionCall:
apiErr = ErrParseExpectedLeftParenBuiltinFunctionCall
case s3select.ErrParseExpectedArgumentDelimiter:
apiErr = ErrParseExpectedArgumentDelimiter
case s3select.ErrParseCastArity:
apiErr = ErrParseCastArity
case s3select.ErrParseInvalidTypeParam:
apiErr = ErrParseInvalidTypeParam
case s3select.ErrParseEmptySelect:
apiErr = ErrParseEmptySelect
case s3select.ErrParseSelectMissingFrom:
apiErr = ErrParseSelectMissingFrom
case s3select.ErrParseExpectedIdentForGroupName:
apiErr = ErrParseExpectedIdentForGroupName
case s3select.ErrParseExpectedIdentForAlias:
apiErr = ErrParseExpectedIdentForAlias
case s3select.ErrParseUnsupportedCallWithStar:
apiErr = ErrParseUnsupportedCallWithStar
case s3select.ErrParseNonUnaryAgregateFunctionCall:
apiErr = ErrParseNonUnaryAgregateFunctionCall
case s3select.ErrParseMalformedJoin:
apiErr = ErrParseMalformedJoin
case s3select.ErrParseExpectedIdentForAt:
apiErr = ErrParseExpectedIdentForAt
case s3select.ErrParseAsteriskIsNotAloneInSelectList:
apiErr = ErrParseAsteriskIsNotAloneInSelectList
case s3select.ErrParseCannotMixSqbAndWildcardInSelectList:
apiErr = ErrParseCannotMixSqbAndWildcardInSelectList
case s3select.ErrParseInvalidContextForWildcardInSelectList:
apiErr = ErrParseInvalidContextForWildcardInSelectList
case s3select.ErrIncorrectSQLFunctionArgumentType:
apiErr = ErrIncorrectSQLFunctionArgumentType
case s3select.ErrValueParseFailure:
apiErr = ErrValueParseFailure
case s3select.ErrIntegerOverflow:
apiErr = ErrIntegerOverflow
case s3select.ErrLikeInvalidInputs:
apiErr = ErrLikeInvalidInputs
case s3select.ErrCastFailed:
apiErr = ErrCastFailed
case s3select.ErrInvalidCast:
apiErr = ErrInvalidCast
case s3select.ErrEvaluatorInvalidTimestampFormatPattern:
apiErr = ErrEvaluatorInvalidTimestampFormatPattern
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing:
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
case s3select.ErrEvaluatorTimestampFormatPatternDuplicateFields:
apiErr = ErrEvaluatorTimestampFormatPatternDuplicateFields
case s3select.ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch:
apiErr = ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
case s3select.ErrEvaluatorUnterminatedTimestampFormatPatternToken:
apiErr = ErrEvaluatorUnterminatedTimestampFormatPatternToken
case s3select.ErrEvaluatorInvalidTimestampFormatPatternToken:
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
case s3select.ErrInvalidColumnIndex:
apiErr = ErrInvalidColumnIndex
case s3select.ErrEvaluatorBindingDoesNotExist:
apiErr = ErrEvaluatorBindingDoesNotExist
case s3select.ErrMissingHeaders:
apiErr = ErrMissingHeaders
}
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.
@@ -903,10 +1611,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
// etcd specific errors, a key is always a bucket for us return // etcd specific errors, a key is always a bucket for us return
// ErrNoSuchBucket in such a case. // ErrNoSuchBucket in such a case.
if e, ok := err.(*client.Error); ok { if err == dns.ErrNoEntriesFound {
if e.Code == client.ErrorCodeKeyNotFound { return ErrNoSuchBucket
return ErrNoSuchBucket
}
} }
switch err.(type) { switch err.(type) {
@@ -972,8 +1678,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrEntityTooLarge apiErr = ErrEntityTooLarge
case UnsupportedMetadata: case UnsupportedMetadata:
apiErr = ErrUnsupportedMetadata apiErr = ErrUnsupportedMetadata
case PartsSizeUnequal:
apiErr = ErrPartsSizeUnequal
case BucketPolicyNotFound: case BucketPolicyNotFound:
apiErr = ErrNoSuchBucketPolicy apiErr = ErrNoSuchBucketPolicy
case *event.ErrInvalidEventName: case *event.ErrInvalidEventName:
@@ -1000,6 +1704,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrUnsupportedNotification apiErr = ErrUnsupportedNotification
case BackendDown: case BackendDown:
apiErr = ErrBackendDown apiErr = ErrBackendDown
case crypto.Error:
apiErr = ErrObjectTampered
default: default:
apiErr = ErrInternalError apiErr = ErrInternalError
} }
@@ -1014,12 +1720,12 @@ func getAPIError(code APIErrorCode) APIError {
// getErrorResponse gets in standard error and resource value and // getErrorResponse gets in standard error and resource value and
// provides a encodable populated response values // provides a encodable populated response values
func getAPIErrorResponse(err APIError, resource string) APIErrorResponse { func getAPIErrorResponse(err APIError, resource, requestid string) APIErrorResponse {
return APIErrorResponse{ return APIErrorResponse{
Code: err.Code, Code: err.Code,
Message: err.Description, Message: err.Description,
Resource: resource, Resource: resource,
RequestID: "3L137", RequestID: requestid,
HostID: "3L137", HostID: "3L137",
} }
} }
+6 -5
View File
@@ -20,6 +20,7 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
) )
@@ -52,11 +53,11 @@ var toAPIErrorCodeTests = []struct {
// SSE-C errors // SSE-C errors
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest}, {err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
{err: errInvalidSSEAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm}, {err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
{err: errMissingSSEKey, errCode: ErrMissingSSECustomerKey}, {err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
{err: errInvalidSSEKey, errCode: ErrInvalidSSECustomerKey}, {err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
{err: errMissingSSEKeyMD5, errCode: ErrMissingSSECustomerKeyMD5}, {err: crypto.ErrMissingCustomerKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
{err: errSSEKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch}, {err: crypto.ErrCustomerKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
{err: errObjectTampered, errCode: ErrObjectTampered}, {err: errObjectTampered, errCode: ErrObjectTampered},
{err: nil, errCode: ErrNone}, {err: nil, errCode: ErrNone},
-3
View File
@@ -34,8 +34,6 @@ func mustGetRequestID(t time.Time) string {
// Write http common headers // Write http common headers
func setCommonHeaders(w http.ResponseWriter) { func setCommonHeaders(w http.ResponseWriter) {
// Set unique request ID for each reply.
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
w.Header().Set("Server", globalServerUserAgent) w.Header().Set("Server", globalServerUserAgent)
// Set `x-amz-bucket-region` only if region is set on the server // Set `x-amz-bucket-region` only if region is set on the server
// by default minio uses an empty region. // by default minio uses an empty region.
@@ -102,6 +100,5 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
// Override content-length // Override content-length
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10)) w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
w.Header().Set("Content-Range", contentRange.String()) w.Header().Set("Content-Range", contentRange.String())
w.WriteHeader(http.StatusPartialContent)
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@ func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err Par
apiError := getAPIError(toAPIErrorCode(err)) apiError := getAPIError(toAPIErrorCode(err))
// Generate complete multipart error response. // Generate complete multipart error response.
errorResponse := getAPIErrorResponse(apiError, r.URL.Path) errorResponse := getAPIErrorResponse(apiError, r.URL.Path, w.Header().Get(responseRequestIDKey))
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse} cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
encodedErrorResponse := encodeResponse(cmpErrResp) encodedErrorResponse := encodeResponse(cmpErrResp)
+2 -2
View File
@@ -577,7 +577,7 @@ func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *u
} }
apiError := getAPIError(errorCode) apiError := getAPIError(errorCode)
// Generate error response. // Generate error response.
errorResponse := getAPIErrorResponse(apiError, reqURL.Path) errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
encodedErrorResponse := encodeResponse(errorResponse) encodedErrorResponse := encodeResponse(errorResponse)
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML) writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML)
} }
@@ -592,7 +592,7 @@ func writeErrorResponseHeadersOnly(w http.ResponseWriter, errorCode APIErrorCode
func writeErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) { func writeErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
apiError := getAPIError(errorCode) apiError := getAPIError(errorCode)
// Generate error response. // Generate error response.
errorResponse := getAPIErrorResponse(apiError, reqURL.Path) errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
encodedErrorResponse := encodeResponseJSON(errorResponse) encodedErrorResponse := encodeResponseJSON(errorResponse)
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON) writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON)
} }
+3 -10
View File
@@ -20,7 +20,6 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/cmd/logger"
) )
// objectAPIHandler implements and provides http handlers for S3 API. // objectAPIHandler implements and provides http handlers for S3 API.
@@ -31,14 +30,6 @@ type objectAPIHandlers struct {
// registerAPIRouter - registers S3 compatible APIs. // registerAPIRouter - registers S3 compatible APIs.
func registerAPIRouter(router *mux.Router) { func registerAPIRouter(router *mux.Router) {
var err error
var cacheConfig = globalServerConfig.GetCacheConfig()
if len(cacheConfig.Drives) > 0 {
// initialize the new disk cache objects.
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
logger.FatalIf(err, "Unable to initialize disk caching")
}
// Initialize API. // Initialize API.
api := objectAPIHandlers{ api := objectAPIHandlers{
ObjectAPI: newObjectLayerFn, ObjectAPI: newObjectLayerFn,
@@ -71,6 +62,8 @@ func registerAPIRouter(router *mux.Router) {
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}") bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
// GetObjectACL - this is a dummy call. // GetObjectACL - this is a dummy call.
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectACLHandler)).Queries("acl", "") bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectACLHandler)).Queries("acl", "")
// SelectObjectContent
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.SelectObjectContentHandler)).Queries("select", "").Queries("select-type", "2")
// GetObject // GetObject
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler)) bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
// CopyObject // CopyObject
@@ -108,7 +101,7 @@ func registerAPIRouter(router *mux.Router) {
// HeadBucket // HeadBucket
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler)) bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
// PostPolicy // PostPolicy
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler)) bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceHdrs(api.PostPolicyBucketHandler))
// DeleteMultipleObjects // DeleteMultipleObjects
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "") bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
// DeleteBucketPolicy // DeleteBucketPolicy
+11 -4
View File
@@ -344,11 +344,14 @@ func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int6
// Tests is requested authenticated function, tests replies for s3 errors. // Tests is requested authenticated function, tests replies for s3 errors.
func TestIsReqAuthenticated(t *testing.T) { func TestIsReqAuthenticated(t *testing.T) {
path, err := newTestConfig(globalMinioDefaultRegion) objLayer, fsDir, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
t.Fatalf("unable initialize config file, %s", err) t.Fatalf("unable initialize config file, %s", err)
} }
defer os.RemoveAll(path)
creds, err := auth.CreateCredentials("myuser", "mypassword") creds, err := auth.CreateCredentials("myuser", "mypassword")
if err != nil { if err != nil {
@@ -384,11 +387,15 @@ func TestIsReqAuthenticated(t *testing.T) {
} }
} }
func TestCheckAdminRequestAuthType(t *testing.T) { func TestCheckAdminRequestAuthType(t *testing.T) {
path, err := newTestConfig(globalMinioDefaultRegion) objLayer, fsDir, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
t.Fatalf("unable initialize config file, %s", err) t.Fatalf("unable initialize config file, %s", err)
} }
defer os.RemoveAll(path)
creds, err := auth.CreateCredentials("myuser", "mypassword") creds, err := auth.CreateCredentials("myuser", "mypassword")
if err != nil { if err != nil {
+8 -52
View File
@@ -22,7 +22,6 @@ import (
"io/ioutil" "io/ioutil"
"math" "math"
"math/rand" "math/rand"
"os"
"strconv" "strconv"
"testing" "testing"
@@ -138,12 +137,6 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function. // creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) { func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// create a temp XL/FS backend. // create a temp XL/FS backend.
objLayer, disks, err := prepareBenchmarkBackend(instanceType) objLayer, disks, err := prepareBenchmarkBackend(instanceType)
if err != nil { if err != nil {
@@ -151,18 +144,13 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
} }
// cleaning up the backend by removing all the directories and files created on function return. // cleaning up the backend by removing all the directories and files created on function return.
defer removeRoots(disks) defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark. // uses *testing.B and the object Layer to run the benchmark.
runPutObjectPartBenchmark(b, objLayer, objSize) runPutObjectPartBenchmark(b, objLayer, objSize)
} }
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function. // creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) { func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// create a temp XL/FS backend. // create a temp XL/FS backend.
objLayer, disks, err := prepareBenchmarkBackend(instanceType) objLayer, disks, err := prepareBenchmarkBackend(instanceType)
if err != nil { if err != nil {
@@ -170,18 +158,13 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
} }
// cleaning up the backend by removing all the directories and files created on function return. // cleaning up the backend by removing all the directories and files created on function return.
defer removeRoots(disks) defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark. // uses *testing.B and the object Layer to run the benchmark.
runPutObjectBenchmark(b, objLayer, objSize) runPutObjectBenchmark(b, objLayer, objSize)
} }
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object. // creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) { func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// create a temp XL/FS backend. // create a temp XL/FS backend.
objLayer, disks, err := prepareBenchmarkBackend(instanceType) objLayer, disks, err := prepareBenchmarkBackend(instanceType)
if err != nil { if err != nil {
@@ -189,6 +172,7 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
} }
// cleaning up the backend by removing all the directories and files created on function return. // cleaning up the backend by removing all the directories and files created on function return.
defer removeRoots(disks) defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark. // uses *testing.B and the object Layer to run the benchmark.
runPutObjectBenchmarkParallel(b, objLayer, objSize) runPutObjectBenchmarkParallel(b, objLayer, objSize)
} }
@@ -196,16 +180,10 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
// Benchmark utility functions for ObjectLayer.GetObject(). // Benchmark utility functions for ObjectLayer.GetObject().
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark. // Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) { func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// obtains random bucket name. // obtains random bucket name.
bucket := getRandomBucketName() bucket := getRandomBucketName()
// create bucket. // create bucket.
err = obj.MakeBucketWithLocation(context.Background(), bucket, "") err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@@ -269,12 +247,6 @@ func generateBytesData(size int) []byte {
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function. // creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) { func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// create a temp XL/FS backend. // create a temp XL/FS backend.
objLayer, disks, err := prepareBenchmarkBackend(instanceType) objLayer, disks, err := prepareBenchmarkBackend(instanceType)
if err != nil { if err != nil {
@@ -282,18 +254,13 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
} }
// cleaning up the backend by removing all the directories and files created. // cleaning up the backend by removing all the directories and files created.
defer removeRoots(disks) defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark. // uses *testing.B and the object Layer to run the benchmark.
runGetObjectBenchmark(b, objLayer, objSize) runGetObjectBenchmark(b, objLayer, objSize)
} }
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() . // creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) { func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// create a temp XL/FS backend. // create a temp XL/FS backend.
objLayer, disks, err := prepareBenchmarkBackend(instanceType) objLayer, disks, err := prepareBenchmarkBackend(instanceType)
if err != nil { if err != nil {
@@ -301,6 +268,7 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
} }
// cleaning up the backend by removing all the directories and files created. // cleaning up the backend by removing all the directories and files created.
defer removeRoots(disks) defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark. // uses *testing.B and the object Layer to run the benchmark.
runGetObjectBenchmarkParallel(b, objLayer, objSize) runGetObjectBenchmarkParallel(b, objLayer, objSize)
} }
@@ -308,16 +276,10 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
// Parallel benchmark utility functions for ObjectLayer.PutObject(). // Parallel benchmark utility functions for ObjectLayer.PutObject().
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark. // Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) { func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// obtains random bucket name. // obtains random bucket name.
bucket := getRandomBucketName() bucket := getRandomBucketName()
// create bucket. // create bucket.
err = obj.MakeBucketWithLocation(context.Background(), bucket, "") err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@@ -359,16 +321,10 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
// Parallel benchmark utility functions for ObjectLayer.GetObject(). // Parallel benchmark utility functions for ObjectLayer.GetObject().
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark. // Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) { func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("Unable to initialize config. %s", err)
}
defer os.RemoveAll(rootPath)
// obtains random bucket name. // obtains random bucket name.
bucket := getRandomBucketName() bucket := getRandomBucketName()
// create bucket. // create bucket.
err = obj.MakeBucketWithLocation(context.Background(), bucket, "") err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
+192
View File
@@ -0,0 +1,192 @@
/*
* 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"
"errors"
"hash"
"github.com/minio/highwayhash"
"github.com/minio/minio/cmd/logger"
"github.com/minio/sha256-simd"
"golang.org/x/crypto/blake2b"
)
// magic HH-256 key as HH-256 hash of the first 100 decimals of π as utf-8 string with a zero key.
var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
// BitrotAlgorithm specifies a algorithm used for bitrot protection.
type BitrotAlgorithm uint
const (
// SHA256 represents the SHA-256 hash function
SHA256 BitrotAlgorithm = 1 + iota
// HighwayHash256 represents the HighwayHash-256 hash function
HighwayHash256
// BLAKE2b512 represents the BLAKE2b-512 hash function
BLAKE2b512
)
// DefaultBitrotAlgorithm is the default algorithm used for bitrot protection.
const (
DefaultBitrotAlgorithm = HighwayHash256
)
var bitrotAlgorithms = map[BitrotAlgorithm]string{
SHA256: "sha256",
BLAKE2b512: "blake2b",
HighwayHash256: "highwayhash256",
}
// New returns a new hash.Hash calculating the given bitrot algorithm.
func (a BitrotAlgorithm) New() hash.Hash {
switch a {
case SHA256:
return sha256.New()
case BLAKE2b512:
b2, _ := blake2b.New512(nil) // New512 never returns an error if the key is nil
return b2
case HighwayHash256:
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
return hh
default:
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
return nil
}
}
// Available reports whether the given algorihm is available.
func (a BitrotAlgorithm) Available() bool {
_, ok := bitrotAlgorithms[a]
return ok
}
// String returns the string identifier for a given bitrot algorithm.
// If the algorithm is not supported String panics.
func (a BitrotAlgorithm) String() string {
name, ok := bitrotAlgorithms[a]
if !ok {
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
}
return name
}
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
return &BitrotVerifier{algorithm, checksum}
}
// BitrotVerifier can be used to verify protected data.
type BitrotVerifier struct {
algorithm BitrotAlgorithm
sum []byte
}
// BitrotAlgorithmFromString returns a bitrot algorithm from the given string representation.
// It returns 0 if the string representation does not match any supported algorithm.
// The zero value of a bitrot algorithm is never supported.
func BitrotAlgorithmFromString(s string) (a BitrotAlgorithm) {
for alg, name := range bitrotAlgorithms {
if name == s {
return alg
}
}
return
}
// To read bit-rot verified data.
type bitrotReader struct {
disk StorageAPI
volume string
filePath string
verifier *BitrotVerifier // Holds the bit-rot info
endOffset int64 // Affects the length of data requested in disk.ReadFile depending on Read()'s offset
buf []byte // Holds bit-rot verified data
}
// newBitrotReader returns bitrotReader.
// Note that the buffer is allocated later in Read(). This is because we will know the buffer length only
// during the bitrotReader.Read(). Depending on when parallelReader fails-over, the buffer length can be different.
func newBitrotReader(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, endOffset int64, sum []byte) *bitrotReader {
return &bitrotReader{
disk: disk,
volume: volume,
filePath: filePath,
verifier: &BitrotVerifier{algo, sum},
endOffset: endOffset,
buf: nil,
}
}
// ReadChunk returns requested data.
func (b *bitrotReader) ReadChunk(offset int64, length int64) ([]byte, error) {
if b.buf == nil {
b.buf = make([]byte, b.endOffset-offset)
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
logger.LogIf(context.Background(), err)
return nil, err
}
}
if int64(len(b.buf)) < length {
logger.LogIf(context.Background(), errLessData)
return nil, errLessData
}
retBuf := b.buf[:length]
b.buf = b.buf[length:]
return retBuf, nil
}
// To calculate the bit-rot of the written data.
type bitrotWriter struct {
disk StorageAPI
volume string
filePath string
h hash.Hash
}
// newBitrotWriter returns bitrotWriter.
func newBitrotWriter(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm) *bitrotWriter {
return &bitrotWriter{
disk: disk,
volume: volume,
filePath: filePath,
h: algo.New(),
}
}
// Append appends the data and while calculating the hash.
func (b *bitrotWriter) Append(buf []byte) error {
n, err := b.h.Write(buf)
if err != nil {
return err
}
if n != len(buf) {
logger.LogIf(context.Background(), errUnexpected)
return errUnexpected
}
if err = b.disk.AppendFile(b.volume, b.filePath, buf); err != nil {
logger.LogIf(context.Background(), err)
return err
}
return nil
}
// Sum returns bit-rot sum.
func (b *bitrotWriter) Sum() []byte {
return b.h.Sum(nil)
}
+71
View File
@@ -0,0 +1,71 @@
/*
* 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 (
"io/ioutil"
"log"
"os"
"testing"
)
func TestBitrotReaderWriter(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tmpDir)
volume := "testvol"
filePath := "testfile"
disk, err := newPosix(tmpDir)
if err != nil {
t.Fatal(err)
}
disk.MakeVol(volume)
writer := newBitrotWriter(disk, volume, filePath, HighwayHash256)
err = writer.Append([]byte("aaaaaaaaa"))
if err != nil {
log.Fatal(err)
}
err = writer.Append([]byte("a"))
if err != nil {
log.Fatal(err)
}
err = writer.Append([]byte("aaaaaaaaaa"))
if err != nil {
log.Fatal(err)
}
err = writer.Append([]byte("aaaaa"))
if err != nil {
log.Fatal(err)
}
err = writer.Append([]byte("aaaaaaaaaa"))
if err != nil {
log.Fatal(err)
}
reader := newBitrotReader(disk, volume, filePath, HighwayHash256, 35, writer.Sum())
if _, err = reader.ReadChunk(0, 35); err != nil {
log.Fatal(err)
}
}
+4 -3
View File
@@ -56,11 +56,12 @@ func (bf *BoolFlag) UnmarshalJSON(data []byte) (err error) {
// ParseBoolFlag - parses string into BoolFlag. // ParseBoolFlag - parses string into BoolFlag.
func ParseBoolFlag(s string) (bf BoolFlag, err error) { func ParseBoolFlag(s string) (bf BoolFlag, err error) {
if s == "on" { switch s {
case "on":
bf = true bf = true
} else if s == "off" { case "off":
bf = false bf = false
} else { default:
err = fmt.Errorf("invalid value %s for BoolFlag", s) err = fmt.Errorf("invalid value %s for BoolFlag", s)
} }
+8 -14
View File
@@ -20,6 +20,8 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
) )
@@ -54,7 +56,7 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
// NOTE: It is recommended that this API to be used for application development. // NOTE: It is recommended that this API to be used for application development.
// Minio continues to support ListObjectsV1 for supporting legacy tools. // Minio continues to support ListObjectsV1 for supporting legacy tools.
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "ListObjectsV2") ctx := newContext(r, w, "ListObjectsV2")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -80,17 +82,9 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
return return
} }
// In ListObjectsV2 'continuation-token' is the marker.
marker := token
// Check if 'continuation-token' is empty.
if token == "" {
// Then we need to use 'start-after' as marker instead.
marker = startAfter
}
// Validate the query params before beginning to serve the request. // Validate the query params before beginning to serve the request.
// fetch-owner is not validated since it is a boolean // fetch-owner is not validated since it is a boolean
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone { if s3Error := validateListObjectsArgs(prefix, token, delimiter, maxKeys); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@@ -101,14 +95,14 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
// Inititate a list objects operation based on the input params. // Inititate a list objects operation based on the input params.
// On success would return back ListObjectsInfo object to be // On success would return back ListObjectsInfo object to be
// marshaled into S3 compatible XML header. // marshaled into S3 compatible XML header.
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, marker, delimiter, maxKeys, fetchOwner, startAfter) listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
if err != nil { if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
for i := range listObjectsV2Info.Objects { for i := range listObjectsV2Info.Objects {
if listObjectsV2Info.Objects[i].IsEncrypted() { 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)
@@ -131,7 +125,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
// criteria to return a subset of the objects in a bucket. // criteria to return a subset of the objects in a bucket.
// //
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "ListObjectsV1") ctx := newContext(r, w, "ListObjectsV1")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -174,7 +168,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
} }
for i := range listObjectsInfo.Objects { for i := range listObjectsInfo.Objects {
if listObjectsInfo.Objects[i].IsEncrypted() { 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)
+44 -62
View File
@@ -28,20 +28,18 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
etcd "github.com/coreos/etcd/client"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"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/handlers"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio-go/pkg/set"
) )
// Check if there are buckets on server without corresponding entry in etcd backend and // Check if there are buckets on server without corresponding entry in etcd backend and
@@ -64,7 +62,7 @@ func initFederatorBackend(objLayer ObjectLayer) {
g.Go(func() error { g.Go(func() error {
r, gerr := globalDNSConfig.Get(b[index].Name) r, gerr := globalDNSConfig.Get(b[index].Name)
if gerr != nil { if gerr != nil {
if etcd.IsKeyNotFound(gerr) || gerr == dns.ErrNoEntriesFound { if gerr == dns.ErrNoEntriesFound {
return globalDNSConfig.Put(b[index].Name) return globalDNSConfig.Put(b[index].Name)
} }
return gerr return gerr
@@ -89,7 +87,7 @@ func initFederatorBackend(objLayer ObjectLayer) {
// ------------------------- // -------------------------
// This operation returns bucket location. // This operation returns bucket location.
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetBucketLocation") ctx := newContext(r, w, "GetBucketLocation")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -105,12 +103,6 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
return return
} }
bucketLock := globalNSMutex.NewNSLock(bucket, "")
if err := bucketLock.GetRLock(globalObjectTimeout); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
defer bucketLock.RUnlock()
getBucketInfo := objectAPI.GetBucketInfo getBucketInfo := objectAPI.GetBucketInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getBucketInfo = api.CacheAPI().GetBucketInfo getBucketInfo = api.CacheAPI().GetBucketInfo
@@ -143,7 +135,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
// uploads in the response. // uploads in the response.
// //
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "ListMultipartUploads") ctx := newContext(r, w, "ListMultipartUploads")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -190,7 +182,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
// This implementation of the GET operation returns a list of all buckets // This implementation of the GET operation returns a list of all buckets
// owned by the authenticated sender of the request. // owned by the authenticated sender of the request.
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "ListBuckets") ctx := newContext(r, w, "ListBuckets")
objectAPI := api.ObjectAPI() objectAPI := api.ObjectAPI()
if objectAPI == nil { if objectAPI == nil {
@@ -211,7 +203,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
var bucketsInfo []BucketInfo var bucketsInfo []BucketInfo
if globalDNSConfig != nil { if globalDNSConfig != nil {
dnsBuckets, err := globalDNSConfig.List() dnsBuckets, err := globalDNSConfig.List()
if err != nil && !etcd.IsKeyNotFound(err) && err != dns.ErrNoEntriesFound { if err != nil && err != dns.ErrNoEntriesFound {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
@@ -246,7 +238,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
// DeleteMultipleObjectsHandler - deletes multiple objects. // DeleteMultipleObjectsHandler - deletes multiple objects.
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "DeleteMultipleObjects") ctx := newContext(r, w, "DeleteMultipleObjects")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -313,34 +305,24 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
return return
} }
var wg = &sync.WaitGroup{} // Allocate a new wait group. deleteObject := objectAPI.DeleteObject
var dErrs = make([]error, len(deleteObjects.Objects)) if api.CacheAPI() != nil {
deleteObject = api.CacheAPI().DeleteObject
// Delete all requested objects in parallel. }
for index, object := range deleteObjects.Objects {
wg.Add(1) var dErrs = make([]error, len(deleteObjects.Objects))
go func(i int, obj ObjectIdentifier) { for index, object := range deleteObjects.Objects {
defer wg.Done() // If the request is denied access, each item
// If the request is denied access, each item // should be marked as 'AccessDenied'
// should be marked as 'AccessDenied' if s3Error == ErrAccessDenied {
if s3Error == ErrAccessDenied { dErrs[index] = PrefixAccessDenied{
dErrs[i] = PrefixAccessDenied{ Bucket: bucket,
Bucket: bucket, Object: object.ObjectName,
Object: obj.ObjectName, }
} continue
return }
} dErrs[index] = deleteObject(ctx, bucket, object.ObjectName)
deleteObject := objectAPI.DeleteObject
if api.CacheAPI() != nil {
deleteObject = api.CacheAPI().DeleteObject
}
dErr := deleteObject(ctx, bucket, obj.ObjectName)
if dErr != nil {
dErrs[i] = dErr
}
}(index, object)
} }
wg.Wait()
// Collect deleted objects and errors if any. // Collect deleted objects and errors if any.
var deletedObjects []ObjectIdentifier var deletedObjects []ObjectIdentifier
@@ -375,7 +357,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// Get host and port from Request.RemoteAddr failing which // Get host and port from Request.RemoteAddr failing which
// fill them with empty strings. // fill them with empty strings.
host, port, err := net.SplitHostPort(r.RemoteAddr) host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
if err != nil { if err != nil {
host, port = "", "" host, port = "", ""
} }
@@ -400,7 +382,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// ---------- // ----------
// This implementation of the PUT operation creates a new bucket for authenticated request // This implementation of the PUT operation creates a new bucket for authenticated request
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "PutBucket") ctx := newContext(r, w, "PutBucket")
objectAPI := api.ObjectAPI() objectAPI := api.ObjectAPI()
if objectAPI == nil { if objectAPI == nil {
@@ -432,7 +414,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
if globalDNSConfig != nil { if globalDNSConfig != nil {
if _, err := globalDNSConfig.Get(bucket); err != nil { if _, err := globalDNSConfig.Get(bucket); err != nil {
if etcd.IsKeyNotFound(err) || err == dns.ErrNoEntriesFound { if err == dns.ErrNoEntriesFound {
// Proceed to creating a bucket. // Proceed to creating a bucket.
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil { if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@@ -476,7 +458,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
// This implementation of the POST operation handles object creation with a specified // This implementation of the POST operation handles object creation with a specified
// signature policy in multipart/form-data // signature policy in multipart/form-data
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "PostPolicyBucket") ctx := newContext(r, w, "PostPolicyBucket")
objectAPI := api.ObjectAPI() objectAPI := api.ObjectAPI()
if objectAPI == nil { if objectAPI == nil {
@@ -601,7 +583,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
// Extract metadata to be saved from received Form. // Extract metadata to be saved from received Form.
metadata, err := extractMetadataFromHeader(ctx, formValues) metadata := make(map[string]string)
err = extractMetadataFromMap(ctx, formValues, metadata)
if err != nil { if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL) writeErrorResponse(w, ErrInternalError, r.URL)
return return
@@ -615,15 +598,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
} }
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
if hasSSECustomerHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C requests if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
var reader io.Reader var reader io.Reader
var key []byte var key []byte
key, err = ParseSSECustomerHeader(formValues) if crypto.SSEC.IsRequested(formValues) {
if err != nil { key, err = ParseSSECustomerHeader(formValues)
writeErrorResponse(w, toAPIErrorCode(err), r.URL) if err != nil {
return writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
} }
reader, err = newEncryptReader(hashReader, key, metadata) reader, err = newEncryptReader(hashReader, key, bucket, object, metadata, crypto.S3.IsRequested(formValues))
if err != nil { if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
@@ -648,7 +633,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
w.Header().Set("Location", location) w.Header().Set("Location", location)
// Get host and port from Request.RemoteAddr. // Get host and port from Request.RemoteAddr.
host, port, err := net.SplitHostPort(r.RemoteAddr) host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
if err != nil { if err != nil {
host, port = "", "" host, port = "", ""
} }
@@ -695,7 +680,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// have permission to access it. Otherwise, the operation might // have permission to access it. Otherwise, the operation might
// return responses such as 404 Not Found and 403 Forbidden. // return responses such as 404 Not Found and 403 Forbidden.
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "HeadBucket") ctx := newContext(r, w, "HeadBucket")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -725,7 +710,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
// DeleteBucketHandler - Delete bucket // DeleteBucketHandler - Delete bucket
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "DeleteBucket") ctx := newContext(r, w, "DeleteBucket")
vars := mux.Vars(r) vars := mux.Vars(r)
bucket := vars["bucket"] bucket := vars["bucket"]
@@ -753,10 +738,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
globalNotificationSys.RemoveNotification(bucket) globalNotificationSys.RemoveNotification(bucket)
globalPolicySys.Remove(bucket) globalPolicySys.Remove(bucket)
for nerr := range globalNotificationSys.DeleteBucket(bucket) { globalNotificationSys.DeleteBucket(ctx, bucket)
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
logger.LogIf(ctx, nerr.Err)
}
if globalDNSConfig != nil { if globalDNSConfig != nil {
if err := globalDNSConfig.Delete(bucket); err != nil { if err := globalDNSConfig.Delete(bucket); err != nil {
+6 -13
View File
@@ -42,7 +42,7 @@ var errNoSuchNotifications = errors.New("The specified bucket does not have buck
// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html. // as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
// It returns empty configuration if its not set. // It returns empty configuration if its not set.
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetBucketNotification") ctx := newContext(r, w, "GetBucketNotification")
vars := mux.Vars(r) vars := mux.Vars(r)
bucketName := vars["bucket"] bucketName := vars["bucket"]
@@ -94,7 +94,7 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per // PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html. // http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "PutBucketNotification") ctx := newContext(r, w, "PutBucketNotification")
objectAPI := api.ObjectAPI() objectAPI := api.ObjectAPI()
if objectAPI == nil { if objectAPI == nil {
@@ -146,10 +146,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
rulesMap := config.ToRulesMap() rulesMap := config.ToRulesMap()
globalNotificationSys.AddRulesMap(bucketName, rulesMap) globalNotificationSys.AddRulesMap(bucketName, rulesMap)
for nerr := range globalNotificationSys.PutBucketNotification(bucketName, rulesMap) { globalNotificationSys.PutBucketNotification(ctx, bucketName, rulesMap)
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
logger.LogIf(ctx, nerr.Err)
}
writeSuccessResponseHeadersOnly(w) writeSuccessResponseHeadersOnly(w)
} }
@@ -157,7 +154,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
// ListenBucketNotificationHandler - This HTTP handler sends events to the connected HTTP client. // ListenBucketNotificationHandler - This HTTP handler sends events to the connected HTTP client.
// Client should send prefix/suffix object name to match and events to watch as query parameters. // Client should send prefix/suffix object name to match and events to watch as query parameters.
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "ListenBucketNotification") ctx := newContext(r, w, "ListenBucketNotification")
// Validate if bucket exists. // Validate if bucket exists.
objAPI := api.ObjectAPI() objAPI := api.ObjectAPI()
@@ -238,7 +235,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
rulesMap := event.NewRulesMap(eventNames, pattern, target.ID()) rulesMap := event.NewRulesMap(eventNames, pattern, target.ID())
if err := globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil { if err = globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name) logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@@ -260,11 +257,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
return return
} }
errCh := globalNotificationSys.ListenBucketNotification(bucketName, eventNames, pattern, target.ID(), *thisAddr) globalNotificationSys.ListenBucketNotification(ctx, bucketName, eventNames, pattern, target.ID(), *thisAddr)
for nerr := range errCh {
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
logger.LogIf(ctx, nerr.Err)
}
<-target.DoneCh <-target.DoneCh
+5 -11
View File
@@ -38,7 +38,7 @@ const (
// PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per // PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per
// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html // https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "PutBucketPolicy") ctx := newContext(r, w, "PutBucketPolicy")
objAPI := api.ObjectAPI() objAPI := api.ObjectAPI()
if objAPI == nil { if objAPI == nil {
@@ -91,10 +91,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
} }
globalPolicySys.Set(bucket, *bucketPolicy) globalPolicySys.Set(bucket, *bucketPolicy)
for nerr := range globalNotificationSys.SetBucketPolicy(bucket, bucketPolicy) { globalNotificationSys.SetBucketPolicy(ctx, bucket, bucketPolicy)
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
logger.LogIf(ctx, nerr.Err)
}
// Success. // Success.
writeSuccessNoContent(w) writeSuccessNoContent(w)
@@ -102,7 +99,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
// DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration. // DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration.
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "DeleteBucketPolicy") ctx := newContext(r, w, "DeleteBucketPolicy")
objAPI := api.ObjectAPI() objAPI := api.ObjectAPI()
if objAPI == nil { if objAPI == nil {
@@ -130,10 +127,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
} }
globalPolicySys.Remove(bucket) globalPolicySys.Remove(bucket)
for nerr := range globalNotificationSys.RemoveBucketPolicy(bucket) { globalNotificationSys.RemoveBucketPolicy(ctx, bucket)
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
logger.LogIf(ctx, nerr.Err)
}
// Success. // Success.
writeSuccessNoContent(w) writeSuccessNoContent(w)
@@ -141,7 +135,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
// GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration. // GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration.
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetBucketPolicy") ctx := newContext(r, w, "GetBucketPolicy")
objAPI := api.ObjectAPI() objAPI := api.ObjectAPI()
if objAPI == nil { if objAPI == nil {
+4 -2
View File
@@ -437,11 +437,13 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
if recV4.Code != testCase.expectedRespStatus { if recV4.Code != testCase.expectedRespStatus {
// Verify whether the bucket policy fetched is same as the one inserted. // Verify whether the bucket policy fetched is same as the one inserted.
expectedPolicy, err := policy.ParseConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName) var expectedPolicy *policy.Policy
expectedPolicy, err = policy.ParseConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
if err != nil { if err != nil {
t.Fatalf("unexpected error. %v", err) t.Fatalf("unexpected error. %v", err)
} }
gotPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName) var gotPolicy *policy.Policy
gotPolicy, err = policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
if err != nil { if err != nil {
t.Fatalf("unexpected error. %v", err) t.Fatalf("unexpected error. %v", err)
} }
+38 -35
View File
@@ -17,7 +17,6 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"net" "net"
"os" "os"
@@ -26,9 +25,9 @@ import (
"strings" "strings"
"time" "time"
etcd "github.com/coreos/etcd/client" etcd "github.com/coreos/etcd/clientv3"
"github.com/minio/cli" "github.com/minio/cli"
"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"
@@ -48,34 +47,17 @@ func checkUpdate(mode string) {
} }
} }
// Initialize and load config from remote etcd or local config directory // Load logger targets based on user's configuration
func initConfig() { func loadLoggers() {
if globalEtcdClient != nil { if globalServerConfig.Logger.Console.Enabled {
kapi := etcd.NewKeysAPI(globalEtcdClient) // Enable console logging
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) logger.AddTarget(logger.NewConsole())
_, err := kapi.Get(ctx, getConfigFile(), nil)
cancel()
if err == nil {
logger.FatalIf(migrateConfig(), "Config migration failed.")
logger.FatalIf(loadConfig(), "Unable to load config version: '%s'.", serverConfigVersion)
} else {
if etcd.IsKeyNotFound(err) {
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time.")
logger.Info("Created minio configuration file successfully at %v", globalEtcdClient.Endpoints())
} else {
logger.FatalIf(err, "Unable to load config version: '%s'.", serverConfigVersion)
}
}
return
} }
for _, l := range globalServerConfig.Logger.HTTP {
if isFile(getConfigFile()) { if l.Enabled {
logger.FatalIf(migrateConfig(), "Config migration failed") // Enable http logging
logger.FatalIf(loadConfig(), "Unable to load the configuration file") logger.AddTarget(logger.NewHTTP(l.Endpoint, NewCustomHTTPTransport()))
} else { }
// Config file does not exist, we create it fresh and return upon success.
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time")
logger.Info("Created minio configuration file successfully at " + getConfigDir())
} }
} }
@@ -83,9 +65,10 @@ func handleCommonCmdArgs(ctx *cli.Context) {
var configDir string var configDir string
if ctx.IsSet("config-dir") { switch {
case ctx.IsSet("config-dir"):
configDir = ctx.String("config-dir") configDir = ctx.String("config-dir")
} else if ctx.GlobalIsSet("config-dir") { case ctx.GlobalIsSet("config-dir"):
configDir = ctx.GlobalString("config-dir") configDir = ctx.GlobalString("config-dir")
// cli package does not expose parent's "config-dir" option. Below code is workaround. // cli package does not expose parent's "config-dir" option. Below code is workaround.
if configDir == "" || configDir == getConfigDir() { if configDir == "" || configDir == getConfigDir() {
@@ -93,7 +76,7 @@ func handleCommonCmdArgs(ctx *cli.Context) {
configDir = ctx.Parent().GlobalString("config-dir") configDir = ctx.Parent().GlobalString("config-dir")
} }
} }
} else { default:
// Neither local nor global config-dir option is provided. In this case, try to use // Neither local nor global config-dir option is provided. In this case, try to use
// default config directory. // default config directory.
configDir = getConfigDir() configDir = getConfigDir()
@@ -131,6 +114,11 @@ func handleCommonEnvVars() {
globalActiveCred = cred globalActiveCred = cred
} }
// In distributed setup users need to set ENVs always.
if !globalIsEnvCreds && globalIsDistXL {
logger.Fatal(uiErrEnvCredentialsMissingServer(nil), "Unable to start distributed server mode")
}
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 {
@@ -155,8 +143,9 @@ func handleCommonEnvVars() {
etcdEndpoints := strings.Split(etcdEndpointsEnv, ",") etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")
var err error var err error
globalEtcdClient, err = etcd.New(etcd.Config{ globalEtcdClient, err = etcd.New(etcd.Config{
Endpoints: etcdEndpoints, Endpoints: etcdEndpoints,
Transport: NewCustomHTTPTransport(), DialTimeout: defaultDialTimeout,
DialKeepAliveTime: defaultDialKeepAlive,
}) })
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints) logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
} }
@@ -262,4 +251,18 @@ func handleCommonEnvVars() {
globalIsEnvWORM = true globalIsEnvWORM = true
globalWORMEnabled = bool(wormFlag) globalWORMEnabled = bool(wormFlag)
} }
kmsConf, err := crypto.NewVaultConfig()
if err != nil {
logger.Fatal(err, "Unable to initialize hashicorp vault")
}
if kmsConf.Vault.Endpoint != "" {
kms, err := crypto.NewVault(kmsConf)
if err != nil {
logger.Fatal(err, "Unable to initialize KMS")
}
globalKMS = kms
globalKMSKeyID = kmsConf.Vault.Key.Name
globalKMSConfig = kmsConf
}
} }
+235 -168
View File
@@ -17,17 +17,19 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"sync" "sync"
"github.com/miekg/dns"
"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/quick"
) )
// Steps to move from version N to version N+1 // Steps to move from version N to version N+1
@@ -39,9 +41,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 = "26" const serverConfigVersion = "28"
type serverConfig = serverConfigV26 type serverConfig = serverConfigV28
var ( var (
// globalServerConfig server config. // globalServerConfig server config.
@@ -67,6 +69,10 @@ func (s *serverConfig) GetRegion() string {
// SetCredential sets new credential and returns the previous credential. // SetCredential sets new credential and returns the previous credential.
func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) { func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) {
if creds.IsValid() && globalActiveCred.IsValid() {
globalActiveCred = creds
}
// Save previous credential. // Save previous credential.
prevCred = s.Credential prevCred = s.Credential
@@ -79,6 +85,9 @@ func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Cred
// GetCredentials get current credentials. // GetCredentials get current credentials.
func (s *serverConfig) GetCredential() auth.Credentials { func (s *serverConfig) GetCredential() auth.Credentials {
if globalActiveCred.IsValid() {
return globalActiveCred
}
return s.Credential return s.Credential
} }
@@ -128,19 +137,117 @@ func (s *serverConfig) GetCacheConfig() CacheConfig {
return s.Cache return s.Cache
} }
// Save config file to corresponding backend func (s *serverConfig) Validate() error {
func Save(configFile string, data interface{}) error { if s.Version != serverConfigVersion {
return quick.SaveConfig(data, configFile, globalEtcdClient) return fmt.Errorf("configuration version mismatch. Expected: %s, Got: %s", serverConfigVersion, s.Version)
}
// Validate credential fields only when
// they are not set via the environment
// Error out if global is env credential is not set and config has invalid credential
if !globalIsEnvCreds && !s.Credential.IsValid() {
return errors.New("invalid credential in config file")
}
// Region: nothing to validate
// Browser, 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 {
if err := v.Validate(); err != nil {
return fmt.Errorf("amqp: %s", err.Error())
}
}
for _, v := range s.Notify.Elasticsearch {
if err := v.Validate(); err != nil {
return fmt.Errorf("elasticsearch: %s", err.Error())
}
}
for _, v := range s.Notify.Kafka {
if err := v.Validate(); err != nil {
return fmt.Errorf("kafka: %s", err.Error())
}
}
for _, v := range s.Notify.MQTT {
if err := v.Validate(); err != nil {
return fmt.Errorf("mqtt: %s", err.Error())
}
}
for _, v := range s.Notify.MySQL {
if err := v.Validate(); err != nil {
return fmt.Errorf("mysql: %s", err.Error())
}
}
for _, v := range s.Notify.NATS {
if err := v.Validate(); err != nil {
return fmt.Errorf("nats: %s", err.Error())
}
}
for _, v := range s.Notify.PostgreSQL {
if err := v.Validate(); err != nil {
return fmt.Errorf("postgreSQL: %s", err.Error())
}
}
for _, v := range s.Notify.Redis {
if err := v.Validate(); err != nil {
return fmt.Errorf("redis: %s", err.Error())
}
}
for _, v := range s.Notify.Webhook {
if err := v.Validate(); err != nil {
return fmt.Errorf("webhook: %s", err.Error())
}
}
return nil
} }
// Load config from backend func (s *serverConfig) loadFromEnvs() {
func Load(configFile string, data interface{}) (quick.Config, error) { // If env is set override the credentials from config file.
return quick.LoadConfig(configFile, globalEtcdClient, data) if globalIsEnvCreds {
} s.SetCredential(globalActiveCred)
}
// GetVersion gets config version from backend if globalIsEnvBrowser {
func GetVersion(configFile string) (string, error) { s.SetBrowser(globalIsBrowserEnabled)
return quick.GetVersion(configFile, globalEtcdClient) }
if globalIsEnvWORM {
s.SetWorm(globalWORMEnabled)
}
if globalIsEnvRegion {
s.SetRegion(globalServerRegion)
}
if globalIsEnvDomainName {
s.Domain = globalDomainName
}
if globalIsStorageClass {
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
}
if globalIsDiskCacheEnabled {
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
}
if globalKMS != nil {
s.KMS = globalKMSConfig
}
} }
// Returns the string describing a difference with the given // Returns the string describing a difference with the given
@@ -180,6 +287,10 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
return "MySQL Notification configuration differs" return "MySQL Notification configuration differs"
case !reflect.DeepEqual(s.Notify.MQTT, t.Notify.MQTT): case !reflect.DeepEqual(s.Notify.MQTT, t.Notify.MQTT):
return "MQTT Notification configuration differs" return "MQTT Notification configuration differs"
case !reflect.DeepEqual(s.Logger, t.Logger):
return "Logger configuration differs"
case !reflect.DeepEqual(s.KMS, t.KMS):
return "KMS configuration differs"
case reflect.DeepEqual(s, t): case reflect.DeepEqual(s, t):
return "" return ""
default: default:
@@ -208,6 +319,7 @@ func newServerConfig() *serverConfig {
Expiry: globalCacheExpiry, Expiry: globalCacheExpiry,
MaxUse: globalCacheMaxUse, MaxUse: globalCacheMaxUse,
}, },
KMS: crypto.KMSConfig{},
Notify: notifier{}, Notify: notifier{},
} }
@@ -235,163 +347,99 @@ func newServerConfig() *serverConfig {
srvCfg.Cache.Exclude = make([]string, 0) srvCfg.Cache.Exclude = make([]string, 0)
srvCfg.Cache.Expiry = globalCacheExpiry srvCfg.Cache.Expiry = globalCacheExpiry
srvCfg.Cache.MaxUse = globalCacheMaxUse srvCfg.Cache.MaxUse = globalCacheMaxUse
// Console logging is on by default
srvCfg.Logger.Console.Enabled = true
// Create an example of HTTP logger
srvCfg.Logger.HTTP = make(map[string]loggerHTTP)
srvCfg.Logger.HTTP["target1"] = loggerHTTP{Endpoint: "https://username:password@example.com/api"}
return srvCfg return srvCfg
} }
// newConfig - initialize a new server config, saves env parameters if func (s *serverConfig) loadToCachedConfigs() {
// found, otherwise use default parameters
func newConfig() error {
// Initialize server config.
srvCfg, err := newQuickConfig(newServerConfig())
if err != nil {
return err
}
// If env is set override the credentials from config file.
if globalIsEnvCreds {
srvCfg.SetCredential(globalActiveCred)
}
if globalIsEnvBrowser {
srvCfg.SetBrowser(globalIsBrowserEnabled)
}
if globalIsEnvWORM {
srvCfg.SetWorm(globalWORMEnabled)
}
if globalIsEnvRegion {
srvCfg.SetRegion(globalServerRegion)
}
if globalIsEnvDomainName {
srvCfg.Domain = globalDomainName
}
if globalIsStorageClass {
srvCfg.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
}
if globalIsDiskCacheEnabled {
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
}
// hold the mutex lock before a new config is assigned.
// Save the new config globally.
// unlock the mutex.
globalServerConfigMu.Lock()
globalServerConfig = srvCfg
globalServerConfigMu.Unlock()
// Save config into file.
return Save(getConfigFile(), globalServerConfig)
}
// newQuickConfig - initialize a new server config, with an allocated
// quick.Config interface.
func newQuickConfig(srvCfg *serverConfig) (*serverConfig, error) {
qcfg, err := quick.NewConfig(srvCfg, globalEtcdClient)
if err != nil {
return nil, err
}
srvCfg.Config = qcfg
return srvCfg, nil
}
// getValidConfig - returns valid server configuration
func getValidConfig() (*serverConfig, error) {
srvCfg := &serverConfig{
Region: globalMinioDefaultRegion,
Browser: true,
}
var err error
srvCfg, err = newQuickConfig(srvCfg)
if err != nil {
return nil, err
}
configFile := getConfigFile()
if err = srvCfg.Load(configFile); err != nil {
return nil, err
}
if srvCfg.Version != serverConfigVersion {
return nil, fmt.Errorf("configuration version mismatch. Expected: %s, Got: %s", serverConfigVersion, srvCfg.Version)
}
// Validate credential fields only when
// they are not set via the environment
// Error out if global is env credential is not set and config has invalid credential
if !globalIsEnvCreds && !srvCfg.Credential.IsValid() {
return nil, errors.New("invalid credential in config file " + getConfigFile())
}
return srvCfg, nil
}
// loadConfig - loads a new config from disk, overrides params from env
// if found and valid
func loadConfig() error {
srvCfg, err := getValidConfig()
if err != nil {
return uiErrInvalidConfig(nil).Msg(err.Error())
}
// If env is set override the credentials from config file.
if globalIsEnvCreds {
srvCfg.SetCredential(globalActiveCred)
}
if globalIsEnvBrowser {
srvCfg.SetBrowser(globalIsBrowserEnabled)
}
if globalIsEnvRegion {
srvCfg.SetRegion(globalServerRegion)
}
if globalIsEnvDomainName {
srvCfg.Domain = globalDomainName
}
if globalIsStorageClass {
srvCfg.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
}
if globalIsDiskCacheEnabled {
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
}
// hold the mutex lock before a new config is assigned.
globalServerConfigMu.Lock()
globalServerConfig = srvCfg
if !globalIsEnvCreds { if !globalIsEnvCreds {
globalActiveCred = globalServerConfig.GetCredential() globalActiveCred = s.GetCredential()
} }
if !globalIsEnvBrowser { if !globalIsEnvBrowser {
globalIsBrowserEnabled = globalServerConfig.GetBrowser() globalIsBrowserEnabled = s.GetBrowser()
} }
if !globalIsEnvWORM { if !globalIsEnvWORM {
globalWORMEnabled = globalServerConfig.GetWorm() globalWORMEnabled = s.GetWorm()
} }
if !globalIsEnvRegion { if !globalIsEnvRegion {
globalServerRegion = globalServerConfig.GetRegion() globalServerRegion = s.GetRegion()
} }
if !globalIsEnvDomainName { if !globalIsEnvDomainName {
globalDomainName = globalServerConfig.Domain globalDomainName = s.Domain
} }
if !globalIsStorageClass { if !globalIsStorageClass {
globalStandardStorageClass, globalRRStorageClass = globalServerConfig.GetStorageClass() globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
} }
if !globalIsDiskCacheEnabled { if !globalIsDiskCacheEnabled {
cacheConf := globalServerConfig.GetCacheConfig() cacheConf := s.GetCacheConfig()
globalCacheDrives = cacheConf.Drives globalCacheDrives = cacheConf.Drives
globalCacheExcludes = cacheConf.Exclude globalCacheExcludes = cacheConf.Exclude
globalCacheExpiry = cacheConf.Expiry globalCacheExpiry = cacheConf.Expiry
globalCacheMaxUse = cacheConf.MaxUse globalCacheMaxUse = cacheConf.MaxUse
} }
if globalKMS == nil {
globalKMSConfig = s.KMS
if kms, err := crypto.NewVault(globalKMSConfig); err == nil {
globalKMS = kms
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
}
}
}
// newConfig - initialize a new server config, saves env parameters if
// found, otherwise use default parameters
func newConfig(objAPI ObjectLayer) error {
// 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()
// Save config into file.
return saveServerConfig(objAPI, globalServerConfig)
}
// getValidConfig - returns valid server configuration
func getValidConfig(objAPI ObjectLayer) (*serverConfig, error) {
srvCfg, err := readServerConfig(context.Background(), objAPI)
if err != nil {
return nil, err
}
return srvCfg, srvCfg.Validate()
}
// loadConfig - loads a new config from disk, overrides params from env
// if found and valid
func loadConfig(objAPI ObjectLayer) error {
srvCfg, err := getValidConfig(objAPI)
if err != nil {
return uiErrInvalidConfig(nil).Msg(err.Error())
}
// 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() globalServerConfigMu.Unlock()
return nil return nil
@@ -402,17 +450,19 @@ func loadConfig() error {
// * Add a new target in pkg/event/target package. // * Add a new target in pkg/event/target package.
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>. // * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
// * Handle the configuration in this function to create/add into TargetList. // * Handle the configuration in this function to create/add into TargetList.
func getNotificationTargets(config *serverConfig) (*event.TargetList, error) { func getNotificationTargets(config *serverConfig) *event.TargetList {
targetList := event.NewTargetList() targetList := event.NewTargetList()
for id, args := range config.Notify.AMQP { for id, args := range config.Notify.AMQP {
if args.Enable { if args.Enable {
newTarget, err := target.NewAMQPTarget(id, args) newTarget, err := target.NewAMQPTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -421,10 +471,14 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewElasticsearchTarget(id, args) newTarget, err := target.NewElasticsearchTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -433,10 +487,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewKafkaTarget(id, args) newTarget, err := target.NewKafkaTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -445,10 +501,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewMQTTTarget(id, args) newTarget, err := target.NewMQTTTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -457,10 +515,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewMySQLTarget(id, args) newTarget, err := target.NewMySQLTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -469,10 +529,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewNATSTarget(id, args) newTarget, err := target.NewNATSTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -481,10 +543,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewPostgreSQLTarget(id, args) newTarget, err := target.NewPostgreSQLTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -493,10 +557,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget, err := target.NewRedisTarget(id, args) newTarget, err := target.NewRedisTarget(id, args)
if err != nil { if err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
if err = targetList.Add(newTarget); err != nil { if err = targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
@@ -505,10 +571,11 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
if args.Enable { if args.Enable {
newTarget := target.NewWebhookTarget(id, args) newTarget := target.NewWebhookTarget(id, args)
if err := targetList.Add(newTarget); err != nil { if err := targetList.Add(newTarget); err != nil {
return nil, err logger.LogIf(context.Background(), err)
continue
} }
} }
} }
return targetList, nil return targetList
} }
+58 -45
View File
@@ -17,9 +17,8 @@
package cmd package cmd
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path"
"testing" "testing"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
@@ -27,12 +26,15 @@ import (
) )
func TestServerConfig(t *testing.T) { func TestServerConfig(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) objLayer, fsDir, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
t.Fatalf("Init Test config failed") t.Fatalf("Init Test config failed")
} }
// remove the root directory after the test ends.
defer os.RemoveAll(rootPath)
if globalServerConfig.GetRegion() != globalMinioDefaultRegion { if globalServerConfig.GetRegion() != globalMinioDefaultRegion {
t.Errorf("Expecting region `us-east-1` found %s", globalServerConfig.GetRegion()) t.Errorf("Expecting region `us-east-1` found %s", globalServerConfig.GetRegion())
@@ -49,16 +51,12 @@ func TestServerConfig(t *testing.T) {
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion) t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
} }
// Attempt to save. if err := saveServerConfig(objLayer, globalServerConfig); err != nil {
if err := globalServerConfig.Save(getConfigFile()); err != nil {
t.Fatalf("Unable to save updated config file %s", err) t.Fatalf("Unable to save updated config file %s", err)
} }
// Do this only once here.
setConfigDir(rootPath)
// Initialize server config. // Initialize server config.
if err := loadConfig(); 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)
} }
} }
@@ -82,23 +80,25 @@ func TestServerConfigWithEnvs(t *testing.T) {
defer resetGlobalIsEnvs() defer resetGlobalIsEnvs()
// Get test root. objLayer, fsDir, err := prepareFS()
rootPath, err := getTestRoot()
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
} }
defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
t.Fatalf("Init Test config failed")
}
globalObjLayerMutex.Lock()
globalObjectAPI = objLayer
globalObjLayerMutex.Unlock()
serverHandleEnvVars() serverHandleEnvVars()
// Do this only once here.
setConfigDir(rootPath)
// Init config // Init config
initConfig() initConfig()
// remove the root directory after the test ends.
defer os.RemoveAll(rootPath)
// Check if serverConfig has // Check if serverConfig has
if globalServerConfig.GetBrowser() { if globalServerConfig.GetBrowser() {
t.Errorf("Expecting browser is set to false found %v", globalServerConfig.GetBrowser()) t.Errorf("Expecting browser is set to false found %v", globalServerConfig.GetBrowser())
@@ -127,16 +127,17 @@ func TestServerConfigWithEnvs(t *testing.T) {
// Tests config validator.. // Tests config validator..
func TestValidateConfig(t *testing.T) { func TestValidateConfig(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) objLayer, fsDir, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
t.Fatalf("Init Test config failed") t.Fatalf("Init Test config failed")
} }
// remove the root directory after the test ends. configPath := path.Join(minioConfigPrefix, minioConfigFile)
defer os.RemoveAll(rootPath)
configPath := filepath.Join(rootPath, minioConfigFile)
v := serverConfigVersion v := serverConfigVersion
testCases := []struct { testCases := []struct {
@@ -174,66 +175,66 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false}, {`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
// Test 11 - Test AMQP // Test 11 - Test AMQP
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, false},
// Test 12 - Test NATS // Test 12 - Test NATS
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
// Test 13 - Test ElasticSearch // Test 13 - Test ElasticSearch
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, false},
// Test 14 - Test Redis // Test 14 - Test Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, false},
// Test 15 - Test PostgreSQL // Test 15 - Test PostgreSQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
// Test 16 - Test Kafka // Test 16 - Test Kafka
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, false},
// Test 17 - Test Webhook // Test 17 - Test Webhook
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, false},
// Test 18 - Test MySQL // Test 18 - Test MySQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
// Test 19 - Test Format for MySQL // Test 19 - Test Format for MySQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
// Test 20 - Test valid Format for MySQL // Test 20 - Test valid Format for MySQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "namespace", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "namespace", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
// Test 21 - Test Format for PostgreSQL // Test 21 - Test Format for PostgreSQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "invalid", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "invalid", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
// Test 22 - Test valid Format for PostgreSQL // Test 22 - Test valid Format for PostgreSQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "namespace", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "namespace", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
// Test 23 - Test Format for ElasticSearch // Test 23 - Test Format for ElasticSearch
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, false},
// Test 24 - Test valid Format for ElasticSearch // Test 24 - Test valid Format for ElasticSearch
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true},
// Test 25 - Test Format for Redis // Test 25 - Test Format for Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, false},
// Test 26 - Test valid Format for Redis // Test 26 - Test valid Format for Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
// Test 27 - Test MQTT // Test 27 - Test MQTT
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
if werr := ioutil.WriteFile(configPath, []byte(testCase.configData), 0700); werr != nil { if err = saveConfig(objLayer, configPath, []byte(testCase.configData)); err != nil {
t.Fatal(werr) t.Fatal(err)
} }
_, verr := getValidConfig() _, err = getValidConfig(objLayer)
if testCase.shouldPass && verr != nil { if testCase.shouldPass && err != nil {
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, verr) t.Errorf("Test %d, should pass but it failed with err = %v", i+1, err)
} }
if !testCase.shouldPass && verr == nil { if !testCase.shouldPass && err == nil {
t.Errorf("Test %d, should fail but it succeeded.", i+1) t.Errorf("Test %d, should fail but it succeeded.", i+1)
} }
} }
@@ -319,6 +320,18 @@ func TestConfigDiff(t *testing.T) {
&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
{
&serverConfig{Logger: loggerConfig{
Console: loggerConsole{Enabled: true},
HTTP: map[string]loggerHTTP{"1": {Endpoint: "http://address1"}},
}},
&serverConfig{Logger: loggerConfig{
Console: loggerConsole{Enabled: true},
HTTP: map[string]loggerHTTP{"1": {Endpoint: "http://address2"}},
}},
"Logger configuration differs",
},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
-3
View File
@@ -28,9 +28,6 @@ const (
// Default minio configuration directory where below configuration files/directories are stored. // Default minio configuration directory where below configuration files/directories are stored.
defaultMinioConfigDir = ".minio" defaultMinioConfigDir = ".minio"
// Minio configuration file.
minioConfigFile = "config.json"
// Directory contains below files/directories for HTTPS configuration. // Directory contains below files/directories for HTTPS configuration.
certsDir = "certs" certsDir = "certs"
+133 -3
View File
@@ -17,13 +17,16 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path"
"path/filepath" "path/filepath"
etcd "github.com/coreos/etcd/client" "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/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/event/target" "github.com/minio/minio/pkg/event/target"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
@@ -33,7 +36,22 @@ import (
// DO NOT EDIT following message template, please open a github issue to discuss instead. // DO NOT EDIT following message template, please open a github issue to discuss instead.
var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully." var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully."
// Migrates all config versions from "1" to "18". // Save config file to corresponding backend
func Save(configFile string, data interface{}) error {
return quick.SaveConfig(data, configFile, globalEtcdClient)
}
// Load config from backend
func Load(configFile string, data interface{}) (quick.Config, error) {
return quick.LoadConfig(configFile, globalEtcdClient, data)
}
// GetVersion gets config version from backend
func GetVersion(configFile string) (string, error) {
return quick.GetVersion(configFile, globalEtcdClient)
}
// Migrates all config versions from "1" to "28".
func migrateConfig() error { func migrateConfig() error {
// Purge all configs with version '1', // Purge all configs with version '1',
// this is a special case since version '1' used // this is a special case since version '1' used
@@ -45,6 +63,9 @@ func migrateConfig() error {
// Load only config version information. // Load only config version information.
version, err := GetVersion(getConfigFile()) version, err := GetVersion(getConfigFile())
if err != nil { if err != nil {
if os.IsNotExist(err) {
return nil
}
return err return err
} }
@@ -188,6 +209,16 @@ func migrateConfig() error {
return err return err
} }
fallthrough fallthrough
case "26":
if err = migrateV26ToV27(); err != nil {
return err
}
fallthrough
case "27":
if err = migrateV27ToV28(); err != nil {
return err
}
fallthrough
case serverConfigVersion: case serverConfigVersion:
// No migration needed. this always points to current version. // No migration needed. this always points to current version.
err = nil err = nil
@@ -201,7 +232,7 @@ func purgeV1() error {
cv1 := &configV1{} cv1 := &configV1{}
_, err := Load(configFile, cv1) _, err := Load(configFile, cv1)
if os.IsNotExist(err) || etcd.IsKeyNotFound(err) { if os.IsNotExist(err) || err == dns.ErrNoEntriesFound {
return nil return nil
} else if err != nil { } else if err != nil {
return fmt.Errorf("Unable to load config version 1. %v", err) return fmt.Errorf("Unable to load config version 1. %v", err)
@@ -2317,3 +2348,102 @@ func migrateV25ToV26() error {
logger.Info(configMigrateMSGTemplate, configFile, cv25.Version, srvConfig.Version) logger.Info(configMigrateMSGTemplate, configFile, cv25.Version, srvConfig.Version)
return nil return nil
} }
func migrateV26ToV27() error {
configFile := getConfigFile()
// config V27 is backward compatible with V26, load the old
// config file in serverConfigV27 struct and put some examples
// in the new `logger` field
srvConfig := &serverConfigV27{}
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %v", err)
}
if srvConfig.Version != "26" {
return nil
}
srvConfig.Version = "27"
// Enable console logging by default to avoid breaking users
// current deployments
srvConfig.Logger.Console.Enabled = true
srvConfig.Logger.HTTP = make(map[string]loggerHTTP)
srvConfig.Logger.HTTP["1"] = loggerHTTP{}
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
return fmt.Errorf("Failed to migrate config from 26 to 27. %v", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "26", "27")
return nil
}
func migrateV27ToV28() error {
configFile := getConfigFile()
// config V28 is backward compatible with V27, load the old
// config file in serverConfigV28 struct and initialize KMSConfig
srvConfig := &serverConfigV28{}
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %v", err)
}
if srvConfig.Version != "27" {
return nil
}
srvConfig.Version = "28"
srvConfig.KMS = crypto.KMSConfig{}
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
return fmt.Errorf("Failed to migrate config from 27 to 28. %v", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
return nil
}
// Migrates '.minio.sys/config.json' v27 to v28.
func migrateMinioSysConfig(objAPI ObjectLayer) error {
// Construct path to config.json for the given bucket.
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
transactionConfigFile := configFile + ".transaction"
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
// and configFile, take a transaction lock to avoid race.
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
if err := objLock.GetLock(globalOperationTimeout); err != nil {
return err
}
defer objLock.Unlock()
return migrateV27ToV28MinioSys(objAPI)
}
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
configFile := path.Join(minioConfigPrefix, minioConfigFile)
srvConfig, err := readServerConfig(context.Background(), objAPI)
if err == errConfigNotFound {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config file. %v", err)
}
if srvConfig.Version != "27" {
return nil
}
srvConfig.Version = "28"
srvConfig.KMS = crypto.KMSConfig{}
if err = saveServerConfig(objAPI, srvConfig); err != nil {
return fmt.Errorf("Failed to migrate config from 27 to 28. %v", err)
}
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
return nil
}
+76 -28
View File
@@ -25,15 +25,26 @@ import (
// Test if config v1 is purged // Test if config v1 is purged
func TestServerConfigMigrateV1(t *testing.T) { func TestServerConfigMigrateV1(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) objLayer, fsDir, err := prepareFS()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(fsDir)
err = newTestConfig(globalMinioDefaultRegion, objLayer)
if err != nil { if err != nil {
t.Fatalf("Init Test config failed") t.Fatalf("Init Test config failed")
} }
// remove the root directory after the test ends. rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
setConfigDir(rootPath) setConfigDir(rootPath)
globalObjLayerMutex.Lock()
globalObjectAPI = objLayer
globalObjLayerMutex.Unlock()
// Create a V1 config json file and store it // Create a V1 config json file and store it
configJSON := "{ \"version\":\"1\", \"accessKeyId\":\"abcde\", \"secretAccessKey\":\"abcdefgh\"}" configJSON := "{ \"version\":\"1\", \"accessKeyId\":\"abcde\", \"secretAccessKey\":\"abcdefgh\"}"
configPath := rootPath + "/fsUsers.json" configPath := rootPath + "/fsUsers.json"
@@ -45,13 +56,14 @@ func TestServerConfigMigrateV1(t *testing.T) {
if err := migrateConfig(); err != nil { if err := migrateConfig(); err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
// Check if config v1 is removed from filesystem // Check if config v1 is removed from filesystem
if _, err := os.Stat(configPath); err == nil || !os.IsNotExist(err) { if _, err := os.Stat(configPath); err == nil || !os.IsNotExist(err) {
t.Fatal("Config V1 file is not purged") t.Fatal("Config V1 file is not purged")
} }
// Initialize server config and check again if everything is fine // Initialize server config and check again if everything is fine
if err := loadConfig(); 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)
} }
} }
@@ -59,20 +71,13 @@ func TestServerConfigMigrateV1(t *testing.T) {
// Test if all migrate code returns nil when config file does not // Test if all migrate code returns nil when config file does not
// exist // exist
func TestServerConfigMigrateInexistentConfig(t *testing.T) { func TestServerConfigMigrateInexistentConfig(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil { if err != nil {
t.Fatalf("Init Test config failed") t.Fatal(err)
} }
// remove the root directory after the test ends.
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
setConfigDir(rootPath) setConfigDir(rootPath)
configPath := rootPath + "/" + minioConfigFile
// Remove config file
if err := os.Remove(configPath); err != nil {
t.Fatal("Unexpected error: ", err)
}
if err := migrateV2ToV3(); err != nil { if err := migrateV2ToV3(); err != nil {
t.Fatal("migrate v2 to v3 should succeed when no config file is found") t.Fatal("migrate v2 to v3 should succeed when no config file is found")
@@ -134,18 +139,45 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
if err := migrateV21ToV22(); err != nil { if err := migrateV21ToV22(); err != nil {
t.Fatal("migrate v21 to v22 should succeed when no config file is found") t.Fatal("migrate v21 to v22 should succeed when no config file is found")
} }
if err := migrateV22ToV23(); err != nil {
t.Fatal("migrate v22 to v23 should succeed when no config file is found")
}
if err := migrateV23ToV24(); err != nil {
t.Fatal("migrate v23 to v24 should succeed when no config file is found")
}
if err := migrateV24ToV25(); err != nil {
t.Fatal("migrate v24 to v25 should succeed when no config file is found")
}
if err := migrateV25ToV26(); err != nil {
t.Fatal("migrate v25 to v26 should succeed when no config file is found")
}
if err := migrateV26ToV27(); err != nil {
t.Fatal("migrate v26 to v27 should succeed when no config file is found")
}
if err := migrateV27ToV28(); err != nil {
t.Fatal("migrate v27 to v28 should succeed when no config file is found")
}
} }
// Test if a config migration from v2 to v23 is successfully done // Test if a config migration from v2 to v28 is successfully done
func TestServerConfigMigrateV2toV23(t *testing.T) { func TestServerConfigMigrateV2toV28(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil { if err != nil {
t.Fatalf("Init Test config failed") t.Fatal(err)
} }
// remove the root directory after the test ends.
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
setConfigDir(rootPath) setConfigDir(rootPath)
objLayer, fsDir, err := prepareFS()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(fsDir)
globalObjLayerMutex.Lock()
globalObjectAPI = objLayer
globalObjLayerMutex.Unlock()
configPath := rootPath + "/" + minioConfigFile configPath := rootPath + "/" + minioConfigFile
// Create a corrupted config file // Create a corrupted config file
@@ -171,8 +203,12 @@ func TestServerConfigMigrateV2toV23(t *testing.T) {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
if err := migrateConfigToMinioSys(); 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(); err != nil { if err := loadConfig(newObjectLayerFn()); err != nil {
t.Fatalf("Unable to initialize from updated config file %s", err) t.Fatalf("Unable to initialize from updated config file %s", err)
} }
@@ -193,13 +229,11 @@ func TestServerConfigMigrateV2toV23(t *testing.T) {
// Test if all migrate code returns error with corrupted config files // Test if all migrate code returns error with corrupted config files
func TestServerConfigMigrateFaultyConfig(t *testing.T) { func TestServerConfigMigrateFaultyConfig(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil { if err != nil {
t.Fatalf("Init Test config failed") t.Fatal(err)
} }
// remove the root directory after the test ends.
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
setConfigDir(rootPath) setConfigDir(rootPath)
configPath := rootPath + "/" + minioConfigFile configPath := rootPath + "/" + minioConfigFile
@@ -272,17 +306,31 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
if err := migrateV22ToV23(); err == nil { if err := migrateV22ToV23(); err == nil {
t.Fatal("migrateConfigV22ToV23() should fail with a corrupted json") t.Fatal("migrateConfigV22ToV23() should fail with a corrupted json")
} }
if err := migrateV23ToV24(); err == nil {
t.Fatal("migrateConfigV23ToV24() should fail with a corrupted json")
}
if err := migrateV24ToV25(); err == nil {
t.Fatal("migrateConfigV24ToV25() should fail with a corrupted json")
}
if err := migrateV25ToV26(); err == nil {
t.Fatal("migrateConfigV25ToV26() should fail with a corrupted json")
}
if err := migrateV26ToV27(); err == nil {
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
}
if err := migrateV27ToV28(); err == nil {
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
}
} }
// Test if all migrate code returns error with corrupted config files // Test if all migrate code returns error with corrupted config files
func TestServerConfigMigrateCorruptedConfig(t *testing.T) { func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion) rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil { if err != nil {
t.Fatalf("Init Test config failed") t.Fatal(err)
} }
// remove the root directory after the test ends.
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
setConfigDir(rootPath) setConfigDir(rootPath)
configPath := rootPath + "/" + minioConfigFile configPath := rootPath + "/" + minioConfigFile
+78 -3
View File
@@ -19,6 +19,7 @@ package cmd
import ( import (
"sync" "sync"
"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/quick" "github.com/minio/minio/pkg/quick"
@@ -657,9 +658,6 @@ type serverConfigV25 struct {
// serverConfigV26 is just like version '25', stores additionally // serverConfigV26 is just like version '25', stores additionally
// cache max use value in 'CacheConfig'. // cache max use value in 'CacheConfig'.
//
// IMPORTANT NOTE: When updating this struct make sure that
// serverConfig.ConfigDiff() is updated as necessary.
type serverConfigV26 struct { type serverConfigV26 struct {
quick.Config `json:"-"` // ignore interfaces quick.Config `json:"-"` // ignore interfaces
@@ -681,3 +679,80 @@ type serverConfigV26 struct {
// Notification queue configuration. // Notification queue configuration.
Notify notifier `json:"notify"` Notify notifier `json:"notify"`
} }
type loggerConsole struct {
Enabled bool `json:"enabled"`
}
type loggerHTTP struct {
Enabled bool `json:"enabled"`
Endpoint string `json:"endpoint"`
}
type loggerConfig struct {
Console loggerConsole `json:"console"`
HTTP map[string]loggerHTTP `json:"http"`
}
// serverConfigV27 is just like version '26', stores additionally
// the logger field
//
// IMPORTANT NOTE: When updating this struct make sure that
// serverConfig.ConfigDiff() is updated as necessary.
type serverConfigV27 struct {
quick.Config `json:"-"` // ignore interfaces
Version string `json:"version"`
// S3 API configuration.
Credential auth.Credentials `json:"credential"`
Region string `json:"region"`
Browser BoolFlag `json:"browser"`
Worm BoolFlag `json:"worm"`
Domain string `json:"domain"`
// Storage class configuration
StorageClass storageClassConfig `json:"storageclass"`
// Cache configuration
Cache CacheConfig `json:"cache"`
// Notification queue configuration.
Notify notifier `json:"notify"`
// Logger configuration
Logger loggerConfig `json:"logger"`
}
// serverConfigV28 is just like version '27', additionally
// storing KMS config
//
// IMPORTANT NOTE: When updating this struct make sure that
// serverConfig.ConfigDiff() is updated as necessary.
type serverConfigV28 struct {
quick.Config `json:"-"` // ignore interfaces
Version string `json:"version"`
// S3 API configuration.
Credential auth.Credentials `json:"credential"`
Region string `json:"region"`
Browser BoolFlag `json:"browser"`
Worm BoolFlag `json:"worm"`
Domain string `json:"domain"`
// 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"`
}
+267
View File
@@ -0,0 +1,267 @@
/*
* 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"
"encoding/json"
"errors"
"io"
"io/ioutil"
"path"
"runtime"
"time"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/quick"
)
const (
minioConfigPrefix = "config"
// Minio configuration file.
minioConfigFile = "config.json"
)
func saveServerConfig(objAPI ObjectLayer, config *serverConfig) error {
if err := quick.CheckData(config); err != nil {
return err
}
data, err := json.Marshal(config)
if err != nil {
return err
}
configFile := path.Join(minioConfigPrefix, minioConfigFile)
if globalEtcdClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
_, err := globalEtcdClient.Put(ctx, configFile, string(data))
defer cancel()
return err
}
return saveConfig(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) {
var configData []byte
var err error
configFile := path.Join(minioConfigPrefix, minioConfigFile)
if globalEtcdClient != nil {
configData, err = readConfigEtcd(configFile)
} else {
var reader io.Reader
reader, err = readConfig(ctx, objAPI, configFile)
if err != nil {
return nil, err
}
configData, err = ioutil.ReadAll(reader)
}
if err != nil {
return nil, err
}
if runtime.GOOS == "windows" {
configData = bytes.Replace(configData, []byte("\r\n"), []byte("\n"), -1)
}
if err = quick.CheckDuplicateKeys(string(configData)); err != nil {
return nil, err
}
var config = &serverConfig{}
if err := json.Unmarshal(configData, config); err != nil {
return nil, err
}
if err := quick.CheckData(config); err != nil {
return nil, err
}
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); err != nil {
if isErrObjectNotFound(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)
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, ""); err != nil {
// Ignore if err is ObjectNotFound or IncompleteBody when bucket is not configured with notification
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.
type ConfigSys struct{}
// Load - load config.json.
func (sys *ConfigSys) Load(objAPI ObjectLayer) error {
return sys.Init(objAPI)
}
// Init - initializes config system from config.json.
func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
if objAPI == nil {
return errInvalidArgument
}
return loadConfig(objAPI)
}
// NewConfigSys - creates new config system object.
func NewConfigSys() *ConfigSys {
return &ConfigSys{}
}
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/minio.json'
func migrateConfigToMinioSys() error {
// Construct path to config.json for the given bucket.
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
transactionConfigFile := configFile + ".transaction"
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
// and configFile, take a transaction lock to avoid race.
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
if err := objLock.GetLock(globalOperationTimeout); err != nil {
return err
}
defer objLock.Unlock()
// Verify if backend already has the file.
if err := checkServerConfig(context.Background(), newObjectLayerFn()); err != errConfigNotFound {
return err
} // if errConfigNotFound proceed to migrate..
var config = &serverConfig{}
if _, err := Load(getConfigFile(), config); err != nil {
return err
}
return saveServerConfig(newObjectLayerFn(), config)
}
// Initialize and load config from remote etcd or local config directory
func initConfig() {
if globalEtcdClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
cancel()
if err == nil && resp.Count > 0 {
logger.FatalIf(migrateConfig(), "Config migration failed")
}
} else {
if isFile(getConfigFile()) {
logger.FatalIf(migrateConfig(), "Config migration failed")
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
if err := migrateConfigToMinioSys(); err != nil {
logger.Fatal(err, "Unable to migrate 'config.json' to '.minio.sys/config/config.json'")
}
}
}
objAPI := newObjectLayerFn()
if objAPI == nil {
logger.FatalIf(errServerNotInitialized, "Server is not initialized yet unable to proceed")
}
if err := checkServerConfig(context.Background(), objAPI); err != nil {
if err == errConfigNotFound {
// Config file does not exist, we create it fresh and return upon success.
logger.FatalIf(newConfig(objAPI), "Unable to initialize minio config for the first time")
logger.Info("Created minio configuration file successfully at " + getConfigDir())
} else {
logger.FatalIf(err, "Unable to load the configuration file")
}
}
logger.FatalIf(migrateMinioSysConfig(objAPI), "Config migration failed for minio.sys config")
logger.FatalIf(loadConfig(objAPI), "Unable to load the configuration file")
}
+20
View File
@@ -0,0 +1,20 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
// KMSConfig has the KMS config for hashicorp vault
type KMSConfig struct {
Vault VaultConfig `json:"vault"`
}
+6 -6
View File
@@ -32,7 +32,7 @@
// Input: ClientKey, bucket, object, metadata, object_data // Input: ClientKey, bucket, object, metadata, object_data
// - IV := Random({0,1}²⁵⁶) // - IV := Random({0,1}²⁵⁶)
// - ObjectKey := SHA256(ClientKey || Random({0,1}²⁵⁶)) // - ObjectKey := SHA256(ClientKey || Random({0,1}²⁵⁶))
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || bucket || object) // - KeyEncKey := HMAC-SHA256(ClientKey, IV || 'SSE-C' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey) // - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data) // - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
// - metadata <- IV // - metadata <- IV
@@ -43,7 +43,7 @@
// Input: ClientKey, bucket, object, metadata, enc_object_data // Input: ClientKey, bucket, object, metadata, enc_object_data
// - IV <- metadata // - IV <- metadata
// - SealedKey <- metadata // - SealedKey <- metadata
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || bucket || object) // - KeyEncKey := HMAC-SHA256(ClientKey, IV || 'SSE-C' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey) // - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data) // - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
// Output: object_data // Output: object_data
@@ -64,7 +64,7 @@
// Input: MasterKey, bucket, object, metadata, object_data // Input: MasterKey, bucket, object, metadata, object_data
// - IV := Random({0,1}²⁵⁶) // - IV := Random({0,1}²⁵⁶)
// - ObjectKey := SHA256(MasterKey || Random({0,1}²⁵⁶)) // - ObjectKey := SHA256(MasterKey || Random({0,1}²⁵⁶))
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || bucket || object) // - KeyEncKey := HMAC-SHA256(MasterKey, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey) // - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data) // - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
// - metadata <- IV // - metadata <- IV
@@ -75,7 +75,7 @@
// Input: MasterKey, bucket, object, metadata, enc_object_data // Input: MasterKey, bucket, object, metadata, enc_object_data
// - IV <- metadata // - IV <- metadata
// - SealedKey <- metadata // - SealedKey <- metadata
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || bucket || object) // - KeyEncKey := HMAC-SHA256(MasterKey, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey) // - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data) // - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
// Output: object_data // Output: object_data
@@ -92,7 +92,7 @@
// - Key, EncKey := Generate(KeyID) // - Key, EncKey := Generate(KeyID)
// - IV := Random({0,1}²⁵⁶) // - IV := Random({0,1}²⁵⁶)
// - ObjectKey := SHA256(Key, Random({0,1}²⁵⁶)) // - ObjectKey := SHA256(Key, Random({0,1}²⁵⁶))
// - KeyEncKey := HMAC-SHA256(Key, IV || bucket || object) // - KeyEncKey := HMAC-SHA256(Key, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey) // - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data) // - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
// - metadata <- IV // - metadata <- IV
@@ -108,7 +108,7 @@
// - IV <- metadata // - IV <- metadata
// - SealedKey <- metadata // - SealedKey <- metadata
// - Key := Unseal(KeyID, EncKey) // - Key := Unseal(KeyID, EncKey)
// - KeyEncKey := HMAC-SHA256(Key, IV || bucket || object) // - KeyEncKey := HMAC-SHA256(Key, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey) // - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data) // - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
// Output: object_data // Output: object_data
+44
View File
@@ -16,8 +16,52 @@ package crypto
import "errors" import "errors"
// Error is the generic type for any error happening during decrypting
// an object. It indicates that the object itself or its metadata was
// modified accidentally or maliciously.
type Error struct{ msg string }
func (e Error) Error() string { return e.msg }
var ( var (
// ErrInvalidEncryptionMethod indicates that the specified SSE encryption method // ErrInvalidEncryptionMethod indicates that the specified SSE encryption method
// is not supported. // is not supported.
ErrInvalidEncryptionMethod = errors.New("The encryption method is not supported") ErrInvalidEncryptionMethod = errors.New("The encryption method is not supported")
// ErrInvalidCustomerAlgorithm indicates that the specified SSE-C algorithm
// is not supported.
ErrInvalidCustomerAlgorithm = errors.New("The SSE-C algorithm is not supported")
// ErrMissingCustomerKey indicates that the HTTP headers contains no SSE-C client key.
ErrMissingCustomerKey = errors.New("The SSE-C request is missing the customer key")
// ErrMissingCustomerKeyMD5 indicates that the HTTP headers contains no SSE-C client key
// MD5 checksum.
ErrMissingCustomerKeyMD5 = errors.New("The SSE-C request is missing the customer key MD5")
// ErrInvalidCustomerKey indicates that the SSE-C client key is not valid - e.g. not a
// base64-encoded string or not 256 bits long.
ErrInvalidCustomerKey = errors.New("The SSE-C client key is invalid")
// ErrCustomerKeyMD5Mismatch indicates that the SSE-C key MD5 does not match the
// computed MD5 sum. This means that the client provided either the wrong key for
// a certain MD5 checksum or the wrong MD5 for a certain key.
ErrCustomerKeyMD5Mismatch = errors.New("The provided SSE-C key MD5 does not match the computed MD5 of the SSE-C key")
// ErrIncompatibleEncryptionMethod indicates that both SSE-C headers and SSE-S3 headers were specified, and are incompatible
// The client needs to remove the SSE-S3 header or the SSE-C headers
ErrIncompatibleEncryptionMethod = errors.New("Server side encryption specified with both SSE-C and SSE-S3 headers")
)
var (
errMissingInternalIV = Error{"The object metadata is missing the internal encryption IV"}
errMissingInternalSealAlgorithm = Error{"The object metadata is missing the internal seal algorithm"}
errInvalidInternalIV = Error{"The internal encryption IV is malformed"}
errInvalidInternalSealAlgorithm = Error{"The internal seal algorithm is invalid and not supported"}
)
var (
// errOutOfEntropy indicates that the a source of randomness (PRNG) wasn't able
// to produce enough random data. This is fatal error and should cause a panic.
errOutOfEntropy = errors.New("Unable to read enough randomness from the system")
) )
+172 -7
View File
@@ -15,16 +15,64 @@
package crypto package crypto
import ( import (
"bytes"
"crypto/md5"
"encoding/base64"
"net/http" "net/http"
"strings"
) )
// SSEHeader is the general AWS SSE HTTP header key. // SSEHeader is the general AWS SSE HTTP header key.
const SSEHeader = "X-Amz-Server-Side-Encryption" const SSEHeader = "X-Amz-Server-Side-Encryption"
// SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header. const (
// For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html // SSEKmsID is the HTTP header key referencing the SSE-KMS
// For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html // key ID.
const SSEAlgorithmAES256 = "AES256" SSEKmsID = SSEHeader + "-Aws-Kms-Key-Id"
// SSEKmsContext is the HTTP header key referencing the
// SSE-KMS encryption context.
SSEKmsContext = SSEHeader + "-Context"
)
const (
// SSECAlgorithm is the HTTP header key referencing
// the SSE-C algorithm.
SSECAlgorithm = SSEHeader + "-Customer-Algorithm"
// SSECKey is the HTTP header key referencing the
// SSE-C client-provided key..
SSECKey = SSEHeader + "-Customer-Key"
// SSECKeyMD5 is the HTTP header key referencing
// the MD5 sum of the client-provided key.
SSECKeyMD5 = SSEHeader + "-Customer-Key-Md5"
)
const (
// SSECopyAlgorithm is the HTTP header key referencing
// the SSE-C algorithm for SSE-C copy requests.
SSECopyAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
// SSECopyKey is the HTTP header key referencing the SSE-C
// client-provided key for SSE-C copy requests.
SSECopyKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
// SSECopyKeyMD5 is the HTTP header key referencing the
// MD5 sum of the client key for SSE-C copy requests.
SSECopyKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5"
)
const (
// SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header.
// For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html
// For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html
SSEAlgorithmAES256 = "AES256"
// SSEAlgorithmKMS is the value of 'X-Amz-Server-Side-Encryption' for SSE-KMS.
// See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html
SSEAlgorithmKMS = "aws:kms"
)
// 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.
@@ -36,14 +84,131 @@ type s3 struct{}
// the S3 client requests SSE-S3. // the S3 client requests SSE-S3.
func (s3) IsRequested(h http.Header) bool { func (s3) IsRequested(h http.Header) bool {
_, ok := h[SSEHeader] _, ok := h[SSEHeader]
return ok return ok && strings.ToLower(h.Get(SSEHeader)) != SSEAlgorithmKMS // Return only true if the SSE header is specified and does not contain the SSE-KMS value
} }
// Parse parses the SSE-S3 related HTTP headers and checks // ParseHTTP parses the SSE-S3 related HTTP headers and checks
// whether they contain valid values. // whether they contain valid values.
func (s3) Parse(h http.Header) (err error) { func (s3) ParseHTTP(h http.Header) (err error) {
if h.Get(SSEHeader) != SSEAlgorithmAES256 { if h.Get(SSEHeader) != SSEAlgorithmAES256 {
err = ErrInvalidEncryptionMethod err = ErrInvalidEncryptionMethod
} }
return return
} }
// S3KMS represents AWS SSE-KMS. It provides functionality to
// handle SSE-KMS requests.
var S3KMS = s3KMS{}
type s3KMS struct{}
// IsRequested returns true if the HTTP headers indicates that
// the S3 client requests SSE-KMS.
func (s3KMS) IsRequested(h http.Header) bool {
if _, ok := h[SSEKmsID]; ok {
return true
}
if _, ok := h[SSEKmsContext]; ok {
return true
}
if _, ok := h[SSEHeader]; ok {
return strings.ToUpper(h.Get(SSEHeader)) != SSEAlgorithmAES256 // Return only true if the SSE header is specified and does not contain the SSE-S3 value
}
return false
}
var (
// SSEC represents AWS SSE-C. It provides functionality to handle
// SSE-C requests.
SSEC = ssec{}
// SSECopy represents AWS SSE-C for copy requests. It provides
// functionality to handle SSE-C copy requests.
SSECopy = ssecCopy{}
)
type ssec struct{}
type ssecCopy struct{}
// IsRequested returns true if the HTTP headers contains
// at least one SSE-C header. SSE-C copy headers are ignored.
func (ssec) IsRequested(h http.Header) bool {
if _, ok := h[SSECAlgorithm]; ok {
return true
}
if _, ok := h[SSECKey]; ok {
return true
}
if _, ok := h[SSECKeyMD5]; ok {
return true
}
return false
}
// IsRequested returns true if the HTTP headers contains
// at least one SSE-C copy header. Regular SSE-C headers
// are ignored.
func (ssecCopy) IsRequested(h http.Header) bool {
if _, ok := h[SSECopyAlgorithm]; ok {
return true
}
if _, ok := h[SSECopyKey]; ok {
return true
}
if _, ok := h[SSECopyKeyMD5]; ok {
return true
}
return false
}
// ParseHTTP parses the SSE-C headers and returns the SSE-C client key
// on success. SSE-C copy headers are ignored.
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 {
return key, ErrInvalidCustomerAlgorithm
}
if h.Get(SSECKey) == "" {
return key, ErrMissingCustomerKey
}
if h.Get(SSECKeyMD5) == "" {
return key, ErrMissingCustomerKeyMD5
}
clientKey, err := base64.StdEncoding.DecodeString(h.Get(SSECKey))
if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long
return key, ErrInvalidCustomerKey
}
keyMD5, err := base64.StdEncoding.DecodeString(h.Get(SSECKeyMD5))
if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) {
return key, ErrCustomerKeyMD5Mismatch
}
copy(key[:], clientKey)
return key, nil
}
// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key
// on success. Regular SSE-C headers are ignored.
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 {
return key, ErrInvalidCustomerAlgorithm
}
if h.Get(SSECopyKey) == "" {
return key, ErrMissingCustomerKey
}
if h.Get(SSECopyKeyMD5) == "" {
return key, ErrMissingCustomerKeyMD5
}
clientKey, err := base64.StdEncoding.DecodeString(h.Get(SSECopyKey))
if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long
return key, ErrInvalidCustomerKey
}
keyMD5, err := base64.StdEncoding.DecodeString(h.Get(SSECopyKeyMD5))
if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) {
return key, ErrCustomerKeyMD5Mismatch
}
copy(key[:], clientKey)
return key, nil
}
+292 -9
View File
@@ -19,25 +19,60 @@ import (
"testing" "testing"
) )
var isRequestedTests = []struct { var kmsIsRequestedTests = []struct {
Header http.Header Header http.Header
Expected bool Expected bool
}{ }{
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: true}, // 0 {Header: http.Header{}, Expected: false}, // 0
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, Expected: true}, // 1 {Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"aws:kms"}}, Expected: true}, // 1
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, Expected: true}, // 2 {Header: http.Header{"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{"0839-9047947-844842874-481"}}, Expected: true}, // 2
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, Expected: false}, // 3 {Header: http.Header{"X-Amz-Server-Side-Encryption-Context": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
{
Header: http.Header{
"X-Amz-Server-Side-Encryption": []string{""},
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{""},
"X-Amz-Server-Side-Encryption-Context": []string{""},
},
Expected: true,
}, // 4
{
Header: http.Header{
"X-Amz-Server-Side-Encryption": []string{"AES256"},
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{""},
},
Expected: true,
}, // 5
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: false}, // 6
}
func TestKMSIsRequested(t *testing.T) {
for i, test := range kmsIsRequestedTests {
if got := S3KMS.IsRequested(test.Header); got != test.Expected {
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
}
}
}
var s3IsRequestedTests = []struct {
Header http.Header
Expected bool
}{
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: true}, // 0
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, Expected: true}, // 1
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, Expected: true}, // 2
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, Expected: false}, // 3
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{SSEAlgorithmKMS}}, Expected: false}, // 4
} }
func TestS3IsRequested(t *testing.T) { func TestS3IsRequested(t *testing.T) {
for i, test := range isRequestedTests { for i, test := range s3IsRequestedTests {
if got := S3.IsRequested(test.Header); got != test.Expected { if got := S3.IsRequested(test.Header); got != test.Expected {
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got) t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
} }
} }
} }
var parseTests = []struct { var s3ParseTests = []struct {
Header http.Header Header http.Header
ExpectedErr error ExpectedErr error
}{ }{
@@ -48,9 +83,257 @@ var parseTests = []struct {
} }
func TestS3Parse(t *testing.T) { func TestS3Parse(t *testing.T) {
for i, test := range parseTests { for i, test := range s3ParseTests {
if err := S3.Parse(test.Header); err != test.ExpectedErr { if err := S3.ParseHTTP(test.Header); err != test.ExpectedErr {
t.Errorf("Test %d: Wanted '%v' but got '%v'", i, test.ExpectedErr, err) t.Errorf("Test %d: Wanted '%v' but got '%v'", i, test.ExpectedErr, err)
} }
} }
} }
var ssecIsRequestedTests = []struct {
Header http.Header
Expected bool
}{
{Header: http.Header{}, Expected: false}, // 0
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
{
Header: http.Header{
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{""},
"X-Amz-Server-Side-Encryption-Customer-Key": []string{""},
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{""},
},
Expected: true,
}, // 4
{
Header: 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=="},
},
Expected: true,
}, // 5
{
Header: 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=="},
},
Expected: false,
}, // 6
}
func TestSSECIsRequested(t *testing.T) {
for i, test := range ssecIsRequestedTests {
if got := SSEC.IsRequested(test.Header); got != test.Expected {
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
}
}
}
var ssecCopyIsRequestedTests = []struct {
Header http.Header
Expected bool
}{
{Header: http.Header{}, Expected: false}, // 0
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
{
Header: http.Header{
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{""},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{""},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{""},
},
Expected: true,
}, // 4
{
Header: 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=="},
},
Expected: true,
}, // 5
{
Header: 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=="},
},
Expected: false,
}, // 6
}
func TestSSECopyIsRequested(t *testing.T) {
for i, test := range ssecCopyIsRequestedTests {
if got := SSECopy.IsRequested(test.Header); got != test.Expected {
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
}
}
}
var ssecParseTests = []struct {
Header http.Header
ExpectedErr error
}{
{
Header: 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=="},
},
ExpectedErr: nil, // 0
},
{
Header: http.Header{
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES-256"}, // invalid algorithm
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrInvalidCustomerAlgorithm, // 1
},
{
Header: http.Header{
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
"X-Amz-Server-Side-Encryption-Customer-Key": []string{""}, // no client key
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrMissingCustomerKey, // 2
},
{
Header: http.Header{
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRr.ZXltdXN0cHJvdmlkZWQ="}, // invalid key
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrInvalidCustomerKey, // 3
},
{
Header: 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{""}, // no key MD5
},
ExpectedErr: ErrMissingCustomerKeyMD5, // 4
},
{
Header: http.Header{
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"DzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, // wrong client key
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 5
},
{
Header: 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=="}, // wrong key MD5
},
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 6
},
}
func TestSSECParse(t *testing.T) {
var zeroKey [32]byte
for i, test := range ssecParseTests {
key, err := SSEC.ParseHTTP(test.Header)
if err != test.ExpectedErr {
t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err)
}
if err != nil && key != zeroKey {
t.Errorf("Test %d: parsing failed and client key is not zero key", i)
}
if err == nil && key == zeroKey {
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)
}
}
}
var ssecCopyParseTests = []struct {
Header http.Header
ExpectedErr error
}{
{
Header: 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=="},
},
ExpectedErr: nil, // 0
},
{
Header: http.Header{
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES-256"}, // invalid algorithm
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrInvalidCustomerAlgorithm, // 1
},
{
Header: http.Header{
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{""}, // no client key
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrMissingCustomerKey, // 2
},
{
Header: http.Header{
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRr.ZXltdXN0cHJvdmlkZWQ="}, // invalid key
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrInvalidCustomerKey, // 3
},
{
Header: 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{""}, // no key MD5
},
ExpectedErr: ErrMissingCustomerKeyMD5, // 4
},
{
Header: http.Header{
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"DzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, // wrong client key
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
},
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 5
},
{
Header: 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=="}, // wrong key MD5
},
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 6
},
}
func TestSSECopyParse(t *testing.T) {
var zeroKey [32]byte
for i, test := range ssecCopyParseTests {
key, err := SSECopy.ParseHTTP(test.Header)
if err != test.ExpectedErr {
t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err)
}
if err != nil && key != zeroKey {
t.Errorf("Test %d: parsing failed and client key is not zero key", i)
}
if err == nil && key == zeroKey {
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)
}
}
}
+71 -22
View File
@@ -21,8 +21,9 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"io" "io"
"path/filepath" "path"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd" sha256 "github.com/minio/sha256-simd"
@@ -34,50 +35,98 @@ import (
type ObjectKey [32]byte type ObjectKey [32]byte
// GenerateKey generates a unique ObjectKey from a 256 bit external key // GenerateKey generates a unique ObjectKey from a 256 bit external key
// and a source of randomness. If random is nil the default PRNG of system // and a source of randomness. If random is nil the default PRNG of the
// (crypto/rand) is used. // system (crypto/rand) is used.
func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) { func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) {
if random == nil { if random == nil {
random = rand.Reader random = rand.Reader
} }
var nonce [32]byte var nonce [32]byte
if _, err := io.ReadFull(random, nonce[:]); err != nil { if _, err := io.ReadFull(random, nonce[:]); err != nil {
logger.CriticalIf(context.Background(), errors.New("Unable to read enough randomness from the system")) logger.CriticalIf(context.Background(), errOutOfEntropy)
} }
sha := sha256.New() sha := sha256.New()
sha.Write(extKey[:]) sha.Write(extKey[:])
sha.Write(nonce[:]) sha.Write(nonce[:])
sha.Sum(key[:0]) sha.Sum(key[:0])
return return key
}
// GenerateIV generates a new random 256 bit IV from the provided source
// of randomness. If random is nil the default PRNG of the system
// (crypto/rand) is used.
func GenerateIV(random io.Reader) (iv [32]byte) {
if random == nil {
random = rand.Reader
}
if _, err := io.ReadFull(random, iv[:]); err != nil {
logger.CriticalIf(context.Background(), errOutOfEntropy)
}
return iv
}
// SealedKey represents a sealed object key. It can be stored
// at an untrusted location.
type SealedKey struct {
Key [64]byte // The encrypted and authenticted object-key.
IV [32]byte // The random IV used to encrypt the object-key.
Algorithm string // The sealing algorithm used to encrypt the object key.
} }
// Seal encrypts the ObjectKey using the 256 bit external key and IV. The sealed // Seal encrypts the ObjectKey using the 256 bit external key and IV. The sealed
// key is also cryptographically bound to the object's path (bucket/object). // key is also cryptographically bound to the object's path (bucket/object) and the
func (key ObjectKey) Seal(extKey, iv [32]byte, bucket, object string) []byte { // domain (SSE-C or SSE-S3).
var sealedKey bytes.Buffer func (key ObjectKey) Seal(extKey, iv [32]byte, domain, bucket, object string) SealedKey {
var (
sealingKey [32]byte
encryptedKey bytes.Buffer
)
mac := hmac.New(sha256.New, extKey[:]) mac := hmac.New(sha256.New, extKey[:])
mac.Write(iv[:]) mac.Write(iv[:])
mac.Write([]byte(filepath.Join(bucket, object))) mac.Write([]byte(domain))
mac.Write([]byte(SealAlgorithm))
if n, err := sio.Encrypt(&sealedKey, bytes.NewReader(key[:]), sio.Config{Key: mac.Sum(nil)}); n != 64 || err != nil { mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
mac.Sum(sealingKey[:0])
if n, err := sio.Encrypt(&encryptedKey, bytes.NewReader(key[:]), sio.Config{Key: sealingKey[:]}); n != 64 || err != nil {
logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key")) logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key"))
} }
return sealedKey.Bytes() sealedKey := SealedKey{
IV: iv,
Algorithm: SealAlgorithm,
}
copy(sealedKey.Key[:], encryptedKey.Bytes())
return sealedKey
} }
// Unseal decrypts a sealed key using the 256 bit external key and IV. Since the sealed key // Unseal decrypts a sealed key using the 256 bit external key. Since the sealed key
// is cryptographically bound to the object's path the same bucket/object as during sealing // may be cryptographically bound to the object's path the same bucket/object as during sealing
// must be provided. On success the ObjectKey contains the decrypted sealed key. // must be provided. On success the ObjectKey contains the decrypted sealed key.
func (key *ObjectKey) Unseal(sealedKey []byte, extKey, iv [32]byte, bucket, object string) error { func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucket, object string) error {
var unsealedKey bytes.Buffer var (
mac := hmac.New(sha256.New, extKey[:]) unsealConfig sio.Config
mac.Write(iv[:]) decryptedKey bytes.Buffer
mac.Write([]byte(filepath.Join(bucket, object))) )
switch sealedKey.Algorithm {
default:
return Error{fmt.Sprintf("The sealing algorithm '%s' is not supported", sealedKey.Algorithm)}
case SealAlgorithm:
mac := hmac.New(sha256.New, extKey[:])
mac.Write(sealedKey.IV[:])
mac.Write([]byte(domain))
mac.Write([]byte(SealAlgorithm))
mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
unsealConfig = sio.Config{MinVersion: sio.Version20, Key: mac.Sum(nil)}
case InsecureSealAlgorithm:
sha := sha256.New()
sha.Write(extKey[:])
sha.Write(sealedKey.IV[:])
unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil)}
}
if n, err := sio.Decrypt(&unsealedKey, bytes.NewReader(sealedKey), sio.Config{Key: mac.Sum(nil)}); n != 32 || err != nil { if n, err := sio.Decrypt(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil {
return err // TODO(aead): upgrade sio to use sio.Error return err // TODO(aead): upgrade sio to use sio.Error
} }
copy(key[:], unsealedKey.Bytes()) copy(key[:], decryptedKey.Bytes())
return nil return nil
} }
@@ -89,5 +138,5 @@ func (key ObjectKey) DerivePartKey(id uint32) (partKey [32]byte) {
mac := hmac.New(sha256.New, key[:]) mac := hmac.New(sha256.New, key[:])
mac.Write(bin[:]) mac.Write(bin[:])
mac.Sum(partKey[:0]) mac.Sum(partKey[:0])
return return partKey
} }
+62 -21
View File
@@ -20,6 +20,8 @@ import (
"encoding/hex" "encoding/hex"
"io" "io"
"testing" "testing"
"github.com/minio/minio/cmd/logger"
) )
var shortRandom = func(limit int64) io.Reader { return io.LimitReader(rand.Reader, limit) } var shortRandom = func(limit int64) io.Reader { return io.LimitReader(rand.Reader, limit) }
@@ -37,14 +39,18 @@ var generateKeyTests = []struct {
Random io.Reader Random io.Reader
ShouldPass bool ShouldPass bool
}{ }{
{ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0 {ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0
{ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1 {ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1
{ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2 {ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2
// {ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3 See: https://github.com/minio/minio/issues/6064 {ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3
} }
func TestGenerateKey(t *testing.T) { func TestGenerateKey(t *testing.T) {
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
logger.Disable = true
for i, test := range generateKeyTests { for i, test := range generateKeyTests {
i, test := i, test
func() { func() {
defer recoverTest(i, test.ShouldPass, t) defer recoverTest(i, test.ShouldPass, t)
key := GenerateKey(test.ExtKey, test.Random) key := GenerateKey(test.ExtKey, test.Random)
@@ -55,38 +61,64 @@ func TestGenerateKey(t *testing.T) {
} }
} }
var sealUnsealKeyTests = []struct { var generateIVTests = []struct {
SealExtKey, SealIV [32]byte Random io.Reader
SealBucket, SealObject string ShouldPass bool
}{
{Random: nil, ShouldPass: true}, // 0
{Random: rand.Reader, ShouldPass: true}, // 1
{Random: shortRandom(32), ShouldPass: true}, // 2
{Random: shortRandom(31), ShouldPass: false}, // 3
}
UnsealExtKey, UnsealIV [32]byte func TestGenerateIV(t *testing.T) {
UnsealBucket, UnsealObject string defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
logger.Disable = true
for i, test := range generateIVTests {
i, test := i, test
func() {
defer recoverTest(i, test.ShouldPass, t)
iv := GenerateIV(test.Random)
if iv == [32]byte{} {
t.Errorf("Test %d: generated IV is zero IV", i) // check that we generate random and unique IV
}
}()
}
}
var sealUnsealKeyTests = []struct {
SealExtKey, SealIV [32]byte
SealDomain, SealBucket, SealObject string
UnsealExtKey [32]byte
UnsealDomain, UnsealBucket, UnsealObject string
ShouldPass bool ShouldPass bool
}{ }{
{ {
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "bucket", UnsealObject: "object", UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object",
ShouldPass: true, ShouldPass: true,
}, // 0 }, // 0
{ {
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
UnsealExtKey: [32]byte{1}, UnsealIV: [32]byte{0}, UnsealBucket: "bucket", UnsealObject: "object", UnsealExtKey: [32]byte{1}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different ext-key
ShouldPass: false, ShouldPass: false,
}, // 1 }, // 1
{ {
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-S3", SealBucket: "bucket", SealObject: "object",
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{1}, UnsealBucket: "bucket", UnsealObject: "object", UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different domain
ShouldPass: false, ShouldPass: false,
}, // 2 }, // 2
{ {
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "Bucket", UnsealObject: "object", UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "Bucket", UnsealObject: "object", // different bucket
ShouldPass: false, ShouldPass: false,
}, // 3 }, // 3
{ {
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object", SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "bucket", UnsealObject: "Object", UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "Object", // different object
ShouldPass: false, ShouldPass: false,
}, // 4 }, // 4
} }
@@ -94,13 +126,22 @@ var sealUnsealKeyTests = []struct {
func TestSealUnsealKey(t *testing.T) { func TestSealUnsealKey(t *testing.T) {
for i, test := range sealUnsealKeyTests { for i, test := range sealUnsealKeyTests {
key := GenerateKey(test.SealExtKey, rand.Reader) key := GenerateKey(test.SealExtKey, rand.Reader)
sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealBucket, test.SealObject) sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealDomain, test.SealBucket, test.SealObject)
if err := key.Unseal(sealedKey, test.UnsealExtKey, test.UnsealIV, test.UnsealBucket, test.UnsealObject); err == nil && !test.ShouldPass { if err := key.Unseal(test.UnsealExtKey, sealedKey, test.UnsealDomain, test.UnsealBucket, test.UnsealObject); err == nil && !test.ShouldPass {
t.Errorf("Test %d should fail but passed successfully", i) t.Errorf("Test %d should fail but passed successfully", i)
} else if err != nil && test.ShouldPass { } else if err != nil && test.ShouldPass {
t.Errorf("Test %d should pass put failed: %v", i, err) t.Errorf("Test %d should pass put failed: %v", i, err)
} }
} }
// Test legacy InsecureSealAlgorithm
var extKey, iv [32]byte
key := GenerateKey(extKey, rand.Reader)
sealedKey := key.Seal(extKey, iv, "SSE-S3", "bucket", "object")
sealedKey.Algorithm = InsecureSealAlgorithm
if err := key.Unseal(extKey, sealedKey, "SSE-S3", "bucket", "object"); err == nil {
t.Errorf("'%s' test succeeded but it should fail because the legacy algorithm was used", sealedKey.Algorithm)
}
} }
var derivePartKeyTest = []struct { var derivePartKeyTest = []struct {
+138
View File
@@ -0,0 +1,138 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"errors"
"fmt"
"io"
"sort"
"github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd"
"github.com/minio/sio"
)
// Context is a list of key-value pairs cryptographically
// associated with a certain object.
type Context map[string]string
// WriteTo writes the context in a canonical from to w.
// It returns the number of bytes and the first error
// encounter during writing to w, if any.
//
// WriteTo sorts the context keys and writes the sorted
// key-value pairs as canonical JSON object to w.
func (c Context) WriteTo(w io.Writer) (n int64, err error) {
sortedKeys := make(sort.StringSlice, 0, len(c))
for k := range c {
sortedKeys = append(sortedKeys, k)
}
sort.Sort(sortedKeys)
nn, err := io.WriteString(w, "{")
if err != nil {
return n + int64(nn), err
}
n += int64(nn)
for i, k := range sortedKeys {
s := fmt.Sprintf("\"%s\":\"%s\",", k, c[k])
if i == len(sortedKeys)-1 {
s = s[:len(s)-1] // remove last ','
}
nn, err = io.WriteString(w, s)
if err != nil {
return n + int64(nn), err
}
n += int64(nn)
}
nn, err = io.WriteString(w, "}")
return n + int64(nn), err
}
// KMS represents an active and authenticted connection
// to a Key-Management-Service. It supports generating
// data key generation and unsealing of KMS-generated
// data keys.
type KMS interface {
// GenerateKey generates a new random data key using
// the master key referenced by the keyID. It returns
// the plaintext key and the sealed plaintext key
// on success.
//
// The context is cryptographically bound to the
// generated key. The same context must be provided
// again to unseal the generated key.
GenerateKey(keyID string, context Context) (key [32]byte, sealedKey []byte, err error)
// UnsealKey unseals the sealedKey using the master key
// referenced by the keyID. The provided context must
// match the context used to generate the sealed key.
UnsealKey(keyID string, sealedKey []byte, context Context) (key [32]byte, err error)
}
type masterKeyKMS struct {
masterKey [32]byte
}
// NewKMS returns a basic KMS implementation from a single 256 bit master key.
//
// The KMS accepts any keyID but binds the keyID and context cryptographically
// to the generated keys.
func NewKMS(key [32]byte) KMS { return &masterKeyKMS{masterKey: key} }
func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
if _, err = io.ReadFull(rand.Reader, key[:]); err != nil {
logger.CriticalIf(context.Background(), errOutOfEntropy)
}
var (
buffer bytes.Buffer
derivedKey = kms.deriveKey(keyID, ctx)
)
if n, err := sio.Encrypt(&buffer, bytes.NewReader(key[:]), sio.Config{Key: derivedKey[:]}); err != nil || n != 64 {
logger.CriticalIf(context.Background(), errors.New("KMS: unable to encrypt data key"))
}
sealedKey = buffer.Bytes()
return key, sealedKey, nil
}
func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
var (
buffer bytes.Buffer
derivedKey = kms.deriveKey(keyID, ctx)
)
if n, err := sio.Decrypt(&buffer, bytes.NewReader(sealedKey), sio.Config{Key: derivedKey[:]}); err != nil || n != 32 {
return key, err // TODO(aead): upgrade sio to use sio.Error
}
copy(key[:], buffer.Bytes())
return key, nil
}
func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte) {
if context == nil {
context = Context{}
}
mac := hmac.New(sha256.New, kms.masterKey[:])
mac.Write([]byte(keyID))
context.WriteTo(mac)
mac.Sum(key[:0])
return key
}
+84
View File
@@ -0,0 +1,84 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
import (
"bytes"
"path"
"strings"
"testing"
)
var masterKeyKMSTests = []struct {
GenKeyID, UnsealKeyID string
GenContext, UnsealContext Context
ShouldFail bool
}{
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{}, UnsealContext: nil, ShouldFail: false}, // 0
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{}, UnsealContext: Context{}, ShouldFail: false}, // 1
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{"bucket": "object"}, UnsealContext: Context{"bucket": "object"}, ShouldFail: false}, // 2
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"bucket": path.Join("bucket", "object")}, UnsealContext: Context{"bucket": path.Join("bucket", "object")}, ShouldFail: false}, // 3
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"a": "a", "0": "0", "b": "b"}, UnsealContext: Context{"b": "b", "a": "a", "0": "0"}, ShouldFail: false}, // 4
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7e", GenContext: Context{}, UnsealContext: Context{}, ShouldFail: true}, // 5
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{"bucket": "object"}, UnsealContext: Context{"Bucket": "object"}, ShouldFail: true}, // 6
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"bucket": path.Join("bucket", "Object")}, UnsealContext: Context{"bucket": path.Join("bucket", "object")}, ShouldFail: true}, // 7
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"a": "a", "0": "1", "b": "b"}, UnsealContext: Context{"b": "b", "a": "a", "0": "0"}, ShouldFail: true}, // 8
}
func TestMasterKeyKMS(t *testing.T) {
kms := NewKMS([32]byte{})
for i, test := range masterKeyKMSTests {
key, sealedKey, err := kms.GenerateKey(test.GenKeyID, test.GenContext)
if err != nil {
t.Errorf("Test %d: KMS failed to generate key: %v", i, err)
}
unsealedKey, err := kms.UnsealKey(test.UnsealKeyID, sealedKey, test.UnsealContext)
if err != nil && !test.ShouldFail {
t.Errorf("Test %d: KMS failed to unseal the generated key: %v", i, err)
}
if err == nil && test.ShouldFail {
t.Errorf("Test %d: KMS unsealed the generated successfully but should have failed", i)
}
if !test.ShouldFail && !bytes.Equal(key[:], unsealedKey[:]) {
t.Errorf("Test %d: The generated and unsealed key differ", i)
}
}
}
var contextWriteToTests = []struct {
Context Context
ExpectedJSON string
}{
{Context: Context{}, ExpectedJSON: "{}"}, // 0
{Context: Context{"a": "b"}, ExpectedJSON: `{"a":"b"}`}, // 1
{Context: Context{"a": "b", "c": "d"}, ExpectedJSON: `{"a":"b","c":"d"}`}, // 2
{Context: Context{"c": "d", "a": "b"}, ExpectedJSON: `{"a":"b","c":"d"}`}, // 3
{Context: Context{"0": "1", "-": "2", ".": "#"}, ExpectedJSON: `{"-":"2",".":"#","0":"1"}`}, // 4
}
func TestContextWriteTo(t *testing.T) {
for i, test := range contextWriteToTests {
var jsonContext strings.Builder
if _, err := test.Context.WriteTo(&jsonContext); err != nil {
t.Errorf("Test %d: Failed to encode context: %v", i, err)
continue
}
if s := jsonContext.String(); s != test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON)
}
}
}
+213
View File
@@ -0,0 +1,213 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
import (
"context"
"encoding/base64"
"fmt"
"github.com/minio/minio/cmd/logger"
)
// IsMultiPart returns true if the object metadata indicates
// that it was uploaded using some form of server-side-encryption
// and the S3 multipart API.
func IsMultiPart(metadata map[string]string) bool {
if _, ok := metadata[SSEMultipart]; ok {
return true
}
return false
}
// IsEncrypted returns true if the object metadata indicates
// that it was uploaded using some form of server-side-encryption.
//
// IsEncrypted only checks whether the metadata contains at least
// one entry indicating SSE-C or SSE-S3.
func IsEncrypted(metadata map[string]string) bool {
if _, ok := metadata[SSEIV]; ok {
return true
}
if _, ok := metadata[SSESealAlgorithm]; ok {
return true
}
if IsMultiPart(metadata) {
return true
}
if S3.IsEncrypted(metadata) {
return true
}
if SSEC.IsEncrypted(metadata) {
return true
}
return false
}
// IsEncrypted returns true if the object metadata indicates
// that the object was uploaded using SSE-S3.
func (s3) IsEncrypted(metadata map[string]string) bool {
if _, ok := metadata[S3SealedKey]; ok {
return true
}
if _, ok := metadata[S3KMSKeyID]; ok {
return true
}
if _, ok := metadata[S3KMSSealedKey]; ok {
return true
}
return false
}
// IsEncrypted returns true if the object metadata indicates
// that the object was uploaded using SSE-C.
func (ssec) IsEncrypted(metadata map[string]string) bool {
if _, ok := metadata[SSECSealedKey]; ok {
return true
}
return false
}
// CreateMultipartMetadata adds the multipart flag entry to metadata
// and returns modifed metadata. It allocates a new metadata map if
// metadata is nil.
func CreateMultipartMetadata(metadata map[string]string) map[string]string {
if metadata == nil {
metadata = map[string]string{}
}
metadata[SSEMultipart] = ""
return metadata
}
// CreateMetadata encodes the keyID, the sealed kms data key and the sealed key
// into the metadata and returns the modified metadata. It allocates a new
// metadata map if metadata is nil.
func (s3) CreateMetadata(metadata map[string]string, keyID string, kmsKey []byte, sealedKey SealedKey) map[string]string {
if sealedKey.Algorithm != SealAlgorithm {
logger.CriticalIf(context.Background(), fmt.Errorf("The seal algorithm '%s' is invalid for SSE-S3", sealedKey.Algorithm))
}
if metadata == nil {
metadata = map[string]string{}
}
metadata[S3KMSKeyID] = keyID
metadata[SSESealAlgorithm] = sealedKey.Algorithm
metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:])
metadata[S3SealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Key[:])
metadata[S3KMSSealedKey] = base64.StdEncoding.EncodeToString(kmsKey)
return metadata
}
// ParseMetadata extracts all SSE-S3 related values from the object metadata
// and checks whether they are well-formed. It returns the KMS key-ID, the
// sealed KMS key and the sealed object key on success.
func (s3) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte, sealedKey SealedKey, err error) {
// Extract all required values from object metadata
b64IV, ok := metadata[SSEIV]
if !ok {
return keyID, kmsKey, sealedKey, errMissingInternalIV
}
algorithm, ok := metadata[SSESealAlgorithm]
if !ok {
return keyID, kmsKey, sealedKey, errMissingInternalSealAlgorithm
}
b64SealedKey, ok := metadata[S3SealedKey]
if !ok {
return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal sealed key for SSE-S3"}
}
keyID, ok = metadata[S3KMSKeyID]
if !ok {
return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal KMS key-ID for SSE-S3"}
}
b64KMSSealedKey, ok := metadata[S3KMSSealedKey]
if !ok {
return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal sealed KMS data key for SSE-S3"}
}
// Check whether all extracted values are well-formed
iv, err := base64.StdEncoding.DecodeString(b64IV)
if err != nil || len(iv) != 32 {
return keyID, kmsKey, sealedKey, errInvalidInternalIV
}
if algorithm != SealAlgorithm {
return keyID, kmsKey, sealedKey, errInvalidInternalSealAlgorithm
}
encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey)
if err != nil || len(encryptedKey) != 64 {
return keyID, kmsKey, sealedKey, Error{"The internal sealed key for SSE-S3 is invalid"}
}
kmsKey, err = base64.StdEncoding.DecodeString(b64KMSSealedKey)
if err != nil {
return keyID, kmsKey, sealedKey, Error{"The internal sealed KMS data key for SSE-S3 is invalid"}
}
sealedKey.Algorithm = algorithm
copy(sealedKey.IV[:], iv)
copy(sealedKey.Key[:], encryptedKey)
return keyID, kmsKey, sealedKey, nil
}
// CreateMetadata encodes the sealed key into the metadata and returns the modified metadata.
// It allocates a new metadata map if metadata is nil.
func (ssec) CreateMetadata(metadata map[string]string, sealedKey SealedKey) map[string]string {
if sealedKey.Algorithm != SealAlgorithm {
logger.CriticalIf(context.Background(), fmt.Errorf("The seal algorithm '%s' is invalid for SSE-C", sealedKey.Algorithm))
}
if metadata == nil {
metadata = map[string]string{}
}
metadata[SSESealAlgorithm] = SealAlgorithm
metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:])
metadata[SSECSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Key[:])
return metadata
}
// ParseMetadata extracts all SSE-C related values from the object metadata
// and checks whether they are well-formed. It returns the sealed object key
// on success.
func (ssec) ParseMetadata(metadata map[string]string) (sealedKey SealedKey, err error) {
// Extract all required values from object metadata
b64IV, ok := metadata[SSEIV]
if !ok {
return sealedKey, errMissingInternalIV
}
algorithm, ok := metadata[SSESealAlgorithm]
if !ok {
return sealedKey, errMissingInternalSealAlgorithm
}
b64SealedKey, ok := metadata[SSECSealedKey]
if !ok {
return sealedKey, Error{"The object metadata is missing the internal sealed key for SSE-C"}
}
// Check whether all extracted values are well-formed
iv, err := base64.StdEncoding.DecodeString(b64IV)
if err != nil || len(iv) != 32 {
return sealedKey, errInvalidInternalIV
}
if algorithm != SealAlgorithm && algorithm != InsecureSealAlgorithm {
return sealedKey, errInvalidInternalSealAlgorithm
}
encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey)
if err != nil || len(encryptedKey) != 64 {
return sealedKey, Error{"The internal sealed key for SSE-C is invalid"}
}
sealedKey.Algorithm = algorithm
copy(sealedKey.IV[:], iv)
copy(sealedKey.Key[:], encryptedKey)
return sealedKey, nil
}
+361
View File
@@ -0,0 +1,361 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
import (
"bytes"
"encoding/base64"
"testing"
"github.com/minio/minio/cmd/logger"
)
var isMultipartTests = []struct {
Metadata map[string]string
Multipart bool
}{
{Multipart: true, Metadata: map[string]string{SSEMultipart: ""}}, // 0
{Multipart: true, Metadata: map[string]string{"X-Minio-Internal-Encrypted-Multipart": ""}}, // 1
{Multipart: true, Metadata: map[string]string{SSEMultipart: "some-value"}}, // 2
{Multipart: false, Metadata: map[string]string{"": ""}}, // 3
{Multipart: false, Metadata: map[string]string{"X-Minio-Internal-EncryptedMultipart": ""}}, // 4
}
func TestIsMultipart(t *testing.T) {
for i, test := range isMultipartTests {
if isMultipart := IsMultiPart(test.Metadata); isMultipart != test.Multipart {
t.Errorf("Test %d: got '%v' - want '%v'", i, isMultipart, test.Multipart)
}
}
}
var isEncryptedTests = []struct {
Metadata map[string]string
Encrypted bool
}{
{Encrypted: true, Metadata: map[string]string{SSEMultipart: ""}}, // 0
{Encrypted: true, Metadata: map[string]string{SSEIV: ""}}, // 1
{Encrypted: true, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2
{Encrypted: true, Metadata: map[string]string{SSECSealedKey: ""}}, // 3
{Encrypted: true, Metadata: map[string]string{S3SealedKey: ""}}, // 4
{Encrypted: true, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5
{Encrypted: true, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
}
func TestIsEncrypted(t *testing.T) {
for i, test := range isEncryptedTests {
if isEncrypted := IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
}
}
}
var s3IsEncryptedTests = []struct {
Metadata map[string]string
Encrypted bool
}{
{Encrypted: false, Metadata: map[string]string{SSEMultipart: ""}}, // 0
{Encrypted: false, Metadata: map[string]string{SSEIV: ""}}, // 1
{Encrypted: false, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2
{Encrypted: false, Metadata: map[string]string{SSECSealedKey: ""}}, // 3
{Encrypted: true, Metadata: map[string]string{S3SealedKey: ""}}, // 4
{Encrypted: true, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5
{Encrypted: true, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
}
func TestS3IsEncrypted(t *testing.T) {
for i, test := range s3IsEncryptedTests {
if isEncrypted := S3.IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
}
}
}
var ssecIsEncryptedTests = []struct {
Metadata map[string]string
Encrypted bool
}{
{Encrypted: false, Metadata: map[string]string{SSEMultipart: ""}}, // 0
{Encrypted: false, Metadata: map[string]string{SSEIV: ""}}, // 1
{Encrypted: false, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2
{Encrypted: true, Metadata: map[string]string{SSECSealedKey: ""}}, // 3
{Encrypted: false, Metadata: map[string]string{S3SealedKey: ""}}, // 4
{Encrypted: false, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5
{Encrypted: false, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
}
func TestSSECIsEncrypted(t *testing.T) {
for i, test := range ssecIsEncryptedTests {
if isEncrypted := SSEC.IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
}
}
}
var s3ParseMetadataTests = []struct {
Metadata map[string]string
ExpectedErr error
DataKey []byte
KeyID string
SealedKey SealedKey
}{
{ExpectedErr: errMissingInternalIV, Metadata: map[string]string{}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}}, // 0
{
ExpectedErr: errMissingInternalSealAlgorithm, Metadata: map[string]string{SSEIV: ""},
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 1
{
ExpectedErr: Error{"The object metadata is missing the internal sealed key for SSE-S3"},
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: ""}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 2
{
ExpectedErr: Error{"The object metadata is missing the internal KMS key-ID for SSE-S3"},
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: ""}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 3
{
ExpectedErr: Error{"The object metadata is missing the internal sealed KMS data key for SSE-S3"},
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: ""},
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 4
{
ExpectedErr: errInvalidInternalIV,
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: "", S3KMSSealedKey: ""},
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 5
{
ExpectedErr: errInvalidInternalSealAlgorithm,
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: "", S3KMSSealedKey: "",
},
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 6
{
ExpectedErr: Error{"The internal sealed key for SSE-S3 is invalid"},
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, S3SealedKey: "",
S3KMSKeyID: "", S3KMSSealedKey: "",
},
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
}, // 7
{
ExpectedErr: Error{"The internal sealed KMS data key for SSE-S3 is invalid"},
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm,
S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), S3KMSKeyID: "key-1",
S3KMSSealedKey: ".MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=", // invalid base64
},
DataKey: []byte{}, KeyID: "key-1", SealedKey: SealedKey{},
}, // 8
{
ExpectedErr: nil,
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm,
S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), S3KMSKeyID: "", S3KMSSealedKey: "",
},
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{Algorithm: SealAlgorithm},
}, // 9
{
ExpectedErr: nil,
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 31)...)), SSESealAlgorithm: SealAlgorithm,
S3SealedKey: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 63)...)), S3KMSKeyID: "key-1",
S3KMSSealedKey: base64.StdEncoding.EncodeToString(make([]byte, 48)),
},
DataKey: make([]byte, 48), KeyID: "key-1", SealedKey: SealedKey{Algorithm: SealAlgorithm, Key: [64]byte{1}, IV: [32]byte{1}},
}, // 10
}
func TestS3ParseMetadata(t *testing.T) {
for i, test := range s3ParseMetadataTests {
keyID, dataKey, sealedKey, err := S3.ParseMetadata(test.Metadata)
if err != test.ExpectedErr {
t.Errorf("Test %d: got error '%v' - want error '%v'", i, err, test.ExpectedErr)
}
if !bytes.Equal(dataKey, test.DataKey) {
t.Errorf("Test %d: got data key '%v' - want data key '%v'", i, dataKey, test.DataKey)
}
if keyID != test.KeyID {
t.Errorf("Test %d: got key-ID '%v' - want key-ID '%v'", i, keyID, test.KeyID)
}
if sealedKey.Algorithm != test.SealedKey.Algorithm {
t.Errorf("Test %d: got sealed key algorithm '%v' - want sealed key algorithm '%v'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
}
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
t.Errorf("Test %d: got sealed key '%v' - want sealed key '%v'", i, sealedKey.Key, test.SealedKey.Key)
}
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
t.Errorf("Test %d: got sealed key IV '%v' - want sealed key IV '%v'", i, sealedKey.IV, test.SealedKey.IV)
}
}
}
var ssecParseMetadataTests = []struct {
Metadata map[string]string
ExpectedErr error
SealedKey SealedKey
}{
{ExpectedErr: errMissingInternalIV, Metadata: map[string]string{}, SealedKey: SealedKey{}}, // 0
{ExpectedErr: errMissingInternalSealAlgorithm, Metadata: map[string]string{SSEIV: ""}, SealedKey: SealedKey{}}, // 1
{
ExpectedErr: Error{"The object metadata is missing the internal sealed key for SSE-C"},
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: ""}, SealedKey: SealedKey{},
}, // 2
{
ExpectedErr: errInvalidInternalIV,
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", SSECSealedKey: ""}, SealedKey: SealedKey{},
}, // 3
{
ExpectedErr: errInvalidInternalSealAlgorithm,
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: "", SSECSealedKey: "",
},
SealedKey: SealedKey{},
}, // 4
{
ExpectedErr: Error{"The internal sealed key for SSE-C is invalid"},
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, SSECSealedKey: "",
},
SealedKey: SealedKey{},
}, // 5
{
ExpectedErr: nil,
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm,
SSECSealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)),
},
SealedKey: SealedKey{Algorithm: SealAlgorithm},
}, // 6
{
ExpectedErr: nil,
Metadata: map[string]string{
SSEIV: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 31)...)), SSESealAlgorithm: InsecureSealAlgorithm,
SSECSealedKey: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 63)...)),
},
SealedKey: SealedKey{Algorithm: InsecureSealAlgorithm, Key: [64]byte{1}, IV: [32]byte{1}},
}, // 7
}
func TestCreateMultipartMetadata(t *testing.T) {
metadata := CreateMultipartMetadata(nil)
if v, ok := metadata[SSEMultipart]; !ok || v != "" {
t.Errorf("Metadata is missing the correct value for '%s': got '%s' - want '%s'", SSEMultipart, v, "")
}
}
func TestSSECParseMetadata(t *testing.T) {
for i, test := range ssecParseMetadataTests {
sealedKey, err := SSEC.ParseMetadata(test.Metadata)
if err != test.ExpectedErr {
t.Errorf("Test %d: got error '%v' - want error '%v'", i, err, test.ExpectedErr)
}
if sealedKey.Algorithm != test.SealedKey.Algorithm {
t.Errorf("Test %d: got sealed key algorithm '%v' - want sealed key algorithm '%v'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
}
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
t.Errorf("Test %d: got sealed key '%v' - want sealed key '%v'", i, sealedKey.Key, test.SealedKey.Key)
}
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
t.Errorf("Test %d: got sealed key IV '%v' - want sealed key IV '%v'", i, sealedKey.IV, test.SealedKey.IV)
}
}
}
var s3CreateMetadataTests = []struct {
KeyID string
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 TestS3CreateMetadata(t *testing.T) {
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
logger.Disable = true
for i, test := range s3CreateMetadataTests {
metadata := S3.CreateMetadata(nil, test.KeyID, test.SealedDataKey, test.SealedKey)
keyID, kmsKey, sealedKey, err := S3.ParseMetadata(metadata)
if err != nil {
t.Errorf("Test %d: failed to parse metadata: %v", i, err)
continue
}
if keyID != test.KeyID {
t.Errorf("Test %d: Key-ID mismatch: got '%s' - want '%s'", i, keyID, test.KeyID)
}
if !bytes.Equal(kmsKey, test.SealedDataKey) {
t.Errorf("Test %d: sealed KMS data mismatch: got '%v' - want '%v'", i, kmsKey, test.SealedDataKey)
}
if sealedKey.Algorithm != test.SealedKey.Algorithm {
t.Errorf("Test %d: seal algorithm mismatch: got '%s' - want '%s'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
}
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
t.Errorf("Test %d: IV mismatch: got '%v' - want '%v'", i, sealedKey.IV, test.SealedKey.IV)
}
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
t.Errorf("Test %d: sealed key mismatch: got '%v' - want '%v'", i, sealedKey.Key, test.SealedKey.Key)
}
}
defer func() {
if err := recover(); err == nil || err != logger.ErrCritical {
t.Errorf("Expected '%s' panic for invalid seal algorithm but got '%s'", logger.ErrCritical, err)
}
}()
_ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm})
}
var ssecCreateMetadataTests = []SealedKey{
{Algorithm: SealAlgorithm},
{IV: [32]byte{0xff}, Key: [64]byte{0x7e}, Algorithm: SealAlgorithm},
}
func TestSSECCreateMetadata(t *testing.T) {
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
logger.Disable = true
for i, test := range s3CreateMetadataTests {
metadata := SSEC.CreateMetadata(nil, test.SealedKey)
sealedKey, err := SSEC.ParseMetadata(metadata)
if err != nil {
t.Errorf("Test %d: failed to parse metadata: %v", i, err)
continue
}
if sealedKey.Algorithm != test.SealedKey.Algorithm {
t.Errorf("Test %d: seal algorithm mismatch: got '%s' - want '%s'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
}
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
t.Errorf("Test %d: IV mismatch: got '%v' - want '%v'", i, sealedKey.IV, test.SealedKey.IV)
}
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
t.Errorf("Test %d: sealed key mismatch: got '%v' - want '%v'", i, sealedKey.Key, test.SealedKey.Key)
}
}
defer func() {
if err := recover(); err == nil || err != logger.ErrCritical {
t.Errorf("Expected '%s' panic for invalid seal algorithm but got '%s'", logger.ErrCritical, err)
}
}()
_ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm})
}
+46
View File
@@ -25,16 +25,54 @@ import (
) )
const ( const (
// SSEMultipart is the metadata key indicating that the object
// was uploaded using the S3 multipart API and stored using
// some from of server-side-encryption.
SSEMultipart = "X-Minio-Internal-Encrypted-Multipart"
// SSEIV is the metadata key referencing the random initialization
// vector (IV) used for SSE-S3 and SSE-C key derivation.
SSEIV = "X-Minio-Internal-Server-Side-Encryption-Iv"
// SSESealAlgorithm is the metadata key referencing the algorithm
// used by SSE-C and SSE-S3 to encrypt the object.
SSESealAlgorithm = "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm"
// SSECSealedKey is the metadata key referencing the sealed object-key for SSE-C.
SSECSealedKey = "X-Minio-Internal-Server-Side-Encryption-Sealed-Key"
// S3SealedKey is the metadata key referencing the sealed object-key for SSE-S3. // S3SealedKey is the metadata key referencing the sealed object-key for SSE-S3.
S3SealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key" S3SealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"
// S3KMSKeyID is the metadata key referencing the KMS key-id used to // S3KMSKeyID is the metadata key referencing the KMS key-id used to
// generate/decrypt the S3-KMS-Sealed-Key. It is only used for SSE-S3 + KMS. // generate/decrypt the S3-KMS-Sealed-Key. It is only used for SSE-S3 + KMS.
S3KMSKeyID = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id" S3KMSKeyID = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"
// S3KMSSealedKey is the metadata key referencing the encrypted key generated // S3KMSSealedKey is the metadata key referencing the encrypted key generated
// by KMS. It is only used for SSE-S3 + KMS. // by KMS. It is only used for SSE-S3 + KMS.
S3KMSSealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key" S3KMSSealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key"
) )
const (
// SealAlgorithm is the encryption/sealing algorithm used to derive & seal
// the key-encryption-key and to en/decrypt the object data.
SealAlgorithm = "DAREv2-HMAC-SHA256"
// InsecureSealAlgorithm is the legacy encryption/sealing algorithm used
// to derive & seal the key-encryption-key and to en/decrypt the object data.
// This algorithm should not be used for new objects because its key derivation
// is not optimal. See: https://github.com/minio/minio/pull/6121
InsecureSealAlgorithm = "DARE-SHA256"
)
// String returns the SSE domain as string. For SSE-S3 the
// domain is "SSE-S3".
func (s3) String() string { return "SSE-S3" }
// String returns the SSE domain as string. For SSE-C the
// domain is "SSE-C".
func (ssec) String() string { return "SSE-C" }
// 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 {
@@ -45,6 +83,14 @@ func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
return r return r
} }
// EncryptMultiPart encrypts an io.Reader which must be the body of
// multi-part PUT request. It derives an unique encryption key from
// the partID and the object key.
func EncryptMultiPart(r io.Reader, partID int, key ObjectKey) io.Reader {
partKey := key.DerivePartKey(uint32(partID))
return EncryptSinglePart(r, ObjectKey(partKey))
}
// DecryptSinglePart decrypts an io.Writer which must an object // DecryptSinglePart decrypts an io.Writer which must an object
// uploaded with the single-part PUT API. The offset and length // uploaded with the single-part PUT API. The offset and length
// specify the requested range. // specify the requested range.
+31
View File
@@ -0,0 +1,31 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
import "testing"
func TestS3String(t *testing.T) {
const Domain = "SSE-S3"
if domain := S3.String(); domain != Domain {
t.Errorf("S3's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
}
}
func TestSSECString(t *testing.T) {
const Domain = "SSE-C"
if domain := SSEC.String(); domain != Domain {
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
}
}
+256
View File
@@ -0,0 +1,256 @@
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crypto
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
vault "github.com/hashicorp/vault/api"
)
const (
// VaultEndpointEnv Vault endpoint environment variable
VaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
// vaultAuthTypeEnv type of vault auth to be used
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
// vaultAppRoleIDEnv Vault AppRole ID environment variable
vaultAppRoleIDEnv = "MINIO_SSE_VAULT_APPROLE_ID"
// vaultAppSecretIDEnv Vault AppRole Secret environment variable
vaultAppSecretIDEnv = "MINIO_SSE_VAULT_APPROLE_SECRET"
// vaultKeyVersionEnv Vault Key Version environment variable
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
// vaultKeyNameEnv Vault Encryption Key Name environment variable
vaultKeyNameEnv = "MINIO_SSE_VAULT_KEY_NAME"
)
var (
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
)
type vaultService struct {
config *VaultConfig
client *vault.Client
leaseDuration time.Duration
}
// return transit secret engine's path for generate data key operation
func (v *vaultService) genDataKeyEndpoint(key string) string {
return "/transit/datakey/plaintext/" + key
}
// return transit secret engine's path for decrypt operation
func (v *vaultService) decryptEndpoint(key string) string {
return "/transit/decrypt/" + key
}
// VaultKey represents vault encryption key-id name & version
type VaultKey struct {
Name string `json:"name"`
Version int `json:"version"`
}
// VaultAuth represents vault auth type to use. For now, AppRole is the only supported
// auth type.
type VaultAuth struct {
Type string `json:"type"`
AppRole VaultAppRole `json:"approle"`
}
// VaultAppRole represents vault approle credentials
type VaultAppRole struct {
ID string `json:"id"`
Secret string `json:"secret"`
}
// VaultConfig holds config required to start vault service
type VaultConfig struct {
Endpoint string `json:"endpoint"`
Auth VaultAuth `json:"auth"`
Key VaultKey `json:"key-id"`
}
// validate whether all required env variables needed to start vault service have
// been set
func validateVaultConfig(c *VaultConfig) error {
if c.Endpoint == "" {
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", VaultEndpointEnv)
}
if strings.ToLower(c.Auth.Type) != "approle" {
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
}
if c.Auth.AppRole.ID == "" {
return fmt.Errorf("Missing hashicorp vault AppRole ID - %s is empty", vaultAppRoleIDEnv)
}
if c.Auth.AppRole.Secret == "" {
return fmt.Errorf("Missing hashicorp vault AppSecret ID - %s is empty", vaultAppSecretIDEnv)
}
if c.Key.Name == "" {
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyNameEnv)
}
if c.Key.Version < 0 {
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
}
return nil
}
// authenticate to vault with app role id and app role secret, and get a client access token, lease duration
func getVaultAccessToken(client *vault.Client, appRoleID, appSecret string) (token string, duration int, err error) {
data := map[string]interface{}{
"role_id": appRoleID,
"secret_id": appSecret,
}
resp, e := client.Logical().Write("auth/approle/login", data)
if e != nil {
return token, duration, e
}
if resp.Auth == nil {
return token, duration, ErrKMSAuthLogin
}
return resp.Auth.ClientToken, resp.Auth.LeaseDuration, nil
}
// NewVaultConfig sets KMSConfig from environment
// variables and performs validations.
func NewVaultConfig() (KMSConfig, error) {
kc := KMSConfig{}
endpoint := os.Getenv(VaultEndpointEnv)
roleID := os.Getenv(vaultAppRoleIDEnv)
roleSecret := os.Getenv(vaultAppSecretIDEnv)
keyName := os.Getenv(vaultKeyNameEnv)
keyVersion := 0
authType := "approle"
if versionStr := os.Getenv(vaultKeyVersionEnv); versionStr != "" {
version, err := strconv.Atoi(versionStr)
if err != nil {
return kc, fmt.Errorf("Unable to parse %s value (`%s`)", vaultKeyVersionEnv, versionStr)
}
keyVersion = version
}
// return if none of the vault env variables are configured
if (endpoint == "") && (roleID == "") && (roleSecret == "") && (keyName == "") && (keyVersion == 0) {
return kc, nil
}
c := VaultConfig{
Endpoint: endpoint,
Auth: VaultAuth{
Type: authType,
AppRole: VaultAppRole{
ID: roleID,
Secret: roleSecret,
},
},
Key: VaultKey{
Version: keyVersion,
Name: keyName,
},
}
if err := validateVaultConfig(&c); err != nil {
return kc, err
}
kc.Vault = c
return kc, nil
}
// NewVault initializes Hashicorp Vault KMS by
// authenticating to Vault with the credentials in KMSConfig,
// and gets a client token for future api calls.
func NewVault(kmsConf KMSConfig) (KMS, error) {
config := kmsConf.Vault
c, err := vault.NewClient(&vault.Config{
Address: config.Endpoint,
})
if err != nil {
return nil, err
}
accessToken, leaseDuration, err := getVaultAccessToken(c, config.Auth.AppRole.ID, config.Auth.AppRole.Secret)
if err != nil {
return nil, err
}
// authenticate and get the access token
c.SetToken(accessToken)
v := vaultService{client: c, config: &config, leaseDuration: time.Duration(leaseDuration)}
v.renewToken(c)
return &v, nil
}
func (v *vaultService) renewToken(c *vault.Client) {
retryDelay := 1 * time.Minute
go func() {
for {
s, err := c.Auth().Token().RenewSelf(int(v.leaseDuration))
if err != nil {
time.Sleep(retryDelay)
continue
}
nextRenew := s.Auth.LeaseDuration / 2
time.Sleep(time.Duration(nextRenew) * time.Second)
}
}()
}
// Generates a random plain text key, sealed plain text key from
// Vault. It returns the plaintext key and sealed plaintext key on success
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
contextStream := new(bytes.Buffer)
ctx.WriteTo(contextStream)
payload := map[string]interface{}{
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
}
s, err1 := v.client.Logical().Write(v.genDataKeyEndpoint(keyID), payload)
if err1 != nil {
return key, sealedKey, err1
}
sealKey := s.Data["ciphertext"].(string)
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
if err != nil {
return key, sealedKey, err1
}
copy(key[:], []byte(plainKey))
return key, []byte(sealKey), nil
}
// unsealKMSKey unseals the sealedKey using the Vault master key
// referenced by the keyID. The plain text key is returned on success.
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
contextStream := new(bytes.Buffer)
ctx.WriteTo(contextStream)
payload := map[string]interface{}{
"ciphertext": string(sealedKey),
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
}
s, err1 := v.client.Logical().Write(v.decryptEndpoint(keyID), payload)
if err1 != nil {
return key, err1
}
base64Key := s.Data["plaintext"].(string)
plainKey, err1 := base64.StdEncoding.DecodeString(base64Key)
if err1 != nil {
return key, err1
}
copy(key[:], []byte(plainKey))
return key, nil
}
+44 -1
View File
@@ -18,7 +18,11 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"errors"
"path/filepath" "path/filepath"
"strings"
"github.com/minio/minio/pkg/ellipses"
) )
// CacheConfig represents cache config settings // CacheConfig represents cache config settings
@@ -41,6 +45,15 @@ func (cfg *CacheConfig) UnmarshalJSON(data []byte) (err error) {
if err = json.Unmarshal(data, _cfg); err != nil { if err = json.Unmarshal(data, _cfg); err != nil {
return err return err
} }
if _cfg.Expiry < 0 {
return errors.New("config expiry value should not be negative")
}
if _cfg.MaxUse < 0 {
return errors.New("config max use value should not be null or negative")
}
if _, err = parseCacheDrives(_cfg.Drives); err != nil { if _, err = parseCacheDrives(_cfg.Drives); err != nil {
return err return err
} }
@@ -52,12 +65,42 @@ func (cfg *CacheConfig) UnmarshalJSON(data []byte) (err error) {
// Parses given cacheDrivesEnv and returns a list of cache drives. // Parses given cacheDrivesEnv and returns a list of cache drives.
func parseCacheDrives(drives []string) ([]string, error) { func parseCacheDrives(drives []string) ([]string, error) {
if len(drives) == 0 {
return drives, nil
}
var endpoints []string
for _, d := range drives { for _, d := range drives {
if ellipses.HasEllipses(d) {
s, err := parseCacheDrivePaths(d)
if err != nil {
return nil, err
}
endpoints = append(endpoints, s...)
} else {
endpoints = append(endpoints, d)
}
}
for _, d := range endpoints {
if !filepath.IsAbs(d) { if !filepath.IsAbs(d) {
return nil, uiErrInvalidCacheDrivesValue(nil).Msg("cache dir should be absolute path: %s", d) return nil, uiErrInvalidCacheDrivesValue(nil).Msg("cache dir should be absolute path: %s", d)
} }
} }
return drives, nil return endpoints, nil
}
// Parses all arguments and returns a slice of drive paths following the ellipses pattern.
func parseCacheDrivePaths(arg string) (ep []string, err error) {
patterns, perr := ellipses.FindEllipsesPatterns(arg)
if perr != nil {
return []string{}, uiErrInvalidCacheDrivesValue(nil).Msg(perr.Error())
}
for _, lbls := range patterns.Expand() {
ep = append(ep, strings.Join(lbls, ""))
}
return ep, nil
} }
// Parses given cacheExcludesEnv and returns a list of cache exclude patterns. // Parses given cacheExcludesEnv and returns a list of cache exclude patterns.
+20
View File
@@ -41,12 +41,32 @@ func TestParseCacheDrives(t *testing.T) {
expectedPatterns []string expectedPatterns []string
success bool success bool
}{"C:/home/drive1;C:/home/drive2;C:/home/drive3", []string{"C:/home/drive1", "C:/home/drive2", "C:/home/drive3"}, true}) }{"C:/home/drive1;C:/home/drive2;C:/home/drive3", []string{"C:/home/drive1", "C:/home/drive2", "C:/home/drive3"}, true})
testCases = append(testCases, struct {
driveStr string
expectedPatterns []string
success bool
}{"C:/home/drive{1...3}", []string{"C:/home/drive1", "C:/home/drive2", "C:/home/drive3"}, true})
testCases = append(testCases, struct {
driveStr string
expectedPatterns []string
success bool
}{"C:/home/drive{1..3}", []string{}, false})
} else { } else {
testCases = append(testCases, struct { testCases = append(testCases, struct {
driveStr string driveStr string
expectedPatterns []string expectedPatterns []string
success bool success bool
}{"/home/drive1;/home/drive2;/home/drive3", []string{"/home/drive1", "/home/drive2", "/home/drive3"}, true}) }{"/home/drive1;/home/drive2;/home/drive3", []string{"/home/drive1", "/home/drive2", "/home/drive3"}, true})
testCases = append(testCases, struct {
driveStr string
expectedPatterns []string
success bool
}{"/home/drive{1...3}", []string{"/home/drive1", "/home/drive2", "/home/drive3"}, true})
testCases = append(testCases, struct {
driveStr string
expectedPatterns []string
success bool
}{"/home/drive{1..3}", []string{}, false})
} }
for i, testCase := range testCases { for i, testCase := range testCases {
drives, err := parseCacheDrives(strings.Split(testCase.driveStr, cacheEnvDelimiter)) drives, err := parseCacheDrives(strings.Split(testCase.driveStr, cacheEnvDelimiter))
+12 -24
View File
@@ -32,14 +32,10 @@ import (
"github.com/djherbis/atime" "github.com/djherbis/atime"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/wildcard"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/wildcard"
) )
// list of all errors that can be ignored in tree walk operation in disk cache
var cacheTreeWalkIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errVolumeNotFound, errFileNotFound)
const ( const (
// disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created. // disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created.
cacheSizeMultiplier = 100 cacheSizeMultiplier = 100
@@ -291,40 +287,32 @@ func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string)
// Returns function "listDir" of the type listDirFunc. // Returns function "listDir" of the type listDirFunc.
// isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry. // isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry.
// disks - list of fsObjects // disks - list of fsObjects
func listDirCacheFactory(isLeaf isLeafFunc, treeWalkIgnoredErrs []error, disks []*cacheFSObjects) listDirFunc { func listDirCacheFactory(isLeaf isLeafFunc, disks []*cacheFSObjects) listDirFunc {
listCacheDirs := func(bucket, prefixDir, prefixEntry string) (dirs []string, err error) { listCacheDirs := func(bucket, prefixDir, prefixEntry string) (dirs []string) {
var entries []string var entries []string
for _, disk := range disks { for _, disk := range disks {
// ignore disk-caches that might be missing/offline // ignore disk-caches that might be missing/offline
if disk == nil { if disk == nil {
continue continue
} }
fs := disk.FSObjects
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
// For any reason disk was deleted or goes offline, continue fs := disk.FSObjects
// and list from other disks if possible. var err error
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
if err != nil { if err != nil {
if IsErrIgnored(err, treeWalkIgnoredErrs...) { continue
continue
}
return nil, err
} }
// Filter entries that have the prefix prefixEntry. // Filter entries that have the prefix prefixEntry.
entries = filterMatchingPrefix(entries, prefixEntry) entries = filterMatchingPrefix(entries, prefixEntry)
dirs = append(dirs, entries...) dirs = append(dirs, entries...)
} }
return dirs, nil return dirs
} }
// listDir - lists all the entries at a given prefix and given entry in the prefix. // listDir - lists all the entries at a given prefix and given entry in the prefix.
listDir := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool, err error) { listDir := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool) {
var cacheEntries []string cacheEntries := listCacheDirs(bucket, prefixDir, prefixEntry)
cacheEntries, err = listCacheDirs(bucket, prefixDir, prefixEntry)
if err != nil {
return nil, false, err
}
for _, entry := range cacheEntries { for _, entry := range cacheEntries {
// Find elements in entries which are not in mergedEntries // Find elements in entries which are not in mergedEntries
idx := sort.SearchStrings(mergedEntries, entry) idx := sort.SearchStrings(mergedEntries, entry)
@@ -335,7 +323,7 @@ func listDirCacheFactory(isLeaf isLeafFunc, treeWalkIgnoredErrs []error, disks [
mergedEntries = append(mergedEntries, entry) mergedEntries = append(mergedEntries, entry)
sort.Strings(mergedEntries) sort.Strings(mergedEntries)
} }
return mergedEntries, false, nil return mergedEntries, false
} }
return listDir return listDir
} }
@@ -371,7 +359,7 @@ func (c cacheObjects) listCacheObjects(ctx context.Context, bucket, prefix, mark
return fs.isObjectDir(bucket, object) return fs.isObjectDir(bucket, object)
} }
listDir := listDirCacheFactory(isLeaf, cacheTreeWalkIgnoredErrs, c.cache.cfs) listDir := listDirCacheFactory(isLeaf, c.cache.cfs)
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh) walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
} }
+3 -15
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"os"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@@ -28,21 +27,15 @@ import (
) )
// Initialize cache FS objects. // Initialize cache FS objects.
func initCacheFSObjects(disk string, cacheMaxUse int, t *testing.T) (*cacheFSObjects, error) { func initCacheFSObjects(disk string, cacheMaxUse int) (*cacheFSObjects, error) {
newTestConfig(globalMinioDefaultRegion) return newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
var err error
obj, err := newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
if err != nil {
t.Fatal(err)
}
return obj, nil
} }
// inits diskCache struct for nDisks // inits diskCache struct for nDisks
func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) (*diskCache, error) { func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) (*diskCache, error) {
var cfs []*cacheFSObjects var cfs []*cacheFSObjects
for _, d := range drives { for _, d := range drives {
obj, err := initCacheFSObjects(d, cacheMaxUse, t) obj, err := initCacheFSObjects(d, cacheMaxUse)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -131,11 +124,6 @@ func TestGetCacheFSMaxUse(t *testing.T) {
// test wildcard patterns for excluding entries from cache // test wildcard patterns for excluding entries from cache
func TestCacheExclusion(t *testing.T) { func TestCacheExclusion(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootPath)
fsDirs, err := getRandomDisks(1) fsDirs, err := getRandomDisks(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
-9
View File
@@ -19,7 +19,6 @@ package cmd
import ( import (
"context" "context"
"io" "io"
"time"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
@@ -132,14 +131,6 @@ func (api *DummyObjectLayer) ListObjectsHeal(ctx context.Context, bucket, prefix
return return
} }
func (api *DummyObjectLayer) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) (info []VolumeLockInfo, err error) {
return
}
func (api *DummyObjectLayer) ClearLocks(context.Context, []VolumeLockInfo) (err error) {
return
}
func (api *DummyObjectLayer) SetBucketPolicy(context.Context, string, *policy.Policy) (err error) { func (api *DummyObjectLayer) SetBucketPolicy(context.Context, string, *policy.Policy) (err error) {
return return
} }
+288 -371
View File
@@ -17,19 +17,18 @@
package cmd package cmd
import ( import (
"bytes"
"context" "context"
"crypto/hmac" "crypto/hmac"
"crypto/md5"
"crypto/rand" "crypto/rand"
"crypto/subtle" "crypto/subtle"
"encoding/base64"
"encoding/binary" "encoding/binary"
"errors" "errors"
"io" "io"
"net/http" "net/http"
"path"
"strconv" "strconv"
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/ioutil"
sha256 "github.com/minio/sha256-simd" sha256 "github.com/minio/sha256-simd"
@@ -40,34 +39,12 @@ var (
// AWS errors for invalid SSE-C requests. // AWS errors for invalid SSE-C requests.
errInsecureSSERequest = errors.New("SSE-C requests require TLS connections") 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")
errInvalidSSEAlgorithm = errors.New("The SSE-C algorithm is not valid")
errMissingSSEKey = errors.New("The SSE-C request is missing the customer key")
errInvalidSSEKey = errors.New("The SSE-C key is invalid")
errMissingSSEKeyMD5 = errors.New("The SSE-C request is missing the customer key MD5")
errSSEKeyMD5Mismatch = errors.New("The key MD5 does not match the SSE-C key")
errSSEKeyMismatch = errors.New("The SSE-C key is not correct") // access denied
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")
// Additional Minio errors for SSE-C requests. // Additional Minio errors for SSE-C requests.
errObjectTampered = errors.New("The requested object was modified and may be compromised") errObjectTampered = errors.New("The requested object was modified and may be compromised")
) )
const (
// SSECustomerAlgorithm is the AWS SSE-C algorithm HTTP header key.
SSECustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm"
// SSECustomerKey is the AWS SSE-C encryption key HTTP header key.
SSECustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key"
// SSECustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key.
SSECustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5"
// SSECopyCustomerAlgorithm is the AWS SSE-C algorithm HTTP header key for CopyObject API.
SSECopyCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
// SSECopyCustomerKey is the AWS SSE-C encryption key HTTP header key for CopyObject API.
SSECopyCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
// SSECopyCustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key for CopyObject API.
SSECopyCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5"
)
const ( const (
// SSECustomerKeySize is the size of valid client provided encryption keys in bytes. // SSECustomerKeySize is the size of valid client provided encryption keys in bytes.
// Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes. // Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes.
@@ -76,9 +53,6 @@ const (
// SSEIVSize is the size of the IV data // SSEIVSize is the size of the IV data
SSEIVSize = 32 // 32 bytes SSEIVSize = 32 // 32 bytes
// SSECustomerAlgorithmAES256 the only valid S3 SSE-C encryption algorithm identifier.
SSECustomerAlgorithmAES256 = "AES256"
// SSE dare package block size. // SSE dare package block size.
sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
@@ -87,82 +61,26 @@ const (
) )
// SSE-C key derivation, key verification and key update:
// H: Hash function [32 = |H(m)|]
// AE: authenticated encryption scheme, AD: authenticated decryption scheme [m = AD(k, AE(k, m))]
//
// Key derivation:
// Input:
// key := 32 bytes # client provided key
// Re, Rm := 32 bytes, 32 bytes # uniformly random
//
// Seal:
// k := H(key || Re) # object encryption key
// r := H(Rm) # save as object metadata [ServerSideEncryptionIV]
// KeK := H(key || r) # key encryption key
// K := AE(KeK, k) # save as object metadata [ServerSideEncryptionSealedKey]
// ------------------------------------------------------------------------------------------------
// Key verification:
// Input:
// key := 32 bytes # client provided key
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
//
// Open:
// KeK := H(key || r) # key encryption key
// k := AD(Kek, K) # object encryption key
// -------------------------------------------------------------------------------------------------
// Key update:
// Input:
// key := 32 bytes # old client provided key
// key' := 32 bytes # new client provided key
// Rm := 32 bytes # uniformly random
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
//
// Update:
// 1. open:
// KeK := H(key || r) # key encryption key
// k := AD(Kek, K) # object encryption key
// 2. seal:
// r' := H(Rm) # save as object metadata [ServerSideEncryptionIV]
// KeK' := H(key' || r') # new key encryption key
// K' := AE(KeK', k) # save as object metadata [ServerSideEncryptionSealedKey]
const ( const (
// ServerSideEncryptionIV is a 32 byte randomly generated IV used to derive an // SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
// unique key encryption key from the client provided key. The combination of this value // hash function. The key derivation of DARE-SHA256 is not optimal and does not include the object path.
// and the client-provided key MUST be unique. // It is considered legacy and should not be used anymore.
ServerSideEncryptionIV = ReservedMetadataPrefix + "Server-Side-Encryption-Iv" SSESealAlgorithmDareSha256 = "DARE-SHA256"
// ServerSideEncryptionSealAlgorithm identifies a combination of a cryptographic hash function and // SSESealAlgorithmDareV2HmacSha256 specifies DAREv2 as authenticated en/decryption scheme and SHA256 as cryptographic
// an authenticated en/decryption scheme to seal the object encryption key. // hash function for the HMAC PRF.
ServerSideEncryptionSealAlgorithm = ReservedMetadataPrefix + "Server-Side-Encryption-Seal-Algorithm" SSESealAlgorithmDareV2HmacSha256 = "DAREv2-HMAC-SHA256"
// ServerSideEncryptionSealedKey is the sealed object encryption key. The sealed key can be decrypted
// by the key encryption key derived from the client provided key and the server-side-encryption IV.
ServerSideEncryptionSealedKey = ReservedMetadataPrefix + "Server-Side-Encryption-Sealed-Key"
) )
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic // hasServerSideEncryptionHeader returns true if the given HTTP header
// hash function. // contains server-side-encryption.
const SSESealAlgorithmDareSha256 = "DARE-SHA256" func hasServerSideEncryptionHeader(header http.Header) bool {
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
// hasSSECustomerHeader returns true if the given HTTP header
// contains server-side-encryption with customer provided key fields.
func hasSSECustomerHeader(header http.Header) bool {
return header.Get(SSECustomerAlgorithm) != "" || header.Get(SSECustomerKey) != "" || header.Get(SSECustomerKeyMD5) != ""
}
// hasSSECopyCustomerHeader returns true if the given HTTP header
// contains copy source server-side-encryption with customer provided key fields.
func hasSSECopyCustomerHeader(header http.Header) bool {
return header.Get(SSECopyCustomerAlgorithm) != "" || header.Get(SSECopyCustomerKey) != "" || header.Get(SSECopyCustomerKeyMD5) != ""
} }
// 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) (key []byte, err error) { func ParseSSECopyCustomerRequest(r *http.Request, metadata map[string]string) (key []byte, err error) {
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time 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 // 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. // the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
@@ -170,36 +88,11 @@ func ParseSSECopyCustomerRequest(r *http.Request) (key []byte, err error) {
// will always fail -> r.TLS is always nil even for TLS requests. // will always fail -> r.TLS is always nil even for TLS requests.
return nil, errInsecureSSERequest return nil, errInsecureSSERequest
} }
header := r.Header if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(r.Header) {
if algorithm := header.Get(SSECopyCustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 { return nil, crypto.ErrIncompatibleEncryptionMethod
return nil, errInvalidSSEAlgorithm
} }
if header.Get(SSECopyCustomerKey) == "" { k, err := crypto.SSECopy.ParseHTTP(r.Header)
return nil, errMissingSSEKey return k[:], err
}
if header.Get(SSECopyCustomerKeyMD5) == "" {
return nil, errMissingSSEKeyMD5
}
key, err = base64.StdEncoding.DecodeString(header.Get(SSECopyCustomerKey))
if err != nil {
return nil, errInvalidSSEKey
}
if len(key) != SSECustomerKeySize {
return nil, errInvalidSSEKey
}
// Make sure we purged the keys from http headers by now.
header.Del(SSECopyCustomerKey)
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECopyCustomerKeyMD5))
if err != nil {
return nil, errSSEKeyMD5Mismatch
}
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
return nil, errSSEKeyMD5Mismatch
}
return key, nil
} }
// ParseSSECustomerRequest parses the SSE-C header fields of the provided request. // ParseSSECustomerRequest parses the SSE-C header fields of the provided request.
@@ -218,209 +111,182 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
// will always fail -> r.TLS is always nil even for TLS requests. // will always fail -> r.TLS is always nil even for TLS requests.
return nil, errInsecureSSERequest return nil, errInsecureSSERequest
} }
if algorithm := header.Get(SSECustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 { if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
return nil, errInvalidSSEAlgorithm return key, crypto.ErrIncompatibleEncryptionMethod
}
if header.Get(SSECustomerKey) == "" {
return nil, errMissingSSEKey
}
if header.Get(SSECustomerKeyMD5) == "" {
return nil, errMissingSSEKeyMD5
} }
key, err = base64.StdEncoding.DecodeString(header.Get(SSECustomerKey)) k, err := crypto.SSEC.ParseHTTP(header)
if err != nil { return k[:], err
return nil, errInvalidSSEKey
}
if len(key) != SSECustomerKeySize {
return nil, errInvalidSSEKey
}
// Make sure we purged the keys from http headers by now.
header.Del(SSECustomerKey)
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECustomerKeyMD5))
if err != nil {
return nil, errSSEKeyMD5Mismatch
}
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
return nil, errSSEKeyMD5Mismatch
}
return key, nil
} }
// This function rotates old to new key. // This function rotates old to new key.
func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error { func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option switch {
default:
return errObjectTampered return errObjectTampered
} case crypto.SSEC.IsEncrypted(metadata):
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV]) sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
if err != nil || len(iv) != SSEIVSize { if err != nil {
return errObjectTampered return err
}
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
if err != nil || len(sealedKey) != 64 {
return errObjectTampered
}
sha := sha256.New() // derive key encryption key
sha.Write(oldKey)
sha.Write(iv)
keyEncryptionKey := sha.Sum(nil)
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
Key: keyEncryptionKey,
})
if n != 32 || err != nil { // Either the provided key does not match or the object was tampered.
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
} }
return errSSEKeyMismatch // To provide strict AWS S3 compatibility we return: access denied.
}
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
return nil // we don't need to rotate keys if newKey == oldKey
}
nonce := make([]byte, 32) // generate random values for key derivation var objectKey crypto.ObjectKey
if _, err = io.ReadFull(rand.Reader, nonce); err != nil { var extKey [32]byte
return err copy(extKey[:], oldKey)
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
}
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
}
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
return nil // don't rotate on equal keys if seal algorithm is latest
}
copy(extKey[:], newKey)
sealedKey = objectKey.Seal(extKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
return nil
} }
niv := sha256.Sum256(nonce[:]) // derive key encryption key
sha = sha256.New()
sha.Write(newKey)
sha.Write(niv[:])
keyEncryptionKey = sha.Sum(nil)
sealedKeyW := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
n, err = sio.Encrypt(sealedKeyW, bytes.NewReader(objectEncryptionKey.Bytes()), sio.Config{
Key: keyEncryptionKey,
})
if n != 64 || err != nil {
return errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
}
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(niv[:])
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKeyW.Bytes())
return nil
} }
func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error) { func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
// security notice: var sealedKey crypto.SealedKey
// - If the first 32 bytes of the random value are ever repeated under the same client-provided if sseS3 {
// key the encrypted object will not be tamper-proof. [ P(coll) ~= 1 / 2^(256 / 2)] if globalKMS == nil {
// - If the last 32 bytes of the random value are ever repeated under the same client-provided return nil, errKMSNotConfigured
// key an adversary may be able to extract the object encryption key. This depends on the }
// authenticated en/decryption scheme. The DARE format will generate an 8 byte nonce which must key, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
// be repeated in addition to reveal the object encryption key. if err != nil {
// [ P(coll) ~= 1 / 2^((256 + 64) / 2) ] return nil, err
nonce := make([]byte, 32+SSEIVSize) // generate random values for key derivation }
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, globalKMSKeyID, encKey, sealedKey)
return objectKey[:], nil
} }
sha := sha256.New() // derive object encryption key var extKey [32]byte
sha.Write(key) copy(extKey[:], key)
sha.Write(nonce[:32]) objectKey := crypto.GenerateKey(extKey, rand.Reader)
objectEncryptionKey := sha.Sum(nil) sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
crypto.SSEC.CreateMetadata(metadata, sealedKey)
return objectKey[:], nil
iv := sha256.Sum256(nonce[32:]) // derive key encryption key
sha = sha256.New()
sha.Write(key)
sha.Write(iv[:])
keyEncryptionKey := sha.Sum(nil)
sealedKey := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
n, err := sio.Encrypt(sealedKey, bytes.NewReader(objectEncryptionKey), sio.Config{
Key: keyEncryptionKey,
})
if n != 64 || err != nil {
return nil, errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
}
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Bytes())
return objectEncryptionKey, nil
} }
func newEncryptReader(content io.Reader, key []byte, metadata map[string]string) (io.Reader, error) { func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, error) {
objectEncryptionKey, err := newEncryptMetadata(key, metadata) objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
if err != nil { if err != nil {
return nil, err return nil, err
} }
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey}) reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
if err != nil { if err != nil {
return nil, errInvalidSSEKey return nil, crypto.ErrInvalidCustomerKey
} }
return reader, nil return reader, nil
} }
// set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of
// SSE-S3
func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
var (
key []byte
)
if crypto.SSEC.IsRequested(r.Header) {
key, err = ParseSSECustomerRequest(r)
if err != nil {
return
}
}
_, err = newEncryptMetadata(key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
return
}
// EncryptRequest takes the client provided content and encrypts the data // EncryptRequest takes the client provided content and encrypts the data
// with the client provided key. It also marks the object as client-side-encrypted // with the client provided key. It also marks the object as client-side-encrypted
// and sets the correct headers. // and sets the correct headers.
func EncryptRequest(content io.Reader, r *http.Request, metadata map[string]string) (io.Reader, error) { func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, error) {
key, err := ParseSSECustomerRequest(r)
if err != nil { var (
return nil, err key []byte
err error
)
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
return nil, crypto.ErrIncompatibleEncryptionMethod
} }
return newEncryptReader(content, key, metadata) if crypto.SSEC.IsRequested(r.Header) {
key, err = ParseSSECustomerRequest(r)
if err != nil {
return nil, err
}
}
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
} }
// DecryptCopyRequest decrypts the object with the client provided key. It also removes // DecryptCopyRequest 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 DecryptCopyRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) { func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
key, err := ParseSSECopyCustomerRequest(r) var (
if err != nil { key []byte
return nil, err err error
)
if crypto.SSECopy.IsRequested(r.Header) {
key, err = ParseSSECopyCustomerRequest(r, metadata)
if err != nil {
return nil, err
}
} }
delete(metadata, SSECopyCustomerKey) // make sure we do not save the key by accident delete(metadata, crypto.SSECopyKey) // make sure we do not save the key by accident
return newDecryptWriter(client, key, 0, metadata) return newDecryptWriter(client, key, bucket, object, 0, metadata)
} }
func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) { func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option switch {
default:
return nil, errObjectTampered return nil, errObjectTampered
} case crypto.S3.IsEncrypted(metadata):
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV]) if globalKMS == nil {
if err != nil || len(iv) != SSEIVSize { return nil, errKMSNotConfigured
return nil, errObjectTampered }
} keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
if err != nil || len(sealedKey) != 64 {
return nil, errObjectTampered
}
sha := sha256.New() // derive key encryption key if err != nil {
sha.Write(key) return nil, err
sha.Write(iv) }
keyEncryptionKey := sha.Sum(nil) extKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key return nil, err
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{ }
Key: keyEncryptionKey, var objectKey crypto.ObjectKey
}) if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
if n != 32 || err != nil { return nil, err
// Either the provided key does not match or the object was tampered. }
// To provide strict AWS S3 compatibility we return: access denied. return objectKey[:], nil
return nil, errSSEKeyMismatch case crypto.SSEC.IsEncrypted(metadata):
var extKey [32]byte
copy(extKey[:], key)
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
if err != nil {
return nil, err
}
var objectKey crypto.ObjectKey
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
return nil, err
}
return objectKey[:], nil
} }
return objectEncryptionKey.Bytes(), nil
} }
func newDecryptWriter(client io.Writer, key []byte, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) { func newDecryptWriter(client io.Writer, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
objectEncryptionKey, err := decryptObjectInfo(key, metadata) objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata) return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
} }
@@ -431,31 +297,37 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
SequenceNumber: seqNumber, SequenceNumber: seqNumber,
}) })
if err != nil { if err != nil {
return nil, errInvalidSSEKey return nil, crypto.ErrInvalidCustomerKey
} }
delete(metadata, crypto.SSEIV)
delete(metadata, ServerSideEncryptionIV) delete(metadata, crypto.SSESealAlgorithm)
delete(metadata, ServerSideEncryptionSealAlgorithm) delete(metadata, crypto.SSECSealedKey)
delete(metadata, ServerSideEncryptionSealedKey) delete(metadata, crypto.SSEMultipart)
delete(metadata, ReservedMetadataPrefix+"Encrypted-Multipart") delete(metadata, crypto.S3SealedKey)
delete(metadata, crypto.S3KMSSealedKey)
delete(metadata, crypto.S3KMSKeyID)
return writer, nil return writer, 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, 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) {
if crypto.S3.IsEncrypted(metadata) {
return newDecryptWriter(client, nil, bucket, object, seqNumber, metadata)
}
key, err := ParseSSECustomerRequest(r) key, err := ParseSSECustomerRequest(r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
return newDecryptWriter(client, key, seqNumber, metadata) return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
} }
// DecryptRequest decrypts the object with the client provided key. It also removes // DecryptRequest decrypts the object with client provided key for SSE-C and SSE-S3. It also removes
// the client-side-encryption metadata from the object and sets the correct headers. // the encryption metadata from the object and sets the correct headers.
func DecryptRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) { func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
return DecryptRequestWithSequenceNumber(client, r, 0, metadata) return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
} }
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface. // DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
@@ -469,9 +341,10 @@ type DecryptBlocksWriter struct {
// Current part index // Current part index
partIndex int partIndex int
// Parts information // Parts information
parts []objectPartInfo parts []objectPartInfo
req *http.Request req *http.Request
metadata map[string]string bucket, object string
metadata map[string]string
partEncRelOffset int64 partEncRelOffset int64
@@ -489,17 +362,21 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
var key []byte var key []byte
var err error var err error
if w.copySource { if w.copySource {
w.req.Header.Set(SSECopyCustomerKey, w.customerKeyHeader) if crypto.SSEC.IsEncrypted(w.metadata) {
key, err = ParseSSECopyCustomerRequest(w.req) w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
key, err = ParseSSECopyCustomerRequest(w.req, w.metadata)
}
} else { } else {
w.req.Header.Set(SSECustomerKey, w.customerKeyHeader) if crypto.SSEC.IsEncrypted(w.metadata) {
key, err = ParseSSECustomerRequest(w.req) w.req.Header.Set(crypto.SSECKey, w.customerKeyHeader)
key, err = ParseSSECustomerRequest(w.req)
}
} }
if err != nil { if err != nil {
return err return err
} }
objectEncryptionKey, err := decryptObjectInfo(key, m) objectEncryptionKey, err := decryptObjectInfo(key, w.bucket, w.object, m)
if err != nil { if err != nil {
return err return err
} }
@@ -513,9 +390,9 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
// make sure we do not save the key by accident // make sure we do not save the key by accident
if w.copySource { if w.copySource {
delete(m, SSECopyCustomerKey) delete(m, crypto.SSECopyKey)
} else { } else {
delete(m, SSECustomerKey) delete(m, crypto.SSECKey)
} }
// make sure to provide a NopCloser such that a Close // make sure to provide a NopCloser such that a Close
@@ -594,29 +471,26 @@ func (w *DecryptBlocksWriter) Close() error {
// DecryptAllBlocksCopyRequest - setup a struct which can decrypt many concatenated encrypted data // DecryptAllBlocksCopyRequest - setup a struct which can decrypt many concatenated encrypted data
// parts information helps to know the boundaries of each encrypted data block, this function decrypts // parts information helps to know the boundaries of each encrypted data block, this function decrypts
// all parts starting from part-1. // all parts starting from part-1.
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, objInfo ObjectInfo) (io.WriteCloser, int64, error) { func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, bucket, object string, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
w, _, size, err := DecryptBlocksRequest(client, r, 0, objInfo.Size, objInfo, true) w, _, size, err := DecryptBlocksRequest(client, r, bucket, object, 0, objInfo.Size, objInfo, true)
return w, size, err return w, size, err
} }
// DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data // DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data
// parts information helps to know the boundaries of each encrypted data block. // parts information helps to know the boundaries of each encrypted data block.
func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) { func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object string, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
seqNumber, encStartOffset, encLength := getEncryptedStartOffset(startOffset, length) var seqNumber uint32
var encStartOffset, encLength int64
// Encryption length cannot be bigger than the file size, if it is if len(objInfo.Parts) == 0 || !crypto.IsMultiPart(objInfo.UserDefined) {
// which is allowed in AWS S3, we simply default to EncryptedSize(). seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
if encLength+encStartOffset > objInfo.EncryptedSize() {
encLength = objInfo.EncryptedSize() - encStartOffset
}
if len(objInfo.Parts) == 0 || !objInfo.IsEncryptedMultipart() {
var writer io.WriteCloser var writer io.WriteCloser
var err error var err error
if copySource { if copySource {
writer, err = DecryptCopyRequest(client, r, objInfo.UserDefined) writer, err = DecryptCopyRequest(client, r, bucket, object, objInfo.UserDefined)
} else { } else {
writer, err = DecryptRequestWithSequenceNumber(client, r, seqNumber, objInfo.UserDefined) writer, err = DecryptRequestWithSequenceNumber(client, r, bucket, object, seqNumber, objInfo.UserDefined)
} }
if err != nil { if err != nil {
return nil, 0, 0, err return nil, 0, 0, err
@@ -624,6 +498,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
return writer, encStartOffset, encLength, nil return writer, encStartOffset, encLength, nil
} }
seqNumber, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
var partStartIndex int var partStartIndex int
var partStartOffset = startOffset var partStartOffset = startOffset
// Skip parts until final offset maps to a particular part offset. // Skip parts until final offset maps to a particular part offset.
@@ -656,7 +531,9 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
parts: objInfo.Parts, parts: objInfo.Parts,
partIndex: partStartIndex, partIndex: partStartIndex,
req: r, req: r,
customerKeyHeader: r.Header.Get(SSECustomerKey), bucket: bucket,
object: object,
customerKeyHeader: r.Header.Get(crypto.SSECKey),
copySource: copySource, copySource: copySource,
} }
@@ -667,24 +544,76 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
} }
// Purge all the encryption headers. // Purge all the encryption headers.
delete(objInfo.UserDefined, ServerSideEncryptionIV) delete(objInfo.UserDefined, crypto.SSEIV)
delete(objInfo.UserDefined, ServerSideEncryptionSealAlgorithm) delete(objInfo.UserDefined, crypto.SSESealAlgorithm)
delete(objInfo.UserDefined, ServerSideEncryptionSealedKey) delete(objInfo.UserDefined, crypto.SSECSealedKey)
delete(objInfo.UserDefined, ReservedMetadataPrefix+"Encrypted-Multipart") delete(objInfo.UserDefined, crypto.SSEMultipart)
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
delete(objInfo.UserDefined, crypto.S3SealedKey)
delete(objInfo.UserDefined, crypto.S3KMSKeyID)
delete(objInfo.UserDefined, crypto.S3KMSSealedKey)
}
if w.copySource { if w.copySource {
w.customerKeyHeader = r.Header.Get(SSECopyCustomerKey) w.customerKeyHeader = r.Header.Get(crypto.SSECopyKey)
} }
if err := w.buildDecrypter(partStartIndex + 1); err != nil { if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
return nil, 0, 0, err return nil, 0, 0, err
} }
return w, encStartOffset, encLength, nil return w, encStartOffset, encLength, nil
} }
// getEncryptedStartOffset - fetch sequence number, encrypted start offset and encrypted length. // getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
func getEncryptedStartOffset(offset, length int64) (seqNumber uint32, encOffset int64, encLength int64) { func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (uint32, int64, int64) {
// Calculate encrypted offset of a multipart object
computeEncOffset := func(off int64, obj ObjectInfo) (seqNumber uint32, encryptedOffset int64, err error) {
var curPartEndOffset uint64
var prevPartsEncSize int64
for _, p := range obj.Parts {
size, decErr := sio.DecryptedSize(uint64(p.Size))
if decErr != nil {
err = errObjectTampered // assign correct error type
return
}
if off < int64(curPartEndOffset+size) {
seqNumber, encryptedOffset, _ = getEncryptedSinglePartOffsetLength(off-int64(curPartEndOffset), 1, obj)
encryptedOffset += int64(prevPartsEncSize)
break
}
curPartEndOffset += size
prevPartsEncSize += p.Size
}
return
}
// Calculate the encrypted start offset corresponding to the plain offset
seqNumber, encStartOffset, _ := computeEncOffset(offset, obj)
// Calculate also the encrypted end offset corresponding to plain offset + plain length
_, encEndOffset, _ := computeEncOffset(offset+length-1, obj)
// encLength is the diff between encrypted end offset and encrypted start offset + one package size
// to ensure all encrypted data are covered
encLength := encEndOffset - encStartOffset + (64*1024 + 32)
// Calculate total size of all parts
var totalPartsLength int64
for _, p := range obj.Parts {
totalPartsLength += p.Size
}
// Set encLength to maximum possible value if it exceeded total parts size
if encLength+encStartOffset > totalPartsLength {
encLength = totalPartsLength - encStartOffset
}
return seqNumber, encStartOffset, encLength
}
// getEncryptedSinglePartOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo) (seqNumber uint32, encOffset int64, encLength int64) {
onePkgSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize) onePkgSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
seqNumber = uint32(offset / sseDAREPackageBlockSize) seqNumber = uint32(offset / sseDAREPackageBlockSize)
@@ -702,55 +631,36 @@ func getEncryptedStartOffset(offset, length int64) (seqNumber uint32, encOffset
encLength += onePkgSize encLength += onePkgSize
} }
if encLength+encOffset > objInfo.EncryptedSize() {
encLength = objInfo.EncryptedSize() - encOffset
}
return seqNumber, encOffset, encLength return seqNumber, encOffset, encLength
} }
// IsEncryptedMultipart - is the encrypted content multiparted?
func (o *ObjectInfo) IsEncryptedMultipart() bool {
_, ok := o.UserDefined[ReservedMetadataPrefix+"Encrypted-Multipart"]
return ok
}
// IsEncrypted returns true if the object is marked as encrypted.
func (o *ObjectInfo) IsEncrypted() bool {
if _, ok := o.UserDefined[ServerSideEncryptionIV]; ok {
return true
}
if _, ok := o.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
return true
}
if _, ok := o.UserDefined[ServerSideEncryptionSealedKey]; ok {
return true
}
return false
}
// IsEncrypted returns true if the object is marked as encrypted.
func (li *ListPartsInfo) IsEncrypted() bool {
if _, ok := li.UserDefined[ServerSideEncryptionIV]; ok {
return true
}
if _, ok := li.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
return true
}
if _, ok := li.UserDefined[ServerSideEncryptionSealedKey]; ok {
return true
}
return false
}
// DecryptedSize returns the size of the object after decryption in bytes. // DecryptedSize returns the size of the object after decryption in bytes.
// It returns an error if the object is not encrypted or marked as encrypted // It returns an error if the object is not encrypted or marked as encrypted
// but has an invalid size. // but has an invalid size.
func (o *ObjectInfo) DecryptedSize() (int64, error) { func (o *ObjectInfo) DecryptedSize() (int64, error) {
if !o.IsEncrypted() { 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")
} }
size, err := sio.DecryptedSize(uint64(o.Size)) if len(o.Parts) == 0 || !crypto.IsMultiPart(o.UserDefined) {
if err != nil { size, err := sio.DecryptedSize(uint64(o.Size))
err = errObjectTampered // assign correct error type if err != nil {
err = errObjectTampered // assign correct error type
}
return int64(size), err
} }
return int64(size), err
var size int64
for _, part := range o.Parts {
partSize, err := sio.DecryptedSize(uint64(part.Size))
if err != nil {
return 0, errObjectTampered
}
size += int64(partSize)
}
return size, nil
} }
// EncryptedSize returns the size of the object after encryption. // EncryptedSize returns the size of the object after encryption.
@@ -780,10 +690,11 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
if info.IsDir { if info.IsDir {
return ErrNone, false return ErrNone, false
} }
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && hasSSECopyCustomerHeader(headers) { if apiErr, encrypted = ErrNone, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSECopy.IsRequested(headers) {
apiErr = ErrInvalidEncryptionParameters apiErr = ErrInvalidEncryptionParameters
} else if encrypted { } else if encrypted {
if !hasSSECopyCustomerHeader(headers) { if (!crypto.SSECopy.IsRequested(headers) && crypto.SSEC.IsEncrypted(info.UserDefined)) ||
(crypto.SSECopy.IsRequested(headers) && crypto.S3.IsEncrypted(info.UserDefined)) {
apiErr = ErrSSEEncryptedObject apiErr = ErrSSEEncryptedObject
return return
} }
@@ -807,10 +718,16 @@ func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErrorCo
if info.IsDir { if info.IsDir {
return ErrNone, false return ErrNone, false
} }
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && hasSSECustomerHeader(headers) { // disallow X-Amz-Server-Side-Encryption header on HEAD and GET
if crypto.S3.IsRequested(headers) {
apiErr = ErrInvalidEncryptionParameters
return
}
if apiErr, encrypted = ErrNone, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSEC.IsRequested(headers) {
apiErr = ErrInvalidEncryptionParameters apiErr = ErrInvalidEncryptionParameters
} else if encrypted { } else if encrypted {
if !hasSSECustomerHeader(headers) { if (crypto.SSEC.IsEncrypted(info.UserDefined) && !crypto.SSEC.IsRequested(headers)) ||
(crypto.S3.IsEncrypted(info.UserDefined) && crypto.SSEC.IsRequested(headers)) {
apiErr = ErrSSEEncryptedObject apiErr = ErrSSEEncryptedObject
return return
} }
+254 -150
View File
@@ -18,21 +18,52 @@ package cmd
import ( import (
"bytes" "bytes"
"encoding/base64"
"net/http" "net/http"
"testing" "testing"
"github.com/minio/minio/cmd/crypto"
) )
var hasServerSideEncryptionHeaderTests = []struct {
headers map[string]string
sseRequest bool
}{
{headers: map[string]string{crypto.SSECAlgorithm: "AES256", crypto.SSECKey: "key", crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 0
{headers: map[string]string{crypto.SSECAlgorithm: "AES256"}, sseRequest: true}, // 1
{headers: map[string]string{crypto.SSECKey: "key"}, sseRequest: true}, // 2
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
{headers: map[string]string{}, sseRequest: false}, // 4
{headers: map[string]string{crypto.SSECopyAlgorithm + " ": "AES256", " " + crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
{headers: map[string]string{crypto.SSECopyAlgorithm: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: false}, // 6
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: true}, // 6
}
func TestHasServerSideEncryptionHeader(t *testing.T) {
for i, test := range hasServerSideEncryptionHeaderTests {
headers := http.Header{}
for k, v := range test.headers {
headers.Set(k, v)
}
if hasServerSideEncryptionHeader(headers) != test.sseRequest {
t.Errorf("Test %d: Expected hasServerSideEncryptionHeader to return %v", i, test.sseRequest)
}
}
}
var hasSSECopyCustomerHeaderTests = []struct { var hasSSECopyCustomerHeaderTests = []struct {
headers map[string]string headers map[string]string
sseRequest bool sseRequest bool
}{ }{
{headers: map[string]string{SSECopyCustomerAlgorithm: "AES256", SSECopyCustomerKey: "key", SSECopyCustomerKeyMD5: "md5"}, sseRequest: true}, // 0 {headers: map[string]string{crypto.SSECopyAlgorithm: "AES256", crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5: "md5"}, sseRequest: true}, // 0
{headers: map[string]string{SSECopyCustomerAlgorithm: "AES256"}, sseRequest: true}, // 1 {headers: map[string]string{crypto.SSECopyAlgorithm: "AES256"}, sseRequest: true}, // 1
{headers: map[string]string{SSECopyCustomerKey: "key"}, sseRequest: true}, // 2 {headers: map[string]string{crypto.SSECopyKey: "key"}, sseRequest: true}, // 2
{headers: map[string]string{SSECopyCustomerKeyMD5: "md5"}, sseRequest: true}, // 3 {headers: map[string]string{crypto.SSECopyKeyMD5: "md5"}, sseRequest: true}, // 3
{headers: map[string]string{}, sseRequest: false}, // 4 {headers: map[string]string{}, sseRequest: false}, // 4
{headers: map[string]string{SSECopyCustomerAlgorithm + " ": "AES256", " " + SSECopyCustomerKey: "key", SSECopyCustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5 {headers: map[string]string{crypto.SSECopyAlgorithm + " ": "AES256", " " + crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
{headers: map[string]string{SSECopyCustomerAlgorithm: "", SSECopyCustomerKey: "", SSECopyCustomerKeyMD5: ""}, sseRequest: false}, // 6 {headers: map[string]string{crypto.SSECopyAlgorithm: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: true}, // 6
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
} }
func TestIsSSECopyCustomerRequest(t *testing.T) { func TestIsSSECopyCustomerRequest(t *testing.T) {
@@ -41,8 +72,8 @@ func TestIsSSECopyCustomerRequest(t *testing.T) {
for k, v := range test.headers { for k, v := range test.headers {
headers.Set(k, v) headers.Set(k, v)
} }
if hasSSECopyCustomerHeader(headers) != test.sseRequest { if crypto.SSECopy.IsRequested(headers) != test.sseRequest {
t.Errorf("Test %d: Expected hasSSECopyCustomerHeader to return %v", i, test.sseRequest) t.Errorf("Test %d: Expected crypto.SSECopy.IsRequested to return %v", i, test.sseRequest)
} }
} }
} }
@@ -51,13 +82,15 @@ var hasSSECustomerHeaderTests = []struct {
headers map[string]string headers map[string]string
sseRequest bool sseRequest bool
}{ }{
{headers: map[string]string{SSECustomerAlgorithm: "AES256", SSECustomerKey: "key", SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 0 {headers: map[string]string{crypto.SSECAlgorithm: "AES256", crypto.SSECKey: "key", crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 0
{headers: map[string]string{SSECustomerAlgorithm: "AES256"}, sseRequest: true}, // 1 {headers: map[string]string{crypto.SSECAlgorithm: "AES256"}, sseRequest: true}, // 1
{headers: map[string]string{SSECustomerKey: "key"}, sseRequest: true}, // 2 {headers: map[string]string{crypto.SSECKey: "key"}, sseRequest: true}, // 2
{headers: map[string]string{SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 3 {headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
{headers: map[string]string{}, sseRequest: false}, // 4 {headers: map[string]string{}, sseRequest: false}, // 4
{headers: map[string]string{SSECustomerAlgorithm + " ": "AES256", " " + SSECustomerKey: "key", SSECustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5 {headers: map[string]string{crypto.SSECAlgorithm + " ": "AES256", " " + crypto.SSECKey: "key", crypto.SSECKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
{headers: map[string]string{SSECustomerAlgorithm: "", SSECustomerKey: "", SSECustomerKeyMD5: ""}, sseRequest: false}, // 6 {headers: map[string]string{crypto.SSECAlgorithm: "", crypto.SSECKey: "", crypto.SSECKeyMD5: ""}, sseRequest: false}, // 6
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
} }
func TesthasSSECustomerHeader(t *testing.T) { func TesthasSSECustomerHeader(t *testing.T) {
@@ -66,7 +99,7 @@ func TesthasSSECustomerHeader(t *testing.T) {
for k, v := range test.headers { for k, v := range test.headers {
headers.Set(k, v) headers.Set(k, v)
} }
if hasSSECustomerHeader(headers) != test.sseRequest { if crypto.SSEC.IsRequested(headers) != test.sseRequest {
t.Errorf("Test %d: Expected hasSSECustomerHeader to return %v", i, test.sseRequest) t.Errorf("Test %d: Expected hasSSECustomerHeader to return %v", i, test.sseRequest)
} }
} }
@@ -79,75 +112,84 @@ var parseSSECustomerRequestTests = []struct {
}{ }{
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0 crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: true, err: nil, useTLS: true, err: nil,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1 crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: false, err: errInsecureSSERequest, useTLS: false, err: errInsecureSSERequest,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES 256", crypto.SSECAlgorithm: "AES 256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2 crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: true, err: errInvalidSSEAlgorithm, useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3 crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==", crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
}, },
useTLS: true, err: errSSEKeyMD5Mismatch, useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4 crypto.SSECKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==", crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
}, },
useTLS: true, err: errInvalidSSEKey, useTLS: true, err: crypto.ErrInvalidCustomerKey,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5 crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
SSECustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==", crypto.SSECKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
}, },
useTLS: true, err: errSSEKeyMD5Mismatch, useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6 crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==", crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
}, },
useTLS: true, err: errInvalidSSEKey, useTLS: true, err: crypto.ErrInvalidCustomerKey,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "", // 7 crypto.SSECKey: "", // 7
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==", crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
}, },
useTLS: true, err: errMissingSSEKey, useTLS: true, err: crypto.ErrMissingCustomerKey,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8 crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
SSECustomerKeyMD5: "", crypto.SSECKeyMD5: "",
}, },
useTLS: true, err: errMissingSSEKeyMD5, 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,
}, },
} }
@@ -166,8 +208,8 @@ func TestParseSSECustomerRequest(t *testing.T) {
if err != test.err { if err != test.err {
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err) t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
} }
key := request.Header.Get(SSECustomerKey) key := request.Header.Get(crypto.SSECKey)
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" { if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key) t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
} }
@@ -175,81 +217,100 @@ func TestParseSSECustomerRequest(t *testing.T) {
} }
var parseSSECopyCustomerRequestTests = []struct { var parseSSECopyCustomerRequestTests = []struct {
headers map[string]string headers map[string]string
useTLS bool metadata map[string]string
err error useTLS bool
err error
}{ }{
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0 crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: true, err: nil, metadata: map[string]string{},
useTLS: true, err: nil,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1 crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: false, err: errInsecureSSERequest, 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{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES 256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2 crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: true, err: errInvalidSSEAlgorithm, metadata: map[string]string{},
useTLS: false, err: errInsecureSSERequest,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES 256",
SSECopyCustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3 crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
SSECopyCustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==", crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
useTLS: true, err: errSSEKeyMD5Mismatch, metadata: map[string]string{},
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4 crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
SSECopyCustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==", crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
}, },
useTLS: true, err: errInvalidSSEKey, metadata: map[string]string{},
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5 crypto.SSECopyKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
SSECopyCustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==", crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
}, },
useTLS: true, err: errSSEKeyMD5Mismatch, metadata: map[string]string{},
useTLS: true, err: crypto.ErrInvalidCustomerKey,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6 crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
SSECopyCustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==", crypto.SSECopyKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
}, },
useTLS: true, err: errInvalidSSEKey, metadata: map[string]string{},
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "", // 7 crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
SSECopyCustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==", crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
}, },
useTLS: true, err: errMissingSSEKey, metadata: map[string]string{},
useTLS: true, err: crypto.ErrInvalidCustomerKey,
}, },
{ {
headers: map[string]string{ headers: map[string]string{
SSECopyCustomerAlgorithm: "AES256", crypto.SSECopyAlgorithm: "AES256",
SSECopyCustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8 crypto.SSECopyKey: "", // 7
SSECopyCustomerKeyMD5: "", crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
}, },
useTLS: true, err: errMissingSSEKeyMD5, 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,
}, },
} }
@@ -264,12 +325,12 @@ func TestParseSSECopyCustomerRequest(t *testing.T) {
request.Header = headers request.Header = headers
globalIsSSL = test.useTLS globalIsSSL = test.useTLS
_, err := ParseSSECopyCustomerRequest(request) _, err := ParseSSECopyCustomerRequest(request, test.metadata)
if err != test.err { if err != test.err {
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err) t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
} }
key := request.Header.Get(SSECopyCustomerKey) key := request.Header.Get(crypto.SSECopyKey)
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" { if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key) t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
} }
} }
@@ -281,20 +342,20 @@ var encryptRequestTests = []struct {
}{ }{
{ {
header: map[string]string{ header: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
metadata: map[string]string{}, metadata: map[string]string{},
}, },
{ {
header: map[string]string{ header: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
metadata: map[string]string{ metadata: map[string]string{
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
}, },
}, },
} }
@@ -308,79 +369,119 @@ func TestEncryptRequest(t *testing.T) {
for k, v := range test.header { for k, v := range test.header {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
_, err := EncryptRequest(content, req, test.metadata) _, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
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[SSECustomerKey]; ok { if key, ok := test.metadata[crypto.SSECKey]; ok {
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key) t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
} }
if kdf, ok := test.metadata[ServerSideEncryptionSealAlgorithm]; !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)
} }
if iv, ok := test.metadata[ServerSideEncryptionIV]; !ok { if iv, ok := test.metadata[crypto.SSEIV]; !ok {
t.Errorf("Test %d: ServerSideEncryptionIV must be part of metadata: %v", i, iv) t.Errorf("Test %d: crypto.SSEIV must be part of metadata: %v", i, iv)
} }
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; !ok { if mac, ok := test.metadata[crypto.SSECSealedKey]; !ok {
t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac) t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac)
} }
} }
} }
var decryptRequestTests = []struct { var decryptRequestTests = []struct {
header map[string]string bucket, object string
metadata map[string]string header map[string]string
shouldFail bool metadata map[string]string
shouldFail bool
}{ }{
{ {
bucket: "bucket",
object: "object",
header: map[string]string{ header: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=", crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==", crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
}, },
metadata: map[string]string{ metadata: map[string]string{
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256, crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
ServerSideEncryptionIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=", crypto.SSEIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
ServerSideEncryptionSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==", crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
}, },
shouldFail: false, shouldFail: false,
}, },
{ {
bucket: "bucket",
object: "object",
header: map[string]string{ header: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
}, },
metadata: map[string]string{ metadata: map[string]string{
ServerSideEncryptionSealAlgorithm: "HMAC-SHA3", crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=", crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
},
shouldFail: false,
},
{
bucket: "bucket",
object: "object",
header: map[string]string{
crypto.SSECAlgorithm: "AES256",
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
},
metadata: map[string]string{
crypto.SSESealAlgorithm: "HMAC-SHA3",
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
}, },
shouldFail: true, shouldFail: true,
}, },
{ {
bucket: "bucket",
object: "object",
header: map[string]string{ header: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
metadata: map[string]string{ metadata: map[string]string{
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256, crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
ServerSideEncryptionIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSEIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=", crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
}, },
shouldFail: true, shouldFail: true,
}, },
{ {
bucket: "bucket",
object: "object",
header: map[string]string{ header: map[string]string{
SSECustomerAlgorithm: "AES256", crypto.SSECAlgorithm: "AES256",
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==", crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
}, },
metadata: map[string]string{ metadata: map[string]string{
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256, crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=", crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=", crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
},
shouldFail: true,
},
{
bucket: "bucket",
object: "object-2",
header: map[string]string{
crypto.SSECAlgorithm: "AES256",
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
},
metadata: map[string]string{
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
}, },
shouldFail: true, shouldFail: true,
}, },
@@ -389,26 +490,29 @@ var decryptRequestTests = []struct {
func TestDecryptRequest(t *testing.T) { func TestDecryptRequest(t *testing.T) {
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL) defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
globalIsSSL = true globalIsSSL = true
for i, test := range decryptRequestTests { for i, test := range decryptRequestTests[1:] {
client := bytes.NewBuffer(nil) client := bytes.NewBuffer(nil)
req := &http.Request{Header: http.Header{}} req := &http.Request{Header: http.Header{}}
for k, v := range test.header { for k, v := range test.header {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
_, err := DecryptRequest(client, req, test.metadata) _, err := DecryptRequest(client, req, test.bucket, test.object, test.metadata)
if err != nil && !test.shouldFail { if err != nil && !test.shouldFail {
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[SSECustomerKey]; ok { if err == nil && test.shouldFail {
t.Fatalf("Test %d: should fail but passed", i)
}
if key, ok := test.metadata[crypto.SSECKey]; ok {
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key) t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
} }
if kdf, ok := test.metadata[ServerSideEncryptionSealAlgorithm]; ok && !test.shouldFail { if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; ok && !test.shouldFail {
t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf) t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf)
} }
if iv, ok := test.metadata[ServerSideEncryptionIV]; ok && !test.shouldFail { if iv, ok := test.metadata[crypto.SSEIV]; ok && !test.shouldFail {
t.Errorf("Test %d: ServerSideEncryptionIV should not be part of metadata: %v", i, iv) t.Errorf("Test %d: crypto.SSEIV should not be part of metadata: %v", i, iv)
} }
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; ok && !test.shouldFail { if mac, ok := test.metadata[crypto.SSECSealedKey]; ok && !test.shouldFail {
t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac) t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac)
} }
} }
@@ -425,28 +529,28 @@ var decryptObjectInfoTests = []struct {
expErr: ErrNone, expErr: ErrNone,
}, },
{ {
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}}, info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}}, headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
expErr: ErrNone, expErr: ErrNone,
}, },
{ {
info: ObjectInfo{Size: 0, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}}, info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}}, headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
expErr: ErrNone, expErr: ErrNone,
}, },
{ {
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}}, info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA=="}},
headers: http.Header{}, headers: http.Header{},
expErr: ErrSSEEncryptedObject, expErr: ErrSSEEncryptedObject,
}, },
{ {
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}}, info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}}, headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
expErr: ErrInvalidEncryptionParameters, expErr: ErrInvalidEncryptionParameters,
}, },
{ {
info: ObjectInfo{Size: 31, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}}, info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}}, headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
expErr: ErrObjectTampered, expErr: ErrObjectTampered,
}, },
} }
@@ -455,7 +559,7 @@ func TestDecryptObjectInfo(t *testing.T) {
for i, test := range decryptObjectInfoTests { for i, test := range decryptObjectInfoTests {
if err, encrypted := DecryptObjectInfo(&test.info, test.headers); err != test.expErr { if err, encrypted := 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 := test.info.IsEncrypted(); 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)
} else if !encrypted && enc != encrypted { } else if !encrypted && enc != encrypted {
t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i) t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
+37 -4
View File
@@ -18,6 +18,8 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"strconv"
"strings" "strings"
"github.com/minio/minio-go/pkg/set" "github.com/minio/minio-go/pkg/set"
@@ -69,10 +71,26 @@ func getSetIndexes(args []string, totalSizes []uint64) (setIndexes [][]uint64, e
return nil, errInvalidArgument return nil, errInvalidArgument
} }
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
isValidSetSize := func(count uint64) bool {
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0)
}
var customSetDriveCount uint64
if v := os.Getenv("MINIO_ERASURE_SET_DRIVE_COUNT"); v != "" {
customSetDriveCount, err = strconv.ParseUint(v, 10, 64)
if err != nil {
return nil, uiErrInvalidErasureSetSize(err)
}
if !isValidSetSize(customSetDriveCount) {
return nil, uiErrInvalidErasureSetSize(nil)
}
}
setIndexes = make([][]uint64, len(totalSizes)) setIndexes = make([][]uint64, len(totalSizes))
for _, totalSize := range totalSizes { for _, totalSize := range totalSizes {
// Check if totalSize has minimum range upto setSize // Check if totalSize has minimum range upto setSize
if totalSize < setSizes[0] { if totalSize < setSizes[0] || totalSize < customSetDriveCount {
return nil, uiErrInvalidNumberOfErasureEndpoints(nil) return nil, uiErrInvalidNumberOfErasureEndpoints(nil)
} }
} }
@@ -95,9 +113,24 @@ func getSetIndexes(args []string, totalSizes []uint64) (setIndexes [][]uint64, e
setSize = commonSize setSize = commonSize
} }
// isValidSetSize - checks whether given count is a valid set size for erasure coding. possibleSetCounts := func(setSize uint64) (ss []uint64) {
isValidSetSize := func(count uint64) bool { for _, s := range setSizes {
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0) if setSize%s == 0 {
ss = append(ss, s)
}
}
return ss
}
if customSetDriveCount > 0 {
msg := fmt.Sprintf("Invalid set drive count, leads to non-uniform distribution for the given number of disks. Possible values for custom set count are %d", possibleSetCounts(setSize))
if customSetDriveCount > setSize {
return nil, uiErrInvalidErasureSetSize(nil).Msg(msg)
}
if setSize%customSetDriveCount != 0 {
return nil, uiErrInvalidErasureSetSize(nil).Msg(msg)
}
setSize = customSetDriveCount
} }
// Check whether setSize is with the supported range. // Check whether setSize is with the supported range.
+60
View File
@@ -18,6 +18,7 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"reflect" "reflect"
"testing" "testing"
@@ -84,6 +85,65 @@ func TestGetDivisibleSize(t *testing.T) {
} }
} }
// Test tests calculating set indexes with ENV override for drive count.
func TestGetSetIndexesEnvOverride(t *testing.T) {
testCases := []struct {
args []string
totalSizes []uint64
indexes [][]uint64
envOverride string
success bool
}{
{
[]string{"data{1...64}"},
[]uint64{64},
[][]uint64{{8, 8, 8, 8, 8, 8, 8, 8}},
"8",
true,
},
{
[]string{"data{1...60}"},
nil,
nil,
"8",
false,
},
{
[]string{"data{1...64}"},
nil,
nil,
"-1",
false,
},
{
[]string{"data{1...64}"},
nil,
nil,
"2",
false,
},
}
for i, testCase := range testCases {
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
if err := os.Setenv("MINIO_ERASURE_SET_DRIVE_COUNT", testCase.envOverride); err != nil {
t.Fatal(err)
}
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes)
if err != nil && testCase.success {
t.Errorf("Expected success but failed instead %s", err)
}
if err == nil && !testCase.success {
t.Errorf("Expected failure but passed instead")
}
if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
}
os.Unsetenv("MINIO_ERASURE_SET_DRIVE_COUNT")
})
}
}
// Test tests calculating set indexes. // Test tests calculating set indexes.
func TestGetSetIndexes(t *testing.T) { func TestGetSetIndexes(t *testing.T) {
testCases := []struct { testCases := []struct {
+74 -67
View File
@@ -18,85 +18,92 @@ package cmd
import ( import (
"context" "context"
"hash"
"io" "io"
"sync"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
) )
// CreateFile creates a new bitrot encoded file spread over all available disks. CreateFile will create // Writes in parallel to bitrotWriters
// the file at the given volume and path. It will read from src until an io.EOF occurs. The given algorithm will type parallelWriter struct {
// be used to protect the erasure encoded file. writers []*bitrotWriter
func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, volume, path string, buffer []byte, algorithm BitrotAlgorithm, writeQuorum int) (f ErasureFileInfo, err error) { writeQuorum int
if !algorithm.Available() { errs []error
logger.LogIf(ctx, errBitrotHashAlgoInvalid) }
return f, errBitrotHashAlgoInvalid
}
f.Checksums = make([][]byte, len(s.disks))
hashers := make([]hash.Hash, len(s.disks))
for i := range hashers {
hashers[i] = algorithm.New()
}
errChans, errs := make([]chan error, len(s.disks)), make([]error, len(s.disks))
for i := range errChans {
errChans[i] = make(chan error, 1) // create buffered channel to let finished go-routines die early
}
var blocks [][]byte // Append appends data to bitrotWriters in parallel.
var n = len(buffer) func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
for n == len(buffer) { var wg sync.WaitGroup
n, err = io.ReadFull(src, buffer)
if n == 0 && err == io.EOF {
if f.Size != 0 { // don't write empty block if we have written to the disks
break
}
blocks = make([][]byte, len(s.disks)) // write empty block
} else if err == nil || (n > 0 && err == io.ErrUnexpectedEOF) {
blocks, err = s.ErasureEncode(ctx, buffer[:n])
if err != nil {
return f, err
}
} else {
logger.LogIf(ctx, err)
return f, err
}
for i := range errChans { // span workers for i := range p.writers {
go erasureAppendFile(ctx, s.disks[i], volume, path, hashers[i], blocks[i], errChans[i]) if p.writers[i] == nil {
} p.errs[i] = errDiskNotFound
for i := range errChans { // wait until all workers are finished
errs[i] = <-errChans[i]
}
if err = reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum); err != nil {
return f, err
}
s.disks = evalDisks(s.disks, errs)
f.Size += int64(n)
}
f.Algorithm = algorithm
for i, disk := range s.disks {
if disk == OfflineDisk {
continue continue
} }
f.Checksums[i] = hashers[i].Sum(nil)
wg.Add(1)
go func(i int) {
defer wg.Done()
p.errs[i] = p.writers[i].Append(blocks[i])
if p.errs[i] != nil {
p.writers[i] = nil
}
}(i)
} }
return f, nil wg.Wait()
// If nilCount >= p.writeQuorum, we return nil. This is because HealFile() uses
// CreateFile with p.writeQuorum=1 to accommodate healing of single disk.
// i.e if we do no return here in such a case, reduceWriteQuorumErrs() would
// return a quorum error to HealFile().
nilCount := 0
for _, err := range p.errs {
if err == nil {
nilCount++
}
}
if nilCount >= p.writeQuorum {
return nil
}
return reduceWriteQuorumErrs(ctx, p.errs, objectOpIgnoredErrs, p.writeQuorum)
} }
// erasureAppendFile appends the content of buf to the file on the given disk and updates computes // CreateFile reads from the reader, erasure-encodes the data and writes to the writers.
// the hash of the written data. It sends the write error (or nil) over the error channel. func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, writers []*bitrotWriter, buf []byte, quorum int) (total int64, err error) {
func erasureAppendFile(ctx context.Context, disk StorageAPI, volume, path string, hash hash.Hash, buf []byte, errChan chan<- error) { writer := &parallelWriter{
if disk == OfflineDisk { writers: writers,
logger.LogIf(ctx, errDiskNotFound) writeQuorum: quorum,
errChan <- errDiskNotFound errs: make([]error, len(writers)),
return
} }
err := disk.AppendFile(volume, path, buf)
if err != nil { for {
errChan <- err var blocks [][]byte
return n, err := io.ReadFull(src, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
logger.LogIf(ctx, err)
return 0, err
}
eof := err == io.EOF || err == io.ErrUnexpectedEOF
if n == 0 && total != 0 {
// Reached EOF, nothing more to be done.
break
}
// We take care of the situation where if n == 0 and total == 0 by creating empty data and parity files.
blocks, err = s.ErasureEncode(ctx, buf[:n])
if err != nil {
logger.LogIf(ctx, err)
return 0, err
}
if err = writer.Append(ctx, blocks); err != nil {
logger.LogIf(ctx, err)
return 0, err
}
total += int64(n)
if eof {
break
}
} }
hash.Write(buf) return total, nil
errChan <- err
} }
+51 -25
View File
@@ -54,15 +54,14 @@ var erasureCreateFileTests = []struct {
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9 {dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10 {dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11 {dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
{dataBlocks: 6, onDisks: 12, offDisks: 5, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 102, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 12 {dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14 {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 14
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 15 {dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16 {dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17 {dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18 {dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 19 {dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
} }
func TestErasureCreateFile(t *testing.T) { func TestErasureCreateFile(t *testing.T) {
@@ -71,7 +70,8 @@ func TestErasureCreateFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Test %d: failed to create test setup: %v", i, err) t.Fatalf("Test %d: failed to create test setup: %v", i, err)
} }
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize) disks := setup.disks
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
if err != nil { if err != nil {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err) t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
@@ -83,25 +83,43 @@ func TestErasureCreateFile(t *testing.T) {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to generate random test data: %v", i, err) t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
} }
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), "testbucket", "object", buffer, test.algorithm, test.dataBlocks+1) writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
if disk == OfflineDisk {
continue
}
writers[i] = newBitrotWriter(disk, "testbucket", "object", test.algorithm)
}
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, storage.dataBlocks+1)
if err != nil && !test.shouldFail { if err != nil && !test.shouldFail {
t.Errorf("Test %d: should pass but failed with: %v", i, err) t.Errorf("Test %d: should pass but failed with: %v", i, err)
} }
if err == nil && test.shouldFail { if err == nil && test.shouldFail {
t.Errorf("Test %d: should fail but it passed", i) t.Errorf("Test %d: should fail but it passed", i)
} }
for i, w := range writers {
if err == nil { if w == nil {
if length := int64(len(data[test.offset:])); file.Size != length { disks[i] = OfflineDisk
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
} }
for j := range storage.disks[:test.offDisks] { }
storage.disks[j] = badDisk{nil} if err == nil {
if length := int64(len(data[test.offset:])); n != length {
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
}
writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
if disk == nil {
continue
}
writers[i] = newBitrotWriter(disk, "testbucket", "object2", test.algorithm)
}
for j := range disks[:test.offDisks] {
writers[j].disk = badDisk{nil}
} }
if test.offDisks > 0 { if test.offDisks > 0 {
storage.disks[0] = OfflineDisk writers[0] = nil
} }
file, err = storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), "testbucket", "object2", buffer, test.algorithm, test.dataBlocks+1) n, err = storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, storage.dataBlocks+1)
if err != nil && !test.shouldFailQuorum { if err != nil && !test.shouldFailQuorum {
t.Errorf("Test %d: should pass but failed with: %v", i, err) t.Errorf("Test %d: should pass but failed with: %v", i, err)
} }
@@ -109,8 +127,8 @@ func TestErasureCreateFile(t *testing.T) {
t.Errorf("Test %d: should fail but it passed", i) t.Errorf("Test %d: should fail but it passed", i)
} }
if err == nil { if err == nil {
if length := int64(len(data[test.offset:])); file.Size != length { if length := int64(len(data[test.offset:])); n != length {
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length) t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
} }
} }
} }
@@ -126,25 +144,33 @@ func benchmarkErasureWrite(data, parity, dataDown, parityDown int, size int64, b
b.Fatalf("failed to create test setup: %v", err) b.Fatalf("failed to create test setup: %v", err)
} }
defer setup.Remove() defer setup.Remove()
storage, err := NewErasureStorage(context.Background(), setup.disks, data, parity, blockSizeV1) storage, err := NewErasureStorage(context.Background(), data, parity, blockSizeV1)
if err != nil { if err != nil {
b.Fatalf("failed to create ErasureStorage: %v", err) b.Fatalf("failed to create ErasureStorage: %v", err)
} }
disks := setup.disks
buffer := make([]byte, blockSizeV1, 2*blockSizeV1) buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
content := make([]byte, size) content := make([]byte, size)
for i := 0; i < dataDown; i++ { for i := 0; i < dataDown; i++ {
storage.disks[i] = OfflineDisk disks[i] = OfflineDisk
} }
for i := data; i < data+parityDown; i++ { for i := data; i < data+parityDown; i++ {
storage.disks[i] = OfflineDisk disks[i] = OfflineDisk
} }
b.ResetTimer() b.ResetTimer()
b.SetBytes(size) b.SetBytes(size)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := storage.CreateFile(context.Background(), bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1) writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
if disk == OfflineDisk {
continue
}
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
}
_, err := storage.CreateFile(context.Background(), bytes.NewReader(content), writers, buffer, storage.dataBlocks+1)
if err != nil { if err != nil {
panic(err) panic(err)
} }
+19 -159
View File
@@ -18,171 +18,31 @@ package cmd
import ( import (
"context" "context"
"fmt" "io"
"hash"
"strings"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
) )
// HealFile tries to reconstruct an erasure-coded file spread over all // HealFile heals the shard files on non-nil writers. Note that the quorum passed is 1
// available disks. HealFile will read the valid parts of the file, // as healing should continue even if it has been successful healing only one shard file.
// reconstruct the missing data and write the reconstructed parts back func (s ErasureStorage) HealFile(ctx context.Context, readers []*bitrotReader, writers []*bitrotWriter, size int64) error {
// to `staleDisks` at the destination `dstVol/dstPath/`. Parts are r, w := io.Pipe()
// verified against the given BitrotAlgorithm and checksums. go func() {
// if err := s.ReadFile(ctx, w, readers, 0, size, size); err != nil {
// `staleDisks` is a slice of disks where each non-nil entry has stale w.CloseWithError(err)
// or no data, and so will be healed. return
//
// It is required that `s.disks` have a (read-quorum) majority of
// disks with valid data for healing to work.
//
// In addition, `staleDisks` and `s.disks` must have the same ordering
// of disks w.r.t. erasure coding of the object.
//
// Errors when writing to `staleDisks` are not propagated as long as
// writes succeed for at least one disk. This allows partial healing
// despite stale disks being faulty.
//
// It returns bitrot checksums for the non-nil staleDisks on which
// healing succeeded.
func (s ErasureStorage) HealFile(ctx context.Context, staleDisks []StorageAPI, volume, path string, blocksize int64,
dstVol, dstPath string, size int64, alg BitrotAlgorithm, checksums [][]byte) (
f ErasureFileInfo, err error) {
if !alg.Available() {
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
return f, errBitrotHashAlgoInvalid
}
// Initialization
f.Checksums = make([][]byte, len(s.disks))
hashers := make([]hash.Hash, len(s.disks))
verifiers := make([]*BitrotVerifier, len(s.disks))
for i, disk := range s.disks {
switch {
case staleDisks[i] != nil:
hashers[i] = alg.New()
case disk == nil:
// disregard unavailable disk
continue
default:
verifiers[i] = NewBitrotVerifier(alg, checksums[i])
} }
} w.Close()
writeErrors := make([]error, len(s.disks)) }()
buf := make([]byte, s.blockSize)
// Read part file data on each disk // quorum is 1 because CreateFile should continue writing as long as we are writing to even 1 disk.
chunksize := ceilFrac(blocksize, int64(s.dataBlocks)) n, err := s.CreateFile(ctx, r, writers, buf, 1)
numBlocks := ceilFrac(size, blocksize)
readLen := chunksize * (numBlocks - 1)
lastChunkSize := chunksize
hasSmallerLastBlock := size%blocksize != 0
if hasSmallerLastBlock {
lastBlockLen := size % blocksize
lastChunkSize = ceilFrac(lastBlockLen, int64(s.dataBlocks))
}
readLen += lastChunkSize
var buffers [][]byte
buffers, _, err = s.readConcurrent(ctx, volume, path, 0, readLen, verifiers)
if err != nil { if err != nil {
return f, err return err
} }
if n != size {
// Scan part files on disk, block-by-block reconstruct it and logger.LogIf(ctx, errLessData)
// write to stale disks. return errLessData
blocks := make([][]byte, len(s.disks))
if numBlocks > 1 {
// Allocate once for all the equal length blocks. The
// last block may have a different length - allocation
// for this happens inside the for loop below.
for i := range blocks {
if len(buffers[i]) == 0 {
blocks[i] = make([]byte, chunksize)
}
}
} }
return nil
var buffOffset int64
for blockNumber := int64(0); blockNumber < numBlocks; blockNumber++ {
if blockNumber == numBlocks-1 && lastChunkSize != chunksize {
for i := range blocks {
if len(buffers[i]) == 0 {
blocks[i] = make([]byte, lastChunkSize)
}
}
}
for i := range blocks {
if len(buffers[i]) == 0 {
blocks[i] = blocks[i][0:0]
}
}
csize := chunksize
if blockNumber == numBlocks-1 {
csize = lastChunkSize
}
for i := range blocks {
if len(buffers[i]) != 0 {
blocks[i] = buffers[i][buffOffset : buffOffset+csize]
}
}
buffOffset += csize
if err = s.ErasureDecodeDataAndParityBlocks(ctx, blocks); err != nil {
return f, err
}
// write computed shards as chunks on file in each
// stale disk
writeSucceeded := false
for i, disk := range staleDisks {
// skip nil disk or disk that had error on
// previous write
if disk == nil || writeErrors[i] != nil {
continue
}
writeErrors[i] = disk.AppendFile(dstVol, dstPath, blocks[i])
if writeErrors[i] == nil {
hashers[i].Write(blocks[i])
writeSucceeded = true
}
}
// If all disks had write errors we quit.
if !writeSucceeded {
// build error from all write errors
err := joinWriteErrors(writeErrors)
logger.LogIf(ctx, err)
return f, err
}
}
// copy computed file hashes into output variable
f.Size = size
f.Algorithm = alg
for i, disk := range staleDisks {
if disk == nil || writeErrors[i] != nil {
continue
}
f.Checksums[i] = hashers[i].Sum(nil)
}
return f, nil
}
func joinWriteErrors(errs []error) error {
msgs := []string{}
for i, err := range errs {
if err == nil {
continue
}
msgs = append(msgs, fmt.Sprintf("disk %d: %v", i+1, err))
}
return fmt.Errorf("all stale disks had write errors during healing: %s",
strings.Join(msgs, ", "))
} }
+33 -27
View File
@@ -21,7 +21,6 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"io" "io"
"reflect"
"testing" "testing"
) )
@@ -56,11 +55,9 @@ var erasureHealFileTests = []struct {
{dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14 {dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
{dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15 {dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16 {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
{dataBlocks: 2, disks: 4, offDisks: 0, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 17 {dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 17
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 18 {dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 18
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 19 {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19
{dataBlocks: 7, disks: 10, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 20
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 21
} }
func TestErasureHealFile(t *testing.T) { func TestErasureHealFile(t *testing.T) {
@@ -75,7 +72,8 @@ func TestErasureHealFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err) t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
} }
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.disks-test.dataBlocks, test.blocksize) disks := setup.disks
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
if err != nil { if err != nil {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err) t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
@@ -85,36 +83,50 @@ func TestErasureHealFile(t *testing.T) {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to create random test data: %v", i, err) t.Fatalf("Test %d: failed to create random test data: %v", i, err)
} }
algorithm := test.algorithm
if !algorithm.Available() {
algorithm = DefaultBitrotAlgorithm
}
buffer := make([]byte, test.blocksize, 2*test.blocksize) buffer := make([]byte, test.blocksize, 2*test.blocksize)
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data), "testbucket", "testobject", buffer, algorithm, test.dataBlocks+1) writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
}
_, err = storage.CreateFile(context.Background(), bytes.NewReader(data), writers, buffer, storage.dataBlocks+1)
if err != nil { if err != nil {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to create random test data: %v", i, err) t.Fatalf("Test %d: failed to create random test data: %v", i, err)
} }
readers := make([]*bitrotReader, len(disks))
for i, disk := range disks {
shardFilesize := getErasureShardFileSize(test.blocksize, test.size, storage.dataBlocks)
readers[i] = newBitrotReader(disk, "testbucket", "testobject", test.algorithm, shardFilesize, writers[i].Sum())
}
// setup stale disks for the test case // setup stale disks for the test case
staleDisks := make([]StorageAPI, len(storage.disks)) staleDisks := make([]StorageAPI, len(disks))
copy(staleDisks, storage.disks) copy(staleDisks, disks)
for j := 0; j < len(storage.disks); j++ { for j := 0; j < len(staleDisks); j++ {
if j < test.offDisks { if j < test.offDisks {
storage.disks[j] = OfflineDisk readers[j] = nil
} else { } else {
staleDisks[j] = nil staleDisks[j] = nil
} }
} }
for j := 0; j < test.badDisks; j++ { for j := 0; j < test.badDisks; j++ {
storage.disks[test.offDisks+j] = badDisk{nil} readers[test.offDisks+j].disk = badDisk{nil}
} }
for j := 0; j < test.badStaleDisks; j++ { for j := 0; j < test.badStaleDisks; j++ {
staleDisks[j] = badDisk{nil} staleDisks[j] = badDisk{nil}
} }
staleWriters := make([]*bitrotWriter, len(staleDisks))
for i, disk := range staleDisks {
if disk == nil {
continue
}
staleWriters[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
}
// test case setup is complete - now call Healfile() // test case setup is complete - now call Healfile()
info, err := storage.HealFile(context.Background(), staleDisks, "testbucket", "testobject", test.blocksize, "testbucket", "healedobject", test.size, test.algorithm, file.Checksums) err = storage.HealFile(context.Background(), readers, staleWriters, test.size)
if err != nil && !test.shouldFail { if err != nil && !test.shouldFail {
t.Errorf("Test %d: should pass but it failed with: %v", i, err) t.Errorf("Test %d: should pass but it failed with: %v", i, err)
} }
@@ -122,19 +134,13 @@ func TestErasureHealFile(t *testing.T) {
t.Errorf("Test %d: should fail but it passed", i) t.Errorf("Test %d: should fail but it passed", i)
} }
if err == nil { if err == nil {
if info.Size != test.size {
t.Errorf("Test %d: healed wrong number of bytes: got: #%d want: #%d", i, info.Size, test.size)
}
if info.Algorithm != test.algorithm {
t.Errorf("Test %d: healed with wrong algorithm: got: %v want: %v", i, info.Algorithm, test.algorithm)
}
// Verify that checksums of staleDisks // Verify that checksums of staleDisks
// match expected values // match expected values
for i, disk := range staleDisks { for i := range staleWriters {
if disk == nil || info.Checksums[i] == nil { if staleWriters[i] == nil {
continue continue
} }
if !reflect.DeepEqual(info.Checksums[i], file.Checksums[i]) { if !bytes.Equal(staleWriters[i].Sum(), writers[i].Sum()) {
t.Errorf("Test %d: heal returned different bitrot checksums", i) t.Errorf("Test %d: heal returned different bitrot checksums", i)
} }
} }
+133 -181
View File
@@ -23,214 +23,166 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
) )
type errIdx struct { // Reads in parallel from bitrotReaders.
idx int type parallelReader struct {
err error readers []*bitrotReader
dataBlocks int
offset int64
shardSize int64
shardFileSize int64
} }
func (s ErasureStorage) readConcurrent(ctx context.Context, volume, path string, offset, length int64, // newParallelReader returns parallelReader.
verifiers []*BitrotVerifier) (buffers [][]byte, needsReconstruction bool, func newParallelReader(readers []*bitrotReader, dataBlocks int, offset int64, fileSize int64, blocksize int64) *parallelReader {
err error) { shardSize := ceilFrac(blocksize, int64(dataBlocks))
shardFileSize := getErasureShardFileSize(blocksize, fileSize, dataBlocks)
return &parallelReader{
readers,
dataBlocks,
(offset / blocksize) * shardSize,
shardSize,
shardFileSize,
}
}
errChan := make(chan errIdx) // Returns if buf can be erasure decoded.
stageBuffers := make([][]byte, len(s.disks)) func (p *parallelReader) canDecode(buf [][]byte) bool {
buffers = make([][]byte, len(s.disks)) bufCount := 0
for _, b := range buf {
readDisk := func(i int) { if b != nil {
stageBuffers[i] = make([]byte, length) bufCount++
disk := s.disks[i]
if disk == OfflineDisk {
logger.LogIf(ctx, errDiskNotFound)
errChan <- errIdx{i, errDiskNotFound}
return
} }
_, rerr := disk.ReadFile(volume, path, offset, stageBuffers[i], verifiers[i]) }
errChan <- errIdx{i, rerr} return bufCount >= p.dataBlocks
}
// Read reads from bitrotReaders in parallel. Returns p.dataBlocks number of bufs.
func (p *parallelReader) Read() ([][]byte, error) {
type errIdx struct {
idx int
buf []byte
err error
} }
var finishedCount, successCount, launchIndex int errCh := make(chan errIdx)
currReaderIndex := 0
newBuf := make([][]byte, len(p.readers))
for ; launchIndex < s.dataBlocks; launchIndex++ { if p.offset+p.shardSize > p.shardFileSize {
go readDisk(launchIndex) p.shardSize = p.shardFileSize - p.offset
} }
for finishedCount < launchIndex {
select {
case errVal := <-errChan:
finishedCount++
if errVal.err != nil {
// TODO: meaningfully log the disk read error
// A disk failed to return data, so we read := func(currReaderIndex int) {
// request an additional disk if possible b, err := p.readers[currReaderIndex].ReadChunk(p.offset, p.shardSize)
if launchIndex < s.dataBlocks+s.parityBlocks { errCh <- errIdx{currReaderIndex, b, err}
needsReconstruction = true }
// requiredBlocks++
go readDisk(launchIndex) readerCount := 0
launchIndex++ for _, r := range p.readers {
} if r != nil {
} else { readerCount++
successCount++ }
buffers[errVal.idx] = stageBuffers[errVal.idx] }
stageBuffers[errVal.idx] = nil if readerCount < p.dataBlocks {
return nil, errXLReadQuorum
}
readerCount = 0
for i, r := range p.readers {
if r == nil {
continue
}
go read(i)
readerCount++
if readerCount == p.dataBlocks {
currReaderIndex = i + 1
break
}
}
for errVal := range errCh {
if errVal.err == nil {
newBuf[errVal.idx] = errVal.buf
if p.canDecode(newBuf) {
p.offset += int64(p.shardSize)
return newBuf, nil
} }
continue
} }
p.readers[errVal.idx] = nil
for currReaderIndex < len(p.readers) {
if p.readers[currReaderIndex] != nil {
break
}
currReaderIndex++
}
if currReaderIndex == len(p.readers) {
break
}
go read(currReaderIndex)
currReaderIndex++
} }
if successCount != s.dataBlocks {
// Not enough disks returns data. return nil, errXLReadQuorum
err = errXLReadQuorum
logger.LogIf(ctx, err)
}
return
} }
// ReadFile reads as much data as requested from the file under the // ReadFile reads from readers, reconstructs data if needed and writes the data to the writer.
// given volume and path and writes the data to the provided writer. func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, readers []*bitrotReader, offset, length, totalLength int64) error {
// The algorithm and the keys/checksums are used to verify the
// integrity of the given file. ReadFile will read data from the given
// offset up to the given length. If parts of the file are corrupted
// ReadFile tries to reconstruct the data.
func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, volume, path string, offset,
length, totalLength int64, checksums [][]byte, algorithm BitrotAlgorithm,
blocksize int64) (f ErasureFileInfo, err error) {
if offset < 0 || length < 0 { if offset < 0 || length < 0 {
logger.LogIf(ctx, errUnexpected) logger.LogIf(ctx, errInvalidArgument)
return f, errUnexpected return errInvalidArgument
} }
if offset+length > totalLength { if offset+length > totalLength {
logger.LogIf(ctx, errUnexpected) logger.LogIf(ctx, errInvalidArgument)
return f, errUnexpected return errInvalidArgument
} }
if !algorithm.Available() {
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
return f, errBitrotHashAlgoInvalid
}
f.Checksums = make([][]byte, len(s.disks))
verifiers := make([]*BitrotVerifier, len(s.disks))
for i, disk := range s.disks {
if disk == OfflineDisk {
continue
}
verifiers[i] = NewBitrotVerifier(algorithm, checksums[i])
}
chunksize := ceilFrac(blocksize, int64(s.dataBlocks))
// We read all whole-blocks of erasure coded data containing
// the requested data range.
//
// The start index of the erasure coded block containing the
// `offset` byte of data is:
partDataStartIndex := (offset / blocksize) * chunksize
// The start index of the erasure coded block containing the
// (last) byte of data at the index `offset + length - 1` is:
blockStartIndex := ((offset + length - 1) / blocksize) * chunksize
// However, we need the end index of the e.c. block containing
// the last byte - we need to check if that block is the last
// block in the part (in that case, it may be have a different
// chunk size)
isLastBlock := (totalLength-1)/blocksize == (offset+length-1)/blocksize
var partDataEndIndex int64
if isLastBlock {
lastBlockChunkSize := chunksize
if totalLength%blocksize != 0 {
lastBlockChunkSize = ceilFrac(totalLength%blocksize, int64(s.dataBlocks))
}
partDataEndIndex = blockStartIndex + lastBlockChunkSize - 1
} else {
partDataEndIndex = blockStartIndex + chunksize - 1
}
// Thus, the length of data to be read from the part file(s) is:
partDataLength := partDataEndIndex - partDataStartIndex + 1
// The calculation above does not apply when length == 0:
if length == 0 { if length == 0 {
partDataLength = 0 return nil
} }
var buffers [][]byte reader := newParallelReader(readers, s.dataBlocks, offset, totalLength, s.blockSize)
var needsReconstruction bool
buffers, needsReconstruction, err = s.readConcurrent(ctx, volume, path,
partDataStartIndex, partDataLength, verifiers)
if err != nil {
// Could not read enough disks.
return
}
numChunks := ceilFrac(partDataLength, chunksize) startBlock := offset / s.blockSize
blocks := make([][]byte, len(s.disks)) endBlock := (offset + length) / s.blockSize
if needsReconstruction && numChunks > 1 { var bytesWritten int64
// Allocate once for all the equal length blocks. The for block := startBlock; block <= endBlock; block++ {
// last block may have a different length - allocation var blockOffset, blockLength int64
// for this happens inside the for loop below. switch {
for i := range blocks { case startBlock == endBlock:
if len(buffers[i]) == 0 { blockOffset = offset % s.blockSize
blocks[i] = make([]byte, chunksize) blockLength = length
} case block == startBlock:
blockOffset = offset % s.blockSize
blockLength = s.blockSize - blockOffset
case block == endBlock:
blockOffset = 0
blockLength = (offset + length) % s.blockSize
default:
blockOffset = 0
blockLength = s.blockSize
} }
} if blockLength == 0 {
break
var buffOffset int64
for chunkNumber := int64(0); chunkNumber < numChunks; chunkNumber++ {
if chunkNumber == numChunks-1 && partDataLength%chunksize != 0 {
chunksize = partDataLength % chunksize
// We allocate again as the last chunk has a
// different size.
for i := range blocks {
if len(buffers[i]) == 0 {
blocks[i] = make([]byte, chunksize)
}
}
} }
bufs, err := reader.Read()
for i := range blocks {
if len(buffers[i]) == 0 {
blocks[i] = blocks[i][0:0]
}
}
for i := range blocks {
if len(buffers[i]) != 0 {
blocks[i] = buffers[i][buffOffset : buffOffset+chunksize]
}
}
buffOffset += chunksize
if needsReconstruction {
if err = s.ErasureDecodeDataBlocks(blocks); err != nil {
logger.LogIf(ctx, err)
return f, err
}
}
var writeStart int64
if chunkNumber == 0 {
writeStart = offset % blocksize
}
writeLength := blocksize - writeStart
if chunkNumber == numChunks-1 {
lastBlockLength := (offset + length) % blocksize
if lastBlockLength != 0 {
writeLength = lastBlockLength - writeStart
}
}
n, err := writeDataBlocks(ctx, writer, blocks, s.dataBlocks, writeStart, writeLength)
if err != nil { if err != nil {
return f, err return err
} }
if err = s.ErasureDecodeDataBlocks(bufs); err != nil {
f.Size += n logger.LogIf(ctx, err)
} return err
f.Algorithm = algorithm
for i, disk := range s.disks {
if disk == OfflineDisk || buffers[i] == nil {
continue
} }
f.Checksums[i] = verifiers[i].Sum(nil) n, err := writeDataBlocks(ctx, writer, bufs, s.dataBlocks, blockOffset, blockLength)
if err != nil {
return err
}
bytesWritten += n
} }
return f, nil if bytesWritten != length {
logger.LogIf(ctx, errLessData)
return errLessData
}
return nil
} }
+106 -47
View File
@@ -19,11 +19,12 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
crand "crypto/rand"
"io" "io"
"math/rand" "math/rand"
"testing" "testing"
crand "crypto/rand"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
) )
@@ -66,19 +67,18 @@ var erasureReadFileTests = []struct {
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23 {dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24 {dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 26 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 26
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 28 {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 28
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29 {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30 {dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31 {dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 32 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 32
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33 {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 34 {dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 34
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35 {dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 36 {dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 36
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 37 {dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 38
} }
func TestErasureReadFile(t *testing.T) { func TestErasureReadFile(t *testing.T) {
@@ -87,29 +87,54 @@ func TestErasureReadFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Test %d: failed to create test setup: %v", i, err) t.Fatalf("Test %d: failed to create test setup: %v", i, err)
} }
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize) storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
if err != nil { if err != nil {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err) t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
} }
disks := setup.disks
data := make([]byte, test.data) data := make([]byte, test.data)
if _, err = io.ReadFull(crand.Reader, data); err != nil { if _, err = io.ReadFull(crand.Reader, data); err != nil {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to generate random test data: %v", i, err) t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
} }
writeAlgorithm := test.algorithm writeAlgorithm := test.algorithm
if !test.algorithm.Available() { if !test.algorithm.Available() {
writeAlgorithm = DefaultBitrotAlgorithm writeAlgorithm = DefaultBitrotAlgorithm
} }
buffer := make([]byte, test.blocksize, 2*test.blocksize) buffer := make([]byte, test.blocksize, 2*test.blocksize)
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data[:]), "testbucket", "object", buffer, writeAlgorithm, test.dataBlocks+1) writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
writers[i] = newBitrotWriter(disk, "testbucket", "object", writeAlgorithm)
}
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data[:]), writers, buffer, storage.dataBlocks+1)
if err != nil { if err != nil {
setup.Remove() setup.Remove()
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err) t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
} }
if n != test.data {
setup.Remove()
t.Fatalf("Test %d: failed to create erasure test file", i)
}
for i, w := range writers {
if w == nil {
disks[i] = nil
}
}
// Get the checksums of the current part.
bitrotReaders := make([]*bitrotReader, len(disks))
for index, disk := range disks {
if disk == OfflineDisk {
continue
}
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, storage.dataBlocks)
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
}
writer := bytes.NewBuffer(nil) writer := bytes.NewBuffer(nil)
readInfo, err := storage.ReadFile(context.Background(), writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize) err = storage.ReadFile(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
if err != nil && !test.shouldFail { if err != nil && !test.shouldFail {
t.Errorf("Test %d: should pass but failed with: %v", i, err) t.Errorf("Test %d: should pass but failed with: %v", i, err)
} }
@@ -117,25 +142,32 @@ func TestErasureReadFile(t *testing.T) {
t.Errorf("Test %d: should fail but it passed", i) t.Errorf("Test %d: should fail but it passed", i)
} }
if err == nil { if err == nil {
if readInfo.Size != test.length {
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
}
if readInfo.Algorithm != test.algorithm {
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
}
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) { if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
t.Errorf("Test %d: read retruns wrong file content", i) t.Errorf("Test %d: read retruns wrong file content", i)
} }
} }
for i, r := range bitrotReaders {
if r == nil {
disks[i] = OfflineDisk
}
}
if err == nil && !test.shouldFail { if err == nil && !test.shouldFail {
writer.Reset() bitrotReaders = make([]*bitrotReader, len(disks))
for j := range storage.disks[:test.offDisks] { for index, disk := range disks {
storage.disks[j] = badDisk{nil} if disk == OfflineDisk {
continue
}
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, storage.dataBlocks)
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
}
for j := range disks[:test.offDisks] {
bitrotReaders[j].disk = badDisk{nil}
} }
if test.offDisks > 0 { if test.offDisks > 0 {
storage.disks[0] = OfflineDisk bitrotReaders[0] = nil
} }
readInfo, err = storage.ReadFile(context.Background(), writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize) writer.Reset()
err = storage.ReadFile(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
if err != nil && !test.shouldFailQuorum { if err != nil && !test.shouldFailQuorum {
t.Errorf("Test %d: should pass but failed with: %v", i, err) t.Errorf("Test %d: should pass but failed with: %v", i, err)
} }
@@ -143,12 +175,6 @@ func TestErasureReadFile(t *testing.T) {
t.Errorf("Test %d: should fail but it passed", i) t.Errorf("Test %d: should fail but it passed", i)
} }
if !test.shouldFailQuorum { if !test.shouldFailQuorum {
if readInfo.Size != test.length {
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
}
if readInfo.Algorithm != test.algorithm {
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
}
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) { if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
t.Errorf("Test %d: read retruns wrong file content", i) t.Errorf("Test %d: read retruns wrong file content", i)
} }
@@ -174,8 +200,8 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
return return
} }
defer setup.Remove() defer setup.Remove()
disks := setup.disks
storage, err := NewErasureStorage(context.Background(), setup.disks, dataBlocks, parityBlocks, blockSize) storage, err := NewErasureStorage(context.Background(), dataBlocks, parityBlocks, blockSize)
if err != nil { if err != nil {
t.Fatalf("failed to create ErasureStorage: %v", err) t.Fatalf("failed to create ErasureStorage: %v", err)
} }
@@ -187,17 +213,25 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
if disk == nil {
continue
}
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
}
// 10000 iterations with random offsets and lengths. // 10000 iterations with random offsets and lengths.
iterations := 10000 iterations := 10000
// Create a test file to read from. // Create a test file to read from.
buffer := make([]byte, blockSize, 2*blockSize) buffer := make([]byte, blockSize, 2*blockSize)
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data), "testbucket", "testobject", buffer, DefaultBitrotAlgorithm, dataBlocks+1) n, err := storage.CreateFile(context.Background(), bytes.NewReader(data), writers, buffer, storage.dataBlocks+1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if file.Size != length { if n != length {
t.Errorf("erasureCreateFile returned %d, expected %d", file.Size, length) t.Errorf("erasureCreateFile returned %d, expected %d", n, length)
} }
// To generate random offset/length. // To generate random offset/length.
@@ -212,7 +246,16 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
expected := data[offset : offset+readLen] expected := data[offset : offset+readLen]
_, err = storage.ReadFile(context.Background(), buf, "testbucket", "testobject", offset, readLen, length, file.Checksums, DefaultBitrotAlgorithm, blockSize) // Get the checksums of the current part.
bitrotReaders := make([]*bitrotReader, len(disks))
for index, disk := range disks {
if disk == OfflineDisk {
continue
}
endOffset := getErasureShardFileEndOffset(offset, readLen, length, blockSize, storage.dataBlocks)
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
}
err = storage.ReadFile(context.Background(), buf, bitrotReaders, offset, readLen, length)
if err != nil { if err != nil {
t.Fatal(err, offset, readLen) t.Fatal(err, offset, readLen)
} }
@@ -232,31 +275,47 @@ func benchmarkErasureRead(data, parity, dataDown, parityDown int, size int64, b
b.Fatalf("failed to create test setup: %v", err) b.Fatalf("failed to create test setup: %v", err)
} }
defer setup.Remove() defer setup.Remove()
storage, err := NewErasureStorage(context.Background(), setup.disks, data, parity, blockSizeV1) disks := setup.disks
storage, err := NewErasureStorage(context.Background(), data, parity, blockSizeV1)
if err != nil { if err != nil {
b.Fatalf("failed to create ErasureStorage: %v", err) b.Fatalf("failed to create ErasureStorage: %v", err)
} }
writers := make([]*bitrotWriter, len(disks))
for i, disk := range disks {
if disk == nil {
continue
}
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
}
content := make([]byte, size) content := make([]byte, size)
buffer := make([]byte, blockSizeV1, 2*blockSizeV1) buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
file, err := storage.CreateFile(context.Background(), bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1) _, err = storage.CreateFile(context.Background(), bytes.NewReader(content), writers, buffer, storage.dataBlocks+1)
if err != nil { if err != nil {
b.Fatalf("failed to create erasure test file: %v", err) b.Fatalf("failed to create erasure test file: %v", err)
} }
checksums := file.Checksums
for i := 0; i < dataDown; i++ { for i := 0; i < dataDown; i++ {
storage.disks[i] = OfflineDisk writers[i] = nil
} }
for i := data; i < data+parityDown; i++ { for i := data; i < data+parityDown; i++ {
storage.disks[i] = OfflineDisk writers[i] = nil
} }
b.ResetTimer() b.ResetTimer()
b.SetBytes(size) b.SetBytes(size)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if file, err = storage.ReadFile(context.Background(), bytes.NewBuffer(content[:0]), "testbucket", "object", 0, size, size, checksums, DefaultBitrotAlgorithm, blockSizeV1); err != nil { bitrotReaders := make([]*bitrotReader, len(disks))
for index, disk := range disks {
if writers[index] == nil {
continue
}
endOffset := getErasureShardFileEndOffset(0, size, size, storage.blockSize, storage.dataBlocks)
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
}
if err = storage.ReadFile(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size); err != nil {
panic(err) panic(err)
} }
} }
+22
View File
@@ -104,3 +104,25 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
// Success. // Success.
return totalWritten, nil return totalWritten, nil
} }
// Returns shard-file size.
func getErasureShardFileSize(blockSize int64, totalLength int64, dataBlocks int) int64 {
shardSize := ceilFrac(int64(blockSize), int64(dataBlocks))
numShards := totalLength / int64(blockSize)
lastBlockSize := totalLength % int64(blockSize)
lastShardSize := ceilFrac(lastBlockSize, int64(dataBlocks))
return shardSize*numShards + lastShardSize
}
// Returns the endOffset till which bitrotReader should read data using disk.ReadFile()
// partOffset, partLength and partSize are values of the object's part file.
func getErasureShardFileEndOffset(partOffset int64, partLength int64, partSize int64, erasureBlockSize int64, dataBlocks int) int64 {
shardSize := ceilFrac(erasureBlockSize, int64(dataBlocks))
shardFileSize := getErasureShardFileSize(erasureBlockSize, partSize, dataBlocks)
endShard := (partOffset + int64(partLength)) / erasureBlockSize
endOffset := endShard*shardSize + shardSize
if endOffset > shardFileSize {
endOffset = shardFileSize
}
return endOffset
}
+19 -42
View File
@@ -18,8 +18,6 @@ package cmd
import ( import (
"context" "context"
"crypto/subtle"
"hash"
"github.com/klauspost/reedsolomon" "github.com/klauspost/reedsolomon"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
@@ -28,43 +26,36 @@ import (
// OfflineDisk represents an unavailable disk. // OfflineDisk represents an unavailable disk.
var OfflineDisk StorageAPI // zero value is nil var OfflineDisk StorageAPI // zero value is nil
// ErasureFileInfo contains information about an erasure file operation (create, read, heal). // ErasureStorage - erasure encoding details.
type ErasureFileInfo struct {
Size int64
Algorithm BitrotAlgorithm
Checksums [][]byte
}
// ErasureStorage represents an array of disks.
// The disks contain erasure coded and bitrot-protected data.
type ErasureStorage struct { type ErasureStorage struct {
disks []StorageAPI
erasure reedsolomon.Encoder erasure reedsolomon.Encoder
dataBlocks, parityBlocks int dataBlocks, parityBlocks int
blockSize int64
} }
// NewErasureStorage creates a new ErasureStorage. The storage erasure codes and protects all data written to // NewErasureStorage creates a new ErasureStorage.
// the disks. func NewErasureStorage(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (s ErasureStorage, err error) {
func NewErasureStorage(ctx context.Context, disks []StorageAPI, dataBlocks, parityBlocks int, blockSize int64) (s ErasureStorage, err error) { shardsize := int(ceilFrac(blockSize, int64(dataBlocks)))
shardsize := (int(blockSize) + dataBlocks - 1) / dataBlocks
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize)) erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return s, err return s, err
} }
s = ErasureStorage{ s = ErasureStorage{
disks: make([]StorageAPI, len(disks)),
erasure: erasure, erasure: erasure,
dataBlocks: dataBlocks, dataBlocks: dataBlocks,
parityBlocks: parityBlocks, parityBlocks: parityBlocks,
blockSize: blockSize,
} }
copy(s.disks, disks)
return return
} }
// ErasureEncode encodes the given data and returns the erasure-coded data. // ErasureEncode encodes the given data and returns the erasure-coded data.
// It returns an error if the erasure coding failed. // It returns an error if the erasure coding failed.
func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]byte, error) { func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]byte, error) {
if len(data) == 0 {
return make([][]byte, s.dataBlocks+s.parityBlocks), nil
}
encoded, err := s.erasure.Split(data) encoded, err := s.erasure.Split(data)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
@@ -81,6 +72,16 @@ func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]by
// It only decodes the data blocks but does not verify them. // It only decodes the data blocks but does not verify them.
// It returns an error if the decoding failed. // It returns an error if the decoding failed.
func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error { func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
needsReconstruction := false
for _, b := range data[:s.dataBlocks] {
if b == nil {
needsReconstruction = true
break
}
}
if !needsReconstruction {
return nil
}
if err := s.erasure.ReconstructData(data); err != nil { if err := s.erasure.ReconstructData(data); err != nil {
return err return err
} }
@@ -96,27 +97,3 @@ func (s *ErasureStorage) ErasureDecodeDataAndParityBlocks(ctx context.Context, d
} }
return nil return nil
} }
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
return &BitrotVerifier{algorithm.New(), algorithm, checksum, false}
}
// BitrotVerifier can be used to verify protected data.
type BitrotVerifier struct {
hash.Hash
algorithm BitrotAlgorithm
sum []byte
verified bool
}
// Verify returns true iff the computed checksum of the verifier matches the the checksum provided when the verifier
// was created.
func (v *BitrotVerifier) Verify() bool {
v.verified = true
return subtle.ConstantTimeCompare(v.Sum(nil), v.sum) == 1
}
// IsVerified returns true iff Verify was called at least once.
func (v *BitrotVerifier) IsVerified() bool { return v.verified }
+1 -2
View File
@@ -52,8 +52,7 @@ func TestErasureDecode(t *testing.T) {
buffer := make([]byte, len(data), 2*len(data)) buffer := make([]byte, len(data), 2*len(data))
copy(buffer, data) copy(buffer, data)
disks := make([]StorageAPI, test.dataBlocks+test.parityBlocks) storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.parityBlocks, blockSizeV1)
storage, err := NewErasureStorage(context.Background(), disks, test.dataBlocks, test.parityBlocks, blockSizeV1)
if err != nil { if err != nil {
t.Fatalf("Test %d: failed to create erasure storage: %v", i, err) t.Fatalf("Test %d: failed to create erasure storage: %v", i, err)
} }
+114 -12
View File
@@ -24,6 +24,7 @@ import (
"path" "path"
"time" "time"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lock" "github.com/minio/minio/pkg/lock"
) )
@@ -55,24 +56,22 @@ type formatFSVersionDetect struct {
} `json:"fs"` } `json:"fs"`
} }
// Generic structure to manage both v1 and v2 structures
type formatFS struct {
formatMetaV1
FS interface{} `json:"fs"`
}
// Returns the latest "fs" format V1 // Returns the latest "fs" format V1
func newFormatFSV1() (format *formatFSV1) { func newFormatFSV1() (format *formatFSV1) {
f := &formatFSV1{} f := &formatFSV1{}
f.Version = formatMetaVersionV1 f.Version = formatMetaVersionV1
f.Format = formatBackendFS f.Format = formatBackendFS
f.ID = mustGetUUID()
f.FS.Version = formatFSVersionV1 f.FS.Version = formatFSVersionV1
return f return f
} }
// Returns the latest "fs" format V2
func newFormatFSV2() (format *formatFSV2) {
f := &formatFSV2{}
f.Version = formatMetaVersionV1
f.Format = formatBackendFS
f.FS.Version = formatFSVersionV2
return f
}
// Returns the field formatMetaV1.Format i.e the string "fs" which is never likely to change. // Returns the field formatMetaV1.Format i.e the string "fs" which is never likely to change.
// We do not use this function in XL to get the format as the file is not fcntl-locked on XL. // We do not use this function in XL to get the format as the file is not fcntl-locked on XL.
func formatMetaGetFormatBackendFS(r io.ReadSeeker) (string, error) { func formatMetaGetFormatBackendFS(r io.ReadSeeker) (string, error) {
@@ -115,7 +114,16 @@ func formatFSMigrateV1ToV2(ctx context.Context, wlk *lock.LockedFile, fsPath str
return err return err
} }
return jsonSave(wlk.File, newFormatFSV2()) formatV1 := formatFSV1{}
if err = jsonLoad(wlk, &formatV1); err != nil {
return err
}
formatV2 := formatFSV2{}
formatV2.formatMetaV1 = formatV1.formatMetaV1
formatV2.FS.Version = formatFSVersionV2
return jsonSave(wlk.File, formatV2)
} }
// Migrate the "fs" backend. // Migrate the "fs" backend.
@@ -179,6 +187,12 @@ func createFormatFS(ctx context.Context, fsFormatPath string) error {
// migrate the backend when we are actively working on the backend. // migrate the backend when we are actively working on the backend.
func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, err error) { func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, err error) {
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile) fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
// Add a deployment ID, if it does not exist.
if err := formatFSFixDeploymentID(fsFormatPath); err != nil {
return nil, err
}
// Any read on format.json should be done with read-lock. // Any read on format.json should be done with read-lock.
// Any write on format.json should be done with write-lock. // Any write on format.json should be done with write-lock.
for { for {
@@ -235,7 +249,8 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
rlk.Close() rlk.Close()
// Hold write lock during migration so that we do not disturb any // Hold write lock during migration so that we do not disturb any
// minio processes running in parallel. // minio processes running in parallel.
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0) var wlk *lock.LockedFile
wlk, err = lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
if err == lock.ErrAlreadyLocked { if err == lock.ErrAlreadyLocked {
// Lock already present, sleep and attempt again. // Lock already present, sleep and attempt again.
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@@ -253,7 +268,94 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
// Successfully migrated, now try to hold a read-lock on format.json // Successfully migrated, now try to hold a read-lock on format.json
continue continue
} }
var id string
if id, err = formatFSGetDeploymentID(rlk); err != nil {
rlk.Close()
return nil, err
}
logger.SetDeploymentID(id)
return rlk, nil return rlk, nil
} }
} }
func formatFSGetDeploymentID(rlk *lock.RLockedFile) (id string, err error) {
format := &formatFS{}
if err := jsonLoad(rlk, format); err != nil {
return "", err
}
return format.ID, nil
}
// Generate a deployment ID if one does not exist already.
func formatFSFixDeploymentID(fsFormatPath string) error {
rlk, err := lock.RLockedOpenFile(fsFormatPath)
if err == nil {
// format.json can be empty in a rare condition when another
// minio process just created the file but could not hold lock
// and write to it.
var fi os.FileInfo
fi, err = rlk.Stat()
if err != nil {
rlk.Close()
return err
}
if fi.Size() == 0 {
rlk.Close()
return nil
}
}
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
formatBackend, err := formatMetaGetFormatBackendFS(rlk)
if err != nil {
rlk.Close()
return err
}
if formatBackend != formatBackendFS {
rlk.Close()
return fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
}
format := &formatFS{}
err = jsonLoad(rlk, format)
rlk.Close()
if err != nil {
return err
}
// Check if it needs to be updated
if format.ID != "" {
return nil
}
for {
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
if err == lock.ErrAlreadyLocked {
// Lock already present, sleep and attempt again.
time.Sleep(100 * time.Millisecond)
continue
}
if err != nil {
return err
}
defer wlk.Close()
err = jsonLoad(wlk, format)
if err != nil {
return err
}
// Check if it needs to be updated
if format.ID != "" {
return nil
}
format.ID = mustGetUUID()
return jsonSave(wlk, format)
}
}
+2
View File
@@ -49,4 +49,6 @@ type formatMetaV1 struct {
Version string `json:"version"` Version string `json:"version"`
// Format indicates the backend format type, supports two values 'xl' and 'fs'. // Format indicates the backend format type, supports two values 'xl' and 'fs'.
Format string `json:"format"` Format string `json:"format"`
// ID is the identifier for the minio deployment
ID string `json:"id"`
} }
+118 -9
View File
@@ -21,12 +21,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "reflect"
"sync" "sync"
"encoding/hex" "encoding/hex"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/minio/cmd/logger"
sha256 "github.com/minio/sha256-simd" sha256 "github.com/minio/sha256-simd"
) )
@@ -86,10 +87,11 @@ type formatXLV1 struct {
// Represents the V2 backend disk structure version // Represents the V2 backend disk structure version
// under `.minio.sys` and actual data namespace. // under `.minio.sys` and actual data namespace.
// formatXLV2 - structure holds format config version '2'. // formatXLV2 - structure holds format config version '2'.
// The V2 format to support "large bucket" support where a bucket
// can span multiple erasure sets.
type formatXLV2 struct { type formatXLV2 struct {
Version string `json:"version"` formatMetaV1
Format string `json:"format"` XL struct {
XL struct {
Version string `json:"version"` // Version of 'xl' format. Version string `json:"version"` // Version of 'xl' format.
This string `json:"this"` // This field carries assigned disk uuid. This string `json:"this"` // This field carries assigned disk uuid.
// Sets field carries the input disk order generated the first // Sets field carries the input disk order generated the first
@@ -107,9 +109,8 @@ type formatXLV2 struct {
// In .minio.sys/multipart we have: // In .minio.sys/multipart we have:
// sha256(bucket/object)/uploadID/[xl.json, part.1, part.2 ....] // sha256(bucket/object)/uploadID/[xl.json, part.1, part.2 ....]
type formatXLV3 struct { type formatXLV3 struct {
Version string `json:"version"` formatMetaV1
Format string `json:"format"` XL struct {
XL struct {
Version string `json:"version"` // Version of 'xl' format. Version string `json:"version"` // Version of 'xl' format.
This string `json:"this"` // This field carries assigned disk uuid. This string `json:"this"` // This field carries assigned disk uuid.
// Sets field carries the input disk order generated the first // Sets field carries the input disk order generated the first
@@ -127,6 +128,7 @@ func newFormatXLV3(numSets int, setLen int) *formatXLV3 {
format := &formatXLV3{} format := &formatXLV3{}
format.Version = formatMetaVersionV1 format.Version = formatMetaVersionV1
format.Format = formatBackendXL format.Format = formatBackendXL
format.ID = mustGetUUID()
format.XL.Version = formatXLVersionV3 format.XL.Version = formatXLVersionV3
format.XL.DistributionAlgo = formatXLVersionV2DistributionAlgo format.XL.DistributionAlgo = formatXLVersionV2DistributionAlgo
format.XL.Sets = make([][]string, numSets) format.XL.Sets = make([][]string, numSets)
@@ -264,10 +266,11 @@ func formatXLMigrateV2ToV3(export string) error {
return err return err
} }
if err = os.RemoveAll(pathJoin(export, minioMetaMultipartBucket)); err != nil { if err = removeAll(pathJoin(export, minioMetaMultipartBucket)); err != nil {
return err return err
} }
if err = os.MkdirAll(pathJoin(export, minioMetaMultipartBucket), 0755); err != nil {
if err = mkdirAll(pathJoin(export, minioMetaMultipartBucket), 0755); err != nil {
return err return err
} }
@@ -443,6 +446,112 @@ func checkFormatXLValues(formats []*formatXLV3) error {
return nil return nil
} }
// Get Deployment ID for the XL sets from format.json.
// This need not be in quorum. Even if one of the format.json
// file has this value, we assume it is valid.
// If more than one format.json's have different id, it is considered a corrupt
// backend format.
func formatXLGetDeploymentID(refFormat *formatXLV3, formats []*formatXLV3) (string, error) {
var deploymentID string
for _, format := range formats {
if format == nil || format.ID == "" {
continue
}
if reflect.DeepEqual(format.XL.Sets, refFormat.XL.Sets) {
// Found an ID in one of the format.json file
// Set deploymentID for the first time.
if deploymentID == "" {
deploymentID = format.ID
} else if deploymentID != format.ID {
// DeploymentID found earlier doesn't match with the
// current format.json's ID.
return "", errCorruptedFormat
}
}
}
return deploymentID, nil
}
// formatXLFixDeploymentID - Add deployment id if it is not present.
func formatXLFixDeploymentID(ctx context.Context, storageDisks []StorageAPI, refFormat *formatXLV3) (err error) {
// Acquire lock on format.json
mutex := newNSLock(globalIsDistXL)
formatLock := mutex.NewNSLock(minioMetaBucket, formatConfigFile)
if err = formatLock.GetLock(globalHealingTimeout); err != nil {
return err
}
defer formatLock.Unlock()
// Attempt to load all `format.json` from all disks.
var sErrs []error
formats, sErrs := loadFormatXLAll(storageDisks)
for i, sErr := range sErrs {
if _, ok := formatCriticalErrors[sErr]; ok {
return fmt.Errorf("Disk %s: %s", globalEndpoints[i], sErr)
}
}
for index := range formats {
// If the XL sets do not match, set those formats to nil,
// We do not have to update the ID on those format.json file.
if formats[index] != nil && !reflect.DeepEqual(formats[index].XL.Sets, refFormat.XL.Sets) {
formats[index] = nil
}
}
refFormat.ID, err = formatXLGetDeploymentID(refFormat, formats)
if err != nil {
return err
}
// If ID is set, then some other node got the lock
// before this node could and generated an ID
// for the deployment. No need to generate one.
if refFormat.ID != "" {
return nil
}
// ID is generated for the first time,
// We set the ID in all the formats and update.
refFormat.ID = mustGetUUID()
for _, format := range formats {
if format != nil {
format.ID = refFormat.ID
}
}
// Deployment ID needs to be set on all the disks.
// Save `format.json` across all disks.
return saveFormatXLAll(ctx, storageDisks, formats)
}
// Update only the valid local disks which have not been updated before.
func formatXLFixLocalDeploymentID(ctx context.Context, storageDisks []StorageAPI, refFormat *formatXLV3) error {
// If this server was down when the deploymentID was updated
// then we make sure that we update the local disks with the deploymentID.
for index, storageDisk := range storageDisks {
if globalEndpoints[index].IsLocal && storageDisk != nil && storageDisk.IsOnline() {
format, err := loadFormatXL(storageDisk)
if err != nil {
// Disk can be offline etc.
// ignore the errors seen here.
continue
}
if format.ID != "" {
continue
}
if !reflect.DeepEqual(format.XL.Sets, refFormat.XL.Sets) {
continue
}
format.ID = refFormat.ID
if err := saveFormatXL(storageDisk, format); err != nil {
logger.LogIf(ctx, err)
return fmt.Errorf("Unable to save format.json, %s", err)
}
}
}
return nil
}
// Get backend XL format in quorum `format.json`. // Get backend XL format in quorum `format.json`.
func getFormatXLInQuorum(formats []*formatXLV3) (*formatXLV3, error) { func getFormatXLInQuorum(formats []*formatXLV3) (*formatXLV3, error) {
formatHashes := make([]string, len(formats)) formatHashes := make([]string, len(formats))
+67 -6
View File
@@ -345,8 +345,10 @@ func TestCheckFormatXLValue(t *testing.T) {
// Invalid XL format version "2". // Invalid XL format version "2".
{ {
&formatXLV3{ &formatXLV3{
Version: "2", formatMetaV1: formatMetaV1{
Format: "XL", Version: "2",
Format: "XL",
},
XL: struct { XL: struct {
Version string `json:"version"` Version string `json:"version"`
This string `json:"this"` This string `json:"this"`
@@ -361,8 +363,10 @@ func TestCheckFormatXLValue(t *testing.T) {
// Invalid XL format "Unknown". // Invalid XL format "Unknown".
{ {
&formatXLV3{ &formatXLV3{
Version: "1", formatMetaV1: formatMetaV1{
Format: "Unknown", Version: "1",
Format: "Unknown",
},
XL: struct { XL: struct {
Version string `json:"version"` Version string `json:"version"`
This string `json:"this"` This string `json:"this"`
@@ -377,8 +381,10 @@ func TestCheckFormatXLValue(t *testing.T) {
// Invalid XL format version "0". // Invalid XL format version "0".
{ {
&formatXLV3{ &formatXLV3{
Version: "1", formatMetaV1: formatMetaV1{
Format: "XL", Version: "1",
Format: "XL",
},
XL: struct { XL: struct {
Version string `json:"version"` Version string `json:"version"`
This string `json:"this"` This string `json:"this"`
@@ -466,6 +472,61 @@ func TestGetFormatXLInQuorumCheck(t *testing.T) {
} }
} }
// Tests formatXLGetDeploymentID()
func TestGetXLID(t *testing.T) {
setCount := 2
disksPerSet := 8
format := newFormatXLV3(setCount, disksPerSet)
formats := make([]*formatXLV3, 16)
for i := 0; i < setCount; i++ {
for j := 0; j < disksPerSet; j++ {
newFormat := *format
newFormat.XL.This = format.XL.Sets[i][j]
formats[i*disksPerSet+j] = &newFormat
}
}
// Return a format from list of formats in quorum.
quorumFormat, err := getFormatXLInQuorum(formats)
if err != nil {
t.Fatal(err)
}
// Check if the reference format and input formats are same.
var id string
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != nil {
t.Fatal(err)
}
if id == "" {
t.Fatal("ID cannot be empty.")
}
formats[0] = nil
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != nil {
t.Fatal(err)
}
if id == "" {
t.Fatal("ID cannot be empty.")
}
formats[1].XL.Sets[0][0] = "bad-uuid"
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != nil {
t.Fatal(err)
}
if id == "" {
t.Fatal("ID cannot be empty.")
}
formats[2].ID = "bad-id"
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != errCorruptedFormat {
t.Fatal("Unexpected Success")
}
}
// Initialize new format sets. // Initialize new format sets.
func TestNewFormatSets(t *testing.T) { func TestNewFormatSets(t *testing.T) {
setCount := 2 setCount := 2
+12 -8
View File
@@ -64,7 +64,7 @@ func fsRemoveAll(ctx context.Context, dirPath string) (err error) {
return err return err
} }
if err = os.RemoveAll(dirPath); err != nil { if err = removeAll(dirPath); err != nil {
if os.IsPermission(err) { if os.IsPermission(err) {
logger.LogIf(ctx, errVolumeAccessDenied) logger.LogIf(ctx, errVolumeAccessDenied)
return errVolumeAccessDenied return errVolumeAccessDenied
@@ -121,23 +121,25 @@ func fsMkdir(ctx context.Context, dirPath string) (err error) {
} }
if err = os.Mkdir((dirPath), 0777); err != nil { if err = os.Mkdir((dirPath), 0777); err != nil {
if os.IsExist(err) { switch {
case os.IsExist(err):
return errVolumeExists return errVolumeExists
} else if os.IsPermission(err) { case os.IsPermission(err):
logger.LogIf(ctx, errDiskAccessDenied) logger.LogIf(ctx, errDiskAccessDenied)
return errDiskAccessDenied return errDiskAccessDenied
} else if isSysErrNotDir(err) { case isSysErrNotDir(err):
// File path cannot be verified since // File path cannot be verified since
// one of the parents is a file. // one of the parents is a file.
logger.LogIf(ctx, errDiskAccessDenied) logger.LogIf(ctx, errDiskAccessDenied)
return errDiskAccessDenied return errDiskAccessDenied
} else if isSysErrPathNotFound(err) { case isSysErrPathNotFound(err):
// Add specific case for windows. // Add specific case for windows.
logger.LogIf(ctx, errDiskAccessDenied) logger.LogIf(ctx, errDiskAccessDenied)
return errDiskAccessDenied return errDiskAccessDenied
default:
logger.LogIf(ctx, err)
return err
} }
logger.LogIf(ctx, err)
return err
} }
return nil return nil
@@ -343,7 +345,9 @@ func fsCreateFile(ctx context.Context, filePath string, reader io.Reader, buf []
if buf != nil { if buf != nil {
bytesWritten, err = io.CopyBuffer(writer, reader, buf) bytesWritten, err = io.CopyBuffer(writer, reader, buf)
if err != nil { if err != nil {
logger.LogIf(ctx, err) if err != io.ErrUnexpectedEOF {
logger.LogIf(ctx, err)
}
return 0, err return 0, err
} }
} else { } else {
+1 -15
View File
@@ -543,25 +543,11 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
// 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(fi.Size()) {
err = PartTooSmall{ return oi, PartTooSmall{
PartNumber: part.PartNumber, PartNumber: part.PartNumber,
PartSize: fi.Size(), PartSize: fi.Size(),
PartETag: part.ETag, PartETag: part.ETag,
} }
logger.LogIf(ctx, err)
return oi, err
}
// TODO: Make necessary changes in future as explained in the below comment.
// All parts except the last part has to be of same size. We are introducing this
// check to see if any clients break. If clients do not break then we can optimize
// multipart PutObjectPart by writing the part at the right offset using pwrite()
// so that we don't need to do background append at all. i.e by the time we get
// CompleteMultipartUpload we already have the full file available which can be
// renamed to the main name-space.
if partSize != fi.Size() {
logger.LogIf(ctx, PartsSizeUnequal{})
return oi, PartsSizeUnequal{}
} }
} }
+2 -7
View File
@@ -90,23 +90,18 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
// TestPutObjectPartFaultyDisk - test PutObjectPart with faulty disks // TestPutObjectPartFaultyDisk - test PutObjectPart with faulty disks
func TestPutObjectPartFaultyDisk(t *testing.T) { func TestPutObjectPartFaultyDisk(t *testing.T) {
root, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// Prepare for tests // Prepare for tests
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix()) disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
defer os.RemoveAll(disk) defer os.RemoveAll(disk)
obj := initFSObjects(disk, t) obj := initFSObjects(disk, t)
fs := obj.(*FSObjects) fs := obj.(*FSObjects)
bucketName := "bucket" bucketName := "bucket"
objectName := "object" objectName := "object"
data := []byte("12345") data := []byte("12345")
dataLen := int64(len(data)) dataLen := int64(len(data))
if err = obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil { if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
t.Fatal("Cannot create bucket, err: ", err) t.Fatal("Cannot create bucket, err: ", err)
} }
+20 -14
View File
@@ -93,18 +93,20 @@ func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
// Open file for reading with read lock. // Open file for reading with read lock.
newRlkFile, err := lock.RLockedOpenFile(path) newRlkFile, err := lock.RLockedOpenFile(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { switch {
case os.IsNotExist(err):
return nil, errFileNotFound return nil, errFileNotFound
} else if os.IsPermission(err) { case os.IsPermission(err):
return nil, errFileAccessDenied return nil, errFileAccessDenied
} else if isSysErrIsDir(err) { case isSysErrIsDir(err):
return nil, errIsNotRegular return nil, errIsNotRegular
} else if isSysErrNotDir(err) { case isSysErrNotDir(err):
return nil, errFileAccessDenied return nil, errFileAccessDenied
} else if isSysErrPathNotFound(err) { case isSysErrPathNotFound(err):
return nil, errFileNotFound return nil, errFileNotFound
default:
return nil, err
} }
return nil, err
} }
/// Save new reader on the map. /// Save new reader on the map.
@@ -148,14 +150,16 @@ func (fsi *fsIOPool) Write(path string) (wlk *lock.LockedFile, err error) {
wlk, err = lock.LockedOpenFile(path, os.O_RDWR, 0666) wlk, err = lock.LockedOpenFile(path, os.O_RDWR, 0666)
if err != nil { if err != nil {
if os.IsNotExist(err) { switch {
case os.IsNotExist(err):
return nil, errFileNotFound return nil, errFileNotFound
} else if os.IsPermission(err) { case os.IsPermission(err):
return nil, errFileAccessDenied return nil, errFileAccessDenied
} else if isSysErrIsDir(err) { case isSysErrIsDir(err):
return nil, errIsNotRegular return nil, errIsNotRegular
default:
return nil, err
} }
return nil, err
} }
return wlk, nil return wlk, nil
} }
@@ -175,14 +179,16 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
// Attempt to create the file. // Attempt to create the file.
wlk, err = lock.LockedOpenFile(path, os.O_RDWR|os.O_CREATE, 0666) wlk, err = lock.LockedOpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil { if err != nil {
if os.IsPermission(err) { switch {
case os.IsPermission(err):
return nil, errFileAccessDenied return nil, errFileAccessDenied
} else if isSysErrIsDir(err) { case isSysErrIsDir(err):
return nil, errIsNotRegular return nil, errIsNotRegular
} else if isSysErrPathNotFound(err) { case isSysErrPathNotFound(err):
return nil, errFileAccessDenied return nil, errFileAccessDenied
default:
return nil, err
} }
return nil, err
} }
// Success. // Success.
+11 -26
View File
@@ -151,16 +151,6 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
// or cause changes on backend format. // or cause changes on backend format.
fs.fsFormatRlk = rlk fs.fsFormatRlk = rlk
// Initialize notification system.
if err = globalNotificationSys.Init(fs); err != nil {
return nil, uiErrUnableToReadFromBackend(err).Msg("Unable to initialize notification system")
}
// Initialize policy system.
if err = globalPolicySys.Init(fs); err != nil {
return nil, uiErrUnableToReadFromBackend(err).Msg("Unable to initialize policy system")
}
if !fs.diskMount { if !fs.diskMount {
go fs.diskUsage(globalServiceDoneCh) go fs.diskUsage(globalServiceDoneCh)
} }
@@ -275,18 +265,6 @@ func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
return storageInfo return storageInfo
} }
// Locking operations
// ListLocks - List namespace locks held in object layer
func (fs *FSObjects) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
return []VolumeLockInfo{}, NotImplemented{}
}
// ClearLocks - Clear namespace locks held in object layer
func (fs *FSObjects) ClearLocks(ctx context.Context, info []VolumeLockInfo) error {
return NotImplemented{}
}
/// Bucket operations /// Bucket operations
// getBucketDir - will convert incoming bucket names to // getBucketDir - will convert incoming bucket names to
@@ -978,13 +956,15 @@ func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string) er
// is a leaf or non-leaf entry. // is a leaf or non-leaf entry.
func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc { func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
// listDir - lists all the entries at a given prefix and given entry in the prefix. // listDir - lists all the entries at a given prefix and given entry in the prefix.
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool, err error) { listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
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 {
return nil, false, err logger.LogIf(context.Background(), err)
return
} }
entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf) entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf)
return entries, delayIsLeaf, nil return entries, delayIsLeaf
} }
// Return list factory instance. // Return list factory instance.
@@ -1250,7 +1230,12 @@ func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) erro
// ListObjectsV2 lists all blobs in bucket filtered by prefix // ListObjectsV2 lists all blobs in bucket filtered by prefix
func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) { func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) {
loi, err := fs.ListObjects(ctx, bucket, prefix, continuationToken, delimiter, maxKeys) marker := continuationToken
if marker == "" {
marker = startAfter
}
loi, err := fs.ListObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
if err != nil { if err != nil {
return result, err return result, err
} }
+1 -12
View File
@@ -26,12 +26,6 @@ import (
// Tests for if parent directory is object // Tests for if parent directory is object
func TestFSParentDirIsObject(t *testing.T) { func TestFSParentDirIsObject(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootPath)
obj, disk, err := prepareFS() obj, disk, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -120,12 +114,6 @@ func TestNewFS(t *testing.T) {
// TestFSShutdown - initialize a new FS object layer then calls // TestFSShutdown - initialize a new FS object layer then calls
// Shutdown to check returned results // Shutdown to check returned results
func TestFSShutdown(t *testing.T) { func TestFSShutdown(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootPath)
bucketName := "testbucket" bucketName := "testbucket"
objectName := "object" objectName := "object"
// Create and return an fsObject with its path in the disk // Create and return an fsObject with its path in the disk
@@ -133,6 +121,7 @@ func TestFSShutdown(t *testing.T) {
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix()) disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
obj := initFSObjects(disk, t) obj := initFSObjects(disk, t)
fs := obj.(*FSObjects) fs := obj.(*FSObjects)
objectContent := "12345" objectContent := "12345"
obj.MakeBucketWithLocation(context.Background(), bucketName, "") obj.MakeBucketWithLocation(context.Background(), bucketName, "")
obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil) obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil)
+16 -11
View File
@@ -150,14 +150,25 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
// Validate if we have access, secret set through environment. // Validate if we have access, secret set through environment.
if !globalIsEnvCreds { if !globalIsEnvCreds {
logger.Fatal(uiErrEnvCredentialsMissing(nil), "Unable to start gateway") logger.Fatal(uiErrEnvCredentialsMissingGateway(nil), "Unable to start gateway")
} }
// Create certs path. // Create certs path.
logger.FatalIf(createConfigDir(), "Unable to create configuration directories") logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
// Initialize gateway config. // Initialize server config.
initConfig() 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()
// Check and load SSL certificates. // Check and load SSL certificates.
var err error var err error
@@ -170,8 +181,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
initNSLock(false) // Enable local namespace lock. initNSLock(false) // Enable local namespace lock.
// Create new notification system. // Create new notification system.
globalNotificationSys, err = NewNotificationSys(globalServerConfig, EndpointList{}) globalNotificationSys = NewNotificationSys(globalServerConfig, EndpointList{})
logger.FatalIf(err, "Unable to create new notification system")
// Create new policy system. // Create new policy system.
globalPolicySys = NewPolicySys() globalPolicySys = NewPolicySys()
@@ -215,12 +225,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
logger.FatalIf(err, "Unable to initialize gateway backend") logger.FatalIf(err, "Unable to initialize gateway backend")
} }
if gw.Name() != "nas" { go globalPolicySys.Init(newObject)
// Initialize policy sys for all gateways. NAS gateway already
// initializes policy sys internally, avoid double initialization.
// Additionally also don't block the initialization of gateway.
go globalPolicySys.Init(newObject)
}
// Once endpoints are finalized, initialize the new object api. // Once endpoints are finalized, initialize the new object api.
globalObjLayerMutex.Lock() globalObjLayerMutex.Lock()
+10 -4
View File
@@ -23,11 +23,14 @@ import (
// Test printing Gateway common message. // Test printing Gateway common message.
func TestPrintGatewayCommonMessage(t *testing.T) { func TestPrintGatewayCommonMessage(t *testing.T) {
root, err := newTestConfig(globalMinioDefaultRegion) obj, fsDir, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(root) defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
t.Fatal(err)
}
apiEndpoints := []string{"http://127.0.0.1:9000"} apiEndpoints := []string{"http://127.0.0.1:9000"}
printGatewayCommonMsg(apiEndpoints) printGatewayCommonMsg(apiEndpoints)
@@ -35,11 +38,14 @@ func TestPrintGatewayCommonMessage(t *testing.T) {
// Test print gateway startup message. // Test print gateway startup message.
func TestPrintGatewayStartupMessage(t *testing.T) { func TestPrintGatewayStartupMessage(t *testing.T) {
root, err := newTestConfig(globalMinioDefaultRegion) obj, fsDir, err := prepareFS()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(root) defer os.RemoveAll(fsDir)
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
t.Fatal(err)
}
apiEndpoints := []string{"http://127.0.0.1:9000"} apiEndpoints := []string{"http://127.0.0.1:9000"}
printGatewayStartupMessage(apiEndpoints, "azure") printGatewayStartupMessage(apiEndpoints, "azure")
-15
View File
@@ -18,7 +18,6 @@ package cmd
import ( import (
"context" "context"
"time"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
@@ -137,20 +136,6 @@ func (a GatewayUnsupported) CopyObject(ctx context.Context, srcBucket string, sr
return objInfo, NotImplemented{} return objInfo, NotImplemented{}
} }
// Locking operations
// ListLocks lists namespace locks held in object layer
func (a GatewayUnsupported) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
logger.LogIf(ctx, NotImplemented{})
return []VolumeLockInfo{}, NotImplemented{}
}
// ClearLocks clears namespace locks held in object layer
func (a GatewayUnsupported) ClearLocks(ctx context.Context, info []VolumeLockInfo) error {
logger.LogIf(ctx, NotImplemented{})
return NotImplemented{}
}
// RefreshBucketPolicy refreshes cache policy with what's on disk. // RefreshBucketPolicy refreshes cache policy with what's on disk.
func (a GatewayUnsupported) RefreshBucketPolicy(ctx context.Context, bucket string) error { func (a GatewayUnsupported) RefreshBucketPolicy(ctx context.Context, bucket string) error {
logger.LogIf(ctx, NotImplemented{}) logger.LogIf(ctx, NotImplemented{})
+1 -48
View File
@@ -193,7 +193,6 @@ func s3MetaToAzureProperties(ctx context.Context, s3Metadata map[string]string)
storage.BlobProperties, error) { storage.BlobProperties, error) {
for k := range s3Metadata { for k := range s3Metadata {
if strings.Contains(k, "--") { if strings.Contains(k, "--") {
logger.LogIf(ctx, minio.UnsupportedMetadata{})
return storage.BlobMetadata{}, storage.BlobProperties{}, minio.UnsupportedMetadata{} return storage.BlobMetadata{}, storage.BlobProperties{}, minio.UnsupportedMetadata{}
} }
} }
@@ -377,18 +376,12 @@ func getAzureUploadID() (string, error) {
// checkAzureUploadID - returns error in case of given string is upload ID. // checkAzureUploadID - returns error in case of given string is upload ID.
func checkAzureUploadID(ctx context.Context, uploadID string) (err error) { func checkAzureUploadID(ctx context.Context, uploadID string) (err error) {
if len(uploadID) != 16 { if len(uploadID) != 16 {
logger.LogIf(ctx, minio.MalformedUploadID{
UploadID: uploadID,
})
return minio.MalformedUploadID{ return minio.MalformedUploadID{
UploadID: uploadID, UploadID: uploadID,
} }
} }
if _, err = hex.DecodeString(uploadID); err != nil { if _, err = hex.DecodeString(uploadID); err != nil {
logger.LogIf(ctx, minio.MalformedUploadID{
UploadID: uploadID,
})
return minio.MalformedUploadID{ return minio.MalformedUploadID{
UploadID: uploadID, UploadID: uploadID,
} }
@@ -449,7 +442,6 @@ func (a *azureObjects) MakeBucketWithLocation(ctx context.Context, bucket, locat
// in azure documentation, so we will simply use the same function here. // in azure documentation, so we will simply use the same function here.
// Ref - https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata // Ref - https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata
if !minio.IsValidBucketName(bucket) { if !minio.IsValidBucketName(bucket) {
logger.LogIf(ctx, minio.BucketNameInvalid{Bucket: bucket})
return minio.BucketNameInvalid{Bucket: bucket} return minio.BucketNameInvalid{Bucket: bucket}
} }
@@ -457,7 +449,6 @@ func (a *azureObjects) MakeBucketWithLocation(ctx context.Context, bucket, locat
err := container.Create(&storage.CreateContainerOptions{ err := container.Create(&storage.CreateContainerOptions{
Access: storage.ContainerAccessTypePrivate, Access: storage.ContainerAccessTypePrivate,
}) })
logger.LogIf(ctx, err)
return azureToObjectError(err, bucket) return azureToObjectError(err, bucket)
} }
@@ -469,7 +460,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
Prefix: bucket, Prefix: bucket,
}) })
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return bi, azureToObjectError(err, bucket) return bi, azureToObjectError(err, bucket)
} }
for _, container := range resp.Containers { for _, container := range resp.Containers {
@@ -483,7 +473,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
} // else continue } // else continue
} }
} }
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
return bi, minio.BucketNotFound{Bucket: bucket} return bi, minio.BucketNotFound{Bucket: bucket}
} }
@@ -491,7 +480,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) { func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
resp, err := a.client.ListContainers(storage.ListContainersParameters{}) resp, err := a.client.ListContainers(storage.ListContainersParameters{})
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return nil, azureToObjectError(err) return nil, azureToObjectError(err)
} }
for _, container := range resp.Containers { for _, container := range resp.Containers {
@@ -512,7 +500,6 @@ func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketI
func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string) error { func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string) error {
container := a.client.GetContainerReference(bucket) container := a.client.GetContainerReference(bucket)
err := container.Delete(nil) err := container.Delete(nil)
logger.LogIf(ctx, err)
return azureToObjectError(err, bucket) return azureToObjectError(err, bucket)
} }
@@ -550,7 +537,6 @@ func (a *azureObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
MaxResults: uint(maxKeys), MaxResults: uint(maxKeys),
}) })
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return result, azureToObjectError(err, bucket, prefix) return result, azureToObjectError(err, bucket, prefix)
} }
@@ -611,7 +597,7 @@ func (a *azureObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
// ListObjectsV2 - list all blobs in Azure bucket filtered by prefix // ListObjectsV2 - list all blobs in Azure bucket filtered by prefix
func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result minio.ListObjectsV2Info, err error) { func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result minio.ListObjectsV2Info, err error) {
marker := continuationToken marker := continuationToken
if startAfter != "" { if marker == "" {
marker = startAfter marker = startAfter
} }
@@ -638,7 +624,6 @@ func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, contin
func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error { func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
// startOffset cannot be negative. // startOffset cannot be negative.
if startOffset < 0 { if startOffset < 0 {
logger.LogIf(ctx, minio.InvalidRange{})
return azureToObjectError(minio.InvalidRange{}, bucket, object) return azureToObjectError(minio.InvalidRange{}, bucket, object)
} }
@@ -658,7 +643,6 @@ func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, sta
}) })
} }
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return azureToObjectError(err, bucket, object) return azureToObjectError(err, bucket, object)
} }
_, err = io.Copy(writer, rc) _, err = io.Copy(writer, rc)
@@ -673,7 +657,6 @@ func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string)
blob := a.client.GetContainerReference(bucket).GetBlobReference(object) blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
err = blob.GetProperties(nil) err = blob.GetProperties(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
@@ -699,7 +682,6 @@ func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, dat
} }
err = blob.CreateBlockBlobFromReader(data, nil) err = blob.CreateBlockBlobFromReader(data, nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
return a.GetObjectInfo(ctx, bucket, object) return a.GetObjectInfo(ctx, bucket, object)
@@ -717,7 +699,6 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
destBlob.Metadata = azureMeta destBlob.Metadata = azureMeta
err = destBlob.Copy(srcBlobURL, nil) err = destBlob.Copy(srcBlobURL, nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, srcBucket, srcObject) return objInfo, azureToObjectError(err, srcBucket, srcObject)
} }
// Azure will copy metadata from the source object when an empty metadata map is provided. // Azure will copy metadata from the source object when an empty metadata map is provided.
@@ -727,14 +708,12 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
destBlob.Metadata = azureMeta destBlob.Metadata = azureMeta
err = destBlob.SetMetadata(nil) err = destBlob.SetMetadata(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, srcBucket, srcObject) return objInfo, azureToObjectError(err, srcBucket, srcObject)
} }
} }
destBlob.Properties = props destBlob.Properties = props
err = destBlob.SetProperties(nil) err = destBlob.SetProperties(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, srcBucket, srcObject) return objInfo, azureToObjectError(err, srcBucket, srcObject)
} }
return a.GetObjectInfo(ctx, destBucket, destObject) return a.GetObjectInfo(ctx, destBucket, destObject)
@@ -770,14 +749,12 @@ func (a *azureObjects) checkUploadIDExists(ctx context.Context, bucketName, obje
blob := a.client.GetContainerReference(bucketName).GetBlobReference( blob := a.client.GetContainerReference(bucketName).GetBlobReference(
getAzureMetadataObjectName(objectName, uploadID)) getAzureMetadataObjectName(objectName, uploadID))
err = blob.GetMetadata(nil) err = blob.GetMetadata(nil)
logger.LogIf(ctx, err)
err = azureToObjectError(err, bucketName, objectName) err = azureToObjectError(err, bucketName, objectName)
oerr := minio.ObjectNotFound{ oerr := minio.ObjectNotFound{
Bucket: bucketName, Bucket: bucketName,
Object: objectName, Object: objectName,
} }
if err == oerr { if err == oerr {
logger.LogIf(ctx, minio.InvalidUploadID{UploadID: uploadID})
err = minio.InvalidUploadID{ err = minio.InvalidUploadID{
UploadID: uploadID, UploadID: uploadID,
} }
@@ -803,7 +780,6 @@ func (a *azureObjects) NewMultipartUpload(ctx context.Context, bucket, object st
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject) blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
err = blob.CreateBlockBlobFromReader(bytes.NewBuffer(jsonData), nil) err = blob.CreateBlockBlobFromReader(bytes.NewBuffer(jsonData), nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return "", azureToObjectError(err, bucket, metadataObject) return "", azureToObjectError(err, bucket, metadataObject)
} }
@@ -840,7 +816,6 @@ func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, upload
blob := a.client.GetContainerReference(bucket).GetBlobReference(object) blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
err = blob.PutBlockWithLength(id, uint64(subPartSize), io.LimitReader(data, subPartSize), nil) err = blob.PutBlockWithLength(id, uint64(subPartSize), io.LimitReader(data, subPartSize), nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return info, azureToObjectError(err, bucket, object) return info, azureToObjectError(err, bucket, object)
} }
subPartNumber++ subPartNumber++
@@ -872,7 +847,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
return result, nil return result, nil
} }
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return result, azureToObjectError(err, bucket, object) return result, azureToObjectError(err, bucket, object)
} }
// Build a sorted list of parts and return the requested entries. // Build a sorted list of parts and return the requested entries.
@@ -882,7 +856,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
var parsedUploadID string var parsedUploadID string
var md5Hex string var md5Hex string
if partNumber, _, parsedUploadID, md5Hex, err = azureParseBlockID(block.Name); err != nil { if partNumber, _, parsedUploadID, md5Hex, err = azureParseBlockID(block.Name); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object) return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
} }
if parsedUploadID != uploadID { if parsedUploadID != uploadID {
@@ -900,7 +873,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
if part.ETag != md5Hex { if part.ETag != md5Hex {
// If two parts of same partNumber were uploaded with different contents // If two parts of same partNumber were uploaded with different contents
// return error as we won't be able to decide which the latest part is. // return error as we won't be able to decide which the latest part is.
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object) return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
} }
part.Size += block.Size part.Size += block.Size
@@ -967,7 +939,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
var metadataReader io.Reader var metadataReader io.Reader
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject) blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
if metadataReader, err = blob.Get(nil); err != nil { if metadataReader, err = blob.Get(nil); err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, metadataObject) return objInfo, azureToObjectError(err, bucket, metadataObject)
} }
@@ -991,7 +962,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
objBlob := a.client.GetContainerReference(bucket).GetBlobReference(object) objBlob := a.client.GetContainerReference(bucket).GetBlobReference(object)
resp, err := objBlob.GetBlockList(storage.BlockListTypeUncommitted, nil) resp, err := objBlob.GetBlockList(storage.BlockListTypeUncommitted, nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
@@ -1039,11 +1009,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
// Error out if parts except last part sizing < 5MiB. // Error out if parts except last part sizing < 5MiB.
for i, size := range partSizes[:len(partSizes)-1] { for i, size := range partSizes[:len(partSizes)-1] {
if size < azureS3MinPartSize { if size < azureS3MinPartSize {
logger.LogIf(ctx, minio.PartTooSmall{
PartNumber: uploadedParts[i].PartNumber,
PartSize: size,
PartETag: uploadedParts[i].ETag,
})
return objInfo, minio.PartTooSmall{ return objInfo, minio.PartTooSmall{
PartNumber: uploadedParts[i].PartNumber, PartNumber: uploadedParts[i].PartNumber,
PartSize: size, PartSize: size,
@@ -1054,23 +1019,19 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
err = objBlob.PutBlockList(allBlocks, nil) err = objBlob.PutBlockList(allBlocks, nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
if len(metadata.Metadata) > 0 { if len(metadata.Metadata) > 0 {
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata) objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
err = objBlob.SetProperties(nil) err = objBlob.SetProperties(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
err = objBlob.SetMetadata(nil) err = objBlob.SetMetadata(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, azureToObjectError(err, bucket, object) return objInfo, azureToObjectError(err, bucket, object)
} }
} }
@@ -1100,15 +1061,12 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucke
} }
prefix := bucket + "/*" // For all objects inside the bucket. prefix := bucket + "/*" // For all objects inside the bucket.
if len(policies) != 1 { if len(policies) != 1 {
logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
if policies[0].Prefix != prefix { if policies[0].Prefix != prefix {
logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly { if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
perm := storage.ContainerPermissions{ perm := storage.ContainerPermissions{
@@ -1117,7 +1075,6 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucke
} }
container := a.client.GetContainerReference(bucket) container := a.client.GetContainerReference(bucket)
err = container.SetPermissions(perm, nil) err = container.SetPermissions(perm, nil)
logger.LogIf(ctx, err)
return azureToObjectError(err, bucket) return azureToObjectError(err, bucket)
} }
@@ -1126,15 +1083,12 @@ func (a *azureObjects) GetBucketPolicy(ctx context.Context, bucket string) (*pol
container := a.client.GetContainerReference(bucket) container := a.client.GetContainerReference(bucket)
perm, err := container.GetPermissions(nil) perm, err := container.GetPermissions(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return nil, azureToObjectError(err, bucket) return nil, azureToObjectError(err, bucket)
} }
if perm.AccessType == storage.ContainerAccessTypePrivate { if perm.AccessType == storage.ContainerAccessTypePrivate {
logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
return nil, minio.BucketPolicyNotFound{Bucket: bucket} return nil, minio.BucketPolicyNotFound{Bucket: bucket}
} else if perm.AccessType != storage.ContainerAccessTypeContainer { } else if perm.AccessType != storage.ContainerAccessTypeContainer {
logger.LogIf(ctx, minio.NotImplemented{})
return nil, azureToObjectError(minio.NotImplemented{}) return nil, azureToObjectError(minio.NotImplemented{})
} }
@@ -1167,6 +1121,5 @@ func (a *azureObjects) DeleteBucketPolicy(ctx context.Context, bucket string) er
} }
container := a.client.GetContainerReference(bucket) container := a.client.GetContainerReference(bucket)
err := container.SetPermissions(perm, nil) err := container.SetPermissions(perm, nil)
logger.LogIf(ctx, err)
return azureToObjectError(err) return azureToObjectError(err)
} }
+10 -4
View File
@@ -278,7 +278,6 @@ func (l *b2Objects) Bucket(ctx context.Context, bucket string) (*b2.Bucket, erro
return bkt, nil return bkt, nil
} }
} }
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
return nil, minio.BucketNotFound{Bucket: bucket} return nil, minio.BucketNotFound{Bucket: bucket}
} }
@@ -355,12 +354,20 @@ func (l *b2Objects) ListObjects(ctx context.Context, bucket string, prefix strin
// ListObjectsV2 lists all objects in B2 bucket filtered by prefix, returns upto max 1000 entries at a time. // ListObjectsV2 lists all objects in B2 bucket filtered by prefix, returns upto max 1000 entries at a time.
func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int,
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) { fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
// fetchOwner, startAfter are not supported and unused. // fetchOwner is not supported and unused.
marker := continuationToken
if marker == "" {
// B2's continuation token is an object name to "start at" rather than "start after"
// startAfter plus the lowest character B2 supports is used so that the startAfter
// object isn't included in the results
marker = startAfter + " "
}
bkt, err := l.Bucket(ctx, bucket) bkt, err := l.Bucket(ctx, bucket)
if err != nil { if err != nil {
return loi, err return loi, err
} }
files, next, lerr := bkt.ListFileNames(l.ctx, maxKeys, continuationToken, prefix, delimiter) files, next, lerr := bkt.ListFileNames(l.ctx, maxKeys, marker, prefix, delimiter)
if lerr != nil { if lerr != nil {
logger.LogIf(ctx, lerr) logger.LogIf(ctx, lerr)
return loi, b2ToObjectError(lerr, bucket) return loi, b2ToObjectError(lerr, bucket)
@@ -772,7 +779,6 @@ func (l *b2Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy
// just return back as policy not found for all cases. // just return back as policy not found for all cases.
// CreateBucket always sets the value to allPrivate by default. // CreateBucket always sets the value to allPrivate by default.
if bkt.Type != bucketTypeReadOnly { if bkt.Type != bucketTypeReadOnly {
logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
return nil, minio.BucketPolicyNotFound{Bucket: bucket} return nil, minio.BucketPolicyNotFound{Bucket: bucket}
} }
+134 -126
View File
@@ -23,8 +23,8 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
"net/http"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@@ -554,16 +554,16 @@ func isGCSMarker(marker string) bool {
// ListObjects - lists all blobs in GCS bucket filtered by prefix // ListObjects - lists all blobs in GCS bucket filtered by prefix
func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (minio.ListObjectsInfo, error) { func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (minio.ListObjectsInfo, error) {
if maxKeys == 0 {
return minio.ListObjectsInfo{}, nil
}
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{ it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
Delimiter: delimiter, Delimiter: delimiter,
Prefix: prefix, Prefix: prefix,
Versions: false, Versions: false,
}) })
isTruncated := false
nextMarker := ""
prefixes := []string{}
// To accommodate S3-compatible applications using // To accommodate S3-compatible applications using
// ListObjectsV1 to use object keys as markers to control the // ListObjectsV1 to use object keys as markers to control the
// listing of objects, we use the following encoding scheme to // listing of objects, we use the following encoding scheme to
@@ -574,83 +574,86 @@ func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix stri
// prefixing "{minio}" to the GCS continuation token, // prefixing "{minio}" to the GCS continuation token,
// e.g, "{minio}CgRvYmoz" // e.g, "{minio}CgRvYmoz"
// //
// - Application supplied markers are used as-is to list // - Application supplied markers are transformed to a
// object keys that appear after it in the lexicographical order. // GCS continuation token.
// If application is using GCS continuation token we should // If application is using GCS continuation token we should
// strip the gcsTokenPrefix we added. // strip the gcsTokenPrefix we added.
gcsMarker := isGCSMarker(marker) token := ""
if gcsMarker { if marker != "" {
it.PageInfo().Token = strings.TrimPrefix(marker, gcsTokenPrefix) if isGCSMarker(marker) {
token = strings.TrimPrefix(marker, gcsTokenPrefix)
} else {
token = toGCSPageToken(marker)
}
} }
nextMarker := ""
it.PageInfo().MaxSize = maxKeys var prefixes []string
var objects []minio.ObjectInfo
var nextPageToken string
var err error
objects := []minio.ObjectInfo{} pager := iterator.NewPager(it, maxKeys, token)
for { for {
if len(objects) >= maxKeys { gcsObjects := make([]*storage.ObjectAttrs, 0)
// check if there is one next object and nextPageToken, err = pager.NextPage(&gcsObjects)
// if that one next object is our hidden
// metadata folder, then just break
// otherwise we've truncated the output
attrs, _ := it.Next()
if attrs != nil && attrs.Prefix == minio.GatewayMinioSysTmp {
break
}
isTruncated = true
break
}
attrs, err := it.Next()
if err == iterator.Done {
break
}
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return minio.ListObjectsInfo{}, gcsToObjectError(err, bucket, prefix) return minio.ListObjectsInfo{}, gcsToObjectError(err, bucket, prefix)
} }
nextMarker = toGCSPageToken(attrs.Name) for _, attrs := range gcsObjects {
if attrs.Prefix == minio.GatewayMinioSysTmp { // Due to minio.GatewayMinioSysTmp keys being skipped, the number of objects + prefixes
// We don't return our metadata prefix. // returned may not total maxKeys. This behavior is compatible with the S3 spec which
continue // allows the response to include less keys than maxKeys.
} if attrs.Prefix == minio.GatewayMinioSysTmp {
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) { // We don't return our metadata prefix.
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
// which will be helpful to observe the "directory structure" for debugging purposes.
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
continue continue
} }
} if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) {
if attrs.Prefix != "" { // If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
prefixes = append(prefixes, attrs.Prefix) // But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
continue // which will be helpful to observe the "directory structure" for debugging purposes.
} if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
if !gcsMarker && attrs.Name <= marker { strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
// if user supplied a marker don't append continue
// objects until we reach marker (and skip it). }
continue }
if attrs.Prefix != "" {
prefixes = append(prefixes, attrs.Prefix)
} else {
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
}
// The NextMarker property should only be set in the response if a delimiter is used
if delimiter != "" {
if attrs.Prefix > nextMarker {
nextMarker = attrs.Prefix
} else if attrs.Name > nextMarker {
nextMarker = attrs.Name
}
}
} }
objects = append(objects, minio.ObjectInfo{ // Exit the loop if at least one item can be returned from
Name: attrs.Name, // the current page or there are no more pages available
Bucket: attrs.Bucket, if nextPageToken == "" || len(prefixes)+len(objects) > 0 {
ModTime: attrs.Updated, break
Size: attrs.Size, }
ETag: minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C)), }
UserDefined: attrs.Metadata,
ContentType: attrs.ContentType, if nextPageToken == "" {
ContentEncoding: attrs.ContentEncoding, nextMarker = ""
}) } else if nextMarker != "" {
nextMarker = gcsTokenPrefix + toGCSPageToken(nextMarker)
} }
return minio.ListObjectsInfo{ return minio.ListObjectsInfo{
IsTruncated: isTruncated, IsTruncated: nextPageToken != "",
NextMarker: gcsTokenPrefix + nextMarker, NextMarker: nextMarker,
Prefixes: prefixes, Prefixes: prefixes,
Objects: objects, Objects: objects,
}, nil }, nil
@@ -658,68 +661,72 @@ func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix stri
// ListObjectsV2 - lists all blobs in GCS bucket filtered by prefix // ListObjectsV2 - lists all blobs in GCS bucket filtered by prefix
func (l *gcsGateway) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (minio.ListObjectsV2Info, error) { func (l *gcsGateway) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (minio.ListObjectsV2Info, error) {
if maxKeys == 0 {
return minio.ListObjectsV2Info{ContinuationToken: continuationToken}, nil
}
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{ it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
Delimiter: delimiter, Delimiter: delimiter,
Prefix: prefix, Prefix: prefix,
Versions: false, Versions: false,
}) })
isTruncated := false token := continuationToken
it.PageInfo().MaxSize = maxKeys if token == "" && startAfter != "" {
token = toGCSPageToken(startAfter)
if continuationToken != "" {
// If client sends continuationToken, set it
it.PageInfo().Token = continuationToken
} else {
// else set the continuationToken to return
continuationToken = it.PageInfo().Token
if continuationToken != "" {
// If GCS SDK sets continuationToken, it means there are more than maxKeys in the current page
// and the response will be truncated
isTruncated = true
}
} }
var prefixes []string var prefixes []string
var objects []minio.ObjectInfo var objects []minio.ObjectInfo
var nextPageToken string
var err error
pager := iterator.NewPager(it, maxKeys, token)
for { for {
attrs, err := it.Next() gcsObjects := make([]*storage.ObjectAttrs, 0)
if err == iterator.Done { nextPageToken, err = pager.NextPage(&gcsObjects)
break
}
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return minio.ListObjectsV2Info{}, gcsToObjectError(err, bucket, prefix) return minio.ListObjectsV2Info{}, gcsToObjectError(err, bucket, prefix)
} }
if attrs.Prefix == minio.GatewayMinioSysTmp { for _, attrs := range gcsObjects {
// We don't return our metadata prefix.
continue // Due to minio.GatewayMinioSysTmp keys being skipped, the number of objects + prefixes
} // returned may not total maxKeys. This behavior is compatible with the S3 spec which
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) { // allows the response to include less keys than maxKeys.
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries. if attrs.Prefix == minio.GatewayMinioSysTmp {
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/ // We don't return our metadata prefix.
// which will be helpful to observe the "directory structure" for debugging purposes.
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
continue continue
} }
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) {
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
// which will be helpful to observe the "directory structure" for debugging purposes.
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
continue
}
}
if attrs.Prefix != "" {
prefixes = append(prefixes, attrs.Prefix)
} else {
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
}
} }
if attrs.Prefix != "" { // Exit the loop if at least one item can be returned from
prefixes = append(prefixes, attrs.Prefix) // the current page or there are no more pages available
continue if nextPageToken == "" || len(prefixes)+len(objects) > 0 {
break
} }
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
} }
return minio.ListObjectsV2Info{ return minio.ListObjectsV2Info{
IsTruncated: isTruncated, IsTruncated: nextPageToken != "",
ContinuationToken: continuationToken, ContinuationToken: continuationToken,
NextContinuationToken: continuationToken, NextContinuationToken: nextPageToken,
Prefixes: prefixes, Prefixes: prefixes,
Objects: objects, Objects: objects,
}, nil }, nil
@@ -761,22 +768,27 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
// Refer https://cloud.google.com/storage/docs/hashes-etags. Use CRC32C for ETag // Refer https://cloud.google.com/storage/docs/hashes-etags. Use CRC32C for ETag
metadata := make(map[string]string) metadata := make(map[string]string)
for k, v := range attrs.Metadata { for k, v := range attrs.Metadata {
k = http.CanonicalHeaderKey(k)
// Translate the GCS custom metadata prefix
if strings.HasPrefix(k, "X-Goog-Meta-") {
k = strings.Replace(k, "X-Goog-Meta-", "X-Amz-Meta-", 1)
}
metadata[k] = v metadata[k] = v
} }
if attrs.ContentType != "" { if attrs.ContentType != "" {
metadata["content-type"] = attrs.ContentType metadata["Content-Type"] = attrs.ContentType
} }
if attrs.ContentEncoding != "" { if attrs.ContentEncoding != "" {
metadata["content-encoding"] = attrs.ContentEncoding metadata["Content-Encoding"] = attrs.ContentEncoding
} }
if attrs.CacheControl != "" { if attrs.CacheControl != "" {
metadata["cache-control"] = attrs.CacheControl metadata["Cache-Control"] = attrs.CacheControl
} }
if attrs.ContentDisposition != "" { if attrs.ContentDisposition != "" {
metadata["content-disposition"] = attrs.ContentDisposition metadata["Content-Disposition"] = attrs.ContentDisposition
} }
if attrs.ContentLanguage != "" { if attrs.ContentLanguage != "" {
metadata["content-language"] = attrs.ContentLanguage metadata["Content-Language"] = attrs.ContentLanguage
} }
return minio.ObjectInfo{ return minio.ObjectInfo{
Name: attrs.Name, Name: attrs.Name,
@@ -792,21 +804,25 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
// applyMetadataToGCSAttrs applies metadata to a GCS ObjectAttrs instance // applyMetadataToGCSAttrs applies metadata to a GCS ObjectAttrs instance
func applyMetadataToGCSAttrs(metadata map[string]string, attrs *storage.ObjectAttrs) { func applyMetadataToGCSAttrs(metadata map[string]string, attrs *storage.ObjectAttrs) {
attrs.ContentType = metadata["content-type"]
attrs.ContentEncoding = metadata["content-encoding"]
attrs.CacheControl = metadata["cache-control"]
attrs.ContentDisposition = metadata["content-disposition"]
attrs.ContentLanguage = metadata["content-language"]
attrs.Metadata = make(map[string]string) attrs.Metadata = make(map[string]string)
for k, v := range metadata { for k, v := range metadata {
attrs.Metadata[k] = v k = http.CanonicalHeaderKey(k)
} switch {
// Filter metadata which is stored as a unique attribute case strings.HasPrefix(k, "X-Amz-Meta-"):
for _, key := range []string{ // Translate the S3 user-defined metadata prefix
"content-type", "content-encoding", "cache-control", "content-disposition", "content-language", k = strings.Replace(k, "X-Amz-Meta-", "x-goog-meta-", 1)
} { attrs.Metadata[k] = v
delete(attrs.Metadata, key) case k == "Content-Type":
attrs.ContentType = v
case k == "Content-Encoding":
attrs.ContentEncoding = v
case k == "Cache-Control":
attrs.CacheControl = v
case k == "Content-Disposition":
attrs.ContentDisposition = v
case k == "Content-Language":
attrs.ContentLanguage = v
}
} }
} }
@@ -1011,12 +1027,8 @@ func (l *gcsGateway) AbortMultipartUpload(ctx context.Context, bucket string, ke
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object // CompleteMultipartUpload completes ongoing multipart upload and finalizes object
// Note that there is a limit (currently 32) to the number of components that can // Note that there is a limit (currently 32) to the number of components that can
// be composed in a single operation. There is a limit (currently 1024) to the total // be composed in a single operation. There is a per-project rate limit (currently 200)
// number of components for a given composite object. This means you can append to // to the number of source objects you can compose per second.
// each object at most 1023 times. There is a per-project rate limit (currently 200)
// to the number of components you can compose per second. This rate counts both the
// components being appended to a composite object as well as the components being
// copied when the composite object of which they are a part is copied.
func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string, key string, uploadID string, uploadedParts []minio.CompletePart) (minio.ObjectInfo, error) { func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string, key string, uploadID string, uploadedParts []minio.CompletePart) (minio.ObjectInfo, error) {
meta := gcsMultipartMetaName(uploadID) meta := gcsMultipartMetaName(uploadID)
object := l.client.Bucket(bucket).Object(meta) object := l.client.Bucket(bucket).Object(meta)
@@ -1136,7 +1148,6 @@ func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string,
func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error { func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy) policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil { if err != nil {
// This should not happen.
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return gcsToObjectError(err, bucket) return gcsToObjectError(err, bucket)
} }
@@ -1192,7 +1203,6 @@ func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketP
func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) { func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
rules, err := l.client.Bucket(bucket).ACL().List(l.ctx) rules, err := l.client.Bucket(bucket).ACL().List(l.ctx)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return nil, gcsToObjectError(err, bucket) return nil, gcsToObjectError(err, bucket)
} }
@@ -1227,7 +1237,6 @@ func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*polic
// Return NoSuchBucketPolicy error, when policy is not set // Return NoSuchBucketPolicy error, when policy is not set
if len(actionSet) == 0 { if len(actionSet) == 0 {
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
return nil, gcsToObjectError(minio.BucketPolicyNotFound{}, bucket) return nil, gcsToObjectError(minio.BucketPolicyNotFound{}, bucket)
} }
@@ -1252,7 +1261,6 @@ func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*polic
func (l *gcsGateway) DeleteBucketPolicy(ctx context.Context, bucket string) error { func (l *gcsGateway) DeleteBucketPolicy(ctx context.Context, bucket string) error {
// This only removes the storage.AllUsers policies // This only removes the storage.AllUsers policies
if err := l.client.Bucket(bucket).ACL().Delete(l.ctx, storage.AllUsers); err != nil { if err := l.client.Bucket(bucket).ACL().Delete(l.ctx, storage.AllUsers); err != nil {
logger.LogIf(ctx, err)
return gcsToObjectError(err, bucket) return gcsToObjectError(err, bucket)
} }
+104
View File
@@ -23,7 +23,9 @@ import (
"path" "path"
"reflect" "reflect"
"testing" "testing"
"time"
"cloud.google.com/go/storage"
"google.golang.org/api/googleapi" "google.golang.org/api/googleapi"
miniogo "github.com/minio/minio-go" miniogo "github.com/minio/minio-go"
@@ -393,3 +395,105 @@ func TestGCSToObjectError(t *testing.T) {
} }
} }
} }
func TestS3MetaToGCSAttributes(t *testing.T) {
headers := map[string]string{
"accept-encoding": "gzip",
"content-encoding": "gzip",
"cache-control": "age: 3600",
"content-disposition": "dummy",
"content-type": "application/javascript",
"Content-Language": "en",
"X-Amz-Meta-Hdr": "value",
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
"X-Amz-Meta-X-Amz-Matdesc": "{}",
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
}
// Only X-Amz-Meta- prefixed entries will be returned in
// Metadata (without the prefix!)
expectedHeaders := map[string]string{
"x-goog-meta-Hdr": "value",
"x-goog-meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
"x-goog-meta-X-Amz-Matdesc": "{}",
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
}
attrs := storage.ObjectAttrs{}
applyMetadataToGCSAttrs(headers, &attrs)
if !reflect.DeepEqual(attrs.Metadata, expectedHeaders) {
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, attrs.Metadata)
}
if attrs.CacheControl != headers["cache-control"] {
t.Fatalf("Test failed with Cache-Control mistmatch, expected %s, got %s", headers["cache-control"], attrs.CacheControl)
}
if attrs.ContentDisposition != headers["content-disposition"] {
t.Fatalf("Test failed with Content-Disposition mistmatch, expected %s, got %s", headers["content-disposition"], attrs.ContentDisposition)
}
if attrs.ContentEncoding != headers["content-encoding"] {
t.Fatalf("Test failed with Content-Encoding mistmatch, expected %s, got %s", headers["content-encoding"], attrs.ContentEncoding)
}
if attrs.ContentLanguage != headers["Content-Language"] {
t.Fatalf("Test failed with Content-Language mistmatch, expected %s, got %s", headers["Content-Language"], attrs.ContentLanguage)
}
if attrs.ContentType != headers["content-type"] {
t.Fatalf("Test failed with Content-Type mistmatch, expected %s, got %s", headers["content-type"], attrs.ContentType)
}
}
func TestGCSAttrsToObjectInfo(t *testing.T) {
metadata := map[string]string{
"x-goog-meta-Hdr": "value",
"x-goog-meta-x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
"x-goog-meta-x-amz-matdesc": "{}",
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
}
expectedMeta := map[string]string{
"X-Amz-Meta-Hdr": "value",
"X-Amz-Meta-X_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
"X-Amz-Meta-X-Amz-Matdesc": "{}",
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
"Cache-Control": "max-age: 3600",
"Content-Disposition": "dummy",
"Content-Encoding": "gzip",
"Content-Language": "en",
"Content-Type": "application/javascript",
}
attrs := storage.ObjectAttrs{
Name: "test-obj",
Bucket: "test-bucket",
Updated: time.Now(),
Size: 123,
CRC32C: 45312398,
CacheControl: "max-age: 3600",
ContentDisposition: "dummy",
ContentEncoding: "gzip",
ContentLanguage: "en",
ContentType: "application/javascript",
Metadata: metadata,
}
expectedETag := minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C))
objInfo := fromGCSAttrsToObjectInfo(&attrs)
if !reflect.DeepEqual(objInfo.UserDefined, expectedMeta) {
t.Fatalf("Test failed, expected %#v, got %#v", expectedMeta, objInfo.UserDefined)
}
if objInfo.Name != attrs.Name {
t.Fatalf("Test failed with Name mistmatch, expected %s, got %s", attrs.Name, objInfo.Name)
}
if objInfo.Bucket != attrs.Bucket {
t.Fatalf("Test failed with Bucket mistmatch, expected %s, got %s", attrs.Bucket, objInfo.Bucket)
}
if objInfo.ModTime != attrs.Updated {
t.Fatalf("Test failed with ModTime mistmatch, expected %s, got %s", attrs.Updated, objInfo.ModTime)
}
if objInfo.Size != attrs.Size {
t.Fatalf("Test failed with Size mistmatch, expected %d, got %d", attrs.Size, objInfo.Size)
}
if objInfo.ETag != expectedETag {
t.Fatalf("Test failed with ETag mistmatch, expected %s, got %s", expectedETag, objInfo.ETag)
}
}
+20 -1
View File
@@ -347,6 +347,13 @@ func (t *tritonObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
dirName = path.Join(mantaRoot, bucket, pathDir) dirName = path.Join(mantaRoot, bucket, pathDir)
} }
if marker != "" {
// Manta uses the marker as the key to start at rather than start after
// A space is appended to the marker so that the corresponding object is not
// included in the results
marker += " "
}
input = &storage.ListDirectoryInput{ input = &storage.ListDirectoryInput{
DirectoryName: dirName, DirectoryName: dirName,
Limit: uint64(maxKeys), Limit: uint64(maxKeys),
@@ -419,6 +426,18 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
pathBase = path.Base(prefix) pathBase = path.Base(prefix)
) )
marker := continuationToken
if marker == "" {
marker = startAfter
}
if marker != "" {
// Manta uses the marker as the key to start at rather than start after.
// A space is appended to the marker so that the corresponding object is not
// included in the results
marker += " "
}
if pathDir := path.Dir(prefix); pathDir == "." { if pathDir := path.Dir(prefix); pathDir == "." {
dirName = path.Join(mantaRoot, bucket) dirName = path.Join(mantaRoot, bucket)
} else { } else {
@@ -428,7 +447,7 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
input = &storage.ListDirectoryInput{ input = &storage.ListDirectoryInput{
DirectoryName: dirName, DirectoryName: dirName,
Limit: uint64(maxKeys), Limit: uint64(maxKeys),
Marker: continuationToken, Marker: marker,
} }
objs, err = t.client.Dir().List(ctx, input) objs, err = t.client.Dir().List(ctx, input)
if err != nil { if err != nil {
+4 -2
View File
@@ -479,8 +479,11 @@ func ossListObjects(ctx context.Context, client *oss.Client, bucket, prefix, mar
// ossListObjectsV2 lists all blobs in OSS bucket filtered by prefix. // ossListObjectsV2 lists all blobs in OSS bucket filtered by prefix.
func ossListObjectsV2(ctx context.Context, client *oss.Client, bucket, prefix, continuationToken, delimiter string, maxKeys int, func ossListObjectsV2(ctx context.Context, client *oss.Client, bucket, prefix, continuationToken, delimiter string, maxKeys int,
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) { fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
// fetchOwner and startAfter are not supported and unused. // fetchOwner is not supported and unused.
marker := continuationToken marker := continuationToken
if marker == "" {
marker = startAfter
}
resultV1, err := ossListObjects(ctx, client, bucket, prefix, marker, delimiter, maxKeys) resultV1, err := ossListObjects(ctx, client, bucket, prefix, marker, delimiter, maxKeys)
if err != nil { if err != nil {
@@ -1018,7 +1021,6 @@ func (l *ossObjects) GetBucketPolicy(ctx context.Context, bucket string) (*polic
switch result.ACL { switch result.ACL {
case string(oss.ACLPrivate): case string(oss.ACLPrivate):
// By default, all buckets starts with a "private" policy. // By default, all buckets starts with a "private" policy.
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
return nil, ossToObjectError(minio.BucketPolicyNotFound{}, bucket) return nil, ossToObjectError(minio.BucketPolicyNotFound{}, bucket)
case string(oss.ACLPublicRead): case string(oss.ACLPublicRead):
readOnly = true readOnly = true
+30 -25
View File
@@ -20,7 +20,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"io" "io"
"math/rand"
"strings" "strings"
"time"
"github.com/minio/cli" "github.com/minio/cli"
miniogo "github.com/minio/minio-go" miniogo "github.com/minio/minio-go"
@@ -121,6 +123,31 @@ func (g *S3) Name() string {
return s3Backend return s3Backend
} }
const letterBytes = "abcdefghijklmnopqrstuvwxyz01234569"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
// randString generates random names and prepends them with a known prefix.
func randString(n int, src rand.Source, prefix string) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return prefix + string(b[0:30-len(prefix)])
}
// 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, accessKey, secretKey string) (*miniogo.Core, error) {
if url == "" { if url == "" {
@@ -137,13 +164,13 @@ func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bucket-sign-")
if _, err = clnt.BucketExists("probe-bucket-sign"); err != nil { if _, err = clnt.BucketExists(probeBucketName); err != nil {
clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure) clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err = clnt.BucketExists("probe-bucket-sign"); err != nil { if _, err = clnt.BucketExists(probeBucketName); err != nil {
return nil, err return nil, err
} }
} }
@@ -196,13 +223,11 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket, location
// access to these buckets. // access to these buckets.
// Ref - http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html // Ref - http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
if s3utils.CheckValidBucketName(bucket) != nil { if s3utils.CheckValidBucketName(bucket) != nil {
logger.LogIf(ctx, minio.BucketNameInvalid{Bucket: bucket})
return minio.BucketNameInvalid{Bucket: bucket} return minio.BucketNameInvalid{Bucket: bucket}
} }
err := l.Client.MakeBucket(bucket, location) err := l.Client.MakeBucket(bucket, location)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket) return minio.ErrorRespToObjectError(err, bucket)
} }
return err return err
@@ -212,7 +237,6 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket, location
func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) { func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) {
buckets, err := l.Client.ListBuckets() buckets, err := l.Client.ListBuckets()
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return bi, minio.ErrorRespToObjectError(err, bucket) return bi, minio.ErrorRespToObjectError(err, bucket)
} }
@@ -227,7 +251,6 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
}, nil }, nil
} }
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
return bi, minio.BucketNotFound{Bucket: bucket} return bi, minio.BucketNotFound{Bucket: bucket}
} }
@@ -235,7 +258,6 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) { func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
buckets, err := l.Client.ListBuckets() buckets, err := l.Client.ListBuckets()
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return nil, minio.ErrorRespToObjectError(err) return nil, minio.ErrorRespToObjectError(err)
} }
@@ -254,7 +276,6 @@ func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error)
func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error { func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
err := l.Client.RemoveBucket(bucket) err := l.Client.RemoveBucket(bucket)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket) return minio.ErrorRespToObjectError(err, bucket)
} }
return nil return nil
@@ -264,7 +285,6 @@ func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) { func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) {
result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys) result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return loi, minio.ErrorRespToObjectError(err, bucket) return loi, minio.ErrorRespToObjectError(err, bucket)
} }
@@ -275,7 +295,6 @@ func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix strin
func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) { func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter) result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return loi, minio.ErrorRespToObjectError(err, bucket) return loi, minio.ErrorRespToObjectError(err, bucket)
} }
@@ -290,7 +309,6 @@ func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
// length indicates the total length of the object. // length indicates the total length of the object.
func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string) error { func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string) error {
if length < 0 && length != -1 { if length < 0 && length != -1 {
logger.LogIf(ctx, minio.InvalidRange{})
return minio.ErrorRespToObjectError(minio.InvalidRange{}, bucket, key) return minio.ErrorRespToObjectError(minio.InvalidRange{}, bucket, key)
} }
@@ -303,7 +321,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
} }
object, _, err := l.Client.GetObject(bucket, key, opts) object, _, err := l.Client.GetObject(bucket, key, opts)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket, key) return minio.ErrorRespToObjectError(err, bucket, key)
} }
defer object.Close() defer object.Close()
@@ -319,7 +336,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string) (objInfo minio.ObjectInfo, err error) { func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string) (objInfo minio.ObjectInfo, err error) {
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{}) oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{})
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object) return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object)
} }
@@ -330,7 +346,6 @@ func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object str
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) { func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
oi, err := l.Client.PutObject(bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), minio.ToMinioClientMetadata(metadata)) oi, err := l.Client.PutObject(bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), minio.ToMinioClientMetadata(metadata))
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return objInfo, minio.ErrorRespToObjectError(err, bucket, object) return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
} }
@@ -346,7 +361,6 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE" srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil { if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil {
logger.LogIf(ctx, err)
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject) return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
} }
return l.GetObjectInfo(ctx, dstBucket, dstObject) return l.GetObjectInfo(ctx, dstBucket, dstObject)
@@ -356,7 +370,6 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
func (l *s3Objects) DeleteObject(ctx context.Context, bucket string, object string) error { func (l *s3Objects) DeleteObject(ctx context.Context, bucket string, object string) error {
err := l.Client.RemoveObject(bucket, object) err := l.Client.RemoveObject(bucket, object)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket, object) return minio.ErrorRespToObjectError(err, bucket, object)
} }
@@ -379,7 +392,6 @@ func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, objec
opts := miniogo.PutObjectOptions{UserMetadata: metadata} opts := miniogo.PutObjectOptions{UserMetadata: metadata}
uploadID, err = l.Client.NewMultipartUpload(bucket, object, opts) uploadID, err = l.Client.NewMultipartUpload(bucket, object, opts)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return uploadID, minio.ErrorRespToObjectError(err, bucket, object) return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
} }
return uploadID, nil return uploadID, nil
@@ -389,7 +401,6 @@ func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, objec
func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader) (pi minio.PartInfo, e error) { func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader) (pi minio.PartInfo, e error) {
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, data, data.Size(), data.MD5Base64String(), data.SHA256HexString()) info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, data, data.Size(), data.MD5Base64String(), data.SHA256HexString())
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return pi, minio.ErrorRespToObjectError(err, bucket, object) return pi, minio.ErrorRespToObjectError(err, bucket, object)
} }
@@ -407,7 +418,6 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject, completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject,
uploadID, partID, startOffset, length, srcInfo.UserDefined) uploadID, partID, startOffset, length, srcInfo.UserDefined)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject) return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
} }
p.PartNumber = completePart.PartNumber p.PartNumber = completePart.PartNumber
@@ -428,7 +438,6 @@ func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object s
// AbortMultipartUpload aborts a ongoing multipart upload // AbortMultipartUpload aborts a ongoing multipart upload
func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error { func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
err := l.Client.AbortMultipartUpload(bucket, object, uploadID) err := l.Client.AbortMultipartUpload(bucket, object, uploadID)
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket, object) return minio.ErrorRespToObjectError(err, bucket, object)
} }
@@ -436,7 +445,6 @@ func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, obj
func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart) (oi minio.ObjectInfo, e error) { func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart) (oi minio.ObjectInfo, e error) {
err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts)) err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts))
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return oi, minio.ErrorRespToObjectError(err, bucket, object) return oi, minio.ErrorRespToObjectError(err, bucket, object)
} }
@@ -453,7 +461,6 @@ func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPo
} }
if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil { if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil {
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket) return minio.ErrorRespToObjectError(err, bucket)
} }
@@ -464,7 +471,6 @@ func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPo
func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) { func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
data, err := l.Client.GetBucketPolicy(bucket) data, err := l.Client.GetBucketPolicy(bucket)
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return nil, minio.ErrorRespToObjectError(err, bucket) return nil, minio.ErrorRespToObjectError(err, bucket)
} }
@@ -475,7 +481,6 @@ func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy
// DeleteBucketPolicy deletes all policies on bucket // DeleteBucketPolicy deletes all policies on bucket
func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error { func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
if err := l.Client.SetBucketPolicy(bucket, ""); err != nil { if err := l.Client.SetBucketPolicy(bucket, ""); err != nil {
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket, "") return minio.ErrorRespToObjectError(err, bucket, "")
} }
return nil return nil
+1 -1
View File
@@ -19,7 +19,6 @@ package sia
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -40,6 +39,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/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/sha256-simd"
) )
const ( const (
+21 -3
View File
@@ -27,7 +27,6 @@ import (
"github.com/minio/minio-go/pkg/set" "github.com/minio/minio-go/pkg/set"
etcd "github.com/coreos/etcd/client"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/dns" "github.com/minio/minio/pkg/dns"
@@ -476,7 +475,6 @@ var notimplementedObjectResourceNames = map[string]bool{
"policy": true, "policy": true,
"tagging": true, "tagging": true,
"restore": true, "restore": true,
"select": true,
} }
// Resource handler ServeHTTP() wrapper // Resource handler ServeHTTP() wrapper
@@ -653,7 +651,7 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
} }
sr, err := globalDNSConfig.Get(bucket) sr, err := globalDNSConfig.Get(bucket)
if err != nil { if err != nil {
if etcd.IsKeyNotFound(err) || err == dns.ErrNoEntriesFound { if err == dns.ErrNoEntriesFound {
writeErrorResponse(w, ErrNoSuchBucket, r.URL) writeErrorResponse(w, ErrNoSuchBucket, r.URL)
} else { } else {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@@ -724,6 +722,26 @@ func (l rateLimit) ServeHTTP(w http.ResponseWriter, r *http.Request) {
l.handler.ServeHTTP(w, r) l.handler.ServeHTTP(w, r)
} }
// requestIDHeaderHandler sets x-amz-request-id header.
// Previously, this value was set right before a response
// was sent to the client.So, logger and Error response XML
// were not using this value.
// This is set here so that this header can be logged as
// part of the log entry and Error response XML.
type requestIDHeaderHandler struct {
handler http.Handler
}
func addrequestIDHeader(h http.Handler) http.Handler {
return requestIDHeaderHandler{handler: h}
}
func (s requestIDHeaderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set unique request ID for each response.
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
s.handler.ServeHTTP(w, r)
}
type securityHeaderHandler struct { type securityHeaderHandler struct {
handler http.Handler handler http.Handler
} }
+5 -3
View File
@@ -20,6 +20,8 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"testing" "testing"
"github.com/minio/minio/cmd/crypto"
) )
// Tests getRedirectLocation function for all its criteria. // Tests getRedirectLocation function for all its criteria.
@@ -153,15 +155,15 @@ var containsReservedMetadataTests = []struct {
header: http.Header{"X-Minio-Key": []string{"value"}}, header: http.Header{"X-Minio-Key": []string{"value"}},
}, },
{ {
header: http.Header{ServerSideEncryptionIV: []string{"iv"}}, header: http.Header{crypto.SSEIV: []string{"iv"}},
shouldFail: true, shouldFail: true,
}, },
{ {
header: http.Header{ServerSideEncryptionSealAlgorithm: []string{SSESealAlgorithmDareSha256}}, header: http.Header{crypto.SSESealAlgorithm: []string{SSESealAlgorithmDareSha256}},
shouldFail: true, shouldFail: true,
}, },
{ {
header: http.Header{ServerSideEncryptionSealedKey: []string{"mac"}}, header: http.Header{crypto.SSECSealedKey: []string{"mac"}},
shouldFail: true, shouldFail: true,
}, },
{ {
+80 -8
View File
@@ -18,15 +18,18 @@ package cmd
import ( import (
"crypto/x509" "crypto/x509"
"fmt"
"os" "os"
"runtime" "runtime"
"time" "time"
isatty "github.com/mattn/go-isatty"
"github.com/minio/minio-go/pkg/set" "github.com/minio/minio-go/pkg/set"
etcd "github.com/coreos/etcd/client" etcd "github.com/coreos/etcd/clientv3"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/minio/minio/cmd/crypto"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/certs" "github.com/minio/minio/pkg/certs"
@@ -124,6 +127,9 @@ var (
// Holds the host that was passed using --address // Holds the host that was passed using --address
globalMinioHost = "" globalMinioHost = ""
// globalConfigSys server config system.
globalConfigSys *ConfigSys
globalNotificationSys *NotificationSys globalNotificationSys *NotificationSys
globalPolicySys *PolicySys globalPolicySys *PolicySys
@@ -204,7 +210,7 @@ var (
globalRPCAPIVersion = RPCVersion{3, 0, 0} globalRPCAPIVersion = RPCVersion{3, 0, 0}
// Allocated etcd endpoint for config and bucket DNS. // Allocated etcd endpoint for config and bucket DNS.
globalEtcdClient etcd.Client globalEtcdClient *etcd.Client
// Allocated DNS config wrapper over etcd client. // Allocated DNS config wrapper over etcd client.
globalDNSConfig dns.Config globalDNSConfig dns.Config
@@ -214,17 +220,83 @@ var (
// Usage check interval value. // Usage check interval value.
globalUsageCheckInterval = globalDefaultUsageCheckInterval globalUsageCheckInterval = globalDefaultUsageCheckInterval
// KMS key id
globalKMSKeyID string
// Allocated KMS
globalKMS crypto.KMS
// KMS config
globalKMSConfig crypto.KMSConfig
// Add new variable global values here. // Add new variable global values here.
) )
// global colors. // global colors.
var ( var (
colorBold = color.New(color.Bold).SprintFunc() // Check if we stderr, stdout are dumb terminals, we do not apply
colorRed = color.New(color.FgRed).SprintfFunc() // ansi coloring on dumb terminals.
colorBlue = color.New(color.FgBlue).SprintfFunc() isTerminal = func() bool {
colorYellow = color.New(color.FgYellow).SprintfFunc() return isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stderr.Fd())
colorBgYellow = color.New(color.BgYellow).SprintfFunc() }
colorBlack = color.New(color.FgBlack).SprintfFunc()
colorBold = func() func(a ...interface{}) string {
if isTerminal() {
return color.New(color.Bold).SprintFunc()
}
return fmt.Sprint
}()
colorRed = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgRed).SprintfFunc()
}
return fmt.Sprintf
}()
colorBlue = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgBlue).SprintfFunc()
}
return fmt.Sprintf
}()
colorYellow = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgYellow).SprintfFunc()
}
return fmt.Sprintf
}()
colorCyanBold = func() func(a ...interface{}) string {
if isTerminal() {
color.New(color.FgCyan, color.Bold).SprintFunc()
}
return fmt.Sprint
}()
colorYellowBold = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgYellow, color.Bold).SprintfFunc()
}
return fmt.Sprintf
}()
colorBgYellow = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.BgYellow).SprintfFunc()
}
return fmt.Sprintf
}()
colorBlack = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgBlack).SprintfFunc()
}
return fmt.Sprintf
}()
colorGreenBold = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgGreen, color.Bold).SprintfFunc()
}
return fmt.Sprintf
}()
colorRedBold = func() func(format string, a ...interface{}) string {
if isTerminal() {
return color.New(color.FgRed, color.Bold).SprintfFunc()
}
return fmt.Sprintf
}()
) )
// Returns minio global information, as a key value map. // Returns minio global information, as a key value map.
+54 -25
View File
@@ -17,8 +17,10 @@
package cmd package cmd
import ( import (
"bytes"
"context" "context"
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"net" "net"
"net/http" "net/http"
@@ -26,6 +28,7 @@ import (
"strings" "strings"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/handlers"
httptracer "github.com/minio/minio/pkg/handlers" httptracer "github.com/minio/minio/pkg/handlers"
) )
@@ -113,40 +116,54 @@ var userMetadataKeyPrefixes = []string{
"X-Minio-Meta-", "X-Minio-Meta-",
} }
// extractMetadataFromHeader extracts metadata from HTTP header. // extractMetadata extracts metadata from HTTP header and HTTP queryString.
func extractMetadataFromHeader(ctx context.Context, header http.Header) (map[string]string, error) { func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
if header == nil { query := r.URL.Query()
logger.LogIf(ctx, errInvalidArgument) header := r.Header
return nil, errInvalidArgument metadata = make(map[string]string)
// Extract all query values.
err = extractMetadataFromMap(ctx, query, metadata)
if err != nil {
return nil, err
} }
metadata := make(map[string]string)
// Extract all header values.
err = extractMetadataFromMap(ctx, header, metadata)
if err != nil {
return nil, err
}
// Success.
return metadata, nil
}
// extractMetadata extracts metadata from map values.
func extractMetadataFromMap(ctx context.Context, v map[string][]string, m map[string]string) error {
if v == nil {
logger.LogIf(ctx, errInvalidArgument)
return errInvalidArgument
}
// Save all supported headers. // Save all supported headers.
for _, supportedHeader := range supportedHeaders { for _, supportedHeader := range supportedHeaders {
canonicalHeader := http.CanonicalHeaderKey(supportedHeader) if value, ok := v[http.CanonicalHeaderKey(supportedHeader)]; ok {
// HTTP headers are case insensitive, look for both canonical m[supportedHeader] = value[0]
// and non canonical entries. } else if value, ok := v[supportedHeader]; ok {
if _, ok := header[canonicalHeader]; ok { m[supportedHeader] = value[0]
metadata[supportedHeader] = header.Get(canonicalHeader)
} else if _, ok := header[supportedHeader]; ok {
metadata[supportedHeader] = header.Get(supportedHeader)
} }
} }
for key := range v {
// Go through all other headers for any additional headers that needs to be saved.
for key := range header {
if key != http.CanonicalHeaderKey(key) {
logger.LogIf(ctx, errInvalidArgument)
return nil, errInvalidArgument
}
for _, prefix := range userMetadataKeyPrefixes { for _, prefix := range userMetadataKeyPrefixes {
if strings.HasPrefix(key, prefix) { if !strings.HasPrefix(strings.ToLower(key), strings.ToLower(prefix)) {
metadata[key] = header.Get(key) continue
}
value, ok := v[key]
if ok {
m[key] = strings.Join(value, ",")
break break
} }
} }
} }
return metadata, nil return nil
} }
// The Query string for the redirect URL the client is // The Query string for the redirect URL the client is
@@ -167,7 +184,7 @@ func extractReqParams(r *http.Request) map[string]string {
// Success. // Success.
return map[string]string{ return map[string]string{
"sourceIPAddress": r.RemoteAddr, "sourceIPAddress": handlers.GetSourceIP(r),
// Add more fields here. // Add more fields here.
} }
} }
@@ -220,6 +237,19 @@ func extractPostPolicyFormValues(ctx context.Context, form *multipart.Form) (fil
return nil, "", 0, nil, err return nil, "", 0, nil, err
} }
// this means that filename="" was not specified for file key and Go has
// an ugly way of handling this situation. Refer here
// https://golang.org/src/mime/multipart/formdata.go#L61
if len(form.File) == 0 {
var b = &bytes.Buffer{}
for _, v := range formValues["File"] {
b.WriteString(v)
}
fileSize = int64(b.Len())
filePart = ioutil.NopCloser(b)
return filePart, fileName, fileSize, formValues, nil
}
// Iterator until we find a valid File field and break // Iterator until we find a valid File field and break
for k, v := range form.File { for k, v := range form.File {
canonicalFormName := http.CanonicalHeaderKey(k) canonicalFormName := http.CanonicalHeaderKey(k)
@@ -254,7 +284,6 @@ func extractPostPolicyFormValues(ctx context.Context, form *multipart.Form) (fil
break break
} }
} }
return filePart, fileName, fileSize, formValues, nil return filePart, fileName, fileSize, formValues, nil
} }

Some files were not shown because too many files have changed in this diff Show More