From cbfda8fa6c969e07a26e743a9f2da12680d60fa2 Mon Sep 17 00:00:00 2001
From: xuty <xty50337@hotmail.com>
Date: Tue, 31 Mar 2020 10:52:25 +0800
Subject: [PATCH] finish presignedUrl

---
 CHANGELOG.md               |  4 +++
 README.md                  |  5 +++-
 example/minio_example.dart |  4 +++
 lib/src/minio.dart         | 33 +++++++++++++++++++++
 lib/src/minio_client.dart  | 46 ++++++++++++++++++++++-------
 lib/src/minio_sign.dart    | 59 ++++++++++++++++++++++++++++++++++++--
 lib/src/utils.dart         |  6 ++++
 pubspec.yaml               |  2 +-
 8 files changed, 144 insertions(+), 15 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c36d42..3ce396a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.2
+
+- support presignedUrl
+
 ## 0.1.1
 
 - update dependency
diff --git a/README.md b/README.md
index 492b757..0d2e8ee 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ This is the _unofficial_ MinIO Dart Client SDK that provides simple APIs to acce
 
 | Bucket operations     	| Object operations      	| Presigned operations 	| Bucket Policy & Notification operations 	|
 |-------------------------|-------------------------|-----------------------|-------------------------------------------|
-| [makeBucket]           	| [getObject]            	| presignedUrl         	| getBucketNotification                   	|
+| [makeBucket]           	| [getObject]            	| [presignedUrl]       	| getBucketNotification                   	|
 | [listBuckets]          	| [getPartialObject]     	| presignedGetObject   	| setBucketNotification                   	|
 | [bucketExists]         	| [fGetObject]           	| presignedPutObject   	| removeAllBucketNotification             	|
 | [removeBucket]         	| [putObject]            	| presignedPostPolicy  	| getBucketPolicy                         	|
@@ -91,3 +91,6 @@ MIT
 
 [fGetObject]: https://pub.dev/documentation/minio/latest/io/MinioX/fGetObject.html
 [fPutObject]: https://pub.dev/documentation/minio/latest/io/MinioX/fPutObject.html
+
+[presignedUrl]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedUrl.html
+
diff --git a/example/minio_example.dart b/example/minio_example.dart
index 345e8ae..8849e3f 100644
--- a/example/minio_example.dart
+++ b/example/minio_example.dart
@@ -30,6 +30,10 @@ void main() async {
   print('--- etag:');
   print(etag);
 
+  final url = await minio.presignedUrl('GET', bucket, object, expires: 1000);
+  print('--- presigned url:');
+  print(url);
+
   final copyResult1 = await minio.copyObject(bucket, copy1, '$bucket/$object');
   final copyResult2 = await minio.copyObject(bucket, copy2, '$bucket/$object');
   print('--- copy1 etag:');
diff --git a/lib/src/minio.dart b/lib/src/minio.dart
index 6726870..328d3a5 100644
--- a/lib/src/minio.dart
+++ b/lib/src/minio.dart
@@ -3,6 +3,7 @@ import 'package:minio/models.dart';
 import 'package:minio/src/minio_client.dart';
 import 'package:minio/src/minio_errors.dart';
 import 'package:minio/src/minio_helpers.dart';
+import 'package:minio/src/minio_sign.dart';
 import 'package:minio/src/minio_uploader.dart';
 import 'package:minio/src/utils.dart';
 import 'package:xml/xml.dart' as xml;
@@ -613,6 +614,38 @@ class Minio {
     return resp.body;
   }
 
+  /// Generate a generic presigned URL which can be
+  /// used for HTTP methods GET, PUT, HEAD and DELETE
+  ///
+  /// - [method]: name of the HTTP method
+  /// - [bucketName]: name of the bucket
+  /// - [objectName]: name of the object
+  /// - [expiry]: expiry in seconds (optional, default 7 days)
+  /// - [reqParams]: request parameters (optional)
+  /// - [requestDate]: A date object, the url will be issued at (optional)
+  Future<String> presignedUrl(
+    String method,
+    String bucket,
+    String object, {
+    int expires,
+    String resource,
+    Map<String, String> reqParams,
+    DateTime requestDate,
+  }) async {
+    MinioInvalidBucketNameError.check(bucket);
+    MinioInvalidObjectNameError.check(object);
+
+    assert(expires == null || expires >= 0);
+
+    expires ??= expires = 24 * 60 * 60 * 7; // 7 days in seconds
+    reqParams ??= {};
+    requestDate ??= DateTime.now().toUtc();
+
+    final region = await getBucketRegion(bucket);
+    final request = _client.getBaseRequest(method, bucket, object, region, resource, reqParams, {});
+    return presignSignatureV4(this, request, region, requestDate, expires);
+  }
+
   /// Uploads the object.
   Future<String> putObject(
     String bucket,
diff --git a/lib/src/minio_client.dart b/lib/src/minio_client.dart
index ee84cf8..2aebd12 100644
--- a/lib/src/minio_client.dart
+++ b/lib/src/minio_client.dart
@@ -26,6 +26,18 @@ class MinioRequest extends BaseRequest {
     }
     throw UnsupportedError('unsupported body type: ${body.runtimeType}');
   }
+
+  MinioRequest replace({
+    String method,
+    Uri url,
+    Map<String, String> headers,
+    body,
+  }) {
+    final result = MinioRequest(method ?? this.method, url ?? this.url);
+    result.body = body ?? this.body;
+    result.headers.addAll(headers ?? this.headers);
+    return result;
+  }
 }
 
 class MinioClient {
@@ -52,26 +64,20 @@ class MinioClient {
     Map<String, String> queries,
     Map<String, String> headers,
   }) async {
-    final url = getRequestUrl(bucket, object, resource, queries);
-    final request = MinioRequest(method, url);
-    final date = DateTime.now().toUtc();
-    final sha256sum = enableSHA256 ? sha256Hex(payload) : 'UNSIGNED-PAYLOAD';
-
     region ??= await minio.getBucketRegion(bucket);
 
+    final request = getBaseRequest(
+        method, bucket, object, region, resource, queries, headers);
     request.body = payload;
 
+    final date = DateTime.now().toUtc();
+    final sha256sum = enableSHA256 ? sha256Hex(payload) : 'UNSIGNED-PAYLOAD';
     request.headers.addAll({
-      'host': url.host,
       'user-agent': userAgent,
       'x-amz-date': makeDateLong(date),
       'x-amz-content-sha256': sha256sum,
     });
 
-    if (headers != null) {
-      request.headers.addAll(headers);
-    }
-
     final authorization = signV4(minio, request, date, 'us-east-1');
     request.headers['authorization'] = authorization;
 
@@ -132,6 +138,26 @@ class MinioClient {
     return response;
   }
 
+  MinioRequest getBaseRequest(
+    String method,
+    String bucket,
+    String object,
+    String region,
+    String resource,
+    Map<String, String> queries,
+    Map<String, String> headers,
+  ) {
+    final url = getRequestUrl(bucket, object, resource, queries);
+    final request = MinioRequest(method, url);
+    request.headers['host'] = url.host;
+
+    if (headers != null) {
+      request.headers.addAll(headers);
+    }
+
+    return request;
+  }
+
   Uri getRequestUrl(
     String bucket,
     String object,
diff --git a/lib/src/minio_sign.dart b/lib/src/minio_sign.dart
index f79fd7c..c2313d6 100644
--- a/lib/src/minio_sign.dart
+++ b/lib/src/minio_sign.dart
@@ -2,6 +2,7 @@ import 'package:convert/convert.dart';
 import 'package:crypto/crypto.dart';
 import 'package:minio/minio.dart';
 import 'package:minio/src/minio_client.dart';
+import 'package:minio/src/minio_errors.dart';
 import 'package:minio/src/minio_helpers.dart';
 import 'package:minio/src/utils.dart';
 
@@ -14,7 +15,9 @@ String signV4(
   String region,
 ) {
   final signedHeaders = getSignedHeaders(request.headers.keys);
-  final canonicalRequest = getCanonicalRequest(request, signedHeaders);
+  final hashedPayload = request.headers['x-amz-content-sha256'];
+  final canonicalRequest =
+      getCanonicalRequest(request, signedHeaders, hashedPayload);
   final stringToSign = getStringToSign(canonicalRequest, requestDate, region);
   final signingKey = getSigningKey(requestDate, region, minio.secretKey);
   final credential = getCredential(minio.accessKey, region, requestDate);
@@ -36,8 +39,11 @@ List<String> getSignedHeaders(Iterable<String> headers) {
   return result;
 }
 
-String getCanonicalRequest(MinioRequest request, List<String> signedHeaders) {
-  final hashedPayload = request.headers['x-amz-content-sha256'];
+String getCanonicalRequest(
+  MinioRequest request,
+  List<String> signedHeaders,
+  String hashedPayload,
+) {
   final requestResource = request.url.path;
   final headers = signedHeaders.map(
     (header) => '${header.toLowerCase()}:${request.headers[header]}',
@@ -93,3 +99,50 @@ List<int> getSigningKey(DateTime date, String region, String secretKey) {
 String getCredential(String accessKey, String region, DateTime requestDate) {
   return '$accessKey/${getScope(region, requestDate)}';
 }
+
+// returns a presigned URL string
+String presignSignatureV4(
+  Minio minio,
+  MinioRequest request,
+  String region,
+  DateTime requestDate,
+  int expires,
+) {
+  if (expires < 1) {
+    throw MinioExpiresParamError('expires param cannot be less than 1 seconds');
+  }
+  if (expires > 604800) {
+    throw MinioExpiresParamError('expires param cannot be greater than 7 days');
+  }
+
+  final iso8601Date = makeDateLong(requestDate);
+  final signedHeaders = getSignedHeaders(request.headers.keys);
+  final credential = getCredential(minio.accessKey, region, requestDate);
+
+  final requestQuery = <String, String>{};
+  requestQuery['X-Amz-Algorithm'] = signV4Algorithm;
+  requestQuery['X-Amz-Credential'] = credential;
+  requestQuery['X-Amz-Date'] = iso8601Date;
+  requestQuery['X-Amz-Expires'] = expires.toString();
+  requestQuery['X-Amz-SignedHeaders'] = signedHeaders.join(';').toLowerCase();
+  if (minio.sessionToken != null) {
+    requestQuery['X-Amz-Security-Token'] = minio.sessionToken;
+  }
+
+  request = request.replace(
+    url: request.url.replace(queryParameters: {
+      ...request.url.queryParameters,
+      ...requestQuery,
+    }),
+  );
+
+  final canonicalRequest =
+      getCanonicalRequest(request, signedHeaders, 'UNSIGNED-PAYLOAD');
+
+  final stringToSign = getStringToSign(canonicalRequest, requestDate, region);
+  final signingKey = getSigningKey(requestDate, region, minio.secretKey);
+  final signature = sha256HmacHex(stringToSign, signingKey);
+  final presignedUrl = request.url.toString() + '&X-Amz-Signature=${signature}';
+
+  return presignedUrl;
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index d62d422..8b586e3 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -20,6 +20,12 @@ String sha256Hex(Object data) {
   return hex.encode(sha256.convert(data).bytes);
 }
 
+String sha256HmacHex(String data, List<int> signingKey) {
+  return hex
+      .encode(Hmac(sha256, signingKey).convert(utf8.encode(data)).bytes)
+      .toLowerCase();
+}
+
 String md5Base64(String source) {
   final md5digest = md5.convert(utf8.encode(source)).bytes;
   return base64.encode(md5digest);
diff --git a/pubspec.yaml b/pubspec.yaml
index ac5e578..3f8b0b4 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.1
+version: 0.1.2
 homepage: https://github.com/xtyxtyx/minio-dart
 issue_tracker: https://github.com/xtyxtyx/minio-dart/issues
 
-- 
GitLab