11import { Inject , Injectable , OnModuleInit } from '@nestjs/common' ;
2+ import * as crypto from 'crypto' ;
23import * as Minio from 'minio' ;
34import { MINIO_CONFIG } from './constants' ;
45import { IFileUpload } from './interfaces/file.interface' ;
@@ -24,6 +25,10 @@ export class MinioService implements OnModuleInit {
2425 await this . initializeBuckets ( ) ;
2526 }
2627
28+ // =======================================================================
29+ // Bucket Initialization
30+ // =======================================================================
31+
2732 private async initializeBuckets ( ) {
2833 if ( this . bucketInitialized ) return ;
2934
@@ -76,6 +81,125 @@ export class MinioService implements OnModuleInit {
7681 this . bucketInitialized = true ;
7782 }
7883
84+ // =======================================================================
85+ // AWS Signature V4 Helper Methods
86+ // =======================================================================
87+
88+ private sha256 ( data : string ) : string {
89+ return crypto . createHash ( 'sha256' ) . update ( data ) . digest ( 'hex' ) ;
90+ }
91+
92+ private hmacSha256 ( key : Buffer | string , data : string ) : Buffer {
93+ return crypto . createHmac ( 'sha256' , key ) . update ( data ) . digest ( ) ;
94+ }
95+
96+ private getSigningKey (
97+ secretKey : string ,
98+ dateStamp : string ,
99+ region : string ,
100+ serviceName : string = 's3' ,
101+ ) : Buffer {
102+ const kSecret = 'AWS4' + secretKey ;
103+ const kDate = this . hmacSha256 ( kSecret , dateStamp ) ;
104+ const kRegion = this . hmacSha256 ( kDate , region ) ;
105+ const kService = this . hmacSha256 ( kRegion , serviceName ) ;
106+ const kSigning = this . hmacSha256 ( kService , 'aws4_request' ) ;
107+ return kSigning ;
108+ }
109+
110+ // Get date in YYYYMMDDTHHMMSSZ format
111+ private getAmzDate ( date : Date ) : string {
112+ return date . toISOString ( ) . replace ( / [: -] | \. \d { 3 } / g, '' ) ;
113+ }
114+
115+ // Get date in YYYYMMDD format
116+ private getDateStamp ( date : Date ) : string {
117+ return date . toISOString ( ) . slice ( 0 , 10 ) . replace ( / - / g, '' ) ;
118+ }
119+
120+ // =======================================================================
121+ // Manual Presigned URL Calculation
122+ // =======================================================================
123+
124+ private async calculatePresignedGetUrl (
125+ endPoint : string ,
126+ bucketName : string ,
127+ objectName : string ,
128+ expirySeconds : number ,
129+ ) : Promise < string > {
130+ const accessKey = this . config . accessKey ;
131+ const secretKey = this . config . secretKey ;
132+ const region = this . config . region || 'us-east-1' ;
133+ const externalHost = endPoint ;
134+ const port = this . config . port ;
135+
136+ // Use host without port for signing
137+ const signingHost = port ? `${ externalHost } :${ port } ` : externalHost ;
138+
139+ const currentDate = new Date ( ) ;
140+ const amzDate = this . getAmzDate ( currentDate ) ;
141+ const dateStamp = this . getDateStamp ( currentDate ) ;
142+ const credentialScope = `${ dateStamp } /${ region } /s3/aws4_request` ;
143+
144+ // Create canonical URI - don't encode forward slashes in object name
145+ const canonicalURI =
146+ '/' +
147+ encodeURIComponent ( bucketName ) +
148+ '/' +
149+ objectName
150+ . split ( '/' )
151+ . map ( ( segment ) => encodeURIComponent ( segment ) )
152+ . join ( '/' ) ;
153+
154+ // Create query parameters in specific order
155+ const params = {
156+ 'X-Amz-Algorithm' : 'AWS4-HMAC-SHA256' ,
157+ 'X-Amz-Credential' : `${ accessKey } /${ credentialScope } ` ,
158+ 'X-Amz-Date' : amzDate ,
159+ 'X-Amz-Expires' : expirySeconds . toString ( ) ,
160+ 'X-Amz-SignedHeaders' : 'host' ,
161+ } ;
162+
163+ // Build canonical query string maintaining strict ordering
164+ const canonicalQueryString = Object . keys ( params )
165+ . sort ( )
166+ . map ( ( key ) => {
167+ return `${ encodeURIComponent ( key ) } =${ encodeURIComponent ( params [ key ] ) } ` ;
168+ } )
169+ . join ( '&' ) ;
170+
171+ // Create canonical headers
172+ const canonicalHeaders = `host:${ signingHost . toLowerCase ( ) } \n` ;
173+
174+ // Create canonical request
175+ const canonicalRequest = [
176+ 'GET' ,
177+ canonicalURI ,
178+ canonicalQueryString ,
179+ canonicalHeaders ,
180+ 'host' , // signed headers
181+ 'UNSIGNED-PAYLOAD' , // Changed from empty string hash
182+ ] . join ( '\n' ) ;
183+
184+ // Create string to sign
185+ const stringToSign = [
186+ 'AWS4-HMAC-SHA256' ,
187+ amzDate ,
188+ credentialScope ,
189+ this . sha256 ( canonicalRequest ) ,
190+ ] . join ( '\n' ) ;
191+
192+ // Calculate signature
193+ const signingKey = this . getSigningKey ( secretKey , dateStamp , region ) ;
194+ const signature = this . hmacSha256 ( signingKey , stringToSign ) . toString ( 'hex' ) ;
195+
196+ // Construct final URL
197+ const protocol = ( this . config . externalUseSSL ?? this . config . useSSL ) ? 'https' : 'http' ;
198+ const finalUrl = `${ protocol } ://${ signingHost } ${ canonicalURI } ?${ canonicalQueryString } &X-Amz-Signature=${ signature } ` ;
199+
200+ return finalUrl ;
201+ }
202+
79203 async uploadFile ( file : IFileUpload , bucketName : string , objectName ?: string ) : Promise < string > {
80204 if ( ! this . bucketInitialized ) {
81205 await this . initializeBuckets ( ) ;
@@ -98,29 +222,11 @@ export class MinioService implements OnModuleInit {
98222 } /${ bucketName } /${ objectName } `;
99223 }
100224
101- // For private buckets, if external endpoint is configured, create a new client with it
102- if ( this . config . externalEndPoint ) {
103- const externalClient = new Minio . Client ( {
104- endPoint : this . config . externalEndPoint ,
105- port : this . config . port ,
106- useSSL : this . config . externalUseSSL ?? this . config . useSSL ,
107- accessKey : this . config . accessKey ,
108- secretKey : this . config . secretKey ,
109- region : this . config . region ,
110- } ) ;
111-
112- return await externalClient . presignedGetObject (
113- bucketName ,
114- objectName ,
115- this . config . urlExpiryHours * 60 * 60 ,
116- ) ;
117- }
118-
119- // If no external endpoint, use the default client
120- return await this . minioClient . presignedGetObject (
225+ return await this . calculatePresignedGetUrl (
226+ this . config . externalEndPoint || this . config . endPoint ,
121227 bucketName ,
122228 objectName ,
123- this . config . urlExpiryHours * 60 * 60 ,
229+ ( this . config . urlExpiryHours || 1 ) * 60 * 60 ,
124230 ) ;
125231 }
126232
0 commit comments