1- #!/usr/bin/env python
2- # -*- coding: utf-8 -*-
1+ # -*- coding: utf-8 -*-
32#
4- # Copyright 2019-2020 Mastercard
3+ # Copyright 2019-2021 Mastercard
54# All rights reserved.
65#
76# Redistribution and use in source and binary forms, with or without modification, are
3130"""
3231import hashlib
3332import base64
33+ import urllib
34+ import time
35+ from random import SystemRandom
36+
37+ from urllib .parse import urlparse , parse_qsl
3438
35- from urllib .parse import urlparse , quote , parse_qsl
3639
3740def normalize_params (url , params ):
3841 """
@@ -44,36 +47,26 @@ def normalize_params(url, params):
4447
4548 # Get the query list
4649 qs_list = parse_qsl (parse .query , keep_blank_values = True )
50+ must_encode = False if parse .query == urllib .parse .unquote (parse .query ) else True
4751 if params is None :
4852 combined_list = qs_list
4953 else :
50- combined_list = list (qs_list )
54+ # Needs to be encoded before sorting
55+ combined_list = [encode_pair (must_encode , key , value ) for (key , value ) in list (qs_list )]
5156 combined_list += params .items ()
5257
53- # Needs to be encoded before sorting
54- encoded_list = [encode_pair (key , value ) for (key , value ) in combined_list ]
55- sorted_list = sorted (encoded_list , key = lambda x :x )
58+ encoded_list = ["%s=%s" % (key , value ) for (key , value ) in combined_list ]
59+ sorted_list = sorted (encoded_list , key = lambda x : x )
5660
5761 return "&" .join (sorted_list )
5862
5963
60- def encode_pair (key , value ):
61- encoded_key = oauth_query_string_element_encode (key )
62- encoded_value = oauth_query_string_element_encode (value if isinstance (value , bytes ) else str (value ))
63- return "%s=%s" % (encoded_key , encoded_value )
64-
65- def oauth_query_string_element_encode (value ):
66- """
67- RFC 3986 encodes the value
64+ def encode_pair (must_encode , key , value ):
65+ encoded_key = percent_encode (key ) if must_encode else key .replace (' ' , '+' )
66+ value = value if isinstance (value , bytes ) else str (value )
67+ encoded_value = percent_encode (value ) if must_encode else value .replace (' ' , '+' )
68+ return encoded_key , encoded_value
6869
69- Note. This is based on RFC3986 but according to https://tools.ietf.org/html/rfc5849#section-3.6
70- it replaces space with %20 not "+".
71- """
72- encoded = quote (value )
73- encoded = str .replace (encoded , ':' , '%3A' )
74- encoded = str .replace (encoded , '+' , '%2B' )
75- encoded = str .replace (encoded , '*' , '%2A' )
76- return encoded
7770
7871def normalize_url (url ):
7972 """
@@ -83,40 +76,69 @@ def normalize_url(url):
8376
8477 # netloc should be lowercase
8578 netloc = parse .netloc .lower ()
86- if parse .scheme == "http" :
79+ if parse .scheme == "http" :
8780 if netloc .endswith (":80" ):
8881 netloc = netloc [:- 3 ]
8982
90- elif parse .scheme == "https" and netloc .endswith (":443" ):
83+ elif parse .scheme == "https" and netloc .endswith (":443" ):
9184 netloc = netloc [:- 4 ]
9285
9386 # add a '/' at the end of the netloc if there in no path
9487 if not parse .path :
95- netloc = netloc + "/"
88+ netloc = netloc + "/"
9689
9790 return "{}://{}{}" .format (parse .scheme , netloc , parse .path )
9891
9992
100- def uri_rfc3986_encode (value ):
101- """
102- RFC 3986 encodes the value
103- """
104- return quote (value , safe = '%' )
105-
106-
10793def sha256_encode (text ):
10894 """
10995 Returns the digest of SHA-256 of the text
11096 """
111- return hashlib .sha256 (str (text ).encode ('utf-8' )).digest ()
97+ _hash = hashlib .sha256
98+ if type (text ) is str :
99+ return _hash (text .encode ('utf8' )).digest ()
100+ elif type (text ) is bytes :
101+ return _hash (text ).digest ()
102+ elif not text :
103+ # Generally for calls where the payload is empty. Eg: get calls
104+ # Fix for AttributeError: 'NoneType' object has no attribute 'encode'
105+ return _hash ("" .encode ('utf8' )).digest ()
106+ else :
107+ return _hash (str (text ).encode ('utf-8' )).digest ()
112108
113109
114110def base64_encode (text ):
115111 """
116112 Base64 encodes the given input
117113 """
114+ if not isinstance (text , (bytes , bytearray )):
115+ text = bytes (text .encode ())
118116 encode = base64 .b64encode (text )
119- if isinstance (encode , (bytearray , bytes )):
120- return encode .decode ('ascii' )
121- else :
122- return encode
117+ return encode .decode ('ascii' )
118+
119+
120+ def percent_encode (text ):
121+ """
122+ Percent encodes a string as per https://tools.ietf.org/html/rfc3986
123+ """
124+ if text is None :
125+ return ''
126+ text = text .encode ('utf-8' ) if isinstance (text , str ) else text
127+ text = urllib .parse .quote (text , safe = b'~' )
128+ return text .replace ('+' , '%20' ).replace ('*' , '%2A' ).replace ('%7E' , '~' )
129+
130+
131+ def get_nonce (length = 16 ):
132+ """
133+ Returns a random string of length=@length
134+ """
135+ characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
136+ charlen = len (characters )
137+ return "" .join ([characters [SystemRandom ().randint (0 , charlen - 1 )] for _ in range (0 , length )])
138+
139+
140+ def get_timestamp ():
141+ """
142+ Returns the UTC timestamp (seconds passed since epoch)
143+ """
144+ return int (time .time ())
0 commit comments