@@ -172,21 +172,118 @@ def chunk_update(self, chunk, eof, *args, **kwargs):
172
172
return chunk
173
173
174
174
175
- class SigV4Mixin (object ):
176
- """
177
- A request class mixin to provide S3 signature v4 functionality
178
- """
175
+ class BaseSigChecker :
176
+ def __init__ (self , req ):
177
+ self .req = req
178
+ self .signature = req .signature
179
+ self .string_to_sign = self ._string_to_sign ()
180
+ self ._secret = None
181
+
182
+ def _string_to_sign (self ):
183
+ raise NotImplementedError
184
+
185
+ def _derive_secret (self , secret ):
186
+ return utf8encode (secret )
187
+
188
+ def _check_signature (self ):
189
+ raise NotImplementedError
179
190
180
191
def check_signature (self , secret ):
181
- secret = utf8encode (secret )
182
- user_signature = self .signature
183
- derived_secret = b'AWS4' + secret
184
- for scope_piece in self .scope .values ():
192
+ self ._secret = self ._derive_secret (secret )
193
+ return self ._check_signature ()
194
+
195
+
196
+ class SigCheckerV2 (BaseSigChecker ):
197
+ def _string_to_sign (self ):
198
+ """
199
+ Create 'StringToSign' value in Amazon terminology for v2.
200
+ """
201
+ buf = [swob .wsgi_to_bytes (wsgi_str ) for wsgi_str in [
202
+ self .req .method ,
203
+ _header_strip (self .req .headers .get ('Content-MD5' )) or '' ,
204
+ _header_strip (self .req .headers .get ('Content-Type' )) or '' ]]
205
+
206
+ if 'headers_raw' in self .req .environ : # eventlet >= 0.19.0
207
+ # See https://github.com/eventlet/eventlet/commit/67ec999
208
+ amz_headers = defaultdict (list )
209
+ for key , value in self .req .environ ['headers_raw' ]:
210
+ key = key .lower ()
211
+ if not key .startswith ('x-amz-' ):
212
+ continue
213
+ amz_headers [key .strip ()].append (value .strip ())
214
+ amz_headers = dict ((key , ',' .join (value ))
215
+ for key , value in amz_headers .items ())
216
+ else : # mostly-functional fallback
217
+ amz_headers = dict ((key .lower (), value )
218
+ for key , value in self .req .headers .items ()
219
+ if key .lower ().startswith ('x-amz-' ))
220
+
221
+ if self .req ._is_header_auth :
222
+ if 'x-amz-date' in amz_headers :
223
+ buf .append (b'' )
224
+ elif 'Date' in self .req .headers :
225
+ buf .append (swob .wsgi_to_bytes (self .req .headers ['Date' ]))
226
+ elif self .req ._is_query_auth :
227
+ buf .append (swob .wsgi_to_bytes (self .req .params ['Expires' ]))
228
+ else :
229
+ # Should have already raised NotS3Request in _parse_auth_info,
230
+ # but as a sanity check...
231
+ raise AccessDenied (reason = 'not_s3' )
232
+
233
+ for key , value in sorted (amz_headers .items ()):
234
+ buf .append (swob .wsgi_to_bytes ("%s:%s" % (key , value )))
235
+
236
+ path = self .req ._canonical_uri ()
237
+ if self .req .query_string :
238
+ path += '?' + self .req .query_string
239
+ params = []
240
+ if '?' in path :
241
+ path , args = path .split ('?' , 1 )
242
+ for key , value in sorted (self .req .params .items ()):
243
+ if key in ALLOWED_SUB_RESOURCES :
244
+ params .append ('%s=%s' % (key , value ) if value else key )
245
+ if params :
246
+ buf .append (swob .wsgi_to_bytes ('%s?%s' % (path , '&' .join (params ))))
247
+ else :
248
+ buf .append (swob .wsgi_to_bytes (path ))
249
+ return b'\n ' .join (buf )
250
+
251
+ def _check_signature (self ):
252
+ valid_signature = base64 .b64encode (hmac .new (
253
+ self ._secret , self .string_to_sign , sha1
254
+ ).digest ()).strip ().decode ('ascii' )
255
+ return streq_const_time (self .signature , valid_signature )
256
+
257
+
258
+ class SigCheckerV4 (BaseSigChecker ):
259
+ def __init__ (self , req ):
260
+ super ().__init__ (req )
261
+ self ._all_chunk_signatures_valid = True
262
+
263
+ def _string_to_sign (self ):
264
+ return b'\n ' .join ([
265
+ b'AWS4-HMAC-SHA256' ,
266
+ self .req .timestamp .amz_date_format .encode ('ascii' ),
267
+ '/' .join (self .req .scope .values ()).encode ('utf8' ),
268
+ sha256 (self .req ._canonical_request ()).hexdigest ().encode ('ascii' )])
269
+
270
+ def _derive_secret (self , secret ):
271
+ derived_secret = b'AWS4' + super ()._derive_secret (secret )
272
+ for scope_piece in self .req .scope .values ():
185
273
derived_secret = hmac .new (
186
274
derived_secret , scope_piece .encode ('utf8' ), sha256 ).digest ()
275
+ return derived_secret
276
+
277
+ def _check_signature (self ):
187
278
valid_signature = hmac .new (
188
- derived_secret , self .string_to_sign , sha256 ).hexdigest ()
189
- return streq_const_time (user_signature , valid_signature )
279
+ self ._secret , self .string_to_sign , sha256 ).hexdigest ()
280
+ return streq_const_time (self .signature , valid_signature )
281
+
282
+
283
+ class SigV4Mixin (object ):
284
+ """
285
+ A request class mixin to provide S3 signature v4 functionality
286
+ """
190
287
191
288
@property
192
289
def _is_query_auth (self ):
@@ -507,16 +604,6 @@ def scope(self):
507
604
('terminal' , 'aws4_request' ),
508
605
])
509
606
510
- def _string_to_sign (self ):
511
- """
512
- Create 'StringToSign' value in Amazon terminology for v4.
513
- """
514
- return b'\n ' .join ([
515
- b'AWS4-HMAC-SHA256' ,
516
- self .timestamp .amz_date_format .encode ('ascii' ),
517
- '/' .join (self .scope .values ()).encode ('utf8' ),
518
- sha256 (self ._canonical_request ()).hexdigest ().encode ('ascii' )])
519
-
520
607
def signature_does_not_match_kwargs (self ):
521
608
kwargs = super (SigV4Mixin , self ).signature_does_not_match_kwargs ()
522
609
cr = self ._canonical_request ()
@@ -566,13 +653,18 @@ def __init__(self, env, app=None, conf=None):
566
653
self .bucket_in_host = self ._parse_host ()
567
654
self .container_name , self .object_name = self ._parse_uri ()
568
655
self ._validate_headers ()
569
- # Lock in string-to-sign now, before we start messing with query params
570
- self .string_to_sign = self ._string_to_sign ()
656
+ if isinstance (self , SigV4Mixin ):
657
+ # this is a deliberate but only partial shift away from the
658
+ # 'inherit and override from mixin' pattern towards a 'compose
659
+ # adapters' pattern.
660
+ self .sig_checker = SigCheckerV4 (self )
661
+ else :
662
+ self .sig_checker = SigCheckerV2 (self )
571
663
self .environ ['s3api.auth_details' ] = {
572
664
'access_key' : self .access_key ,
573
665
'signature' : self .signature ,
574
- 'string_to_sign' : self .string_to_sign ,
575
- 'check_signature' : self .check_signature ,
666
+ 'string_to_sign' : self .sig_checker . string_to_sign ,
667
+ 'check_signature' : self .sig_checker . check_signature ,
576
668
}
577
669
self .account = None
578
670
self .user_id = None
@@ -633,14 +725,6 @@ def validate_part_number(self, parts_count=None, check_max=True):
633
725
634
726
return part_number
635
727
636
- def check_signature (self , secret ):
637
- secret = utf8encode (secret )
638
- user_signature = self .signature
639
- valid_signature = base64 .b64encode (hmac .new (
640
- secret , self .string_to_sign , sha1
641
- ).digest ()).strip ().decode ('ascii' )
642
- return streq_const_time (user_signature , valid_signature )
643
-
644
728
@property
645
729
def timestamp (self ):
646
730
"""
@@ -1041,70 +1125,14 @@ def _canonical_uri(self):
1041
1125
raw_path_info = '/' + self .bucket_in_host + raw_path_info
1042
1126
return raw_path_info
1043
1127
1044
- def _string_to_sign (self ):
1045
- """
1046
- Create 'StringToSign' value in Amazon terminology for v2.
1047
- """
1048
- amz_headers = {}
1049
-
1050
- buf = [swob .wsgi_to_bytes (wsgi_str ) for wsgi_str in [
1051
- self .method ,
1052
- _header_strip (self .headers .get ('Content-MD5' )) or '' ,
1053
- _header_strip (self .headers .get ('Content-Type' )) or '' ]]
1054
-
1055
- if 'headers_raw' in self .environ : # eventlet >= 0.19.0
1056
- # See https://github.com/eventlet/eventlet/commit/67ec999
1057
- amz_headers = defaultdict (list )
1058
- for key , value in self .environ ['headers_raw' ]:
1059
- key = key .lower ()
1060
- if not key .startswith ('x-amz-' ):
1061
- continue
1062
- amz_headers [key .strip ()].append (value .strip ())
1063
- amz_headers = dict ((key , ',' .join (value ))
1064
- for key , value in amz_headers .items ())
1065
- else : # mostly-functional fallback
1066
- amz_headers = dict ((key .lower (), value )
1067
- for key , value in self .headers .items ()
1068
- if key .lower ().startswith ('x-amz-' ))
1069
-
1070
- if self ._is_header_auth :
1071
- if 'x-amz-date' in amz_headers :
1072
- buf .append (b'' )
1073
- elif 'Date' in self .headers :
1074
- buf .append (swob .wsgi_to_bytes (self .headers ['Date' ]))
1075
- elif self ._is_query_auth :
1076
- buf .append (swob .wsgi_to_bytes (self .params ['Expires' ]))
1077
- else :
1078
- # Should have already raised NotS3Request in _parse_auth_info,
1079
- # but as a sanity check...
1080
- raise AccessDenied (reason = 'not_s3' )
1081
-
1082
- for key , value in sorted (amz_headers .items ()):
1083
- buf .append (swob .wsgi_to_bytes ("%s:%s" % (key , value )))
1084
-
1085
- path = self ._canonical_uri ()
1086
- if self .query_string :
1087
- path += '?' + self .query_string
1088
- params = []
1089
- if '?' in path :
1090
- path , args = path .split ('?' , 1 )
1091
- for key , value in sorted (self .params .items ()):
1092
- if key in ALLOWED_SUB_RESOURCES :
1093
- params .append ('%s=%s' % (key , value ) if value else key )
1094
- if params :
1095
- buf .append (swob .wsgi_to_bytes ('%s?%s' % (path , '&' .join (params ))))
1096
- else :
1097
- buf .append (swob .wsgi_to_bytes (path ))
1098
- return b'\n ' .join (buf )
1099
-
1100
1128
def signature_does_not_match_kwargs (self ):
1101
1129
return {
1102
1130
'a_w_s_access_key_id' : self .access_key ,
1103
- 'string_to_sign' : self .string_to_sign ,
1131
+ 'string_to_sign' : self .sig_checker . string_to_sign ,
1104
1132
'signature_provided' : self .signature ,
1105
1133
'string_to_sign_bytes' : ' ' .join (
1106
1134
format (ord (c ), '02x' )
1107
- for c in self .string_to_sign .decode ('latin1' )),
1135
+ for c in self .sig_checker . string_to_sign .decode ('latin1' )),
1108
1136
}
1109
1137
1110
1138
@property
0 commit comments