diff --git a/glare/common/utils.py b/glare/common/utils.py index b7f7570..765380c 100644 --- a/glare/common/utils.py +++ b/glare/common/utils.py @@ -50,30 +50,6 @@ LOG = logging.getLogger(__name__) GLARE_TEST_SOCKET_FD_STR = 'GLARE_TEST_SOCKET_FD' -def chunkreadable(iter, chunk_size=65536): - """Wrap a readable iterator with a reader yielding chunks of - a preferred size, otherwise leave iterator unchanged. - - :param iter: an iter which may also be readable - :param chunk_size: maximum size of chunk - """ - return chunkiter(iter, chunk_size) if hasattr(iter, 'read') else iter - - -def chunkiter(fp, chunk_size=65536): - """Return an iterator to a file-like obj which yields fixed size chunks. - - :param fp: a file-like object - :param chunk_size: maximum size of chunk - """ - while True: - chunk = fp.read(chunk_size) - if chunk: - yield chunk - else: - break - - def cooperative_iter(iter): """Return an iterator which schedules after each iteration. This can prevent eventlet thread starvation. @@ -174,7 +150,7 @@ class CooperativeReader(object): # check should never fire. Regardless it still worths to # make the check, as the code may be reused somewhere else. if len(result) >= MAX_COOP_READER_BUFFER_SIZE: - raise exception.LimitExceeded() + raise exception.RequestEntityTooLarge() self.position += len(chunk) else: try: diff --git a/glare/tests/unit/test_utils.py b/glare/tests/unit/test_utils.py index d927560..aa836fc 100644 --- a/glare/tests/unit/test_utils.py +++ b/glare/tests/unit/test_utils.py @@ -13,10 +13,16 @@ # License for the specific language governing permissions and limitations # under the License. -from glare.common import exception as exc -from glare.tests.unit import base +import os +import tempfile -import glare.common.utils as utils +import mock +from OpenSSL import crypto +import six + +from glare.common import exception as exc +from glare.common import utils +from glare.tests.unit import base class TestUtils(base.BaseTestCase): @@ -39,14 +45,252 @@ class TestUtils(base.BaseTestCase): self.assertRaises(exc.InvalidParameterValue, utils.validate_quotes, '"The quote is not closed') + def test_no_4bytes_params(self): + @utils.no_4byte_params + def test_func(*args, **kwargs): + return args, kwargs + + bad_char = u'\U0001f62a' + + # params without 4bytes unicode are okay + args, kwargs = test_func('val1', param='val2') + self.assertEqual(('val1',), args) + self.assertEqual({'param': 'val2'}, kwargs) + + # test various combinations with bad param + self.assertRaises(exc.BadRequest, test_func, + bad_char) + self.assertRaises(exc.BadRequest, test_func, + **{bad_char: 'val1'}) + self.assertRaises(exc.BadRequest, test_func, + **{'param': bad_char}) -class TestUtilsDictDiff(base.BaseTestCase): - """tests for utils.DictDiffer class""" def test_str_repr(self): past_dict = {"a": 1, "b": 2, "d": 4} current_dic = {"b": 2, "d": "different value!", "e": "new!"} dict_diff = utils.DictDiffer(current_dic, past_dict) + + self.assertEqual({'a'}, dict_diff.removed()) + self.assertEqual({'b'}, dict_diff.unchanged()) + self.assertEqual({'d'}, dict_diff.changed()) + self.assertEqual({'e'}, dict_diff.added()) + expected_dict_str = "\nResult output:\n\tAdded keys: " \ "e\n\tRemoved keys:" \ " a\n\tChanged keys: d\n\tUnchanged keys: b\n" self.assertEqual(str(dict_diff), expected_dict_str) + + +class TestReaders(base.BaseTestCase): + """Test various readers in glare.common.utils""" + + def test_cooperative_reader_iterator(self): + """Ensure cooperative reader class accesses all bytes of file""" + BYTES = 1024 + bytes_read = 0 + with tempfile.TemporaryFile('w+') as tmp_fd: + tmp_fd.write('*' * BYTES) + tmp_fd.seek(0) + for chunk in utils.CooperativeReader(tmp_fd): + bytes_read += len(chunk) + + self.assertEqual(BYTES, bytes_read) + + def test_cooperative_reader_explicit_read(self): + BYTES = 1024 + bytes_read = 0 + with tempfile.TemporaryFile('w+') as tmp_fd: + tmp_fd.write('*' * BYTES) + tmp_fd.seek(0) + reader = utils.CooperativeReader(tmp_fd) + byte = reader.read(1) + while len(byte) != 0: + bytes_read += 1 + byte = reader.read(1) + + self.assertEqual(BYTES, bytes_read) + + def test_cooperative_reader_no_read_method(self): + BYTES = 1024 + stream = [b'*'] * BYTES + reader = utils.CooperativeReader(stream) + bytes_read = 0 + byte = reader.read() + while len(byte) != 0: + bytes_read += 1 + byte = reader.read() + + self.assertEqual(BYTES, bytes_read) + + # some data may be left in the buffer + reader = utils.CooperativeReader(stream) + reader.buffer = 'some data' + buffer_string = reader.read() + self.assertEqual('some data', buffer_string) + + def test_cooperative_reader_no_read_method_buffer_size(self): + # Decrease buffer size to 1000 bytes to test its overflow + with mock.patch('glare.common.utils.MAX_COOP_READER_BUFFER_SIZE', + 1000): + BYTES = 1024 + stream = [b'*'] * BYTES + reader = utils.CooperativeReader(stream) + # Reading 1001 bytes to the buffer leads to 413 error + self.assertRaises(exc.RequestEntityTooLarge, reader.read, 1001) + + def test_cooperative_reader_of_iterator(self): + """Ensure cooperative reader supports iterator backends too""" + data = b'abcdefgh' + data_list = [data[i:i + 1] * 3 for i in range(len(data))] + reader = utils.CooperativeReader(data_list) + chunks = [] + while True: + chunks.append(reader.read(3)) + if chunks[-1] == b'': + break + meat = b''.join(chunks) + self.assertEqual(b'aaabbbcccdddeeefffggghhh', meat) + + def test_cooperative_reader_of_iterator_stop_iteration_err(self): + """Ensure cooperative reader supports iterator backends too""" + reader = utils.CooperativeReader([l * 3 for l in '']) + chunks = [] + while True: + chunks.append(reader.read(3)) + if chunks[-1] == b'': + break + meat = b''.join(chunks) + self.assertEqual(b'', meat) + + def _create_generator(self, chunk_size, max_iterations): + chars = b'abc' + iteration = 0 + while True: + index = iteration % len(chars) + chunk = chars[index:index + 1] * chunk_size + yield chunk + iteration += 1 + if iteration >= max_iterations: + raise StopIteration() + + def _test_reader_chunked(self, chunk_size, read_size, max_iterations=5): + generator = self._create_generator(chunk_size, max_iterations) + reader = utils.CooperativeReader(generator) + result = bytearray() + while True: + data = reader.read(read_size) + if len(data) == 0: + break + self.assertLessEqual(len(data), read_size) + result += data + expected = (b'a' * chunk_size + + b'b' * chunk_size + + b'c' * chunk_size + + b'a' * chunk_size + + b'b' * chunk_size) + self.assertEqual(expected, bytes(result)) + + def test_cooperative_reader_preserves_size_chunk_less_then_read(self): + self._test_reader_chunked(43, 101) + + def test_cooperative_reader_preserves_size_chunk_equals_read(self): + self._test_reader_chunked(1024, 1024) + + def test_cooperative_reader_preserves_size_chunk_more_then_read(self): + chunk_size = 16 * 1024 * 1024 # 16 Mb, as in remote http source + read_size = 8 * 1024 # 8k, as in httplib + self._test_reader_chunked(chunk_size, read_size) + + def test_limiting_reader(self): + """Ensure limiting reader class accesses all bytes of file""" + BYTES = 1024 + bytes_read = 0 + data = six.BytesIO(b"*" * BYTES) + for chunk in utils.LimitingReader(data, BYTES): + bytes_read += len(chunk) + + self.assertEqual(BYTES, bytes_read) + + bytes_read = 0 + data = six.BytesIO(b"*" * BYTES) + reader = utils.LimitingReader(data, BYTES) + byte = reader.read(1) + while len(byte) != 0: + bytes_read += 1 + byte = reader.read(1) + + self.assertEqual(BYTES, bytes_read) + + def test_limiting_reader_fails(self): + """Ensure limiting reader class throws exceptions if limit exceeded""" + BYTES = 1024 + + def _consume_all_iter(): + bytes_read = 0 + data = six.BytesIO(b"*" * BYTES) + for chunk in utils.LimitingReader(data, BYTES - 1): + bytes_read += len(chunk) + + self.assertRaises(exc.RequestEntityTooLarge, _consume_all_iter) + + def _consume_all_read(): + bytes_read = 0 + data = six.BytesIO(b"*" * BYTES) + reader = utils.LimitingReader(data, BYTES - 1) + byte = reader.read(1) + while len(byte) != 0: + bytes_read += 1 + byte = reader.read(1) + + self.assertRaises(exc.RequestEntityTooLarge, _consume_all_read) + + def test_blob_iterator(self): + BYTES = 1024 + bytes_read = 0 + stream = [b'*'] * BYTES + for chunk in utils.BlobIterator(stream, 64): + bytes_read += len(chunk) + + self.assertEqual(BYTES, bytes_read) + + +class TestKeyCert(base.BaseTestCase): + + def test_validate_key_cert_key(self): + var_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), + '../', 'var')) + keyfile = os.path.join(var_dir, 'privatekey.key') + certfile = os.path.join(var_dir, 'certificate.crt') + utils.validate_key_cert(keyfile, certfile) + + def test_validate_key_cert_no_private_key(self): + with tempfile.NamedTemporaryFile('w+') as tmpf: + self.assertRaises(RuntimeError, + utils.validate_key_cert, + "/not/a/file", tmpf.name) + + def test_validate_key_cert_cert_cant_read(self): + with tempfile.NamedTemporaryFile('w+') as keyf: + with tempfile.NamedTemporaryFile('w+') as certf: + os.chmod(certf.name, 0) + self.assertRaises(RuntimeError, + utils.validate_key_cert, + keyf.name, certf.name) + + def test_validate_key_cert_key_cant_read(self): + with tempfile.NamedTemporaryFile('w+') as keyf: + with tempfile.NamedTemporaryFile('w+') as certf: + os.chmod(keyf.name, 0) + self.assertRaises(RuntimeError, + utils.validate_key_cert, + keyf.name, certf.name) + + def test_validate_key_cert_key_crypto_error(self): + var_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), + '../', 'var')) + keyfile = os.path.join(var_dir, 'privatekey.key') + certfile = os.path.join(var_dir, 'certificate.crt') + with mock.patch('OpenSSL.crypto.verify', side_effect=crypto.Error): + self.assertRaises(RuntimeError, + utils.validate_key_cert, + keyfile, certfile) diff --git a/glare/tests/var/certificate.crt b/glare/tests/var/certificate.crt new file mode 100644 index 0000000..e5f415a --- /dev/null +++ b/glare/tests/var/certificate.crt @@ -0,0 +1,92 @@ +# > openssl x509 -in glare/tests/var/certificate.crt -noout -text +# Certificate: +# Data: +# Version: 1 (0x0) +# Serial Number: 1 (0x1) +# Signature Algorithm: sha1WithRSAEncryption +# Issuer: C=AU, ST=Some-State, O=OpenStack, OU=Glare, CN=Glare CA +# Validity +# Not Before: Feb 2 20:22:13 2015 GMT +# Not After : Jan 31 20:22:13 2024 GMT +# Subject: C=AU, ST=Some-State, O=OpenStack, OU=Glare, CN=127.0.0.1 +# Subject Public Key Info: +# Public Key Algorithm: rsaEncryption +# RSA Public Key: (4096 bit) +# Modulus (4096 bit): +# 00:9f:44:13:51:de:e9:5a:f7:ac:33:2a:1a:4c:91: +# a1:73:bc:f3:a6:d3:e6:59:ae:e8:e2:34:68:3e:f4: +# 40:c1:a1:1a:65:9a:a3:67:e9:2c:b9:79:9c:00:b1: +# 7c:c1:e6:9e:de:47:bf:f1:cb:f2:73:d4:c3:62:fe: +# 82:90:6f:b4:75:ca:7e:56:8f:99:3d:06:51:3c:40: +# f4:ff:74:97:4f:0d:d2:e6:66:76:8d:97:bf:89:ce: +# fe:b2:d7:89:71:f2:a0:d9:f5:26:7c:1a:7a:bf:2b: +# 8f:72:80:e7:1f:4d:4a:40:a3:b9:9e:33:f6:55:e0: +# 40:2b:1e:49:e4:8c:71:9d:11:32:cf:21:41:e1:13: +# 28:c6:d6:f6:e0:b3:26:10:6d:5b:63:1d:c3:ee:d0: +# c4:66:63:38:89:6b:8f:2a:c2:bd:4f:e4:bc:03:8f: +# a2:f2:5c:1d:73:11:9c:7b:93:3d:d6:a3:d1:2d:cd: +# 64:23:24:bc:65:3c:71:20:28:60:a0:ea:fe:77:0e: +# 1d:95:36:76:ad:e7:2f:1c:27:62:55:e3:9d:11:c1: +# fb:43:3e:e5:21:ac:fd:0e:7e:3d:c9:44:d2:bd:6f: +# 89:7e:0f:cb:88:54:57:fd:8d:21:c8:34:e1:47:01: +# 28:0f:45:a1:7e:60:1a:9c:4c:0c:b8:c1:37:2d:46: +# ab:18:9e:ca:49:d3:77:b7:92:3a:d2:7f:ca:d5:02: +# f1:75:81:66:39:51:aa:bc:d7:f0:91:23:69:e8:71: +# ae:44:76:5e:87:54:eb:72:fc:ac:fd:60:22:e0:6a: +# e4:ad:37:b7:f6:e5:24:b4:95:2c:26:0e:75:a0:e9: +# ed:57:be:37:42:64:1f:02:49:0c:bd:5d:74:6d:e6: +# f2:da:5c:54:82:fa:fc:ff:3a:e4:1a:7a:a9:3c:3d: +# ee:b5:df:09:0c:69:c3:51:92:67:80:71:9b:10:8b: +# 20:ff:a2:5e:c5:f2:86:a0:06:65:1c:42:f9:91:24: +# 54:29:ed:7e:ec:db:4c:7b:54:ee:b1:25:1b:38:53: +# ae:01:b6:c5:93:1e:a3:4d:1b:e8:73:47:50:57:e8: +# ec:a0:80:53:b1:34:74:37:9a:c1:8c:14:64:2e:16: +# dd:a1:2e:d3:45:3e:2c:46:62:20:2a:93:7a:92:4c: +# b2:cc:64:47:ad:63:32:0b:68:0c:24:98:20:83:08: +# 35:74:a7:68:7a:ef:d6:84:07:d1:5e:d7:c0:6c:3f: +# a7:4a:78:62:a8:70:75:37:fb:ce:1f:09:1e:7c:11: +# 35:cc:b3:5a:a3:cc:3f:35:c9:ee:24:6f:63:f8:54: +# 6f:7c:5b:b4:76:3d:f2:81:6d:ad:64:66:10:d0:c4: +# 0b:2c:2f +# Exponent: 65537 (0x10001) +# Signature Algorithm: sha1WithRSAEncryption +# 5f:e8:a8:93:20:6c:0f:12:90:a6:e2:64:21:ed:63:0e:8c:e0: +# 0f:d5:04:13:4d:2a:e9:a5:91:b7:e4:51:94:bd:0a:70:4b:94: +# c7:1c:94:ed:d7:64:95:07:6b:a1:4a:bc:0b:53:b5:1a:7e:f1: +# 9c:12:59:24:5f:36:72:34:ca:33:ee:28:46:fd:21:e6:52:19: +# 0c:3d:94:6b:bd:cb:76:a1:45:7f:30:7b:71:f1:84:b6:3c:e0: +# ac:af:13:81:9c:0e:6e:3c:9b:89:19:95:de:8e:9c:ef:70:ac: +# 07:ae:74:42:47:35:50:88:36:ec:32:1a:55:24:08:f2:44:57: +# 67:fe:0a:bb:6b:a7:bd:bc:af:bf:2a:e4:dd:53:84:6b:de:1d: +# 2a:28:21:38:06:7a:5b:d8:83:15:65:31:6d:61:67:00:9e:1a: +# 61:85:15:a2:4c:9a:eb:6d:59:8e:34:ac:2c:d5:24:4e:00:ff: +# 30:4d:a3:d5:80:63:17:52:65:ac:7f:f4:0a:8e:56:a4:97:51: +# 39:81:ae:e8:cb:52:09:b3:47:b4:fd:1b:e2:04:f9:f2:76:e3: +# 63:ef:90:aa:54:98:96:05:05:a9:91:76:18:ed:5d:9e:6e:88: +# 50:9a:f7:2c:ce:5e:54:ba:15:ec:62:ff:5d:be:af:35:03:b1: +# 3f:32:3e:0e +-----BEGIN CERTIFICATE----- +MIIEKjCCAxICAQEwDQYJKoZIhvcNAQEFBQAwWzELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxEjAQBgNVBAoMCU9wZW5TdGFjazEPMA0GA1UECwwGR2xh +bmNlMRIwEAYDVQQDDAlHbGFuY2UgQ0EwHhcNMTUwMjAyMjAyMjEzWhcNMjQwMTMx +MjAyMjEzWjBbMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTESMBAG +A1UEChMJT3BlblN0YWNrMQ8wDQYDVQQLEwZHbGFuY2UxEjAQBgNVBAMTCTEyNy4w +LjAuMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ9EE1He6Vr3rDMq +GkyRoXO886bT5lmu6OI0aD70QMGhGmWao2fpLLl5nACxfMHmnt5Hv/HL8nPUw2L+ +gpBvtHXKflaPmT0GUTxA9P90l08N0uZmdo2Xv4nO/rLXiXHyoNn1Jnwaer8rj3KA +5x9NSkCjuZ4z9lXgQCseSeSMcZ0RMs8hQeETKMbW9uCzJhBtW2Mdw+7QxGZjOIlr +jyrCvU/kvAOPovJcHXMRnHuTPdaj0S3NZCMkvGU8cSAoYKDq/ncOHZU2dq3nLxwn +YlXjnRHB+0M+5SGs/Q5+PclE0r1viX4Py4hUV/2NIcg04UcBKA9FoX5gGpxMDLjB +Ny1GqxieyknTd7eSOtJ/ytUC8XWBZjlRqrzX8JEjaehxrkR2XodU63L8rP1gIuBq +5K03t/blJLSVLCYOdaDp7Ve+N0JkHwJJDL1ddG3m8tpcVIL6/P865Bp6qTw97rXf +CQxpw1GSZ4BxmxCLIP+iXsXyhqAGZRxC+ZEkVCntfuzbTHtU7rElGzhTrgG2xZMe +o00b6HNHUFfo7KCAU7E0dDeawYwUZC4W3aEu00U+LEZiICqTepJMssxkR61jMgto +DCSYIIMINXSnaHrv1oQH0V7XwGw/p0p4YqhwdTf7zh8JHnwRNcyzWqPMPzXJ7iRv +Y/hUb3xbtHY98oFtrWRmENDECywvAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAF/o +qJMgbA8SkKbiZCHtYw6M4A/VBBNNKumlkbfkUZS9CnBLlMcclO3XZJUHa6FKvAtT +tRp+8ZwSWSRfNnI0yjPuKEb9IeZSGQw9lGu9y3ahRX8we3HxhLY84KyvE4GcDm48 +m4kZld6OnO9wrAeudEJHNVCINuwyGlUkCPJEV2f+Crtrp728r78q5N1ThGveHSoo +ITgGelvYgxVlMW1hZwCeGmGFFaJMmuttWY40rCzVJE4A/zBNo9WAYxdSZax/9AqO +VqSXUTmBrujLUgmzR7T9G+IE+fJ242PvkKpUmJYFBamRdhjtXZ5uiFCa9yzOXlS6 +Fexi/12+rzUDsT8yPg4= +-----END CERTIFICATE----- diff --git a/glare/tests/var/privatekey.key b/glare/tests/var/privatekey.key new file mode 100644 index 0000000..c7e4cd1 --- /dev/null +++ b/glare/tests/var/privatekey.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAn0QTUd7pWvesMyoaTJGhc7zzptPmWa7o4jRoPvRAwaEaZZqj +Z+ksuXmcALF8weae3ke/8cvyc9TDYv6CkG+0dcp+Vo+ZPQZRPED0/3SXTw3S5mZ2 +jZe/ic7+steJcfKg2fUmfBp6vyuPcoDnH01KQKO5njP2VeBAKx5J5IxxnREyzyFB +4RMoxtb24LMmEG1bYx3D7tDEZmM4iWuPKsK9T+S8A4+i8lwdcxGce5M91qPRLc1k +IyS8ZTxxIChgoOr+dw4dlTZ2recvHCdiVeOdEcH7Qz7lIaz9Dn49yUTSvW+Jfg/L +iFRX/Y0hyDThRwEoD0WhfmAanEwMuME3LUarGJ7KSdN3t5I60n/K1QLxdYFmOVGq +vNfwkSNp6HGuRHZeh1Trcvys/WAi4GrkrTe39uUktJUsJg51oOntV743QmQfAkkM +vV10beby2lxUgvr8/zrkGnqpPD3utd8JDGnDUZJngHGbEIsg/6JexfKGoAZlHEL5 +kSRUKe1+7NtMe1TusSUbOFOuAbbFkx6jTRvoc0dQV+jsoIBTsTR0N5rBjBRkLhbd +oS7TRT4sRmIgKpN6kkyyzGRHrWMyC2gMJJgggwg1dKdoeu/WhAfRXtfAbD+nSnhi +qHB1N/vOHwkefBE1zLNao8w/NcnuJG9j+FRvfFu0dj3ygW2tZGYQ0MQLLC8CAwEA +AQKCAgBL4IvvymqUu0CgE6P57LvlvxS522R4P7uV4W/05jtfxJgl5fmJzO5Q4x4u +umB8pJn1vms1EHxPMQNxS1364C0ynSl5pepUx4i2UyAmAG8B680ZlaFPrgdD6Ykw +vT0vO2/kx0XxhFAMef1aiQ0TvaftidMqCwmGOlN393Mu3rZWJVZ2lhqj15Pqv4lY +3iD5XJBYdVrekTmwqf7KgaLwtVyqDoiAjdMM8lPZeX965FhmxR8oWh0mHR9gf95J +etMmdy6Km//+EbeS/HxWRnE0CD/RsQA7NmDFnXvmhsB6/j4EoHn5xB6ssbpGAxIg +JwlY4bUrKXpaEgE7i4PYFb1q5asnTDdUZYAGAGXSBbDiUZM2YOe1aaFB/SA3Y3K2 +47brnx7UXhAXSPJ16EZHejSeFbzZfWgj2J1t3DLk18Fpi/5AxxIy/N5J38kcP7xZ +RIcSV1QEasYUrHI9buhuJ87tikDBDFEIIeLZxlyeIdwmKrQ7Vzny5Ls94Wg+2UtI +XFLDak5SEugdp3LmmTJaugF+s/OiglBVhcaosoKRXb4K29M7mQv2huEAerFA14Bd +dp2KByd8ue+fJrAiSxhAyMDAe/uv0ixnmBBtMH0YYHbfUIgl+kR1Ns/bxrJu7T7F +kBQWZV4NRbSRB+RGOG2/Ai5jxu0uLu3gtHMO4XzzElWqzHEDoQKCAQEAzfaSRA/v +0831TDL8dmOCO61TQ9GtAa8Ouj+SdyTwk9f9B7NqQWg7qdkbQESpaDLvWYiftoDw +mBFHLZe/8RHBaQpEAfbC/+DO6c7O+g1/0Cls33D5VaZOzFnnbHktT3r5xwkZfVBS +aPPWl/IZOU8TtNqujQA+mmSnrJ7IuXSsBVq71xgBQT9JBZpUcjZ4eQducmtC43CP +GqcSjq559ZKc/sa3PkAtNlKzSUS1abiMcJ86C9PgQ9gOu7y8SSqQ3ivZkVM99rxm +wo8KehCcHOPOcIUQKmx4Bs4V3chm8rvygf3aanUHi83xaMeFtIIuOgAJmE9wGQeo +k0UGvKBUDIenfwKCAQEAxfVFVxMBfI4mHrgTj/HOq7GMts8iykJK1PuELU6FZhex +XOqXRbQ5dCLsyehrKlVPFqUENhXNHaOQrCOZxiVoRje2PfU/1fSqRaPxI7+W1Fsh +Fq4PkdJ66NJZJkK5NHwE8SyQf+wpLdL3YhY5LM3tWdX5U9Rr6N8qelE3sLPssAak +1km4/428+rkp1BlCffr3FyL0KJmOYfMiAr8m6hRZWbhkvm5YqX1monxUrKdFJ218 +dxzyniqoS1yU5RClY6783dql1UO4AvxpzpCPYDFIwbEb9zkUo0przhmi4KzyxknB +/n/viMWzSnsM9YbakH6KunDTUteme1Dri3Drrq9TUQKCAQAVdvL7YOXPnxFHZbDl +7azu5ztcQAfVuxa/1kw/WnwwDDx0hwA13NUK+HNcmUtGbrh/DjwG2x032+UdHUmF +qCIN/mHkCoF8BUPLHiB38tw1J3wPNUjm4jQoG96AcYiFVf2d/pbHdo2AHplosHRs +go89M+UpELN1h7Ppy4qDuWMME86rtfa7hArqKJFQbdjUVC/wgLkx1tMzJeJLOGfB +bgwqiS8jr7CGjsvcgOqfH/qS6iU0glpG98dhTWQaA/OhE9TSzmgQxMW41Qt0eTKr +2Bn1pAhxQ2im3Odue6ou9eNqJLiUi6nDqizUjKakj0SeCs71LqIyGZg58OGo2tSn +kaOlAoIBAQCE/fO4vQcJpAJOLwLNePmM9bqAcoZ/9auKjPNO8OrEHPTGZMB+Tscu +k+wa9a9RgICiyPgcUec8m0+tpjlAGo+EZRdlZqedWUMviCWQC74MKrD/KK9DG3IB +ipfkEX2VmiBD2tm1Z3Z+17XlSuLci/iCmzNnM1XP3GYQSRIt/6Lq23vQjzTfU1z7 +4HwOh23Zb0qjW5NG12sFuS9HQx6kskkY8r2UBlRAggP686Z7W+EkzPSKnYMN6cCo +6KkLf3RtlPlDHwq8TUOJlgSLhykbyeCEaDVOkSWhUnU8wJJheS+dMZ5IGbFWZOPA +DQ02woOCAdG30ebXSBQL0uB8DL/52sYRAoIBAHtW3NomlxIMqWX8ZYRJIoGharx4 +ikTOR/jeETb9t//n6kV19c4ICiXOQp062lwEqFvHkKzxKECFhJZuwFc09hVxUXxC +LJjvDfauHWFHcrDTWWbd25CNeZ4Sq79GKf+HJ+Ov87WYcjuBFlCh8ES+2N4WZGCn +B5oBq1g6E4p1k6xA5eE6VRiHPuFH8N9t1x6IlCZvZBhuVWdDrDd4qMSDEUTlcxSY +mtcAIXTPaPcdb3CjdE5a38r59x7dZ/Te2K7FKETffjSmku7BrJITz3iXEk+sn8ex +o3mdnFgeQ6/hxvMGgdK2qNb5ER/s0teFjnfnwHuTSXngMDIDb3kLL0ecWlQ= +-----END RSA PRIVATE KEY-----