diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d46b935ef8551c032ebe3610432c0217fe1803f..f1155cf743510f0e7e276497f5150d58fb5c8735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +- support presignedPostPolicy + ## 0.1.3 - support presignedGetObject and presignedPutObject diff --git a/README.md b/README.md index cc0c02b08b19f8ed0b4cce50b0ba71274eb03782..9454ad94774ef4902b8cdd4390e19d6f0a08c02e 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ This is the _unofficial_ MinIO Dart Client SDK that provides simple APIs to acce ## API -| Bucket operations | Object operations | Presigned operations | Bucket Policy & Notification operations | -|-------------------------|-------------------------|-----------------------|-------------------------------------------| -| [makeBucket] | [getObject] | [presignedUrl] | getBucketNotification | -| [listBuckets] | [getPartialObject] | [presignedGetObject] | setBucketNotification | -| [bucketExists] | [fGetObject] | [presignedPutObject] | removeAllBucketNotification | -| [removeBucket] | [putObject] | presignedPostPolicy | getBucketPolicy | -| [listObjects] | [fPutObject] | | setBucketPolicy | -| [listObjectsV2] | [copyObject] | | listenBucketNotification | -| [listIncompleteUploads] | [statObject] | | | -| | [removeObject] | | | -| | [removeObjects] | | | -| | [removeIncompleteUpload]| | | +| Bucket operations | Object operations | Presigned operations | Bucket Policy & Notification operations | +|-------------------------|--------------------------|----------------------|-----------------------------------------| +| [makeBucket] | [getObject] | [presignedUrl] | getBucketNotification | +| [listBuckets] | [getPartialObject] | [presignedGetObject] | setBucketNotification | +| [bucketExists] | [fGetObject] | [presignedPutObject] | removeAllBucketNotification | +| [removeBucket] | [putObject] | [presignedPostPolicy]| listenBucketNotification | +| [listObjects] | [fPutObject] | | getBucketPolicy | +| [listObjectsV2] | [copyObject] | | setBucketPolicy | +| [listIncompleteUploads] | [statObject] | | | +| | [removeObject] | | | +| | [removeObjects] | | | +| | [removeIncompleteUpload] | | | ## Usage @@ -95,4 +95,5 @@ MIT [presignedUrl]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedUrl.html [presignedGetObject]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedGetObject.html [presignedPutObject]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedPutObject.html +[presignedPostPolicy]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedPostPolicy.html diff --git a/lib/src/minio.dart b/lib/src/minio.dart index b2baeaa4ab17e619d79418e97108f11120b9699f..0772893058046c6261f725ef28a49634859cd742 100644 --- a/lib/src/minio.dart +++ b/lib/src/minio.dart @@ -641,6 +641,60 @@ class Minio { ); } + /// presignedPostPolicy can be used in situations where we want more control on the upload than what + /// presignedPutObject() provides. i.e Using presignedPostPolicy we will be able to put policy restrictions + /// on the object's `name` `bucket` `expiry` `Content-Type` + Future presignedPostPolicy(PostPolicy postPolicy) async { + if (_client.anonymous) { + throw MinioAnonymousRequestError( + 'Presigned POST policy cannot be generated for anonymous requests'); + } + + final region = await getBucketRegion(postPolicy.formData['bucket']); + var date = DateTime.now().toUtc(); + var dateStr = makeDateLong(date); + + if (postPolicy.policy['expiration'] == null) { + // 'expiration' is mandatory field for S3. + // Set default expiration date of 7 days. + var expires = DateTime.now().toUtc(); + expires.add(Duration(days: 7)); + postPolicy.setExpires(expires); + } + + postPolicy.policy['conditions'].push(['eq', r'$x-amz-date', dateStr]); + postPolicy.formData['x-amz-date'] = dateStr; + + postPolicy.policy['conditions'] + .push(['eq', r'$x-amz-algorithm', 'AWS4-HMAC-SHA256']); + postPolicy.formData['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'; + + postPolicy.policy['conditions'].push( + ['eq', r'$x-amz-credential', accessKey + '/' + getScope(region, date)]); + postPolicy.formData['x-amz-credential'] = + accessKey + '/' + getScope(region, date); + + if (sessionToken != null) { + postPolicy.policy['conditions'] + .push(['eq', r'$x-amz-security-token', sessionToken]); + } + + final policyBase64 = jsonBase64(postPolicy.policy); + postPolicy.formData['policy'] = policyBase64; + + final signature = + postPresignSignatureV4(region, date, secretKey, policyBase64); + + postPolicy.formData['x-amz-signature'] = signature; + final url = _client + .getBaseRequest('POST', postPolicy.formData['bucket'], null, region, + null, null, null) + .url; + var portStr = (port == 80 || port == 443 || port == null) ? '' : ':$port'; + var urlStr = '${url.scheme}://${url.host}${portStr}${url.path}'; + return PostPolicyResult(postURL: urlStr, formData: postPolicy.formData); + } + /// Generate a presigned URL for PUT. /// Using this URL, the browser can upload to S3 only with the specified object name. /// diff --git a/lib/src/minio_models.dart b/lib/src/minio_models.dart index a1e27adc6b12e282f2a785d34a20fdf58613160c..382e6dac985941aeea13dba3b48fe59e2b6626af 100644 --- a/lib/src/minio_models.dart +++ b/lib/src/minio_models.dart @@ -1,4 +1,5 @@ import 'package:minio/models.dart'; +import 'package:minio/src/minio_errors.dart'; import 'package:xml/xml.dart'; class ListObjectsChunk { @@ -113,3 +114,71 @@ class StatObjectResult { final DateTime lastModified; final Map<String, String> metaData; } + +/// Build PostPolicy object that can be signed by presignedPostPolicy +class PostPolicy { + final policy = <String, dynamic>{ + 'conditions': [], + }; + + final formData = <String, String>{}; + + /// set expiration date + void setExpires(DateTime date) { + if (date == null) { + throw MinioInvalidDateError('Invalid date : cannot be null'); + } + policy['expiration'] = date.toIso8601String(); + } + + /// set object name + void setKey(String object) { + MinioInvalidObjectNameError.check(object); + policy['conditions'].add(['eq', r'$key', object]); + formData['key'] = object; + } + + /// set object name prefix, i.e policy allows any keys with this prefix + void setKeyStartsWith(String prefix) { + MinioInvalidPrefixError.check(prefix); + policy['conditions'].push(['starts-with', r'$key', prefix]); + formData['key'] = prefix; + } + + /// set bucket name + void setBucket(bucket) { + MinioInvalidBucketNameError.check(bucket); + policy['conditions'].push(['eq', r'$bucket', bucket]); + formData['bucket'] = bucket; + } + + /// set Content-Type + void setContentType(String type) { + if (type == null) { + throw MinioError('content-type cannot be null'); + } + policy['conditions'].push(['eq', r'$Content-Type', type]); + formData['Content-Type'] = type; + } + + /// set minimum/maximum length of what Content-Length can be. + void setContentLengthRange(int min, int max) { + if (min > max) { + throw MinioError('min cannot be more than max'); + } + if (min < 0) { + throw MinioError('min should be > 0'); + } + if (max < 0) { + throw MinioError('max should be > 0'); + } + policy['conditions'].push(['content-length-range', min, max]); + } +} + +class PostPolicyResult { + PostPolicyResult({this.postURL, this.formData}); + + final String postURL; + final Map<String, String> formData; +} diff --git a/lib/src/minio_sign.dart b/lib/src/minio_sign.dart index c2313d6ea0b94d681f275fb30f90212a19c3af33..fee400cc21e89a3403460989de53fb0a96a7629e 100644 --- a/lib/src/minio_sign.dart +++ b/lib/src/minio_sign.dart @@ -146,3 +146,14 @@ String presignSignatureV4( return presignedUrl; } + +// calculate the signature of the POST policy +String postPresignSignatureV4( + String region, + DateTime date, + String secretKey, + String policyBase64, +) { + final signingKey = getSigningKey(date, region, secretKey); + return sha256HmacHex(policyBase64, signingKey); +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 8b586e3a5e58f5e1175faa1404dcf80d893c550c..f52f794e179a4ed24812c8db44f30628e93ac55e 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -31,6 +31,10 @@ String md5Base64(String source) { return base64.encode(md5digest); } +String jsonBase64(Map<String, dynamic> jsonObject) { + return base64.encode(utf8.encode(json.encode(jsonObject))); +} + XmlElement getNodeProp(XmlElement xml, String name) { final result = xml.findElements(name); return result.isNotEmpty ? result.first : null; diff --git a/pubspec.yaml b/pubspec.yaml index a7b8128f303115f68dae345ef40aeeae736f616f..1d2accc84f39945a3e789b4d11159c2479e93afa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: minio description: Unofficial MinIO Dart Client SDK that provides simple APIs to access any Amazon S3 compatible object storage server. -version: 0.1.3 +version: 0.1.4 homepage: https://github.com/xtyxtyx/minio-dart issue_tracker: https://github.com/xtyxtyx/minio-dart/issues