From b1bb63bcfb49e60ac9bbf0e75f132beb504740b9 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:56:43 +0530 Subject: [PATCH 1/8] DEVTOOLING-997 --- .../extensions/abstract_http_client.py | 15 ++ .../extensions/default_http_client.py | 79 +++++++++ .../extensions/http_request_options.py | 73 ++++++++ .../purecloudpython/scripts/SdkTests_mtls.py | 158 ++++++++++++++++++ .../sdk/purecloudpython/scripts/compile.sh | 1 + .../scripts/mtls-certs/ca-chain.cert.pem | 65 +++++++ .../scripts/mtls-certs/localhost.cert.pem | 28 ++++ .../scripts/mtls-certs/localhost.key.pem | 28 ++++ .../templates/api_client.mustache | 135 +++++++-------- .../templates/configuration.mustache | 51 ++++++ .../purecloudpython/templates/rest.mustache | 57 +++---- 11 files changed, 580 insertions(+), 110 deletions(-) create mode 100644 resources/sdk/purecloudpython/extensions/abstract_http_client.py create mode 100644 resources/sdk/purecloudpython/extensions/default_http_client.py create mode 100644 resources/sdk/purecloudpython/extensions/http_request_options.py create mode 100644 resources/sdk/purecloudpython/scripts/SdkTests_mtls.py create mode 100644 resources/sdk/purecloudpython/scripts/mtls-certs/ca-chain.cert.pem create mode 100644 resources/sdk/purecloudpython/scripts/mtls-certs/localhost.cert.pem create mode 100644 resources/sdk/purecloudpython/scripts/mtls-certs/localhost.key.pem diff --git a/resources/sdk/purecloudpython/extensions/abstract_http_client.py b/resources/sdk/purecloudpython/extensions/abstract_http_client.py new file mode 100644 index 000000000..69072278a --- /dev/null +++ b/resources/sdk/purecloudpython/extensions/abstract_http_client.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +class AbstractHttpClient(ABC): + def __init__(self): + self.timeout = 16000 + self.https_agent = None #it is a http proxy agent will be used later + + def set_timeout(self, timeout): + if not isinstance(timeout, (int, float)): + raise ValueError("The 'timeout' property must be a number") + self.timeout = timeout + + @abstractmethod + def request(self, http_request_options): + raise NotImplementedError("method must be implemented") diff --git a/resources/sdk/purecloudpython/extensions/default_http_client.py b/resources/sdk/purecloudpython/extensions/default_http_client.py new file mode 100644 index 000000000..0c8bb6289 --- /dev/null +++ b/resources/sdk/purecloudpython/extensions/default_http_client.py @@ -0,0 +1,79 @@ +from .abstract_http_client import AbstractHttpClient +from .http_request_options import HttpRequestOptions +from .rest import RESTClientObject +from .configuration import Configuration + +class DefaultHttpClient(AbstractHttpClient): + def __init__(self, timeout=None, https_agent=None): + super().__init__() + if timeout is not None: + self.set_timeout(timeout) + + Configuration().create_mtls_or_ssl_context() + + #This object "self.rest_client" handles all REST API communication (requests/responses). + #Implementation: rest.py (templates/rest.mustache). + self.rest_client = RESTClientObject() + + def request(self, http_request_options): + if not isinstance(http_request_options, HttpRequestOptions): + raise ValueError("httpRequestOptions must be an instance of HttpRequestOptions") + config = self.to_rest_client_config(http_request_options) + + if config['method'] == "GET": + return self.rest_client.GET(config['url'], + query_params=config['query_params'], + headers=config['headers']) + elif config['method'] == "HEAD": + return self.rest_client.HEAD(config['url'], + query_params=config['query_params'], + headers=config['headers']) + elif config['method'] == "OPTIONS": + return self.rest_client.OPTIONS(config['url'], + query_params=config['query_params'], + headers=config['headers'], + post_params=config['post_params'], + body=config['body']) + elif config['method'] == "POST": + return self.rest_client.POST(config['url'], + query_params=config['query_params'], + headers=config['headers'], + post_params=config['post_params'], + body=config['body']) + elif config['method'] == "PUT": + return self.rest_client.PUT(config['url'], + query_params=config['query_params'], + headers=config['headers'], + post_params=config['post_params'], + body=config['body']) + elif config['method'] == "PATCH": + return self.rest_client.PATCH(config['url'], + query_params=config['query_params'], + headers=config['headers'], + post_params=config['post_params'], + body=config['body']) + elif config['method'] == "DELETE": + return self.rest_client.DELETE(config['url'], + query_params=config['query_params'], + headers=config['headers'], + body=config['body']) + else: + raise ValueError( + "http method must be `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`, `HEAD` or `TRACE`." + ) + + + def to_rest_client_config(self, http_request_options): + if not http_request_options.url or not http_request_options.method: + raise ValueError("Mandatory fields 'url' and 'method' must be set before making a request") + + config = { + "url": http_request_options.url, + "method": http_request_options.method, + "headers": http_request_options.headers or {}, + "query_params": http_request_options.query_params or {}, + "post_params": http_request_options.post_params or {}, + "body": http_request_options.body or None, + "timeout": self.timeout if self.timeout else None, + } + return config diff --git a/resources/sdk/purecloudpython/extensions/http_request_options.py b/resources/sdk/purecloudpython/extensions/http_request_options.py new file mode 100644 index 000000000..fce63cf67 --- /dev/null +++ b/resources/sdk/purecloudpython/extensions/http_request_options.py @@ -0,0 +1,73 @@ +class HttpRequestOptions: + + def __init__(self, *, url, method, headers=None, query_params=None, post_params=None, body=None, timeout=16000): + self.url = url + self.method = method + self.headers = headers if headers else {} + self.query_params = query_params if query_params else {} + self.post_params = post_params if post_params else {} + self.body = body if body else None + self.timeout = timeout + + #getters + @property + def url(self): + return self._url + + @property + def method(self): + return self._method + + @property + def headers(self): + return self._headers + + @property + def query_params(self): + return self._query_params + + @property + def post_params(self): + return self._post_params + + @property + def body(self): + return self._body + + @property + def timeout(self): + return self._timeout + + #setters + @url.setter + def url(self, url): + if not url: + raise ValueError("The 'url' property is required") + self._url = url + + @method.setter + def method(self, method): + valid_methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD','TRACE'] + if not method or method.upper() not in valid_methods: + raise ValueError("The 'method' property is invalid or missing") + self._method = method.upper() + + @headers.setter + def headers(self, headers): + self._headers = headers + + @query_params.setter + def query_params(self, query_params): + self._query_params = query_params + + @post_params.setter + def post_params(self, post_params): + self._post_params = post_params + + @body.setter + def body(self, body): + self._body = body + + @timeout.setter + def timeout(self, timeout): + self._timeout = timeout diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py new file mode 100644 index 000000000..acbcd5c06 --- /dev/null +++ b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py @@ -0,0 +1,158 @@ +import base64, imp, os, requests, sys, unittest, uuid, time +from pprint import pprint +from retry import retry + +# Load SDK from local build +sys.path.append('../../../../output/purecloudpython/build/build/lib') +import PureCloudPlatformClientV2 + + +class SdkTestsMTLS(unittest.TestCase): + lastResult = None + + userId = None + userEmail = None + userName = 'Python SDK Tester' + userDepartment = 'Ministry of Testing' + userProfileSkill = 'Testmaster' + busyPresenceId = '31fe3bac-dea6-44b7-bed7-47f91660a1a0' + availablePresenceId = '6a3af858-942f-489d-9700-5f9bcdcdae9b' + + def setUp(self): + # Skip if there has been a failure + if SdkTestsMTLS.lastResult != None and (len(SdkTestsMTLS.lastResult.failures) > 0 or len(SdkTestsMTLS.lastResult.errors) > 0): + print("=== WARNING: Previous test failed, skipping current test ===", flush=True) + self.skipTest('Previous test failed') + + def run(self, result=None): + # Store this execution's result as the last one + SdkTestsMTLS.lastResult = result + + # Run the test + unittest.TestCase.run(self, result) + + def test_1_trace_basic_information(self): + print("=== ENTERING test_1_trace_basic_information() ===") + print('PURECLOUD_ENVIRONMENT=%s' % (os.environ.get('PURECLOUD_ENVIRONMENT'))) + self.assertIsNotNone(os.environ.get('PURECLOUD_ENVIRONMENT')) + + print('PURECLOUD_CLIENT_ID=%s' % (os.environ.get('PURECLOUD_CLIENT_ID'))) + self.assertIsNotNone(os.environ.get('PURECLOUD_CLIENT_ID')) + + self.assertIsNotNone(os.environ.get('PURECLOUD_CLIENT_SECRET')) + + SdkTestsMTLS.userEmail = '%s@%s' % (uuid.uuid4(), os.environ.get('PURECLOUD_ENVIRONMENT')) + print(SdkTestsMTLS.userEmail) + + print(PureCloudPlatformClientV2) + print("=== EXITING test_1_trace_basic_information() ===\n") + + def test_2_mtls_gw_authenticate(self): + print("=== ENTERING test_mtls_authenticate() ===") + environment = os.environ.get('PURECLOUD_ENVIRONMENT'); + region = self.purecloudregiontest(environment) + print(f"Using region: {region}") + if isinstance(region,PureCloudPlatformClientV2.PureCloudRegionHosts): + PureCloudPlatformClientV2.configuration.host = region.get_api_host() + print(f"API host set to: {region.get_api_host()}") + elif isinstance(region,str): + PureCloudPlatformClientV2.configuration.host = 'https://api.%s' % (environment) + print("Environment not found in PureCloudRegionHosts defaulting to string value") + print(f"API host set to: https://api.{environment}") + + #Set the client certificate and key files here. For CA verification, the default Mozilla store is used. + #To use a custom CA, provide its certificate as the third parameter. + cacert = "mtls-certs/ca-chain.cert.pem" + cert = "mtls-certs/localhost.cert.pem" + key = "mtls-certs/localhost.key.pem" + #Join the script full path with the certificate and key files + cacert = os.path.join(os.path.dirname(__file__), cacert) + cert = os.path.join(os.path.dirname(__file__), cert) + key = os.path.join(os.path.dirname(__file__), key) + PureCloudPlatformClientV2.configuration.set_mtls_certificates(cert, key, cacert) + + # Authenticate with client credentials and pass the apiclient instance into the usersapi + print("Authenticating with client credentials...") + SdkTestsMTLS.apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() + SdkTestsMTLS.apiclient_mtls.set_gateway("locahlost","https",4027) + SdkTestsMTLS.apiclient_mtls.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) + SdkTestsMTLS.users_api_mtls = PureCloudPlatformClientV2.UsersApi(SdkTestsMTLS.apiclient_mtls) + print(f"Authentication successful access_token: {SdkTestsMTLS.apiclient_mtls}") + self.create_user(SdkTestsMTLS.users_api_mtls) + self.delete_user(SdkTestsMTLS.users_api_mtls) + print("=== EXITING test_mtls_authenticate() ===\n") + + def test_3_proxy_authenticate(self): + print("=== ENTERING test_proxy_authenticate() ===") + environment = os.environ.get('PURECLOUD_ENVIRONMENT') + region = self.purecloudregiontest(environment) + print(f"Using region: {region}") + if isinstance(region,PureCloudPlatformClientV2.PureCloudRegionHosts): + PureCloudPlatformClientV2.configuration.host = region.get_api_host() + print(f"API host set to: {region.get_api_host()}") + elif isinstance(region,str): + PureCloudPlatformClientV2.configuration.host = 'https://api.%s' % (environment) + print("Environment not found in PureCloudRegionHosts defaulting to string value") + print(f"API host set to: https://api.{environment}") + + #Proxy setting and the request should go via proxy. + PureCloudPlatformClientV2.configuration.proxy="http://localhost:4001" + + # Authenticate with client credentials and pass the apiclient instance into the usersapi + print("Authenticating with client credentials...") + SdkTestsMTLS.apiclient_proxy = PureCloudPlatformClientV2.api_client.ApiClient() + SdkTestsMTLS.apiclient_proxy.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) + SdkTestsMTLS.users_api_proxy = PureCloudPlatformClientV2.UsersApi(SdkTestsMTLS.apiclient_proxy) + print(f"Authentication successful access_token: {SdkTestsMTLS.apiclient_proxy}") + self.create_user(SdkTestsMTLS.users_api_proxy) + self.delete_user(SdkTestsMTLS.users_api_proxy) + print("=== EXITING test_proxy_authenticate() ===\n") + + def create_user(self, users_api): + print("=== ENTERING create_user() ===") + body = PureCloudPlatformClientV2.CreateUser() + body.name = SdkTestsMTLS.userName + body.email = SdkTestsMTLS.userEmail + body.password = '%s!@#$1234asdfASDF' % (uuid.uuid4()) + print(f"Creating user with name: {body.name}, email: {body.email}") + + user = users_api.post_users(body) + print(f"User created successfully") + + SdkTestsMTLS.userId = user.id + print(f"User ID: {SdkTestsMTLS.userId}") + self.assertEqual(user.name, SdkTestsMTLS.userName) + self.assertEqual(user.email, SdkTestsMTLS.userEmail) + print(SdkTestsMTLS.userId) + print("=== EXITING create_user() ===\n") + + def delete_user(self, users_api): + print("=== ENTERING delete_user() ===") + print(f"Deleting user {SdkTestsMTLS.userId}") + users_api.delete_user(SdkTestsMTLS.userId) + print(f"User {SdkTestsMTLS.userId} deleted successfully") + print("=== EXITING delete_user() ===\n") + + def purecloudregiontest(self,x): + print(f"=== ENTERING purecloudregiontest() with environment {x} ===") + result = { + 'mypurecloud.com': PureCloudPlatformClientV2.PureCloudRegionHosts.us_east_1, + 'mypurecloud.ie': PureCloudPlatformClientV2.PureCloudRegionHosts.eu_west_1, + 'mypurecloud.com.au': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_southeast_2, + 'mypurecloud.jp': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_northeast_1, + 'mypurecloud.de': PureCloudPlatformClientV2.PureCloudRegionHosts.eu_central_1, + 'usw2.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.us_west_2, + 'cac1.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.ca_central_1, + 'apne2.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_northeast_2, + 'euw2.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.eu_west_2, + 'aps1.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_south_1, + 'use2.us-gov-pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.us_east_2 + }.get(x,x) + print(f"=== EXITING purecloudregiontest() with result: {result} ===") + return result + + +if __name__ == '__main__': + unittest.sortTestMethodsUsing(None) + unittest.main() + \ No newline at end of file diff --git a/resources/sdk/purecloudpython/scripts/compile.sh b/resources/sdk/purecloudpython/scripts/compile.sh index af1587d84..87bb2b0e9 100644 --- a/resources/sdk/purecloudpython/scripts/compile.sh +++ b/resources/sdk/purecloudpython/scripts/compile.sh @@ -29,3 +29,4 @@ echo "Install retry..." python3.6 -m pip install --user -U retry echo "Run unit tests" python3.6 -m unittest SdkTests +python3.6 -m unittest SdkTests_mtls diff --git a/resources/sdk/purecloudpython/scripts/mtls-certs/ca-chain.cert.pem b/resources/sdk/purecloudpython/scripts/mtls-certs/ca-chain.cert.pem new file mode 100644 index 000000000..c794e60ea --- /dev/null +++ b/resources/sdk/purecloudpython/scripts/mtls-certs/ca-chain.cert.pem @@ -0,0 +1,65 @@ +-----BEGIN CERTIFICATE----- +MIIFeTCCA2GgAwIBAgIDEAISMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlVT +MQ8wDQYDVQQIDAZEZW5pYWwxFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQK +DANEaXMxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNDExMjExMTE1NDVaFw0zNDEx +MTkxMTE1NDVaMEAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZEZW5pYWwxDDAKBgNV +BAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOC +Ag8AMIICCgKCAgEAuqkQkp9XMye38B9Fo9miym2LdHlc89UV02nQ1y5L2Wx7id4p +xmF+fmb+LWSJZKwGEi8v0f8/FvBbnssxy1IZCe1GCEG5o9ZOb2xML6B22jPwNBrs +o9ZwEWfF7nmRN9naUQtakOnsuV8MaK6h3fe7s9xHkjCfvlY0fp1saLsDGj+J4pK0 +FWwvVNzRcxYadUQyFZN/knGhm9M86/RA6S0OOG274R/0KaWD1F/85fsjbAYZEh4V +6dZ+8TSdWz2u1oOjQUGmW9muUzYzEDAbz4BjFTTnnDUJuWOrB3ISRHL1eCJkjgHr +x53BV53SiOQ3YpK6ZqHyBgxNVB6AdkpQrnlqItarQ1HPbvoTgY+DuYS52xNwBx0Y +kcaRyKrLQEMQws+brixtA7mIzOVn2uhl8s+7nPegtovqvKPl1EUHzhF6cXH1eifb +hnBQ2qILEXrPmpxSup9Mk3cQbmtZqXLTs/qUHOczdjRxk+UgYrimVQD+vFujeiir +G5N0ktkle7pOig9nDd1XCWvFyeMCsBW4opyndNqf2DS9mrksTTpuqXZxlYrDyAre +x8LmPsxFUfldDIQn5iyYiIXrVAupBHraR3g+jSee9UetkR92mY0HHVJ990KxzTo1 +2/WmiYYIzX7k+b+/9ITFP6uiascYPefZFQM3h+44Uwkk3ROFKRfUK9Ux2nMCAwEA +AaNmMGQwHQYDVR0OBBYEFNisXWoSWQRxs/uI50LqNkV1nkZ5MB8GA1UdIwQYMBaA +FIVNUSQsKppHYNYdR9vd1PjirYmKMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBfaMA1uzPCpqzeF9Nn6NmhlMmn +q82Bkv3ujAbOJ1wjTkT7Xpte/4run64z6/0wa3pmjdCOAbxlGydFJtH18g/BbIj4 +pEfgj+TXaB6iZEs24XyV20B2tAi3bhIg+WoG+wa8cSIYgiqAiXVSzlNeGLPkHCif +LcDUbntDCIPDkFsFRGhU6BxrjS05y0C8iiC/51A4OL9e8kPtDj3tJRELm39qGvvK +QFqVBoO6woUR3STFP9Rz7u3tbr3/6IRW6Qfz8OCIaRr2SK2hPJ/6klVNlOhgp9KC +mFbuWx31h0+2XETZsJmsoqfH/aXCvr5a/rV7/FhaUXATwqI/l0rd5NSyn48fzC9B +LYdpFQtdIwKieHpwg1fJTH2D9ZEqDV1DPCBTHEvh3oDsyHcMHGYL3PR3pI2ErAY3 +Wil4rxx0bMq+LKjQ5cVHJYrfEnguykgcnCrbCcV2yuIetz1qX/YMvvh/ryhnJAm0 +FWOZqDLZIlFevfpFQLftjjJd9Ly9Z7oGv/fW16wETYp4+y1sfTsFbMM+wTh1UKxZ +zbLd9MjI/ohXqFVQTowPSTbvEF25TA/JyEjXJGEZ3Uv933qRpRO0btjS1DlUInjg +JPp3vtuv+XD438VPPhcqQhJMIYAZTKlPt72Gb9ZygILzS65wsBHxXTUflgXSOndU +9JcJKnHjuBXUggHIdg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFnTCCA4WgAwIBAgIURqDeNAeTfgAupz6l6w1abPDMwvkwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By +aW5nZmllbGQxDDAKBgNVBAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0 +MTEyMTExMTU0NFoXDTQ0MTExNjExMTU0NFowVjELMAkGA1UEBhMCVVMxDzANBgNV +BAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoMA0RpczES +MBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAizgE65i89PDzcfnyr/dZqS61NyTgZsqmkJAunc3lyJ4P5Uu3pPaANVWL3sJR +qeyNUDAv3PvfhXobKuXkiKT1lX8HitlqTro4qVgWxtlnlaanKvs5Li36GR54VRyi +ozk9/KrRqYcO+bC53mmiI9kaD0+sEr/351kvI8jPUFMtkAF8OGhUi/z32OpJjtru +6tiOt8kj0+MvsxxHSljZRTeqrCAAePqbYmx9/n6a+IBGukXjZOEgOPdd+mId7RwM +ZwDU0T5FGL5exGpkMvsAOIgb7Q3vPsIhaqedbFSfQa+m89rUCa8RTZyUd12mPTsD +RyZx1/RJnOlgkT+f0FVlc+gANezkLb2+lW0RsYw5jE94/o30dpQgTLA8JpYgvaPP +mbBADef4KOJ9Z8pVnTjw9wGYl66Hpq8jHSGVKQ0lpFTX1Cwd5ssft5gaS5Fe2C+f +B5fcoF9dV1EHB5nlzGtuw9g9CulN4HIq0r5hbWVguj5TlVu6qL6b7MKWCX6wLNW9 +x3SaNmoKKEqVKn+UCUOlJfuAbpXF2ubSG0agSJ+AFuY7uGwZah9MNvZX6xPlhP/m +Aks1XfkFE8dTLkxCrLDz5iK4RWVU5zGsjVd9nIqFKJ0Z0ksf9avdRVwXs0P8x+ab +dtPJmvP4uf3IkHsrpa3wwTNZBo84IW1yEDhdqV7TxYCSLR8CAwEAAaNjMGEwHQYD +VR0OBBYEFIVNUSQsKppHYNYdR9vd1PjirYmKMB8GA1UdIwQYMBaAFIVNUSQsKppH +YNYdR9vd1PjirYmKMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0G +CSqGSIb3DQEBCwUAA4ICAQCCk7nlg7bchZZp3mUYgxcT9Pxz/MyqzmYRwP8YaWlQ ++P6eZcO/ba4LkpNqkQsjlH+IlFDKo7X2TmrS/nMt8nlqhJbSgN7IZ1wh9ghh1095 +Y0SBcF9O2kKKiCshlwtKT2q189old0KQT66sGB69gyMU6EGciFubHZ4eXGCiI/Ue +l4qHONWKKP1N/FZg5HmNb182bQASV5/su3GKR1POJvf/ieSPoOV8OdaD8ndDeK1a +Ei5I07jobRVNxcxOuVce1OL+2h/MFWbYbGKNtTax0Gcm+R7lO8qabMEIoSuoD761 +h/VCdeCU3+V5Koy9+Mwz7aHLK7PgtJIj+FQgUzQ4LxI2uasdnVV4K+eNYRJEvVXI +S4niOjoVajzXCZUixtnuOQH+yl3Pn2+YYS5j/PCkfVhLHHuo4fYXe++6lOesLXkU +cBE4J7aN4bb0QyYlph9zsLvYFPrq4PiH9Jy5beQCMvEbQ93E1NgMniqYJLaVLeoZ +i3ypsRdcYd+Ig9bQqixAhP84URAFtfhZBpkf7L9IvSqJHoaLpd9tHYE9aj0nVnx7 ++fwCZVWvNHFAQn0TWJThPDXgRo1yf7c6twDlp1DfsLZzfFz6ZS1BIXUFcgWQCoKw +wQHe610yrrA4FiUSrZaheZkwrucYn2Z/9U33Sd1TTYZBMMWmtS1vayrtS1l4fHPE +NQ== +-----END CERTIFICATE----- diff --git a/resources/sdk/purecloudpython/scripts/mtls-certs/localhost.cert.pem b/resources/sdk/purecloudpython/scripts/mtls-certs/localhost.cert.pem new file mode 100644 index 000000000..8f4d813ce --- /dev/null +++ b/resources/sdk/purecloudpython/scripts/mtls-certs/localhost.cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE2TCCAsGgAwIBAgIDEAITMA0GCSqGSIb3DQEBCwUAMEAxCzAJBgNVBAYTAlVT +MQ8wDQYDVQQIDAZEZW5pYWwxDDAKBgNVBAoMA0RpczESMBAGA1UEAwwJbG9jYWxo +b3N0MB4XDTI0MTEyMTExMTU1N1oXDTI1MTIwMTExMTU1N1owVjELMAkGA1UEBhMC +VVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNV +BAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAwJKlWwldUOtsl8B8VSwUy+HuDFccLbh6sxpayNo7qKTvqLDN +xGyXauuxZfplt/UTe2kkce642I48wlYe3jDAkVK9HQru4Al+fjacleI9n3mSjQJ9 +cR+3+n+bThEWBGgGbskC+/AWtax3RgS6f3VCtnOMwTO3gZUdhL/D8Mn/OxEMFix9 +dfla3Vm9RkeYnBNCQ+j2uLPf/8XW6BAGqvb+RdO/2H2G8HhyF+LsKU/ervI1ccTn +pFlNuC6HsRpgW8ssHUSgKh2kpXXiTXJjhvvhEbpE6HWNN8GB5U5WcQrP2FrLZ1PJ +l01ACdOob/MpHfYOE3NdGTpjpH/3myy/ILTLbQIDAQABo4HFMIHCMAkGA1UdEwQC +MAAwEQYJYIZIAYb4QgEBBAQDAgWgMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdl +bmVyYXRlZCBDbGllbnQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFFkrskief6mCBsYg ++KJh8SQcnfDEMB8GA1UdIwQYMBaAFNisXWoSWQRxs/uI50LqNkV1nkZ5MA4GA1Ud +DwEB/wQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwDQYJKoZI +hvcNAQELBQADggIBAB7hec1SrjNqPDfNmSc18RFXrUot9hfFj0sJsdUTHkTDC+Nl +rM8fOgWbtNgwmlPBZQm+UOZPNOzSoPPXVacV4yfua7NLb2zOnUSECW+CGIx2UOj4 +buBptFa/FNfaayFmG/EAO1XdJuf7GVNGuYypyfDLipiGtaV/lg9B8Ig1wflvG0T1 +MM/WwN8quj8hUrFLa6M05bP1uFXllrhx16VhjD8nXe4xTv5k6lLWbJy4wJ3x+AUa +Gg1KjSeabQVEWm8BiimkKKOyEEDdLXW+6IKz4W4N0Y/vFuuWUQ89qaUWiNuvLSru +WPQ7COWu6GQ0qJU0MwEI14q0XqIfRpUMwqwFSi54y7v6+9P9HPbhOArYrpc8Ghb0 +B/VSYjAH9tp8+Fe3RnG5nEYvgu63oKSBxmgIlqubOSIc9E8ZVd3IbFYL+ASK4/lI +p6KaCUt/XEJrZ/Qnozms6VyV4W2ODoXGNYuKKqIw64eud6pwbnMiYVWHap7Hzq/W +8me62mVfmNzNTm8MbMc9WL05AAu9pwyUkeQyx8cmaFNVfyO4FXxDXpVcl23oZmVl +Tfg8s22Mtcz2wj3bDDMhyB9SoUdhityOo0JxMUdakdxZUvFKeas0Eot6XHNdhAPZ +Hph1w1q6VPWqtjhTDOfnjP4P7Gx+YM9fFS14yK7ufiVT4pDTHmZQ0Bucw6IQ +-----END CERTIFICATE----- diff --git a/resources/sdk/purecloudpython/scripts/mtls-certs/localhost.key.pem b/resources/sdk/purecloudpython/scripts/mtls-certs/localhost.key.pem new file mode 100644 index 000000000..fa4d90858 --- /dev/null +++ b/resources/sdk/purecloudpython/scripts/mtls-certs/localhost.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDAkqVbCV1Q62yX +wHxVLBTL4e4MVxwtuHqzGlrI2juopO+osM3EbJdq67Fl+mW39RN7aSRx7rjYjjzC +Vh7eMMCRUr0dCu7gCX5+NpyV4j2feZKNAn1xH7f6f5tOERYEaAZuyQL78Ba1rHdG +BLp/dUK2c4zBM7eBlR2Ev8Pwyf87EQwWLH11+VrdWb1GR5icE0JD6Pa4s9//xdbo +EAaq9v5F07/YfYbweHIX4uwpT96u8jVxxOekWU24LoexGmBbyywdRKAqHaSldeJN +cmOG++ERukTodY03wYHlTlZxCs/YWstnU8mXTUAJ06hv8ykd9g4Tc10ZOmOkf/eb +LL8gtMttAgMBAAECggEAA3vzwXKpLVBhIdrB9SGLy8eN8Mu3rw0WLTzmFRm7uBip +3GW8h1gGolDa4BM4eWxV+qbjmasdzY8/Qq3ft5nutgJm+QpMWaHaV8rBqQWTo1nG +AMahgeU2xTG/W9l7yj8elK0O4IOOyaEx9EKcJDe5KgdrrcgzgwTxaIg2sZnaBBXK +FxpWolpxxLiqH1qAsdo9v6gh1vqWBGYkeFEr2ZBrdrRJTlayKzP7WgBOTofnwLHL +6DosolPYb8Wvddz8a698R44olcdnFz0UyV/DfmT6ZVLyF1eQGfjepsVPPdlGmMMC +qo9e1y5msnscm+Gxg7ckgXrvcCOYP1RrMVJJ3jGxOQKBgQDeIorGAPVUnxmWyS2+ +kN235WpG7TnaMDxKAErfhLy2grriM3l0vwTuF1Z4CzgbZKRpRM+1dSkvyQgC9fLC +ZnsfgO2+5DCS68rdVWGoKvM6FaQt68dfx8yeTrEsF/ym8HpLTUNGvmVUotgKSQ3s +dGy5CQXLgbqmNZ6plu6EbvaTxQKBgQDd7luNiBsLjQN2h1Q90ctZ7BhaKutQyMGM +zl8o0192bE51xWZpHRIGu0QB8XbbuBOzBLq3xS/YEOl6MhJYBkfoHYNg4cCfYJLH +jlGjMulWqORl1dbwtW1+rhnltl+Cw8yLoxjmCnTDwbNvTb7iOJadt7K6N+dJgz8b +cd5wIfFLiQKBgDXt/vEZdHZp9gV0rEVy2FF4QjgcCgqd4VU6AvOHP2W0EXkvdBJS ++fhv9A9K8gE8iun/ycnNOwWd8fL+EUT/Km84lv/cWKqi0mqv+vigDNMSQ/zeHFZ1 +pIjepAcHf9Br29UCWg4H1YQZ0QeWOuooTekSdrS0t0L5C58yxI+kfPrdAoGAS0Zu +zUmuCrPwycJLI5DGTaDT/blzCXqek7jn8/wF9C/2SOIEw4B1vczE23fgXdRI8j6X +an+6o6nQ136mVC8FMsjX6AOKh0SFZKR0Mkuxc5Bjr9rhRdHz6rT42b6LFNp+xZoz +dstzIXbQ+t6+8RJKnAJT3+/YRkn7HL75uMT6iCkCgYAZBoUYaQ6XEuLvKGV3DWvN +BXBkokeIPD+zY7S/b6WnRVJc4WavWzlDE9ytgTOjRB7Jhy2pBtWbOAfZDrEL/JSa +0KqidlhZuxi/joZ9mUisIOkMXujmpT6490hKcBbTBsG/l+edhZ2wrYPDZLdXK1vj +TxVbJW0XMSdDra2soIR7Zw== +-----END PRIVATE KEY----- diff --git a/resources/sdk/purecloudpython/templates/api_client.mustache b/resources/sdk/purecloudpython/templates/api_client.mustache index 991ad4dc0..1869c9e14 100644 --- a/resources/sdk/purecloudpython/templates/api_client.mustache +++ b/resources/sdk/purecloudpython/templates/api_client.mustache @@ -20,7 +20,9 @@ Copyright 2016 SmartBear Software from __future__ import absolute_import from . import models -from .rest import RESTClientObject +from .abstract_http_client import AbstractHttpClient +from .default_http_client import DefaultHttpClient +from .http_request_options import HttpRequestOptions from .rest import ApiException from .api_null_value import ApiNullValue @@ -79,7 +81,7 @@ class ApiClient(object): """ Constructor of the class. """ - self.rest_client = RESTClientObject() + self.http_client = None self.default_headers = {} if header_name is not None: self.default_headers[header_name] = header_value @@ -136,6 +138,21 @@ class ApiClient(object): url = url + "/" + self.gateway_configuration.path_params_api return url + def get_http_client(self): + if self.http_client is None: + self.http_client = DefaultHttpClient() + + return self.http_client + + def set_http_client(self, http_client): + if http_client is None: + raise ValueError("http_client cannot be None") + + if not isinstance(http_client, AbstractHttpClient): + raise ValueError("http_client must be an instance of AbstractHttpClient") + + self.http_client = http_client + def get_client_credentials_token(self, client_id, client_secret): """ :param client_id: Client ID to authenticate with @@ -158,10 +175,13 @@ class ApiClient(object): header_params = self.sanitize_for_serialization(header_params) post_params = self.sanitize_for_serialization(post_params) - response = self.request("POST", url, - query_params=query_params, - headers=header_params, - post_params=post_params, body=body); + request_options = HttpRequestOptions(url = url, method = "POST", + headers = header_params, + post_params = post_params, + body = body) + http_client = self.get_http_client() + response = http_client.request(request_options) + data = json.loads('[' + response.data + ']') self.access_token = data[0]["access_token"] return self; @@ -193,10 +213,13 @@ class ApiClient(object): header_params = self.sanitize_for_serialization(header_params) post_params = self.sanitize_for_serialization(post_params) - response = self.request("POST", url, - query_params=query_params, - headers=header_params, - post_params=post_params, body=body); + request_options = HttpRequestOptions(url = url, method = "POST", + headers = header_params, + post_params = post_params, + body = body) + http_client = self.get_http_client() + response = http_client.request(request_options) + data = json.loads('[' + response.data + ']') self.access_token = data[0]["access_token"] return self; @@ -231,11 +254,12 @@ class ApiClient(object): header_params = self.sanitize_for_serialization(header_params) post_params = self.sanitize_for_serialization(post_params) - response = self.request("POST", url, - query_params=query_params, - headers=header_params, - post_params=post_params, body=body) - data = json.loads('[' + response.data + ']') + request_options = HttpRequestOptions(url = url, method = "POST", + headers = header_params, + post_params = post_params, + body = body) + http_client = self.get_http_client() + response = http_client.request(request_options) self.access_token = data[0]["access_token"] self.refresh_token = data[0]["refresh_token"] @@ -267,12 +291,14 @@ class ApiClient(object): header_params = self.sanitize_for_serialization(header_params) post_params = self.sanitize_for_serialization(post_params) - response = self.request("POST", url, - query_params=query_params, - headers=header_params, - post_params=post_params, body=body) - data = json.loads('[' + response.data + ']') + request_options = HttpRequestOptions(url = url, method = "POST", + headers = header_params, + post_params = post_params, + body = body) + http_client = self.get_http_client() + response = http_client.request(request_options) + data = json.loads('[' + response.data + ']') self.access_token = data[0]["access_token"] self.refresh_token = data[0]["refresh_token"] @@ -328,12 +354,14 @@ class ApiClient(object): header_params = self.sanitize_for_serialization(header_params) post_params = self.sanitize_for_serialization(post_params) - response = self.request("POST", url, - query_params=query_params, - headers=header_params, - post_params=post_params, body=body) - data = json.loads('[' + response.data + ']') + request_options = HttpRequestOptions(url = url, method = "POST", + headers = header_params, + post_params = post_params, + body = body) + http_client = self.get_http_client() + response = http_client.request(request_options) + data = json.loads('[' + response.data + ']') self.access_token = data[0]["access_token"] return self, data[0] @@ -438,8 +466,13 @@ class ApiClient(object): try: # perform request and return response - response_data = self.request(method, url, query_params=query_params, - headers=header_params, post_params=post_params, body=body) + request_options = HttpRequestOptions(url = url, method = method, + headers = header_params, + query_params = query_params, + post_params = post_params, + body = body) + http_client = self.get_http_client() + response_data = http_client.request(request_options) Configuration().logger.trace(method, log_url, body, response_data.status, header_params, response_data.getheaders()) Configuration().logger.debug(method, log_url, body, response_data.status, header_params) except ApiException as e: @@ -648,54 +681,6 @@ class ApiClient(object): thread.start() return thread - def request(self, method, url, query_params=None, headers=None, - post_params=None, body=None): - """ - Makes the HTTP request using RESTClient. - """ - if method == "GET": - return self.rest_client.GET(url, - query_params=query_params, - headers=headers) - elif method == "HEAD": - return self.rest_client.HEAD(url, - query_params=query_params, - headers=headers) - elif method == "OPTIONS": - return self.rest_client.OPTIONS(url, - query_params=query_params, - headers=headers, - post_params=post_params, - body=body) - elif method == "POST": - return self.rest_client.POST(url, - query_params=query_params, - headers=headers, - post_params=post_params, - body=body) - elif method == "PUT": - return self.rest_client.PUT(url, - query_params=query_params, - headers=headers, - post_params=post_params, - body=body) - elif method == "PATCH": - return self.rest_client.PATCH(url, - query_params=query_params, - headers=headers, - post_params=post_params, - body=body) - elif method == "DELETE": - return self.rest_client.DELETE(url, - query_params=query_params, - headers=headers, - body=body) - else: - raise ValueError( - "http method must be `GET`, `HEAD`," - " `POST`, `PATCH`, `PUT` or `DELETE`." - ) - def prepare_post_parameters(self, post_params=None, files=None): """ Builds form parameters. diff --git a/resources/sdk/purecloudpython/templates/configuration.mustache b/resources/sdk/purecloudpython/templates/configuration.mustache index 9e5c05117..522cc5093 100644 --- a/resources/sdk/purecloudpython/templates/configuration.mustache +++ b/resources/sdk/purecloudpython/templates/configuration.mustache @@ -39,6 +39,9 @@ import time import threading import hashlib import json +import io +import ssl +import certifi from .logger import Logger, LogFormat, LogLevel from .gateway_configuration import GatewayConfiguration @@ -99,6 +102,8 @@ class Configuration(object): self.cert_file = None # client key file self.key_file = None + #ssl context + self.ssl_context = None # proxy self.proxy = None @@ -149,6 +154,52 @@ class Configuration(object): if self.live_reload_config: self._config_updater() + def set_mtls_contents(self, certContent, keyContent, caContent): + ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + + if not certContent or not keyContent or not caContent: + raise ValueError("certContent, keyContent, and caContent must be provided for mTLS.") + + if caContent: + ssl_context.load_verify_locations(cafile=io.BytesIO(caContent.encode('utf-8'))) + else: + # If no CA certs are provided, use certifi's bundle. + ssl_context.load_verify_locations(cafile=certifi.where()) + + ssl_context.load_cert_chain(certfile=io.BytesIO(certContent.encode('utf-8')), keyfile=io.BytesIO(keyContent.encode('utf-8'))) + self.ssl_context = ssl_context + + def set_mtls_certificates(self, certPath, keyPath, caPath = None): + ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + + if not certPath or not keyPath: + raise ValueError("certPath, keyPath and caPath must be provided for mTLS.") + + if caPath: + ssl_context.load_verify_locations(cafile=caPath) + else: + # If no CA certs are provided, use certifi's bundle. + ssl_context.load_verify_locations(cafile=certifi.where()) + + ssl_context.load_cert_chain(certfile=certPath, keyfile=keyPath) + self.ssl_context = ssl_context + + def create_mtls_or_ssl_context(self): + if self.ssl_context is None: # set_mtls_contents() or set_mtls_certificates() were not called. + ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + + if self.ssl_ca_cert: + ssl_context.load_verify_locations(cafile=self.ssl_ca_cert) + else: + # If no CA certs are provided, use certifi's bundle. + ssl_context.load_verify_locations(cafile=certifi.where()) + + if self.cert_file and self.key_file: + # mTLS configuration + ssl_context.load_cert_chain(certfile=self.cert_file, keyfile=self.key_file) + + self.ssl_context = ssl_context + def get_api_key_with_prefix(self, identifier): """ Gets API key (with prefix if set). diff --git a/resources/sdk/purecloudpython/templates/rest.mustache b/resources/sdk/purecloudpython/templates/rest.mustache index 060af3271..c4bbdb076 100644 --- a/resources/sdk/purecloudpython/templates/rest.mustache +++ b/resources/sdk/purecloudpython/templates/rest.mustache @@ -80,19 +80,6 @@ class RESTClientObject(object): else: cert_reqs = ssl.CERT_NONE - # ca_certs - if Configuration().ssl_ca_cert: - ca_certs = Configuration().ssl_ca_cert - else: - # if not set certificate file, use Mozilla's root certificates. - ca_certs = certifi.where() - - # cert_file - cert_file = Configuration().cert_file - - # key file - key_file = Configuration().key_file - # proxy proxy = Configuration().proxy proxy_username = Configuration().proxy_username @@ -100,34 +87,34 @@ class RESTClientObject(object): retries = urllib3.util.Retry() retries.allowed_methods = {'DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'TRACE'} + + kwargs = { + 'retries': retries, + 'num_pools': pools_size, + 'maxsize': max_size, + 'block': True, + 'cert_reqs': cert_reqs, + } + + if Configuration().verify_ssl and Configuration().ssl_context: + kwargs['ssl_context'] = Configuration().ssl_context + # https pool manager if proxy: headers = None + kwargs['proxy_url'] = proxy if proxy_username and proxy_password: headers = urllib3.make_headers(proxy_basic_auth=proxy_username + ':' + proxy_password) - self.pool_manager = urllib3.ProxyManager( - retries=retries, - num_pools=pools_size, - maxsize=max_size, - block=True, - cert_reqs=cert_reqs, - ca_certs=ca_certs, - cert_file=cert_file, - key_file=key_file, - proxy_url=proxy, - proxy_headers=headers - ) + kwargs['proxy_headers'] = headers + #self.print_kwargs(**kwargs) #Just for debugging + self.pool_manager = urllib3.ProxyManager(**kwargs) else: - self.pool_manager = urllib3.PoolManager( - retries=retries, - num_pools=pools_size, - maxsize=max_size, - block=True, - cert_reqs=cert_reqs, - ca_certs=ca_certs, - cert_file=cert_file, - key_file=key_file - ) + #self.print_kwargs(**kwargs) #Just for debugging + self.pool_manager = urllib3.PoolManager(**kwargs) + + def print_kwargs(self, **kwargs): + for key in kwargs: + print(f"{key} = {kwargs[key]}") def request(self, method, url, query_params=None, headers=None, body=None, post_params=None): From 381254ce9edff4c57afce6171e3a1aa4c25ec149 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:04:13 +0530 Subject: [PATCH 2/8] updated test case file --- resources/sdk/purecloudpython/scripts/SdkTests_mtls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py index acbcd5c06..7b21cd7ff 100644 --- a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py +++ b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py @@ -74,7 +74,7 @@ def test_2_mtls_gw_authenticate(self): # Authenticate with client credentials and pass the apiclient instance into the usersapi print("Authenticating with client credentials...") SdkTestsMTLS.apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() - SdkTestsMTLS.apiclient_mtls.set_gateway("locahlost","https",4027) + SdkTestsMTLS.apiclient_mtls.set_gateway("locahlost","https",4027,"login","api") SdkTestsMTLS.apiclient_mtls.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) SdkTestsMTLS.users_api_mtls = PureCloudPlatformClientV2.UsersApi(SdkTestsMTLS.apiclient_mtls) print(f"Authentication successful access_token: {SdkTestsMTLS.apiclient_mtls}") From 72baad6ab95b397323ea39462e66674f59256150 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:47:03 +0530 Subject: [PATCH 3/8] added debugging logs in testcase --- resources/scripts/gateway.ts | 15 +++++++++++---- resources/sdk/purecloudpython/scripts/SdkTests.py | 2 ++ .../sdk/purecloudpython/scripts/SdkTests_mtls.py | 2 ++ resources/sdk/purecloudpython/scripts/compile.sh | 4 ++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/resources/scripts/gateway.ts b/resources/scripts/gateway.ts index 58888094c..506ef0789 100644 --- a/resources/scripts/gateway.ts +++ b/resources/scripts/gateway.ts @@ -43,10 +43,14 @@ export default class GatewayServer { }; const proxyReq = https.request(options, (proxyRes) => { + console.log(`Proxy Response Status: ${proxyRes.statusCode}`, { + headers: proxyRes.headers, + statusCode: proxyRes.statusCode + }); res.writeHead(proxyRes.statusCode || 502, proxyRes.headers); proxyRes.pipe(res); }); - + proxyReq.on('error', (err) => { console.error('Error during proxy request:', err.message); res.writeHead(502, { 'Content-Type': 'text/plain' }); @@ -63,19 +67,22 @@ export default class GatewayServer { } private fetchEnvironment():string{ - return "api."+process.env.PURECLOUD_ENV + const envUrl = "api."+process.env.PURECLOUD_ENV; + console.log('Environment URL:', envUrl); + return envUrl } private handleConnectRequest(req: http.IncomingMessage, clientSocket: net.Socket, head: Buffer) { const targetUrl = url.parse(`//${req.url}`, false, true); const environment = this.fetchEnvironment(); + console.log({ targetUrl, environment }); const serverSocket = tls.connect( { host: environment, port: 443, rejectUnauthorized: false }, () => { - console.log("connection extablishes") + console.log("Connection established to:", environment) clientSocket.write( 'HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node.js-Proxy\r\n' + @@ -89,7 +96,7 @@ export default class GatewayServer { ); serverSocket.on('error', (err) => { - console.error('error with server socket:', err.message) + console.error('Server socket error:', { message: err.message, environment: environment }) clientSocket.end(); } ); } diff --git a/resources/sdk/purecloudpython/scripts/SdkTests.py b/resources/sdk/purecloudpython/scripts/SdkTests.py index ff5cb89e3..7d05f9b74 100644 --- a/resources/sdk/purecloudpython/scripts/SdkTests.py +++ b/resources/sdk/purecloudpython/scripts/SdkTests.py @@ -163,4 +163,6 @@ def purecloudregiontest(self,x): if __name__ == '__main__': unittest.sortTestMethodsUsing(None) + print("Running SdkTests Tests") unittest.main() + print("SdkTests Tests Complete") diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py index 7b21cd7ff..c9bf5435c 100644 --- a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py +++ b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py @@ -154,5 +154,7 @@ def purecloudregiontest(self,x): if __name__ == '__main__': unittest.sortTestMethodsUsing(None) + print("Running SdkTestsMTLS Tests") unittest.main() + print("SdkTestsMTLS Tests Complete") \ No newline at end of file diff --git a/resources/sdk/purecloudpython/scripts/compile.sh b/resources/sdk/purecloudpython/scripts/compile.sh index 87bb2b0e9..4c2bd14e7 100644 --- a/resources/sdk/purecloudpython/scripts/compile.sh +++ b/resources/sdk/purecloudpython/scripts/compile.sh @@ -28,5 +28,9 @@ python3.6 -m pip install --user -U watchdog echo "Install retry..." python3.6 -m pip install --user -U retry echo "Run unit tests" +pwd python3.6 -m unittest SdkTests +cd "$TESTS_DIR" +echo "Run unit tests mtls" +pwd python3.6 -m unittest SdkTests_mtls From 20c5630da41ce28d90106ab8c245c331e5d47524 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:07:20 +0530 Subject: [PATCH 4/8] Test case class name updated --- .../purecloudpython/scripts/SdkTests_mtls.py | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py index c9bf5435c..9804f0a86 100644 --- a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py +++ b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py @@ -7,7 +7,7 @@ import PureCloudPlatformClientV2 -class SdkTestsMTLS(unittest.TestCase): +class SdkTests_mtls(unittest.TestCase): lastResult = None userId = None @@ -20,13 +20,13 @@ class SdkTestsMTLS(unittest.TestCase): def setUp(self): # Skip if there has been a failure - if SdkTestsMTLS.lastResult != None and (len(SdkTestsMTLS.lastResult.failures) > 0 or len(SdkTestsMTLS.lastResult.errors) > 0): + if SdkTests_mtls.lastResult != None and (len(SdkTests_mtls.lastResult.failures) > 0 or len(SdkTests_mtls.lastResult.errors) > 0): print("=== WARNING: Previous test failed, skipping current test ===", flush=True) self.skipTest('Previous test failed') def run(self, result=None): # Store this execution's result as the last one - SdkTestsMTLS.lastResult = result + SdkTests_mtls.lastResult = result # Run the test unittest.TestCase.run(self, result) @@ -41,8 +41,8 @@ def test_1_trace_basic_information(self): self.assertIsNotNone(os.environ.get('PURECLOUD_CLIENT_SECRET')) - SdkTestsMTLS.userEmail = '%s@%s' % (uuid.uuid4(), os.environ.get('PURECLOUD_ENVIRONMENT')) - print(SdkTestsMTLS.userEmail) + SdkTests_mtls.userEmail = '%s@%s' % (uuid.uuid4(), os.environ.get('PURECLOUD_ENVIRONMENT')) + print(SdkTests_mtls.userEmail) print(PureCloudPlatformClientV2) print("=== EXITING test_1_trace_basic_information() ===\n") @@ -73,13 +73,13 @@ def test_2_mtls_gw_authenticate(self): # Authenticate with client credentials and pass the apiclient instance into the usersapi print("Authenticating with client credentials...") - SdkTestsMTLS.apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() - SdkTestsMTLS.apiclient_mtls.set_gateway("locahlost","https",4027,"login","api") - SdkTestsMTLS.apiclient_mtls.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) - SdkTestsMTLS.users_api_mtls = PureCloudPlatformClientV2.UsersApi(SdkTestsMTLS.apiclient_mtls) - print(f"Authentication successful access_token: {SdkTestsMTLS.apiclient_mtls}") - self.create_user(SdkTestsMTLS.users_api_mtls) - self.delete_user(SdkTestsMTLS.users_api_mtls) + SdkTests_mtls.apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() + SdkTests_mtls.apiclient_mtls.set_gateway("locahlost","https",4027,"login","api") + SdkTests_mtls.apiclient_mtls.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) + SdkTests_mtls.users_api_mtls = PureCloudPlatformClientV2.UsersApi(SdkTests_mtls.apiclient_mtls) + print(f"Authentication successful access_token: {SdkTests_mtls.apiclient_mtls}") + self.create_user(SdkTests_mtls.users_api_mtls) + self.delete_user(SdkTests_mtls.users_api_mtls) print("=== EXITING test_mtls_authenticate() ===\n") def test_3_proxy_authenticate(self): @@ -100,37 +100,37 @@ def test_3_proxy_authenticate(self): # Authenticate with client credentials and pass the apiclient instance into the usersapi print("Authenticating with client credentials...") - SdkTestsMTLS.apiclient_proxy = PureCloudPlatformClientV2.api_client.ApiClient() - SdkTestsMTLS.apiclient_proxy.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) - SdkTestsMTLS.users_api_proxy = PureCloudPlatformClientV2.UsersApi(SdkTestsMTLS.apiclient_proxy) - print(f"Authentication successful access_token: {SdkTestsMTLS.apiclient_proxy}") - self.create_user(SdkTestsMTLS.users_api_proxy) - self.delete_user(SdkTestsMTLS.users_api_proxy) + SdkTests_mtls.apiclient_proxy = PureCloudPlatformClientV2.api_client.ApiClient() + SdkTests_mtls.apiclient_proxy.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) + SdkTests_mtls.users_api_proxy = PureCloudPlatformClientV2.UsersApi(SdkTests_mtls.apiclient_proxy) + print(f"Authentication successful access_token: {SdkTests_mtls.apiclient_proxy}") + self.create_user(SdkTests_mtls.users_api_proxy) + self.delete_user(SdkTests_mtls.users_api_proxy) print("=== EXITING test_proxy_authenticate() ===\n") def create_user(self, users_api): print("=== ENTERING create_user() ===") body = PureCloudPlatformClientV2.CreateUser() - body.name = SdkTestsMTLS.userName - body.email = SdkTestsMTLS.userEmail + body.name = SdkTests_mtls.userName + body.email = SdkTests_mtls.userEmail body.password = '%s!@#$1234asdfASDF' % (uuid.uuid4()) print(f"Creating user with name: {body.name}, email: {body.email}") user = users_api.post_users(body) print(f"User created successfully") - SdkTestsMTLS.userId = user.id - print(f"User ID: {SdkTestsMTLS.userId}") - self.assertEqual(user.name, SdkTestsMTLS.userName) - self.assertEqual(user.email, SdkTestsMTLS.userEmail) - print(SdkTestsMTLS.userId) + SdkTests_mtls.userId = user.id + print(f"User ID: {SdkTests_mtls.userId}") + self.assertEqual(user.name, SdkTests_mtls.userName) + self.assertEqual(user.email, SdkTests_mtls.userEmail) + print(SdkTests_mtls.userId) print("=== EXITING create_user() ===\n") def delete_user(self, users_api): print("=== ENTERING delete_user() ===") - print(f"Deleting user {SdkTestsMTLS.userId}") - users_api.delete_user(SdkTestsMTLS.userId) - print(f"User {SdkTestsMTLS.userId} deleted successfully") + print(f"Deleting user {SdkTests_mtls.userId}") + users_api.delete_user(SdkTests_mtls.userId) + print(f"User {SdkTests_mtls.userId} deleted successfully") print("=== EXITING delete_user() ===\n") def purecloudregiontest(self,x): @@ -154,7 +154,7 @@ def purecloudregiontest(self,x): if __name__ == '__main__': unittest.sortTestMethodsUsing(None) - print("Running SdkTestsMTLS Tests") + print("Running SdkTests_mtls Tests") unittest.main() - print("SdkTestsMTLS Tests Complete") + print("SdkTests_mtls Tests Complete") \ No newline at end of file From f498fac67c35a790c8840b78fea6132a0d7bc6b6 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:31:36 +0530 Subject: [PATCH 5/8] typo in the localhost --- resources/sdk/purecloudpython/scripts/SdkTests_mtls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py index 9804f0a86..e2dc2b312 100644 --- a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py +++ b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py @@ -74,7 +74,7 @@ def test_2_mtls_gw_authenticate(self): # Authenticate with client credentials and pass the apiclient instance into the usersapi print("Authenticating with client credentials...") SdkTests_mtls.apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() - SdkTests_mtls.apiclient_mtls.set_gateway("locahlost","https",4027,"login","api") + SdkTests_mtls.apiclient_mtls.set_gateway("localhost","https",4027,"login","api") SdkTests_mtls.apiclient_mtls.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) SdkTests_mtls.users_api_mtls = PureCloudPlatformClientV2.UsersApi(SdkTests_mtls.apiclient_mtls) print(f"Authentication successful access_token: {SdkTests_mtls.apiclient_mtls}") From 7ee542e3fde4dca684741bd6a3a9f436fa292ef3 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:53:55 +0530 Subject: [PATCH 6/8] fixed gateway to support route, added more logs into gateway and proxy, moved rest.py to extensions --- resources/scripts/gateway.ts | 114 +++++++++++----- resources/scripts/proxy.ts | 69 +++++++++- .../rest.mustache => extensions/rest.py} | 0 .../purecloudpython/scripts/SdkTests_mtls.py | 26 ---- .../purecloudpython/scripts/SdkTests_proxy.py | 126 ++++++++++++++++++ .../sdk/purecloudpython/scripts/compile.sh | 4 +- 6 files changed, 268 insertions(+), 71 deletions(-) rename resources/sdk/purecloudpython/{templates/rest.mustache => extensions/rest.py} (100%) create mode 100644 resources/sdk/purecloudpython/scripts/SdkTests_proxy.py diff --git a/resources/scripts/gateway.ts b/resources/scripts/gateway.ts index 506ef0789..00ca091ba 100644 --- a/resources/scripts/gateway.ts +++ b/resources/scripts/gateway.ts @@ -7,14 +7,22 @@ import pkg from 'http-proxy'; import * as tls from "tls"; const { createProxyServer } = pkg; +// Logger function to standardize logging format +const log = (activity: string, details?: any) => { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${activity}`, details ? details : ''); +}; + export default class GatewayServer { public gateway: pkg.httpProxy; public server: https.Server; - + private environment: string; constructor() { + log('Initializing GatewayServer'); this.gateway = createProxyServer(); - const environment = this.fetchEnvironment(); + this.environment = this.fetchEnvironment("login"); const domain = 'localhost'; + log('Server configuration', { environment: this.environment, domain }); // SSL/TLS options for the proxy server const serverOptions: https.ServerOptions = { @@ -24,86 +32,120 @@ export default class GatewayServer { requestCert: true, rejectUnauthorized: true, // Verify client certificates }; + log('SSL/TLS certificates loaded successfully'); // HTTPS server to listen for incoming requests this.server = https.createServer(serverOptions, (req, res) => { + log('Incoming request received', { + method: req.method, + url: req.url, + headers: req.headers + }); + + let reqURL: string | undefined; + if (req.url?.includes('/login')) { + reqURL = req.url.replace(/^\/login/, '') + this.environment = this.fetchEnvironment("login"); + } else if (req.url?.includes('/api')) { + reqURL = req.url.replace(/^\/api/, '') + this.environment = this.fetchEnvironment("api"); + } else { + reqURL = req.url || '' + } // Parse incoming request URL - const targetHost = url.parse(req.url || ''); + const targetHost = url.parse(reqURL || ''); + log('Parsed target host', targetHost); const options: https.RequestOptions = { - hostname: environment, + hostname: this.environment, port: 443, // HTTPS port path: targetHost.path, method: req.method, headers: { ...req.headers, - host: environment, + host: this.environment, }, rejectUnauthorized: false, }; + log('Proxy request options prepared', options); const proxyReq = https.request(options, (proxyRes) => { - console.log(`Proxy Response Status: ${proxyRes.statusCode}`, { + log('Proxy response received', { headers: proxyRes.headers, statusCode: proxyRes.statusCode }); res.writeHead(proxyRes.statusCode || 502, proxyRes.headers); proxyRes.pipe(res); + log('Response piped back to client'); }); proxyReq.on('error', (err) => { - console.error('Error during proxy request:', err.message); + log('Proxy request error', { + error: err.message, + stack: err.stack + }); res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end('Bad Gateway'); }); req.pipe(proxyReq); - - + log('Request piped to proxy'); }); // Handle CONNECT method for tunneling this.server.on('connect', this.handleConnectRequest.bind(this)); + log('CONNECT handler registered'); } - private fetchEnvironment():string{ - const envUrl = "api."+process.env.PURECLOUD_ENV; - console.log('Environment URL:', envUrl); + private fetchEnvironment(path: string):string{ + const envUrl = path+"."+process.env.PURECLOUD_ENV; + log('Environment URL resolved', envUrl); return envUrl } private handleConnectRequest(req: http.IncomingMessage, clientSocket: net.Socket, head: Buffer) { + log('CONNECT request received', { + url: req.url, + method: req.method, + headers: req.headers + }); + const targetUrl = url.parse(`//${req.url}`, false, true); - const environment = this.fetchEnvironment(); - console.log({ targetUrl, environment }); - const serverSocket = tls.connect( - { - host: environment, - port: 443, - rejectUnauthorized: false - }, () => { - console.log("Connection established to:", environment) - clientSocket.write( - 'HTTP/1.1 200 Connection Established\r\n' + - 'Proxy-agent: Node.js-Proxy\r\n' + - '\r\n' - ); - serverSocket.write(head); - serverSocket.pipe(clientSocket); - clientSocket.pipe(serverSocket); - } + const environment = this.fetchEnvironment("api"); + log('CONNECT request details', { targetUrl, environment }); - ); + const serverSocket = tls.connect( + { + host: environment, + port: 443, + rejectUnauthorized: false + }, () => { + log('TLS connection established', { host: environment }); + clientSocket.write( + 'HTTP/1.1 200 Connection Established\r\n' + + 'Proxy-agent: Node.js-Proxy\r\n' + + '\r\n' + ); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + log('Bidirectional pipe established'); + } + ); serverSocket.on('error', (err) => { - console.error('Server socket error:', { message: err.message, environment: environment }) + log('Server socket error', { + message: err.message, + environment: environment, + stack: err.stack + }); clientSocket.end(); - } ); + }); } } const gatewayServer = new GatewayServer(); -console.log('HTTPS Gateway server trying to start on port 4027'); +log('Starting HTTPS Gateway server on port 4027'); gatewayServer.server.listen(4027, () => { - console.log('HTTPS Gateway server listening on port 4027'); -}); + log('HTTPS Gateway server successfully started on port 4027'); +}); \ No newline at end of file diff --git a/resources/scripts/proxy.ts b/resources/scripts/proxy.ts index 3065f91a4..fdfb49b77 100644 --- a/resources/scripts/proxy.ts +++ b/resources/scripts/proxy.ts @@ -1,44 +1,99 @@ - import http from 'http'; import net from 'net'; import url from 'url'; import pkg from 'http-proxy'; +import Logger from '../../modules/log/logger'; const { createProxyServer } = pkg; +const logger = new Logger(); + export default class ProxyServer { public proxy: pkg.httpProxy; public server: http.Server; constructor() { + logger.info('Initializing proxy server...'); this.proxy = createProxyServer(); + + // Log proxy errors + this.proxy.on('error', (err, req, res) => { + logger.error(`Proxy error: ${err.message}`); + logger.debug(`Proxy error details: ${err.stack}`); + logger.debug(`Failed request URL: ${req.url}`); + logger.debug(`Failed request headers: ${JSON.stringify(req.headers, null, 2)}`); + if (res instanceof http.ServerResponse) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Proxy error occurred'); + } + }); + this.server = http.createServer((req, res) => { const { hostname, port } = url.parse(req.url); + logger.debug(`Incoming request for ${req.url}`); + logger.debug(`Request method: ${req.method}`); + logger.debug(`Request headers: ${JSON.stringify(req.headers, null, 2)}`); + if (hostname && port) { - this.proxy.web(req, res, { target: `http://${hostname}:${port}` }); + const target = `http://${hostname}:${port}`; + logger.info(`Proxying request to ${target}`); + logger.debug(`Target details - Hostname: ${hostname}, Port: ${port}`); + this.proxy.web(req, res, { target }); } else { + logger.warn(`Invalid request received: ${req.url}`); + logger.debug(`Parse result - Hostname: ${hostname}, Port: ${port}`); res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Invalid request'); } }); this.server.on('connect', this.handleConnectRequest.bind(this)); + logger.info('Proxy server initialized successfully'); } - - private handleConnectRequest(req: http.IncomingMessage, clientSocket: net.Socket, head: Buffer) { const { port, hostname } = url.parse(`//${req.url}`, false, true); + logger.debug(`CONNECT request received for ${req.url}`); + logger.debug(`CONNECT request headers: ${JSON.stringify(req.headers, null, 2)}`); + if (hostname && port) { + logger.info(`Establishing tunnel to ${hostname}:${port}`); + logger.debug(`Attempting connection with parameters - Hostname: ${hostname}, Port: ${port}`); const serverSocket = net.connect(parseInt(port, 10), hostname, () => { + logger.debug(`Tunnel established to ${hostname}:${port}`); + logger.debug(`Local address: ${serverSocket.localAddress}:${serverSocket.localPort}`); clientSocket.write('HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node.js-Proxy\r\n' + '\r\n'); serverSocket.write(head); + + // Setup bidirectional tunnel serverSocket.pipe(clientSocket); clientSocket.pipe(serverSocket); + logger.debug('Bidirectional tunnel established successfully'); + + // Log socket events + serverSocket.on('error', (err) => { + logger.error(`Server socket error: ${err.message}`); + logger.debug(`Server socket error details: ${err.stack}`); + }); + + clientSocket.on('error', (err) => { + logger.error(`Client socket error: ${err.message}`); + logger.debug(`Client socket error details: ${err.stack}`); + }); + }); + + serverSocket.on('error', (err) => { + logger.error(`Failed to establish tunnel to ${hostname}:${port}: ${err.message}`); + clientSocket.write('HTTP/1.1 500 Internal Server Error\r\n' + + 'Content-Type: text/plain\r\n' + + '\r\n' + + 'Failed to establish connection'); + clientSocket.end(); }); } else { + logger.warn(`Invalid CONNECT request received: ${req.url}`); clientSocket.write('HTTP/1.1 400 Bad Request\r\n' + 'Content-Type: text/plain\r\n' + '\r\n' + @@ -46,12 +101,12 @@ export default class ProxyServer { clientSocket.end(); } } - } const proxyServer = new ProxyServer(); -console.log('HTTP proxy server trying to start on port 4001'); +logger.info('HTTP proxy server trying to start on port 4001'); proxyServer.server.listen(4001, () => { - console.log('HTTP proxy server listening on port 4001'); + logger.info('HTTP proxy server listening on port 4001'); }); + diff --git a/resources/sdk/purecloudpython/templates/rest.mustache b/resources/sdk/purecloudpython/extensions/rest.py similarity index 100% rename from resources/sdk/purecloudpython/templates/rest.mustache rename to resources/sdk/purecloudpython/extensions/rest.py diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py index e2dc2b312..bbbcfc0a1 100644 --- a/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py +++ b/resources/sdk/purecloudpython/scripts/SdkTests_mtls.py @@ -82,32 +82,6 @@ def test_2_mtls_gw_authenticate(self): self.delete_user(SdkTests_mtls.users_api_mtls) print("=== EXITING test_mtls_authenticate() ===\n") - def test_3_proxy_authenticate(self): - print("=== ENTERING test_proxy_authenticate() ===") - environment = os.environ.get('PURECLOUD_ENVIRONMENT') - region = self.purecloudregiontest(environment) - print(f"Using region: {region}") - if isinstance(region,PureCloudPlatformClientV2.PureCloudRegionHosts): - PureCloudPlatformClientV2.configuration.host = region.get_api_host() - print(f"API host set to: {region.get_api_host()}") - elif isinstance(region,str): - PureCloudPlatformClientV2.configuration.host = 'https://api.%s' % (environment) - print("Environment not found in PureCloudRegionHosts defaulting to string value") - print(f"API host set to: https://api.{environment}") - - #Proxy setting and the request should go via proxy. - PureCloudPlatformClientV2.configuration.proxy="http://localhost:4001" - - # Authenticate with client credentials and pass the apiclient instance into the usersapi - print("Authenticating with client credentials...") - SdkTests_mtls.apiclient_proxy = PureCloudPlatformClientV2.api_client.ApiClient() - SdkTests_mtls.apiclient_proxy.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) - SdkTests_mtls.users_api_proxy = PureCloudPlatformClientV2.UsersApi(SdkTests_mtls.apiclient_proxy) - print(f"Authentication successful access_token: {SdkTests_mtls.apiclient_proxy}") - self.create_user(SdkTests_mtls.users_api_proxy) - self.delete_user(SdkTests_mtls.users_api_proxy) - print("=== EXITING test_proxy_authenticate() ===\n") - def create_user(self, users_api): print("=== ENTERING create_user() ===") body = PureCloudPlatformClientV2.CreateUser() diff --git a/resources/sdk/purecloudpython/scripts/SdkTests_proxy.py b/resources/sdk/purecloudpython/scripts/SdkTests_proxy.py new file mode 100644 index 000000000..805b0b756 --- /dev/null +++ b/resources/sdk/purecloudpython/scripts/SdkTests_proxy.py @@ -0,0 +1,126 @@ +import base64, imp, os, requests, sys, unittest, uuid, time +from pprint import pprint +from retry import retry + +# Load SDK from local build +sys.path.append('../../../../output/purecloudpython/build/build/lib') +import PureCloudPlatformClientV2 + + +class SdkTests_proxy(unittest.TestCase): + lastResult = None + + userId = None + userEmail = None + userName = 'Python SDK Tester' + userDepartment = 'Ministry of Testing' + userProfileSkill = 'Testmaster' + busyPresenceId = '31fe3bac-dea6-44b7-bed7-47f91660a1a0' + availablePresenceId = '6a3af858-942f-489d-9700-5f9bcdcdae9b' + + def setUp(self): + # Skip if there has been a failure + if SdkTests_proxy.lastResult != None and (len(SdkTests_proxy.lastResult.failures) > 0 or len(SdkTests_proxy.lastResult.errors) > 0): + print("=== WARNING: Previous test failed, skipping current test ===", flush=True) + self.skipTest('Previous test failed') + + def run(self, result=None): + # Store this execution's result as the last one + SdkTests_proxy.lastResult = result + + # Run the test + unittest.TestCase.run(self, result) + + def test_1_trace_basic_information(self): + print("=== ENTERING test_1_trace_basic_information() ===") + print('PURECLOUD_ENVIRONMENT=%s' % (os.environ.get('PURECLOUD_ENVIRONMENT'))) + self.assertIsNotNone(os.environ.get('PURECLOUD_ENVIRONMENT')) + + print('PURECLOUD_CLIENT_ID=%s' % (os.environ.get('PURECLOUD_CLIENT_ID'))) + self.assertIsNotNone(os.environ.get('PURECLOUD_CLIENT_ID')) + + self.assertIsNotNone(os.environ.get('PURECLOUD_CLIENT_SECRET')) + + SdkTests_proxy.userEmail = '%s@%s' % (uuid.uuid4(), os.environ.get('PURECLOUD_ENVIRONMENT')) + print(SdkTests_proxy.userEmail) + + print(PureCloudPlatformClientV2) + print("=== EXITING test_1_trace_basic_information() ===\n") + + def test_2_proxy_authenticate(self): + print("=== ENTERING test_2_proxy_authenticate() ===") + environment = os.environ.get('PURECLOUD_ENVIRONMENT') + region = self.purecloudregiontest(environment) + print(f"Using region: {region}") + if isinstance(region,PureCloudPlatformClientV2.PureCloudRegionHosts): + PureCloudPlatformClientV2.configuration.host = region.get_api_host() + print(f"API host set to: {region.get_api_host()}") + elif isinstance(region,str): + PureCloudPlatformClientV2.configuration.host = 'https://api.%s' % (environment) + print("Environment not found in PureCloudRegionHosts defaulting to string value") + print(f"API host set to: https://api.{environment}") + + #Proxy setting and the request should go via proxy. + PureCloudPlatformClientV2.configuration.verify_ssl=False + PureCloudPlatformClientV2.configuration.proxy="http://localhost:4001" + + # Authenticate with client credentials and pass the apiclient instance into the usersapi + print("Authenticating with client credentials...") + SdkTests_proxy.apiclient_proxy = PureCloudPlatformClientV2.api_client.ApiClient() + SdkTests_proxy.apiclient_proxy.get_client_credentials_token(os.environ.get('PURECLOUD_CLIENT_ID'), os.environ.get('PURECLOUD_CLIENT_SECRET')) + SdkTests_proxy.users_api_proxy = PureCloudPlatformClientV2.UsersApi(SdkTests_proxy.apiclient_proxy) + print(f"Authentication successful access_token: {SdkTests_proxy.apiclient_proxy}") + self.create_user(SdkTests_proxy.users_api_proxy) + self.delete_user(SdkTests_proxy.users_api_proxy) + print("=== EXITING test_2_proxy_authenticate() ===\n") + + def create_user(self, users_api): + print("=== ENTERING create_user() ===") + body = PureCloudPlatformClientV2.CreateUser() + body.name = SdkTests_proxy.userName + body.email = SdkTests_proxy.userEmail + body.password = '%s!@#$1234asdfASDF' % (uuid.uuid4()) + print(f"Creating user with name: {body.name}, email: {body.email}") + + user = users_api.post_users(body) + print(f"User created successfully") + + SdkTests_proxy.userId = user.id + print(f"User ID: {SdkTests_proxy.userId}") + self.assertEqual(user.name, SdkTests_proxy.userName) + self.assertEqual(user.email, SdkTests_proxy.userEmail) + print(SdkTests_proxy.userId) + print("=== EXITING create_user() ===\n") + + def delete_user(self, users_api): + print("=== ENTERING delete_user() ===") + print(f"Deleting user {SdkTests_proxy.userId}") + users_api.delete_user(SdkTests_proxy.userId) + print(f"User {SdkTests_proxy.userId} deleted successfully") + print("=== EXITING delete_user() ===\n") + + def purecloudregiontest(self,x): + print(f"=== ENTERING purecloudregiontest() with environment {x} ===") + result = { + 'mypurecloud.com': PureCloudPlatformClientV2.PureCloudRegionHosts.us_east_1, + 'mypurecloud.ie': PureCloudPlatformClientV2.PureCloudRegionHosts.eu_west_1, + 'mypurecloud.com.au': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_southeast_2, + 'mypurecloud.jp': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_northeast_1, + 'mypurecloud.de': PureCloudPlatformClientV2.PureCloudRegionHosts.eu_central_1, + 'usw2.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.us_west_2, + 'cac1.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.ca_central_1, + 'apne2.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_northeast_2, + 'euw2.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.eu_west_2, + 'aps1.pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.ap_south_1, + 'use2.us-gov-pure.cloud': PureCloudPlatformClientV2.PureCloudRegionHosts.us_east_2 + }.get(x,x) + print(f"=== EXITING purecloudregiontest() with result: {result} ===") + return result + + +if __name__ == '__main__': + unittest.sortTestMethodsUsing(None) + print("Running SdkTests_proxy Tests") + unittest.main() + print("SdkTests_proxy Tests Complete") + \ No newline at end of file diff --git a/resources/sdk/purecloudpython/scripts/compile.sh b/resources/sdk/purecloudpython/scripts/compile.sh index 4c2bd14e7..cd14e6e12 100644 --- a/resources/sdk/purecloudpython/scripts/compile.sh +++ b/resources/sdk/purecloudpython/scripts/compile.sh @@ -30,7 +30,7 @@ python3.6 -m pip install --user -U retry echo "Run unit tests" pwd python3.6 -m unittest SdkTests -cd "$TESTS_DIR" echo "Run unit tests mtls" -pwd python3.6 -m unittest SdkTests_mtls +echo "Run unit tests proxy" +python3.6 -m unittest SdkTests_proxy From 828719be0a8778070b7f72cb90a914657555b05c Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:49:00 +0530 Subject: [PATCH 7/8] addressed review comments --- resources/sdk/purecloudpython/templates/api_client.mustache | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/sdk/purecloudpython/templates/api_client.mustache b/resources/sdk/purecloudpython/templates/api_client.mustache index 1869c9e14..a0218a967 100644 --- a/resources/sdk/purecloudpython/templates/api_client.mustache +++ b/resources/sdk/purecloudpython/templates/api_client.mustache @@ -261,6 +261,7 @@ class ApiClient(object): http_client = self.get_http_client() response = http_client.request(request_options) + data = json.loads('[' + response.data + ']') self.access_token = data[0]["access_token"] self.refresh_token = data[0]["refresh_token"] From 1867532070f97b3d4ede6db31363b6dc8db0e919 Mon Sep 17 00:00:00 2001 From: sureshperiyappan <61573777+sureshperiyappan@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:39:29 +0530 Subject: [PATCH 8/8] readme file updated --- .../purecloudpython/templates/README.mustache | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/resources/sdk/purecloudpython/templates/README.mustache b/resources/sdk/purecloudpython/templates/README.mustache index eac5571f1..52ee6ead6 100644 --- a/resources/sdk/purecloudpython/templates/README.mustache +++ b/resources/sdk/purecloudpython/templates/README.mustache @@ -176,7 +176,7 @@ PureCloudPlatformClientV2.configuration.proxy_username = 'YOUR_PROXY_USERNAME' PureCloudPlatformClientV2.configuration.proxy_password = 'YOUR_PROXY_PASSWORD' ``` -The Python SDK uses `urllib3.ProxyManager` to make requests when `proxy` is provided. +The Python SDK uses `urllib3.ProxyManager` to make requests when `proxy` is provided with default http client implementation (refer to [Inject Custom HTTP Client](#inject-custom-http-client)). #### SDK Logging @@ -379,6 +379,80 @@ worktype_update.default_queue_id = PureCloudPlatformClientV2.ApiNullValue() task_api.patch_taskmanagement_worktype(worktype_id, worktype_update) ``` +## Inject Custom HTTP Client +By default, the SDK uses the urllib3 library as the default HTTP client. If you want to inject a new third-party/custom implementation of the client, you can set the HTTP client instance. + +The CustomHttpClient should be a class that inherits from AbstractHttpClient defined in the SDK and implements the request method. Here's an example: + +```python +from PureCloudPlatformClientV2 import AbstractHttpClient +import requests + +class CustomHttpClient(AbstractHttpClient): + def __init__(self): + super().__init__() + self._session = requests.Session() + + def request(self, options): + return self._session.request(**options) + +# Initialize the API client +api_client = PureCloudPlatformClientV2.api_client.ApiClient() + +# Create and set custom HTTP client +http_client = CustomHttpClient() +api_client.set_http_client(http_client) +``` + +## Using MTLS authentication via a Gateway +With Python Client applications, if there is MTLS authentication that need to be set for a gateway server (i.e. if the Genesys Cloud requests must be sent through an intermediate API gateway or equivalent, with MTLS enabled), you can use set_mtls_certificates or set_mtls_contents to set the certificates. + +An example using set_mtls_certificates to setup MTLS for gateway is shown below + +```python +MTLS_CERT_DIR = "mtls-certs" +CA_CERT_FILENAME = "ca-chain.cert.pem" +CLIENT_CERT_FILENAME = "localhost.cert.pem" +CLIENT_KEY_FILENAME = "localhost.key.pem" +base_dir = os.path.dirname(__file__) +ca_cert_path = os.path.join(base_dir, MTLS_CERT_DIR, CA_CERT_FILENAME) +client_cert_path = os.path.join(base_dir, MTLS_CERT_DIR, CLIENT_CERT_FILENAME) +client_key_path = os.path.join(base_dir, MTLS_CERT_DIR, CLIENT_KEY_FILENAME) +PureCloudPlatformClientV2.configuration.set_mtls_certificates(client_cert_path, client_key_path, ca_cert_path) + +apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() +apiclient_mtls.set_gateway( + hostname="mygateway.mydomain.myextension", + scheme="https", + port=4027, + login_path="login", + api_path="api" +) +``` + + +If you have content of the private keys and cert information instead of the the filepaths , you can directly set this information using set_mtls_contents + +An example using set_mtls_contents to setup MTLS for gateway is shown below + +```python +PureCloudPlatformClientV2.configuration.set_mtls_contents(client_cert_conts, client_key_conts, ca_cert_conts) # make sure that the content of the certificate and key have been feteched properly and supplied to set_mtls_contents() function. + +apiclient_mtls = PureCloudPlatformClientV2.api_client.ApiClient() +apiclient_mtls.set_gateway( + hostname="mygateway.mydomain.myextension", + scheme="https", + port=4027, + login_path="login", + api_path="api" +) +``` + + + +If you require a custom HTTP client to handle mTLS, you can utilize the set_http_client() method of the API client instance to integrate your own implementation. Remember that you will be responsible for configuring the mTLS settings within your custom HTTP client. + + ## SDK Source Code Generation The SDK is automatically regenerated and published from the API's definition after each API release. For more information on the build process, see the [platform-client-sdk-common](https://github.com/MyPureCloud/platform-client-sdk-common) project.