diff --git a/README.md b/README.md index 87b0c639..303819e0 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ The complete API Reference is available here: ### Presigned Operations +- [`presignedUrl`](https://min.io/docs/minio/linux/developers/javascript/API.html#presignedUrl) - [`presignedGetObject`](https://min.io/docs/minio/linux/developers/javascript/API.html#presignedGetObject) - [`presignedPutObject`](https://min.io/docs/minio/linux/developers/javascript/API.html#presignedPutObject) - [`presignedPostPolicy`](https://min.io/docs/minio/linux/developers/javascript/API.html#presignedPostPolicy) @@ -242,9 +243,9 @@ The complete API Reference is available here: #### Presigned Operations -- [presigned-getobject.js](https://github.com/minio/minio-js/blob/master/examples/presigned-getobject.js) -- [presigned-putobject.js](https://github.com/minio/minio-js/blob/master/examples/presigned-putobject.js) -- [presigned-postpolicy.js](https://github.com/minio/minio-js/blob/master/examples/presigned-postpolicy.js) +- [presigned-getobject.mjs](https://github.com/minio/minio-js/blob/master/examples/presigned-getobject.js) +- [presigned-putobject.mjs](https://github.com/minio/minio-js/blob/master/examples/presigned-putobject.js) +- [presigned-postpolicy.mjs](https://github.com/minio/minio-js/blob/master/examples/presigned-postpolicy.js) #### Bucket Notification Operations diff --git a/README_zh_CN.md b/README_zh_CN.md index f8f2b8f8..fac0b193 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -173,9 +173,9 @@ mc ls play/europetrip/ * [stat-object.mjs](https://github.com/minio/minio-js/blob/master/examples/stat-object.mjs) #### 完整示例 : Presigned操作 -* [presigned-getobject.js](https://github.com/minio/minio-js/blob/master/examples/presigned-getobject.js) -* [presigned-putobject.js](https://github.com/minio/minio-js/blob/master/examples/presigned-putobject.js) -* [presigned-postpolicy.js](https://github.com/minio/minio-js/blob/master/examples/presigned-postpolicy.js) +* [presigned-getobject.mjs](https://github.com/minio/minio-js/blob/master/examples/presigned-getobject.js) +* [presigned-putobject.mjs](https://github.com/minio/minio-js/blob/master/examples/presigned-putobject.js) +* [presigned-postpolicy.mjs](https://github.com/minio/minio-js/blob/master/examples/presigned-postpolicy.js) #### 完整示例 : 存储桶通知 * [get-bucket-notification.js](https://github.com/minio/minio-js/blob/master/examples/get-bucket-notification.js) diff --git a/docs/API.md b/docs/API.md index b95f3909..55788e84 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1634,30 +1634,27 @@ Presigned URLs are generated for temporary download/upload access to private obj -### presignedUrl(httpMethod, bucketName, objectName[, expiry, reqParams, requestDate, cb]) +### presignedUrl(httpMethod, bucketName, objectName[, expiry, reqParams, requestDate]) Generates a presigned URL for the provided HTTP method, 'httpMethod'. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. **Parameters** -| Param | Type | Description | -| ----------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bucketName` | _string_ | Name of the bucket. | -| `objectName` | _string_ | Name of the object. | -| `expiry` | _number_ | Expiry time in seconds. Default value is 7 days. (optional) | -| `reqParams` | _object_ | request parameters. (optional) e.g {versionId:"10fa9946-3f64-4137-a58f-888065c0732e"} | -| `requestDate` | _Date_ | A date object, the url will be issued at. Default value is now. (optional) | -| `callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be downloaded using GET request. If no callback is passed, a `Promise` is returned. | +| Param | Type | Description | +| ------------- | -------- | ------------------------------------------------------------------------------------- | +| `bucketName` | _string_ | Name of the bucket. | +| `objectName` | _string_ | Name of the object. | +| `expiry` | _number_ | Expiry time in seconds. Default value is 7 days. (optional) | +| `reqParams` | _object_ | request parameters. (optional) e.g {versionId:"10fa9946-3f64-4137-a58f-888065c0732e"} | +| `requestDate` | _Date_ | A date object, the url will be issued at. Default value is now. (optional) | **Example1** ```js // presigned url for 'getObject' method. // expires in a day. -minioClient.presignedUrl('GET', 'mybucket', 'hello.txt', 24 * 60 * 60, function (err, presignedUrl) { - if (err) return console.log(err) - console.log(presignedUrl) -}) +const presignedUrl = await minioClient.presignedUrl('GET', 'mybucket', 'hello.txt', 24 * 60 * 60) +console.log(presignedUrl) ``` **Example2** @@ -1666,100 +1663,73 @@ minioClient.presignedUrl('GET', 'mybucket', 'hello.txt', 24 * 60 * 60, function // presigned url for 'listObject' method. // Lists objects in 'myBucket' with prefix 'data'. // Lists max 1000 of them. -minioClient.presignedUrl( - 'GET', - 'mybucket', - '', - 1000, - { prefix: 'data', 'max-keys': 1000 }, - function (err, presignedUrl) { - if (err) return console.log(err) - console.log(presignedUrl) - }, -) +await minioClient.presignedUrl('GET', 'mybucket', '', 1000, { prefix: 'data', 'max-keys': 1000 }) ``` **Example 3** ```js // Get Object with versionid -minioClient.presignedUrl( - 'GET', - 'mybucket', - '', - 1000, - { versionId: '10fa9946-3f64-4137-a58f-888065c0732e' }, - function (err, presignedUrl) { - if (err) return console.log(err) - console.log(presignedUrl) - }, -) +await minioClient.presignedUrl('GET', 'mybucket', '', 1000, { versionId: '10fa9946-3f64-4137-a58f-888065c0732e' }) ``` -### presignedGetObject(bucketName, objectName[, expiry, respHeaders, requestDate, cb]) +### presignedGetObject(bucketName, objectName[, expiry, respHeaders, requestDate]) Generates a presigned URL for HTTP GET operations. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. **Parameters** -| Param | Type | Description | -| ----------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bucketName` | _string_ | Name of the bucket. | -| `objectName` | _string_ | Name of the object. | -| `expiry` | _number_ | Expiry time in seconds. Default value is 7 days. (optional) | -| `respHeaders` | _object_ | response headers to override (optional) | -| `requestDate` | _Date_ | A date object, the url will be issued at. Default value is now. (optional) | -| `callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be downloaded using GET request. If no callback is passed, a `Promise` is returned. | +| Param | Type | Description | +| ------------- | -------- | -------------------------------------------------------------------------- | +| `bucketName` | _string_ | Name of the bucket. | +| `objectName` | _string_ | Name of the object. | +| `expiry` | _number_ | Expiry time in seconds. Default value is 7 days. (optional) | +| `respHeaders` | _object_ | response headers to override (optional) | +| `requestDate` | _Date_ | A date object, the url will be issued at. Default value is now. (optional) | **Example** ```js // expires in a day. -minioClient.presignedGetObject('mybucket', 'hello.txt', 24 * 60 * 60, function (err, presignedUrl) { - if (err) return console.log(err) - console.log(presignedUrl) -}) +const presignedUrl = await minioClient.presignedGetObject('mybucket', 'hello.txt', 24 * 60 * 60) +console.log(presignedUrl) ``` -### presignedPutObject(bucketName, objectName, expiry[, callback]) +### presignedPutObject(bucketName, objectName [,expiry]) Generates a presigned URL for HTTP PUT operations. Browsers/Mobile clients may point to this URL to upload objects directly to a bucket even if it is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. **Parameters** -| Param | Type | Description | -| ----------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bucketName` | _string_ | Name of the bucket. | -| `objectName` | _string_ | Name of the object. | -| `expiry` | _number_ | Expiry time in seconds. Default value is 7 days. | -| `callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be uploaded using PUT request. If no callback is passed, a `Promise` is returned. | +| Param | Type | Description | +| ------------ | -------- | ------------------------------------------------ | +| `bucketName` | _string_ | Name of the bucket. | +| `objectName` | _string_ | Name of the object. | +| `expiry` | _number_ | Expiry time in seconds. Default value is 7 days. | **Example** ```js // expires in a day. -minioClient.presignedPutObject('mybucket', 'hello.txt', 24 * 60 * 60, function (err, presignedUrl) { - if (err) return console.log(err) - console.log(presignedUrl) -}) +const presignedUrl = await minioClient.presignedPutObject('mybucket', 'hello.txt', 24 * 60 * 60) +console.log(presignedUrl) ``` -### presignedPostPolicy(policy[, callback]) +### presignedPostPolicy(policy) Allows setting policy conditions to a presigned URL for POST operations. Policies such as bucket name to receive object uploads, key name prefixes, expiry policy may be set. **Parameters** -| Param | Type | Description | -| ------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `policy` | _object_ | Policy object created by minioClient.newPostPolicy() | -| `callback(err, {postURL, formData})` | _function_ | Callback function is called with non `null` err value in case of error. `postURL` will be the URL using which the object can be uploaded using POST request. `formData` is the object having key/value pairs for the Form data of POST body. If no callback is passed, a `Promise` is returned. | +| Param | Type | Description | +| -------- | -------- | ---------------------------------------------------- | +| `policy` | _object_ | Policy object created by minioClient.newPostPolicy() | Create policy: @@ -1806,23 +1776,20 @@ policy.setUserMetaData({ POST your content from the browser using `superagent`: ```js -minioClient.presignedPostPolicy(policy, function (err, data) { - if (err) return console.log(err) - - const req = superagent.post(data.postURL) - _.each(data.formData, function (value, key) { - req.field(key, value) - }) +const { postURL, formData } = minioClient.presignedPostPolicy(policy) +const req = superagent.post(postURL) +_.each(formData, function (value, key) { + req.field(key, value) +}) - // file contents. - req.attach('file', '/path/to/hello.txt', 'hello.txt') +// file contents. +req.attach('file', '/path/to/hello.txt', 'hello.txt') - req.end(function (err, res) { - if (err) { - return console.log(err.toString()) - } - console.log('Upload successful.') - }) +req.end(function (err, res) { + if (err) { + return console.log(err.toString()) + } + console.log('Upload successful.') }) ``` diff --git a/examples/presigned-getobject-request-date.js b/examples/presigned-getobject-request-date.mjs similarity index 80% rename from examples/presigned-getobject-request-date.js rename to examples/presigned-getobject-request-date.mjs index 18d2a055..c8534a0b 100644 --- a/examples/presigned-getobject-request-date.js +++ b/examples/presigned-getobject-request-date.mjs @@ -30,24 +30,15 @@ const s3Client = new Minio.Client({ const requestDate = new Date() requestDate.setHours(0, 0, 0, 0) -s3Client.presignedGetObject('my-bucketname', 'my-objectname', 1000, {}, requestDate, function (e, presignedUrl) { - if (e) { - return console.log(e) - } - console.log(presignedUrl) -}) +const presignedUrl = await s3Client.presignedGetObject('my-bucketname', 'my-objectname', 1000, {}, requestDate) +console.log(presignedUrl) // Versioning support -s3Client.presignedGetObject( +const presignedUrlV = s3Client.presignedGetObject( 'my-bucketname', 'my-objectname', 1000, { versionId: '10fa9946-3f64-4137-a58f-888065c0732e' }, requestDate, - function (e, presignedUrl) { - if (e) { - return console.log(e) - } - console.log(presignedUrl) - }, ) +console.log(presignedUrlV) diff --git a/examples/presigned-getobject.js b/examples/presigned-getobject.mjs similarity index 86% rename from examples/presigned-getobject.js rename to examples/presigned-getobject.mjs index a310cf08..7e5750c8 100644 --- a/examples/presigned-getobject.js +++ b/examples/presigned-getobject.mjs @@ -27,9 +27,5 @@ const s3Client = new Minio.Client({ }) // Presigned get object URL for my-objectname at my-bucketname, it expires in 7 days by default. -s3Client.presignedGetObject('my-bucketname', 'my-objectname', 1000, function (e, presignedUrl) { - if (e) { - return console.log(e) - } - console.log(presignedUrl) -}) +const presignedUrl = await s3Client.presignedGetObject('my-bucketname', 'my-objectname', 1000) +console.log(presignedUrl) diff --git a/examples/presigned-postpolicy.js b/examples/presigned-postpolicy.mjs similarity index 81% rename from examples/presigned-postpolicy.js rename to examples/presigned-postpolicy.mjs index aabefb71..8874d99f 100644 --- a/examples/presigned-postpolicy.js +++ b/examples/presigned-postpolicy.mjs @@ -47,16 +47,12 @@ policy.setUserMetaData({ key: 'value', }) -s3Client.presignedPostPolicy(policy, function (e, data) { - if (e) { - return console.log(e) - } - const curl = [] - curl.push(`curl ${data.postURL}`) - for (const [key, value] of Object.entries(data.formData)) { - curl.push(`-F ${key}=${value}`) - } - // Print curl command to upload files. - curl.push('-F file=@') - console.log(curl.join(' ')) -}) +const { postURL, formData } = await s3Client.presignedPostPolicy(policy) +const curl = [] +curl.push(`curl ${postURL}`) +for (const [key, value] of Object.entries(formData)) { + curl.push(`-F ${key}=${value}`) +} +// Print curl command to upload files. +curl.push('-F file=@') +console.log(curl.join(' ')) diff --git a/examples/presigned-putobject.js b/examples/presigned-putobject.mjs similarity index 85% rename from examples/presigned-putobject.js rename to examples/presigned-putobject.mjs index 26504169..4ca26a12 100644 --- a/examples/presigned-putobject.js +++ b/examples/presigned-putobject.mjs @@ -26,9 +26,5 @@ const s3Client = new Minio.Client({ useSSL: true, // Default is true. }) -s3Client.presignedPutObject('my-bucketname', 'my-objectname', 1000, function (e, presignedUrl) { - if (e) { - return console.log(e) - } - console.log(presignedUrl) -}) +const presignedUrl = await s3Client.presignedPutObject('my-bucketname', 'my-objectname', 1000) +console.log(presignedUrl) diff --git a/src/helpers.ts b/src/helpers.ts index 7d0624ee..54791449 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -21,6 +21,8 @@ export { ENCRYPTION_TYPES, LEGAL_HOLD_STATUS, RETENTION_MODES, RETENTION_VALIDIT export const DEFAULT_REGION = 'us-east-1' +export const PRESIGN_EXPIRY_DAYS_MAX = 24 * 60 * 60 * 7 // 7 days in seconds + export interface ICopySourceOptions { Bucket: string Object: string diff --git a/src/internal/client.ts b/src/internal/client.ts index a3b79c1b..53b978d0 100644 --- a/src/internal/client.ts +++ b/src/internal/client.ts @@ -21,10 +21,12 @@ import { CopySourceOptions, DEFAULT_REGION, LEGAL_HOLD_STATUS, + PRESIGN_EXPIRY_DAYS_MAX, RETENTION_MODES, RETENTION_VALIDITY_UNITS, } from '../helpers.ts' -import { signV4 } from '../signing.ts' +import type { PostPolicyResult } from '../minio' +import { postPresignSignatureV4, presignSignatureV4, signV4 } from '../signing.ts' import { fsp, streamPromise } from './async.ts' import { CopyConditions } from './copy-conditions.ts' import { Extensions } from './extensions.ts' @@ -32,6 +34,7 @@ import { calculateEvenSplits, extractMetadata, getContentLength, + getScope, getSourceVersionId, getVersionId, hashBinary, @@ -40,6 +43,7 @@ import { isBoolean, isDefined, isEmpty, + isFunction, isNumber, isObject, isReadableStream, @@ -62,6 +66,7 @@ import { uriResourceEscape, } from './helper.ts' import { joinHostPort } from './join-host-port.ts' +import { PostPolicy } from './post-policy.ts' import { request } from './request.ts' import { drainResponse, readAsBuffer, readAsString } from './response.ts' import type { Region } from './s3-endpoints.ts' @@ -88,6 +93,7 @@ import type { ObjectLockInfo, ObjectMetaData, ObjectRetentionInfo, + PreSignRequestParams, PutObjectLegalHoldOptions, PutTaggingParams, RemoveObjectsParam, @@ -110,6 +116,7 @@ import type { UploadPartConfig, } from './type.ts' import type { ListMultipartResult, UploadedPart } from './xml-parser.ts' +import * as xmlParsers from './xml-parser.ts' import { parseCompleteMultipart, parseInitiateMultipart, @@ -117,7 +124,6 @@ import { parseSelectObjectContentResponse, uploadPartParser, } from './xml-parser.ts' -import * as xmlParsers from './xml-parser.ts' const xml = new xml2js.Builder({ renderOpts: { pretty: false }, headless: true }) @@ -342,7 +348,6 @@ export class TypedClient { this.reqOptions = {} this.clientExtensions = new Extensions(this) } - /** * Minio extensions that aren't necessary present for Amazon S3 compatible storage servers */ @@ -385,6 +390,27 @@ export class TypedClient { return false } + /** + * Set application specific information. + * Generates User-Agent in the following style. + * MinIO (OS; ARCH) LIB/VER APP/VER + */ + setAppInfo(appName: string, appVersion: string) { + if (!isString(appName)) { + throw new TypeError(`Invalid appName: ${appName}`) + } + if (appName.trim() === '') { + throw new errors.InvalidArgumentError('Input appName cannot be empty.') + } + if (!isString(appVersion)) { + throw new TypeError(`Invalid appVersion: ${appVersion}`) + } + if (appVersion.trim() === '') { + throw new errors.InvalidArgumentError('Input appVersion cannot be empty.') + } + this.userAgent = `${this.userAgent} ${appName}/${appVersion}` + } + /** * returns options object that can be used with http.request() * Takes care of constructing virtual-host-style or path-style hostname @@ -2810,4 +2836,182 @@ export class TypedClient { return await this.abortMultipartUpload(destObjConfig.Bucket, destObjConfig.Object, uploadId) } } + + async presignedUrl( + method: string, + bucketName: string, + objectName: string, + expires?: number | PreSignRequestParams | undefined, + reqParams?: PreSignRequestParams | Date, + requestDate?: Date, + ): Promise { + if (this.anonymous) { + throw new errors.AnonymousRequestError(`Presigned ${method} url cannot be generated for anonymous requests`) + } + + // Handle optional parameters and defaults + if (requestDate === undefined && isFunction(reqParams)) { + requestDate = new Date() + } + if (reqParams === undefined && isFunction(expires)) { + reqParams = {} + requestDate = new Date() + } + if (expires && typeof expires === 'function') { + expires = PRESIGN_EXPIRY_DAYS_MAX + reqParams = {} + requestDate = new Date() + } + if (!requestDate) { + requestDate = new Date() + } + + // Type assertions + if (expires && typeof expires !== 'number') { + throw new TypeError('expires should be of type "number"') + } + if (reqParams && typeof reqParams !== 'object') { + throw new TypeError('reqParams should be of type "object"') + } + if ((requestDate && !(requestDate instanceof Date)) || (requestDate && isNaN(requestDate?.getTime()))) { + throw new TypeError('requestDate should be of type "Date" and valid') + } + + const query = reqParams ? qs.stringify(reqParams) : undefined + + try { + const region = await this.getBucketRegionAsync(bucketName) + await this.checkAndRefreshCreds() + const reqOptions = this.getRequestOptions({ method, region, bucketName, objectName, query }) + + return presignSignatureV4( + reqOptions, + this.accessKey, + this.secretKey, + this.sessionToken, + region, + requestDate, + expires, + ) + } catch (err) { + throw new errors.InvalidArgumentError(`Unable to get bucket region for ${bucketName}.`) + } + } + + async presignedGetObject( + bucketName: string, + objectName: string, + expires?: number, + respHeaders?: PreSignRequestParams | Date, + requestDate?: Date, + ): Promise { + if (!isValidBucketName(bucketName)) { + throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName) + } + if (!isValidObjectName(objectName)) { + throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) + } + if (expires && isFunction(expires)) { + expires = PRESIGN_EXPIRY_DAYS_MAX + respHeaders = {} + requestDate = new Date() + } + if (!expires) { + expires = PRESIGN_EXPIRY_DAYS_MAX + } + if (isFunction(respHeaders)) { + respHeaders = {} + requestDate = new Date() + } + + const validRespHeaders = [ + 'response-content-type', + 'response-content-language', + 'response-expires', + 'response-cache-control', + 'response-content-disposition', + 'response-content-encoding', + ] + validRespHeaders.forEach((header) => { + // @ts-ignore + if (respHeaders !== undefined && respHeaders[header] !== undefined && !isString(respHeaders[header])) { + throw new TypeError(`response header ${header} should be of type "string"`) + } + }) + return this.presignedUrl('GET', bucketName, objectName, expires, respHeaders, requestDate) + } + + async presignedPutObject(bucketName: string, objectName: string, expires?: number): Promise { + if (!isValidBucketName(bucketName)) { + throw new errors.InvalidBucketNameError(`Invalid bucket name: ${bucketName}`) + } + if (!isValidObjectName(objectName)) { + throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) + } + if (expires && isFunction(expires)) { + expires = PRESIGN_EXPIRY_DAYS_MAX + } + + return this.presignedUrl('PUT', bucketName, objectName, expires) + } + + newPostPolicy(): PostPolicy { + return new PostPolicy() + } + + async presignedPostPolicy(postPolicy: PostPolicy): Promise { + if (this.anonymous) { + throw new errors.AnonymousRequestError('Presigned POST policy cannot be generated for anonymous requests') + } + if (!isObject(postPolicy)) { + throw new TypeError('postPolicy should be of type "object"') + } + const bucketName = postPolicy.formData.bucket as string + try { + const region = await this.getBucketRegionAsync(bucketName) + + const date = new Date() + const dateStr = makeDateLong(date) + await this.checkAndRefreshCreds() + + if (!postPolicy.policy.expiration) { + // 'expiration' is mandatory field for S3. + // Set default expiration date of 7 days. + const expires = new Date() + expires.setSeconds(24 * 60 * 60 * 7) + postPolicy.setExpires(expires) + } + + postPolicy.policy.conditions.push(['eq', '$x-amz-date', dateStr]) + postPolicy.formData['x-amz-date'] = dateStr + + postPolicy.policy.conditions.push(['eq', '$x-amz-algorithm', 'AWS4-HMAC-SHA256']) + postPolicy.formData['x-amz-algorithm'] = 'AWS4-HMAC-SHA256' + + postPolicy.policy.conditions.push(['eq', '$x-amz-credential', this.accessKey + '/' + getScope(region, date)]) + postPolicy.formData['x-amz-credential'] = this.accessKey + '/' + getScope(region, date) + + if (this.sessionToken) { + postPolicy.policy.conditions.push(['eq', '$x-amz-security-token', this.sessionToken]) + postPolicy.formData['x-amz-security-token'] = this.sessionToken + } + + const policyBase64 = Buffer.from(JSON.stringify(postPolicy.policy)).toString('base64') + + postPolicy.formData.policy = policyBase64 + + postPolicy.formData['x-amz-signature'] = postPresignSignatureV4(region, date, this.secretKey, policyBase64) + const opts = { + region: region, + bucketName: bucketName, + method: 'POST', + } + const reqOptions = this.getRequestOptions(opts) + const portStr = this.port == 80 || this.port === 443 ? '' : `:${this.port.toString()}` + const urlStr = `${reqOptions.protocol}//${reqOptions.host}${portStr}${reqOptions.path}` + return { postURL: urlStr, formData: postPolicy.formData } + } catch (er) { + throw new errors.InvalidArgumentError(`Unable to get bucket region for ${bucketName}.`) + } + } } diff --git a/src/internal/type.ts b/src/internal/type.ts index 53b420d2..ced05079 100644 --- a/src/internal/type.ts +++ b/src/internal/type.ts @@ -458,3 +458,5 @@ export type UploadPartConfig = { headers: RequestHeaders sourceObj: string } + +export type PreSignRequestParams = { [key: string]: string } diff --git a/src/minio.d.ts b/src/minio.d.ts index 67cf6632..cf3937ae 100644 --- a/src/minio.d.ts +++ b/src/minio.d.ts @@ -141,73 +141,6 @@ export class Client extends TypedClient { listObjectsV2(bucketName: string, prefix?: string, recursive?: boolean, startAfter?: string): BucketStream - // Presigned operations - presignedUrl(httpMethod: string, bucketName: string, objectName: string, callback: ResultCallback): void - presignedUrl( - httpMethod: string, - bucketName: string, - objectName: string, - expiry: number, - callback: ResultCallback, - ): void - presignedUrl( - httpMethod: string, - bucketName: string, - objectName: string, - expiry: number, - reqParams: { [key: string]: any }, - callback: ResultCallback, - ): void - presignedUrl( - httpMethod: string, - bucketName: string, - objectName: string, - expiry: number, - reqParams: { [key: string]: any }, - requestDate: Date, - callback: ResultCallback, - ): void - presignedUrl( - httpMethod: string, - bucketName: string, - objectName: string, - expiry?: number, - reqParams?: { [key: string]: any }, - requestDate?: Date, - ): Promise - - presignedGetObject(bucketName: string, objectName: string, callback: ResultCallback): void - presignedGetObject(bucketName: string, objectName: string, expiry: number, callback: ResultCallback): void - presignedGetObject( - bucketName: string, - objectName: string, - expiry: number, - respHeaders: { [key: string]: any }, - callback: ResultCallback, - ): void - presignedGetObject( - bucketName: string, - objectName: string, - expiry: number, - respHeaders: { [key: string]: any }, - requestDate: Date, - callback: ResultCallback, - ): void - presignedGetObject( - bucketName: string, - objectName: string, - expiry?: number, - respHeaders?: { [key: string]: any }, - requestDate?: Date, - ): Promise - - presignedPutObject(bucketName: string, objectName: string, callback: ResultCallback): void - presignedPutObject(bucketName: string, objectName: string, expiry: number, callback: ResultCallback): void - presignedPutObject(bucketName: string, objectName: string, expiry?: number): Promise - - presignedPostPolicy(policy: PostPolicy, callback: ResultCallback): void - presignedPostPolicy(policy: PostPolicy): Promise - // Bucket Policy & Notification operations getBucketNotification(bucketName: string, callback: ResultCallback): void getBucketNotification(bucketName: string): Promise @@ -228,7 +161,4 @@ export class Client extends TypedClient { suffix: string, events: NotificationEvent[], ): NotificationPoller - - // Other - newPostPolicy(): PostPolicy } diff --git a/src/minio.js b/src/minio.js index 3a5f08a2..ac51d156 100644 --- a/src/minio.js +++ b/src/minio.js @@ -16,7 +16,6 @@ import * as Stream from 'node:stream' -import * as querystring from 'query-string' import xml2js from 'xml2js' import * as errors from './errors.ts' @@ -24,24 +23,19 @@ import { callbackify } from './internal/callbackify.js' import { TypedClient } from './internal/client.ts' import { CopyConditions } from './internal/copy-conditions.ts' import { - getScope, isBoolean, isFunction, isNumber, isObject, isString, isValidBucketName, - isValidDate, - isValidObjectName, isValidPrefix, - makeDateLong, pipesetup, uriEscape, } from './internal/helper.ts' import { PostPolicy } from './internal/post-policy.ts' import { NotificationConfig, NotificationPoller } from './notification.ts' import { promisify } from './promisify.js' -import { postPresignSignatureV4, presignSignatureV4 } from './signing.ts' import * as transformers from './transformers.js' export * from './errors.ts' @@ -50,30 +44,10 @@ export * from './notification.ts' export { CopyConditions, PostPolicy } export class Client extends TypedClient { - // Set application specific information. - // - // Generates User-Agent in the following style. - // - // MinIO (OS; ARCH) LIB/VER APP/VER // // __Arguments__ // * `appName` _string_ - Application name. // * `appVersion` _string_ - Application version. - setAppInfo(appName, appVersion) { - if (!isString(appName)) { - throw new TypeError(`Invalid appName: ${appName}`) - } - if (appName.trim() === '') { - throw new errors.InvalidArgumentError('Input appName cannot be empty.') - } - if (!isString(appVersion)) { - throw new TypeError(`Invalid appVersion: ${appVersion}`) - } - if (appVersion.trim() === '') { - throw new errors.InvalidArgumentError('Input appVersion cannot be empty.') - } - this.userAgent = `${this.userAgent} ${appName}/${appVersion}` - } // list a batch of objects listObjectsQuery(bucketName, prefix, marker, listQueryOpts = {}) { @@ -358,195 +332,6 @@ export class Client extends TypedClient { return readStream } - // Generate a generic presigned URL which can be - // used for HTTP methods GET, PUT, HEAD and DELETE - // - // __Arguments__ - // * `method` _string_: name of the HTTP method - // * `bucketName` _string_: name of the bucket - // * `objectName` _string_: name of the object - // * `expiry` _number_: expiry in seconds (optional, default 7 days) - // * `reqParams` _object_: request parameters (optional) e.g {versionId:"10fa9946-3f64-4137-a58f-888065c0732e"} - // * `requestDate` _Date_: A date object, the url will be issued at (optional) - presignedUrl(method, bucketName, objectName, expires, reqParams, requestDate, cb) { - if (this.anonymous) { - throw new errors.AnonymousRequestError('Presigned ' + method + ' url cannot be generated for anonymous requests') - } - if (isFunction(requestDate)) { - cb = requestDate - requestDate = new Date() - } - if (isFunction(reqParams)) { - cb = reqParams - reqParams = {} - requestDate = new Date() - } - if (isFunction(expires)) { - cb = expires - reqParams = {} - expires = 24 * 60 * 60 * 7 // 7 days in seconds - requestDate = new Date() - } - if (!isNumber(expires)) { - throw new TypeError('expires should be of type "number"') - } - if (!isObject(reqParams)) { - throw new TypeError('reqParams should be of type "object"') - } - if (!isValidDate(requestDate)) { - throw new TypeError('requestDate should be of type "Date" and valid') - } - if (!isFunction(cb)) { - throw new TypeError('callback should be of type "function"') - } - var query = querystring.stringify(reqParams) - this.getBucketRegion(bucketName, (e, region) => { - if (e) { - return cb(e) - } - // This statement is added to ensure that we send error through - // callback on presign failure. - var url - var reqOptions = this.getRequestOptions({ method, region, bucketName, objectName, query }) - - this.checkAndRefreshCreds() - try { - url = presignSignatureV4( - reqOptions, - this.accessKey, - this.secretKey, - this.sessionToken, - region, - requestDate, - expires, - ) - } catch (pe) { - return cb(pe) - } - cb(null, url) - }) - } - - // Generate a presigned URL for GET - // - // __Arguments__ - // * `bucketName` _string_: name of the bucket - // * `objectName` _string_: name of the object - // * `expiry` _number_: expiry in seconds (optional, default 7 days) - // * `respHeaders` _object_: response headers to override or request params for query (optional) e.g {versionId:"10fa9946-3f64-4137-a58f-888065c0732e"} - // * `requestDate` _Date_: A date object, the url will be issued at (optional) - presignedGetObject(bucketName, objectName, expires, respHeaders, requestDate, cb) { - if (!isValidBucketName(bucketName)) { - throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName) - } - if (!isValidObjectName(objectName)) { - throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) - } - - if (isFunction(respHeaders)) { - cb = respHeaders - respHeaders = {} - requestDate = new Date() - } - - var validRespHeaders = [ - 'response-content-type', - 'response-content-language', - 'response-expires', - 'response-cache-control', - 'response-content-disposition', - 'response-content-encoding', - ] - validRespHeaders.forEach((header) => { - if (respHeaders !== undefined && respHeaders[header] !== undefined && !isString(respHeaders[header])) { - throw new TypeError(`response header ${header} should be of type "string"`) - } - }) - return this.presignedUrl('GET', bucketName, objectName, expires, respHeaders, requestDate, cb) - } - - // Generate a presigned URL for PUT. Using this URL, the browser can upload to S3 only with the specified object name. - // - // __Arguments__ - // * `bucketName` _string_: name of the bucket - // * `objectName` _string_: name of the object - // * `expiry` _number_: expiry in seconds (optional, default 7 days) - presignedPutObject(bucketName, objectName, expires, cb) { - if (!isValidBucketName(bucketName)) { - throw new errors.InvalidBucketNameError(`Invalid bucket name: ${bucketName}`) - } - if (!isValidObjectName(objectName)) { - throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`) - } - return this.presignedUrl('PUT', bucketName, objectName, expires, cb) - } - - // return PostPolicy object - newPostPolicy() { - return new PostPolicy() - } - - // 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` `Content-Disposition` `metaData` - presignedPostPolicy(postPolicy, cb) { - if (this.anonymous) { - throw new errors.AnonymousRequestError('Presigned POST policy cannot be generated for anonymous requests') - } - if (!isObject(postPolicy)) { - throw new TypeError('postPolicy should be of type "object"') - } - if (!isFunction(cb)) { - throw new TypeError('cb should be of type "function"') - } - this.getBucketRegion(postPolicy.formData.bucket, (e, region) => { - if (e) { - return cb(e) - } - var date = new Date() - var dateStr = makeDateLong(date) - - this.checkAndRefreshCreds() - - if (!postPolicy.policy.expiration) { - // 'expiration' is mandatory field for S3. - // Set default expiration date of 7 days. - var expires = new Date() - expires.setSeconds(24 * 60 * 60 * 7) - postPolicy.setExpires(expires) - } - - postPolicy.policy.conditions.push(['eq', '$x-amz-date', dateStr]) - postPolicy.formData['x-amz-date'] = dateStr - - postPolicy.policy.conditions.push(['eq', '$x-amz-algorithm', 'AWS4-HMAC-SHA256']) - postPolicy.formData['x-amz-algorithm'] = 'AWS4-HMAC-SHA256' - - postPolicy.policy.conditions.push(['eq', '$x-amz-credential', this.accessKey + '/' + getScope(region, date)]) - postPolicy.formData['x-amz-credential'] = this.accessKey + '/' + getScope(region, date) - - if (this.sessionToken) { - postPolicy.policy.conditions.push(['eq', '$x-amz-security-token', this.sessionToken]) - postPolicy.formData['x-amz-security-token'] = this.sessionToken - } - - var policyBase64 = Buffer.from(JSON.stringify(postPolicy.policy)).toString('base64') - - postPolicy.formData.policy = policyBase64 - - var signature = postPresignSignatureV4(region, date, this.secretKey, policyBase64) - - postPolicy.formData['x-amz-signature'] = signature - var opts = {} - opts.region = region - opts.bucketName = postPolicy.formData.bucket - var reqOptions = this.getRequestOptions(opts) - var portStr = this.port == 80 || this.port === 443 ? '' : `:${this.port.toString()}` - var urlStr = `${reqOptions.protocol}//${reqOptions.host}${portStr}${reqOptions.path}` - cb(null, { postURL: urlStr, formData: postPolicy.formData }) - }) - } - // Remove all the notification configurations in the S3 provider setBucketNotification(bucketName, config, cb) { if (!isValidBucketName(bucketName)) { @@ -618,10 +403,6 @@ export class Client extends TypedClient { } } -Client.prototype.presignedUrl = promisify(Client.prototype.presignedUrl) -Client.prototype.presignedGetObject = promisify(Client.prototype.presignedGetObject) -Client.prototype.presignedPutObject = promisify(Client.prototype.presignedPutObject) -Client.prototype.presignedPostPolicy = promisify(Client.prototype.presignedPostPolicy) Client.prototype.getBucketNotification = promisify(Client.prototype.getBucketNotification) Client.prototype.setBucketNotification = promisify(Client.prototype.setBucketNotification) Client.prototype.removeAllBucketNotification = promisify(Client.prototype.removeAllBucketNotification) @@ -670,3 +451,7 @@ Client.prototype.removeObjects = callbackify(Client.prototype.removeObjects) Client.prototype.removeIncompleteUpload = callbackify(Client.prototype.removeIncompleteUpload) Client.prototype.copyObject = callbackify(Client.prototype.copyObject) Client.prototype.composeObject = callbackify(Client.prototype.composeObject) +Client.prototype.presignedUrl = callbackify(Client.prototype.presignedUrl) +Client.prototype.presignedGetObject = callbackify(Client.prototype.presignedGetObject) +Client.prototype.presignedPutObject = callbackify(Client.prototype.presignedPutObject) +Client.prototype.presignedPostPolicy = callbackify(Client.prototype.presignedPostPolicy) diff --git a/src/signing.ts b/src/signing.ts index a4bb7015..7a5480c9 100644 --- a/src/signing.ts +++ b/src/signing.ts @@ -17,6 +17,7 @@ import * as crypto from 'node:crypto' import * as errors from './errors.ts' +import { PRESIGN_EXPIRY_DAYS_MAX } from './helpers.ts' import { getScope, isNumber, isObject, isString, makeDateLong, makeDateShort, uriEscape } from './internal/helper.ts' import type { ICanonicalRequest, IRequest, RequestHeaders } from './internal/type.ts' @@ -258,7 +259,7 @@ export function presignSignatureV4( sessionToken: string | undefined, region: string, requestDate: Date, - expires: number, + expires: number | undefined, ) { if (!isObject(request)) { throw new TypeError('request should be of type "object"') @@ -280,13 +281,13 @@ export function presignSignatureV4( throw new errors.SecretKeyRequiredError('secretKey is required for presigning') } - if (!isNumber(expires)) { + if (expires && !isNumber(expires)) { throw new TypeError('expires should be of type "number"') } - if (expires < 1) { + if (expires && expires < 1) { throw new errors.ExpiresParamError('expires param cannot be less than 1 seconds') } - if (expires > 604800) { + if (expires && expires > PRESIGN_EXPIRY_DAYS_MAX) { throw new errors.ExpiresParamError('expires param cannot be greater than 7 days') } diff --git a/tests/unit/test.js b/tests/unit/test.js index 62713234..49608d0d 100644 --- a/tests/unit/test.js +++ b/tests/unit/test.js @@ -382,75 +382,84 @@ describe('Client', function () { }) describe('Presigned URL', () => { describe('presigned-get', () => { - it('should not generate presigned url with no access key', (done) => { + it('should not generate presigned url with no access key', async () => { try { - var client = new Minio.Client({ + const client = new Minio.Client({ endPoint: 'localhost', port: 9000, useSSL: false, }) - client.presignedGetObject('bucket', 'object', 1000, function () {}) - } catch (e) { - done() + await client.presignedGetObject('bucket', 'object', 1000) + } catch (err) { + return } + throw new Error('callback should receive error') }) - it('should not generate presigned url with wrong expires param', (done) => { + it('should not generate presigned url with wrong expires param', async () => { try { - client.presignedGetObject('bucket', 'object', '0', function () {}) - } catch (e) { - done() + await client.presignedGetObject('bucket', 'object', '0') + } catch (err) { + return } + throw new Error('callback should receive error') }) }) describe('presigned-put', () => { - it('should not generate presigned url with no access key', (done) => { + it('should not generate presigned url with no access key', async () => { try { - var client = new Minio.Client({ + const client = new Minio.Client({ endPoint: 'localhost', port: 9000, useSSL: false, }) - client.presignedPutObject('bucket', 'object', 1000, function () {}) - } catch (e) { - done() + await client.presignedPutObject('bucket', 'object', 1000) + } catch (err) { + return } + throw new Error('callback should receive error') }) - it('should not generate presigned url with wrong expires param', (done) => { + it('should not generate presigned url with wrong expires param', async () => { try { - client.presignedPutObject('bucket', 'object', '0', function () {}) - } catch (e) { - done() + const client = new Minio.Client({ + endPoint: 'localhost', + port: 9000, + useSSL: false, + }) + await client.presignedPutObject('bucket', 'object', '0') + } catch (err) { + return } + throw new Error('callback should receive error') }) }) describe('presigned-post-policy', () => { it('should not generate content type for undefined value', () => { assert.throws(() => { - var policy = client.newPostPolicy() + const policy = client.newPostPolicy() policy.setContentType() }, /content-type cannot be null/) }) it('should not generate content disposition for undefined value', () => { assert.throws(() => { - var policy = client.newPostPolicy() + const policy = client.newPostPolicy() policy.setContentDisposition() }, /content-disposition cannot be null/) }) it('should not generate user defined metadata for string value', () => { assert.throws(() => { - var policy = client.newPostPolicy() + const policy = client.newPostPolicy() policy.setUserMetaData('123') }, /metadata should be of type "object"/) }) it('should not generate user defined metadata for null value', () => { assert.throws(() => { - var policy = client.newPostPolicy() + const policy = client.newPostPolicy() policy.setUserMetaData(null) }, /metadata should be of type "object"/) }) it('should not generate user defined metadata for undefined value', () => { assert.throws(() => { - var policy = client.newPostPolicy() + const policy = client.newPostPolicy() policy.setUserMetaData() }, /metadata should be of type "object"/) })