diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 87f0995..e98f89d 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -45,7 +45,8 @@ def _verify(self, environ, start_response): uri = ctx.get('uri') px_proxy = PXProxy(config) if px_proxy.should_reverse_request(uri): - return px_proxy.handle_reverse_request(self.config, ctx, start_response, environ) + body = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH'))) if environ.get('CONTENT_LENGTH') else '' + return px_proxy.handle_reverse_request(self.config, ctx, start_response, body) if px_utils.is_static_file(ctx): logger.debug('Filter static file request. uri: ' + uri) return self.app(environ, start_response) @@ -65,11 +66,11 @@ def _verify(self, environ, start_response): return self.handle_verification(ctx, self.config, environ, start_response) except: logger.error("Cought exception, passing request") - self.pass_traffic(ctx) + self.pass_traffic({}) return self.app(environ, start_response) def handle_verification(self, ctx, config, environ, start_response): - score = ctx.get('risk_score', -1) + score = ctx.get('score', -1) result = None headers = None status = None diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index efb7f83..4f6d16d 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -72,7 +72,8 @@ def prepare_risk_body(ctx, config): 'ip': ctx.get('ip'), 'headers': format_headers(ctx.get('headers')), 'uri': ctx.get('uri'), - 'url': ctx.get('full_url', '') + 'url': ctx.get('full_url', ''), + 'firstParty': 'true' if config.first_party else 'false' }, 'vid': ctx.get('vid', ''), 'uuid': ctx.get('uuid', ''), @@ -82,10 +83,15 @@ def prepare_risk_body(ctx, config): 'http_version': ctx.get('http_version', ''), 'module_version': config.module_version, 'risk_mode': config.module_mode, - 'px_cookie_hmac': ctx.get('cookie_hmac', ''), - 'request_cookie_names': ctx.get('cookie_names', '') + 'request_cookie_names': ctx.get('cookie_names', ''), + 'cookie_origin': ctx.get('cookie_origin') } } + if ctx.get('cookie_hmac'): + body['additional']['px_cookie_hmac'] = ctx.get('cookie_hmac') + + + body = add_original_token_data(ctx, body) if config.enrich_custom_parameters: risk_custom_params = config.enrich_custom_parameters(custom_params) @@ -96,7 +102,7 @@ def prepare_risk_body(ctx, config): if ctx['s2s_call_reason'] == 'cookie_decryption_failed': logger.debug('attaching orig_cookie to request') - body['additional']['px_cookie_orig'] = ctx.get('px_orig_cookie') + body['additional']['px_orig_cookie'] = ctx.get('px_orig_cookie') if ctx['s2s_call_reason'] in ['cookie_expired', 'cookie_validation_failed']: logger.debug('attaching px_cookie to request') @@ -105,6 +111,16 @@ def prepare_risk_body(ctx, config): logger.debug("PxAPI[send_risk_request] request body: " + str(body)) return body +def add_original_token_data(ctx, body): + if ctx.get('original_uuid'): + body['additional']['original_uuid'] = ctx.get('original_uuid') + if ctx.get('original_token_error'): + body['additional']['original_token_error'] = ctx.get('original_token_error') + if ctx.get('original_token'): + body['additional']['original_token'] = ctx.get('original_token') + if ctx.get('decoded_original_token'): + body['additional']['decoded_original_token'] = ctx.get('decoded_original_token') + return body def format_headers(headers): ret_val = [] diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 01e634b..1d99271 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -1,5 +1,9 @@ PREFIX_PX_COOKIE_V1 = '_px' PREFIX_PX_COOKIE_V3 = '_px3' +PREFIX_PX_TOKEN_V1 = '1' +PREFIX_PX_TOKEN_V3 = '3' +MOBILE_SDK_HEADER = "x-px-authorization" +MOBILE_SDK_ORIGINAL_HEADER= "x-px-original-token" TRANS_5C = b"".join(chr(x ^ 0x5C) for x in range(256)) TRANS_36 = b"".join(chr(x ^ 0x36) for x in range(256)) diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index ecd29b8..db9aa84 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -1,7 +1,6 @@ import Cookie from px_constants import * - def build_context(environ, config): logger = config.logger headers = {} @@ -12,6 +11,8 @@ def build_context(environ, config): http_protocol = 'http://' px_cookies = {} request_cookie_names = list() + cookie_origin = "cookie" + original_token = None # Extracting: Headers, user agent, http method, http version for key in environ.keys(): @@ -29,22 +30,34 @@ def build_context(environ, config): http_version = protocol_split[1] if key == 'CONTENT_TYPE' or key == 'CONTENT_LENGTH': headers[key.replace('_', '-').lower()] = environ.get(key) + if key == 'HTTP_' + MOBILE_SDK_HEADER.replace('-','_').upper(): + headers[MOBILE_SDK_HEADER] = environ.get(key, '') - - cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE', '')) - cookie_keys = cookies.keys() - - for key in cookie_keys: - request_cookie_names.append(key) - if key == PREFIX_PX_COOKIE_V1 or key == PREFIX_PX_COOKIE_V3: - logger.debug('Found cookie prefix:' + key) - px_cookies[key] = cookies.get(key).value + mobile_header = headers.get(MOBILE_SDK_HEADER) vid = None - if '_pxvid' in cookie_keys: - vid = cookies.get('_pxvid').value + original_token = None + if mobile_header is None: + cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE', '')) + cookie_keys = cookies.keys() + + for key in cookie_keys: + request_cookie_names.append(key) + if key == PREFIX_PX_COOKIE_V1 or key == PREFIX_PX_COOKIE_V3: + logger.debug('Found cookie prefix:' + key) + px_cookies[key] = cookies.get(key).value + vid = None + if '_pxvid' in cookie_keys: + vid = cookies.get('_pxvid').value + else: + vid = '' else: - vid = '' - user_agent = environ.get('HTTP_USER_AGENT', '') + cookie_origin = "header" + original_token = headers.get(MOBILE_SDK_ORIGINAL_HEADER) + logger.debug('Mobile SDK token detected') + cookie_name, cookie = get_token_object(config, mobile_header) + px_cookies[cookie_name] = cookie + + user_agent = headers.get('user-agent','') uri = environ.get('PATH_INFO') or '' full_url = http_protocol + (headers.get('host') or environ.get('SERVER_NAME') or '') + uri hostname = headers.get('host') @@ -62,15 +75,28 @@ def build_context(environ, config): 'cookie_names': request_cookie_names, 'risk_rtt': 0, 'ip': extract_ip(config, environ), - 'vid': vid, + 'vid': vid if vid else None, 'query_params': environ['QUERY_STRING'], 'sensitive_route': sensitive_route, 'whitelist_route': whitelist_route, 's2s_call_reason': 'none', - 'cookie_origin': 'cookie' + 'cookie_origin':cookie_origin, + 'original_token': original_token, + 'is_mobile': cookie_origin == "header" } + return ctx +def get_token_object(config, token): + result = {} + logger = config.logger + sliced_token = token.split(":", 1) + if len(sliced_token) > 1: + key = sliced_token.pop(0) + if key == PREFIX_PX_TOKEN_V1 or key == PREFIX_PX_TOKEN_V3: + logger.debug('Found token prefix:' + key) + return key, sliced_token[0] + return PREFIX_PX_TOKEN_V3, token def extract_ip(config, environ): ip = environ.get('HTTP_X_FORWARDED_FOR') if environ.get('HTTP_X_FORWARDED_FOR') else environ.get('REMOTE_ADDR') diff --git a/perimeterx/px_cookie.py b/perimeterx/px_cookie.py index 8f14128..a638548 100644 --- a/perimeterx/px_cookie.py +++ b/perimeterx/px_cookie.py @@ -11,31 +11,32 @@ import struct -class PxCookie: +class PxCookie(object): def __init__(self, config): self._config = config self._logger = config.logger - def build_px_cookie(self, ctx): + def build_px_cookie(self, px_cookies, user_agent): self._logger.debug("PxCookie[build_px_cookie]") - px_cookies = ctx['px_cookies'].keys() # Check that its not empty if not px_cookies: return None - - px_cookies.sort(reverse=True) - prefix = px_cookies[0] - if prefix == PREFIX_PX_COOKIE_V1: - self._logger.debug("PxCookie[build_px_cookie] using cookie v1") + px_cookie_keys = px_cookies.keys() + px_cookie_keys.sort(reverse=True) + prefix = px_cookie_keys[0] + if prefix == PREFIX_PX_TOKEN_V1 or prefix == PREFIX_PX_COOKIE_V1: + self._logger.debug("PxCookie[build_px_cookie] using token v1") from px_cookie_v1 import PxCookieV1 - return PxCookieV1(ctx, self._config) - - if prefix == PREFIX_PX_COOKIE_V3: - self._logger.debug("PxCookie[build_px_cookie] using cookie v3") + return PxCookieV1(self._config, px_cookies[prefix]) + if prefix == PREFIX_PX_TOKEN_V3 or prefix == PREFIX_PX_COOKIE_V3: + self._logger.debug("PxCookie[build_px_cookie] using token v3") from px_cookie_v3 import PxCookieV3 - return PxCookieV3(ctx, self._config) + ua = '' + if prefix == PREFIX_PX_COOKIE_V3: + ua = user_agent + return PxCookieV3(self._config, px_cookies[prefix], ua) def decode_cookie(self): self._logger.debug("PxCookie[decode_cookie]") @@ -149,7 +150,8 @@ def is_cookie_valid(self, str_to_hmac): return False def deserialize(self): - self._logger.debug("PxCookie[deserialize]") + logger = self._logger + logger.debug("PxCookie[deserialize]") if self._config.encryption_enabled: cookie = self.decrypt_cookie() else: @@ -158,7 +160,7 @@ def deserialize(self): if not cookie: return False - self._logger.debug("PxCookie[deserialize] decoded cookie: " + cookie) + logger.debug("Original token deserialized : " + cookie) self.decoded_cookie = json.loads(cookie) return self.is_cookie_format_valid() diff --git a/perimeterx/px_cookie_v1.py b/perimeterx/px_cookie_v1.py index 3c3cd4d..a675ea5 100644 --- a/perimeterx/px_cookie_v1.py +++ b/perimeterx/px_cookie_v1.py @@ -4,11 +4,10 @@ class PxCookieV1(PxCookie): - def __init__(self, ctx, config): - self._ctx = ctx + def __init__(self, config, raw_cookie): self._config = config self._logger = config.logger - self.raw_cookie = ctx['px_cookies'].get(PREFIX_PX_COOKIE_V1, '') + self.raw_cookie = raw_cookie def get_score(self): return self.decoded_cookie['s']['b'] @@ -23,10 +22,8 @@ def is_cookie_format_valid(self): c = self.decoded_cookie return 't' in c and 'v' in c and 'u' in c and "s" in c and 'a' in c['s'] and 'h' in c - def is_secured(self): + def is_secured(self, user_agent, ip): c = self.decoded_cookie - user_agent = self._ctx.get('user_agent', '') - ip = self._ctx.get('ip', '') base_hmac = str(self.get_timestamp()) + str(c['s']['a']) + str(self.get_score()) + self.get_uuid() + self.get_vid() hmac_with_ip = base_hmac + ip + user_agent hmac_without_ip = base_hmac + user_agent diff --git a/perimeterx/px_cookie_v3.py b/perimeterx/px_cookie_v3.py index 9b4f8b0..65ab594 100644 --- a/perimeterx/px_cookie_v3.py +++ b/perimeterx/px_cookie_v3.py @@ -1,18 +1,18 @@ from px_cookie import PxCookie -from px_constants import * class PxCookieV3(PxCookie): - def __init__(self, ctx, config): + def __init__(self, config, cookie, user_agent): self._config = config self._logger = config.logger - self._ctx = ctx - self.raw_cookie = '' - spliced_cookie = self._ctx['px_cookies'].get(PREFIX_PX_COOKIE_V3, '').split(":", 1) - if len(spliced_cookie) > 1: + self._user_agent = user_agent + spliced_cookie = cookie.split(':') + if len(spliced_cookie) is 4: self.hmac = spliced_cookie[0] - self.raw_cookie = spliced_cookie[1] + self.raw_cookie = ':'.join(spliced_cookie[1:]) + else: + self.raw_cookie = cookie def get_score(self): return self.decoded_cookie['s'] @@ -24,11 +24,11 @@ def get_action(self): return self.decoded_cookie['a'] def is_cookie_format_valid(self): - c = self.decoded_cookie; + c = self.decoded_cookie return 't' in c and 'v' in c and 'u' in c and 's' in c and 'a' in c def is_secured(self): - user_agent = self._ctx.get('user_agent', '') + user_agent = self._user_agent str_hmac = self.raw_cookie + user_agent return self.is_cookie_valid(str_hmac) diff --git a/perimeterx/px_cookie_validator.py b/perimeterx/px_cookie_validator.py index 9f3f68c..3ff96bc 100644 --- a/perimeterx/px_cookie_validator.py +++ b/perimeterx/px_cookie_validator.py @@ -1,4 +1,6 @@ import traceback +import re +import px_original_token_validator from px_cookie import PxCookie @@ -19,8 +21,22 @@ def verify(ctx, config): ctx['s2s_call_reason'] = 'no_cookie' return False + if not config.cookie_key: + logger.debug('No cookie key found, pause cookie evaluation') + ctx['s2s_call_reason'] = 'no_cookie_key' + return False + px_cookie_builder = PxCookie(config) - px_cookie = px_cookie_builder.build_px_cookie(ctx) + px_cookie = px_cookie_builder.build_px_cookie(px_cookies=ctx.get('px_cookies'), + user_agent=ctx.get('user_agent')) + #Mobile SDK traffic + if px_cookie and ctx['is_mobile']: + pattern = re.compile("^\d+$") + if re.match(pattern, px_cookie.raw_cookie): + ctx['s2s_call_reason'] = "mobile_error_" + px_cookie.raw_cookie + if ctx['original_token'] is not None: + px_original_token_validator.verify(ctx, config) + return False if not px_cookie.deserialize(): logger.error('Cookie decryption failed') @@ -28,7 +44,7 @@ def verify(ctx, config): ctx['s2s_call_reason'] = 'cookie_decryption_failed' return False - ctx['risk_score'] = px_cookie.get_score() + ctx['score'] = px_cookie.get_score() ctx['uuid'] = px_cookie.get_uuid() ctx['vid'] = px_cookie.get_vid() ctx['decoded_cookie'] = px_cookie.decoded_cookie @@ -37,8 +53,8 @@ def verify(ctx, config): if px_cookie.is_high_score(): ctx['block_reason'] = 'cookie_high_score' - logger.debug('Cookie with high score: ' + str(ctx['risk_score'])) - return False + logger.debug('Cookie with high score: ' + str(ctx['score'])) + return True if px_cookie.is_cookie_expired(): ctx['s2s_call_reason'] = 'cookie_expired' @@ -55,7 +71,7 @@ def verify(ctx, config): ctx['s2s_call_reason'] = 'sensitive_route' return False - logger.debug('Cookie validation passed with good score: ' + str(ctx['risk_score'])) + logger.debug('Cookie validation passed with good score: ' + str(ctx['score'])) return True except Exception, e: traceback.print_exc() diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index 8e189b6..81e7e68 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -7,7 +7,7 @@ def send(full_url, body, headers, config, method): try: start = time.time() if method == 'GET': - response = requests.get(url='https://' + full_url, headers=headers, timeout=500, stream=True) + response = requests.get(url='https://' + full_url, headers=headers, timeout=config.api_timeout, stream=True) else: response = requests.post(url='https://' + full_url, headers=headers, data=body, timeout=config.api_timeout) @@ -17,6 +17,6 @@ def send(full_url, body, headers, config, method): logger.debug('PerimeterX server call took ' + str(time.time() - start) + 'ms') return response - except requests.exceptions.RequestException as e: + except requests.exceptions as e: logger.debug('Received RequestException, message: ' + e.message) return False diff --git a/perimeterx/px_original_token_validator.py b/perimeterx/px_original_token_validator.py new file mode 100644 index 0000000..9762483 --- /dev/null +++ b/perimeterx/px_original_token_validator.py @@ -0,0 +1,39 @@ + +from px_cookie import PxCookie + +def verify(ctx, config): + """ + main verification function, verifying the content of the perimeterx original token risk if exists + :param ctx: perimeterx request context object + :param config: global configurations + :type ctx: dictionary + :type config: dictionary + :return: Returns True if verification succeeded and False if not + :rtype: Bool + """ + logger = config.logger + try: + logger.debug('Original token found, Evaluating') + original_token = ctx.get('original_token') + version, no_version_token = original_token.split(':', 1) + px_cookie_builder = PxCookie(config) + px_cookie = px_cookie_builder.build_px_cookie({version: no_version_token}, ctx.get('is_mobile'),'') + + if not px_cookie.deserialize(): + logger.error('Original token decryption failed, value:' + px_cookie.raw_cookie) + ctx['original_token_error'] = 'decryption_failed' + return False + + ctx['decoded_original_token'] = px_cookie.decoded_cookie + ctx['vid'] = px_cookie.get_vid() + ctx['original_uuid'] = px_cookie.get_uuid() + if not px_cookie.is_secured(): + logger.debug('Original token HMAC validation failed, value: ' + str(px_cookie.decoded_cookie)) + ctx['original_token_error'] = 'validation_failed' + return False + return True + + except Exception, e: + logger.debug('Could not decrypt original token, exception was thrown, decryption failed ' + e.message) + ctx['original_token_error'] = 'decryption_failed' + return False diff --git a/perimeterx/px_proxy_handler.py b/perimeterx/px_proxy_handler.py index 884e54e..405bc46 100644 --- a/perimeterx/px_proxy_handler.py +++ b/perimeterx/px_proxy_handler.py @@ -10,6 +10,13 @@ } +def delete_extra_headers(filtered_headers): + if 'content-length' in filtered_headers.keys(): + del filtered_headers['content-length'] + if 'content-type' in filtered_headers.keys(): + del filtered_headers['content-type'] + + class PXProxy(object): def __init__(self, config): self._logger = config.logger @@ -26,13 +33,13 @@ def should_reverse_request(self, uri): return True return False - def handle_reverse_request(self, config, ctx, start_response, environ): + def handle_reverse_request(self, config, ctx, start_response, body): uri = ctx.get('uri').lower() if uri.startswith(self.client_reverse_prefix): return self.send_reverse_client_request(config=config, context=ctx, start_response=start_response) if uri.startswith(self.xhr_reverse_prefix): - return self.send_reverse_xhr_request(config=config, context=ctx, start_response=start_response, body = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH', '0')))) + return self.send_reverse_xhr_request(config=config, context=ctx, start_response=start_response, body=body) if uri.startswith(self.captcha_reverse_prefix): return self.send_reverse_captcha_request(config=config, context=ctx, start_response=start_response) @@ -43,21 +50,21 @@ def send_reverse_client_request(self, config, context, start_response): return "" client_request_uri = '/{}/main.min.js'.format(config.app_id) - self._logger.debug('Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(),px_constants.CLIENT_HOST, client_request_uri)) + self._logger.debug( + 'Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(), px_constants.CLIENT_HOST, + client_request_uri)) headers = {'host': px_constants.CLIENT_HOST, px_constants.FIRST_PARTY_HEADER: '1', px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), context.get('ip')) filtered_headers = px_utils.merge_two_dicts(filtered_headers, headers) - del filtered_headers['content-length'] - del filtered_headers['content-type'] + delete_extra_headers(filtered_headers) response = px_httpc.send(full_url=px_constants.CLIENT_HOST + client_request_uri, body='', headers=filtered_headers, config=config, method='GET') self.handle_proxy_response(response, start_response) return response.raw.read() - def send_reverse_xhr_request(self, config, context, start_response, body): uri = context.get('uri') @@ -80,7 +87,8 @@ def send_reverse_xhr_request(self, config, context, start_response, body): filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), context.get('ip')) filtered_headers = px_utils.merge_two_dicts(filtered_headers, headers) - self._logger.debug('Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(), host, suffix_uri)) + self._logger.debug( + 'Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(), host, suffix_uri)) response = px_httpc.send(full_url=host + suffix_uri, body=body, headers=filtered_headers, config=config, method=context.get('http_method')) @@ -114,7 +122,8 @@ def send_reverse_captcha_request(self, config, context, start_response): headers = [('Content-Type', 'application/javascript')] start_response(status, headers) return '' - uri = '/{}{}?{}'.format(config.app_id, context.get('uri').lower().replace(self.captcha_reverse_prefix, ''), context['query_params']) + uri = '/{}{}?{}'.format(config.app_id, context.get('uri').lower().replace(self.captcha_reverse_prefix, ''), + context['query_params']) host = px_constants.CAPTCHA_HOST headers = {'host': px_constants.CAPTCHA_HOST, @@ -122,12 +131,9 @@ def send_reverse_captcha_request(self, config, context, start_response): px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), context.get('ip')) filtered_headers = px_utils.merge_two_dicts(filtered_headers, headers) - del filtered_headers['content-length'] - del filtered_headers['content-type'] + delete_extra_headers(filtered_headers) self._logger.debug('Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(), host, uri)) response = px_httpc.send(full_url=host + uri, body='', headers=filtered_headers, config=config, method='GET') self.handle_proxy_response(response, start_response) return response.raw.read() - - diff --git a/perimeterx/px_token_v1.py b/perimeterx/px_token_v1.py new file mode 100644 index 0000000..182f195 --- /dev/null +++ b/perimeterx/px_token_v1.py @@ -0,0 +1,32 @@ +from px_cookie import PxCookie + + +class PxTokenV1(PxCookie): + + def __init__(self, config, token): + self._config = config + self._logger = config.logger + self.raw_cookie = token + + def get_score(self): + return self.decoded_cookie['s']['b'] + + def get_hmac(self): + return self.decoded_cookie['h'] + + def get_action(self): + return 'c' + + def is_cookie_format_valid(self): + c = self.decoded_cookie + return 't' in c and 'v' in c and 'u' in c and "s" in c and 'a' in c['s'] and 'h' in c + + def is_secured(self, ip): + c = self.decoded_cookie + base_hmac = str(self.get_timestamp()) + str(c['s']['a']) + str(self.get_score()) + self.get_uuid() + self.get_vid() + hmac_with_ip = base_hmac + ip + hmac_without_ip = base_hmac + + return self.is_cookie_valid(hmac_without_ip) or self.is_cookie_valid(hmac_with_ip) + + diff --git a/perimeterx/px_token_v3.py b/perimeterx/px_token_v3.py new file mode 100644 index 0000000..057e2e3 --- /dev/null +++ b/perimeterx/px_token_v3.py @@ -0,0 +1,37 @@ +from px_cookie import PxCookie +from px_constants import * + + +class PxTokenV3(PxCookie): + + def __init__(self, config, token): + + self._config = config + self._logger = config.logger + spliced_cookie = token.split(":", 1) + + print ("Count: " + str(len(spliced_cookie))) + + if len(spliced_cookie) > 1: + self.hmac = spliced_cookie[0] + self.raw_cookie = spliced_cookie[1] + else: + self.raw_cookie = token + + + def get_score(self): + return self.decoded_cookie['s'] + + def get_hmac(self): + return self.hmac + + def get_action(self): + return self.decoded_cookie['a'] + + def is_cookie_format_valid(self): + c = self.decoded_cookie; + return 't' in c and 'v' in c and 'u' in c and 's' in c and 'a' in c + + def is_secured(self): + return self.is_cookie_valid(self.raw_cookie) + diff --git a/tests/px_cookie.py b/tests/px_cookie.py index e69de29..d70de11 100644 --- a/tests/px_cookie.py +++ b/tests/px_cookie.py @@ -0,0 +1,38 @@ + +import unittest +from perimeterx.px_cookie import PxCookie +from perimeterx.px_config import PXConfig + +class TestPXCookie(unittest.TestCase): + + @classmethod + def setUpClass(cls): + config = PXConfig({'app_id': 'fake_app_id'}) + cls.px_cookie = PxCookie(config) + cls.config = config + + def test_build_cookie(self): + px_cookies = {'_px3':'bd078865fa9627f626d6f7d6828ab595028d2c0974065ab6f6c5a9f80c4593cd:OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA='} + cookie = self.px_cookie.build_px_cookie(px_cookies=px_cookies, user_agent='') + self.assertEqual('bd078865fa9627f626d6f7d6828ab595028d2c0974065ab6f6c5a9f80c4593cd', cookie.hmac) + self.assertEqual('OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA=', cookie.raw_cookie) + + px_cookies = {'_px3':'OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA='} + cookie = self.px_cookie.build_px_cookie(px_cookies=px_cookies, user_agent='') + if hasattr(cookie, 'hmac'): + self.assertFalse(True) + + def test_build_token(self): + px_cookies = {'3':'bd078865fa9627f626d6f7d6828ab595028d2c0974065ab6f6c5a9f80c4593cd:OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA='} + cookie = self.px_cookie.build_px_cookie(px_cookies=px_cookies, user_agent='') + self.assertEqual('bd078865fa9627f626d6f7d6828ab595028d2c0974065ab6f6c5a9f80c4593cd', cookie.hmac) + self.assertEqual('OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA=', cookie.raw_cookie) + + px_cookies = {'3':'OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA='} + cookie = self.px_cookie.build_px_cookie(px_cookies=px_cookies, user_agent='') + if hasattr(cookie, 'hmac'): + self.assertFalse(True) + + + + diff --git a/tests/px_cookie_validator.py b/tests/px_cookie_validator.py index 8f984ab..94b0ea4 100644 --- a/tests/px_cookie_validator.py +++ b/tests/px_cookie_validator.py @@ -8,54 +8,51 @@ class Test_PXCookieValidator(unittest.TestCase): @classmethod def setUpClass(cls): cls.cookie_key = 'Pyth0nS3crE7K3Y' + cls.config = PXConfig({'app_id': 'app_id', + 'cookie_key': cls.cookie_key}) def test_verify_no_cookie(self): - config = PXConfig({'app_id': 'app_id'}) - ctx = {'px_cookies': {}} + config = self.config + ctx = {'user_agent':'', 'px_cookies': {}, 'is_mobile': False} verified = px_cookie_validator.verify(ctx, config) self.assertFalse(verified) self.assertEqual('no_cookie', ctx['s2s_call_reason']) def test_verify_valid_cookie(self): - config = PXConfig({'app_id': 'app_id', - 'cookie_key': self.cookie_key}) - ctx = {'px_cookies': { + config = self.config + ctx = {'user_agent':'', 'is_mobile': False, 'px_cookies': { '_px3': 'bd078865fa9627f626d6f7d6828ab595028d2c0974065ab6f6c5a9f80c4593cd:OCIluokZHHvqrWyu8zrWSH8Vu7AefCjrd4CMx/NXsX58LzeV40EZIlPG4gsNMoAYzH88s/GoZwv+DpQa76C21A==:1000:zwT+Rht/YGDNWKkzHtJAB7IiI00u4fOePL/3xWMs1nZ93lzW1XvAMGR2hLlHBmOv8O0CpylEQOZZTK1uQMls6O28Y8aQnTo5DETLkrbhpwCVeNjOcf8GVKTckITwuHfXbEcfHbdtb68s1+jHv1+vt/w/6HZqTzanaIsvFVp8vmA='}} verified = px_cookie_validator.verify(ctx, config) self.assertTrue(verified) self.assertEqual(None, ctx.get('s2s_call_reason')) def test_verify_decryption_failed(self): - config = PXConfig({'app_id': 'app_id', - 'cookie_key': self.cookie_key}) - ctx = {'px_cookies': { + config = self.config + ctx = {'user_agent':'', 'is_mobile': False, 'px_cookies': { '_px3': '774958bcc233ea1a876b92ababf47086d8a4d95165bbd6f98b55d7e61afd2a05:ow3Er5dskpt8ZZ11CRiDMAueEi3ozJTqMBnYzsSM7/8vHTDA0so6ekhruiTrXa/taZINotR5PnTo78D5zM2pWw==:1000:uQ3Tdt7D3mSO5CuHDis3GgrnkGMC+XAghbHuNOE9x4H57RAmtxkTcNQ1DaqL8rx79bHl0iPVYlOcRmRgDiBCUoizBdUCjsSIplofPBLIl8WpfHDDtpxPKzz9I2rUEbFgfhFjiTY3rPGob2PUvTsDXTfPUeHnzKqbNTO8z7H6irFnUE='}} verified = px_cookie_validator.verify(ctx, config) self.assertFalse(verified) self.assertEqual('cookie_decryption_failed', ctx.get('s2s_call_reason')) def test_verify_cookie_high_score(self): - config = PXConfig({'app_id': 'app_id', - 'cookie_key': self.cookie_key}) - ctx = {'px_cookies': { + config = self.config + ctx = {'user_agent':'', 'is_mobile': False, 'px_cookies': { '_px3': 'bf46ceff75278ae166f376cbf741a7639060581035dd4e93641892c905dd0d67:EGFGcwQ2rum7KRmQCeSXBAUt1+25mj2DFJYi7KJkEliF3cBspdXtD2X03Csv8N8B6S5Bte/4ccCcETkBNDVxTw==:1000:x9x+oI6BISFhlKEERpf8HpZD2zXBCW9lzVfuRURHaAnbaMnpii+XjPEd7a7EGGUSMch5ramy3y+KOxyuX3F+LbGYwvn3OJb+u40zU+ixT1w5N15QltX+nBMhC7izC1l8QtgMuG/f3Nts5ebnec9j2V7LS5Y1/5b73rd9s7AMnug='}} verified = px_cookie_validator.verify(ctx, config) self.assertTrue(verified) self.assertEqual(None, ctx.get('s2s_call_reason')) def test_verify_hmac_validation(self): - config = PXConfig({'app_id': 'app_id', - 'cookie_key': self.cookie_key}) - ctx = {'px_cookies': { + config = self.config + ctx = {'user_agent':'', 'is_mobile': False, 'px_cookies': { '_px3': '774958bcc232343ea1a876b92ababf47086d8a4d95165bbd6f98b55d7e61afd2a05:ow3Er5dskpt8ZZ11CRiDMAueEi3ozJTqMBnYzsSM7/8vHTDA0so6ekhruiTrXa/taZINotR5PnTo78D5zM2pWw==:1000:uQ3Tdt7D3mSO5CuHDis3GgrnkGMC+XAghbHuNOE9x4H57RAmtxkTcNQ1DaqL8rx79bHl0iPVYlOcRmRgDiBCUoizBdUCjsSIplofPBLIl8WpfHDDtpxPKzz9I2rUEbFFjiTY3rPGob2PUvTsDXTfPUeHnzKqbNTO8z7H6irFnUE='}} verified = px_cookie_validator.verify(ctx, config) self.assertFalse(verified) self.assertEqual(None, ctx.get('cookie_validation_failed')) def test_verify_expired_cookie(self): - config = PXConfig({'app_id': 'app_id', - 'cookie_key': self.cookie_key}) - ctx = {'px_cookies': { + config = self.config + ctx = {'user_agent':'', 'is_mobile': False, 'px_cookies': { '_px3': '0d67bdf4a58c524b55b9cf0f703e4f0f3cbe23a10bd2671530d3c7e0cfa509eb:HOiYSw11ICB2A+HYx+C+l5Naxcl7hMeEo67QNghCQByyHlhWZT571ZKfqV98JFWg7TvbV9QtlrQtXakPYeIEjQ==:1000:+kuXS/iJUoEqrm8Fo4K0cTebsc4YQZu+f5bRGX0lC1T+l0g1gzRUuKiCtWTar28Y0wjch1ZQvkNy523Pxr07agVi/RL0SUktmEl59qGor+m4FLewZBVdcgx/Ya9kU0riis98AAR0zdTpTtoN5wpNbmztIpOZ0YejeD0Esk3vagU='}} verified = px_cookie_validator.verify(ctx, config) self.assertFalse(verified) diff --git a/tests/px_httpc.py b/tests/px_httpc.py index 16c0cc6..71c42f5 100644 --- a/tests/px_httpc.py +++ b/tests/px_httpc.py @@ -1,20 +1,22 @@ -from perimeterx import px_httpc import unittest -from mock import MagicMock,patch +from perimeterx import px_httpc +import requests_mock from perimeterx.px_config import PXConfig -import httplib - - -class Test_PXHTTPC(unittest.TestCase): +class TestPXHttpc(unittest.TestCase): def test_send(self): - # px_config = PXConfig({'app_id': 'fake_app_id', - # 'auth_token': 'fake_auth_token'}) - # http_client = httplib.HTTPConnection(host='host', timeout=1) - # from httplib2 import Response - # http_client.request = MagicMock(return_value= Response({'status':'200'})) - # with patch('perimeterx.px_httpc.httplib', return_value=http_client): - # message = px_httpc.send('uri', 'body', px_config) + with requests_mock.mock() as m: + config = PXConfig({'app_id': 'PXfake_app_id'}) + full_url = 'this_url.com/uri' + method = 'POST' + body = 'content to post' + headers = {'content-type': 'application/json'} - print 'f' \ No newline at end of file + m.post('https://' + full_url) + response = px_httpc.send(full_url=full_url, config=config,method=method,body=body,headers=headers) + m.called + m.get('https://' + full_url) + method = 'GET' + response = px_httpc.send(full_url=full_url, config=config,method=method,body=body,headers=headers) + self.assertEqual(m.call_count, 2) \ No newline at end of file diff --git a/tests/px_proxy_handler.py b/tests/px_proxy_handler.py index 8e14db5..32dc95b 100644 --- a/tests/px_proxy_handler.py +++ b/tests/px_proxy_handler.py @@ -1,10 +1,9 @@ import unittest - -from httplib import HTTPResponse - +import requests_mock from perimeterx.px_proxy_handler import PXProxy from perimeterx.px_config import PXConfig -from mock import MagicMock,patch +from perimeterx import px_constants + class Test_PXProxy(unittest.TestCase): @@ -18,16 +17,54 @@ def test_should_reverse_request(self): should_reverse = px_proxy.should_reverse_request('/fake_app_id/captcha') self.assertTrue(should_reverse) - # def test_send_reverse_client_request(self): - # content = 'client js content' - # config = PXConfig({'app_id': 'PXfake_app_id'}) - # ctx = {'uri': '/fake_app_id/init.js', 'headers': {'X-FORWARDED-FOR': '127.0.0.1'}} - # fake = HttpResponse(content=content, status=200, reason='OK', content_type='text/html') - # px_proxy = PXProxy(config) - # config = PXConfig({'app_id': 'PXfake_app_id'}) - # with patch('perimeterx.px_httpc.send_https', return_value=fake): - # result = px_proxy.handle_reverse_request(config=config, ctx=ctx, start_response= lambda x: x) - # print '' - + @requests_mock.mock() + def test_send_reverse_client_request(self, mock): + content = 'client js content' + config = PXConfig({'app_id': 'PXfake_app_id'}) + ctx = {'uri': '/fake_app_id/init.js', 'headers': {'X-FORWARDED-FOR': '127.0.0.1'}, 'ip': '127.0.0.1'} + headers = {'host': px_constants.CLIENT_HOST, + px_constants.FIRST_PARTY_HEADER: '1', + px_constants.ENFORCER_TRUE_IP_HEADER: ctx.get('ip'), + px_constants.FIRST_PARTY_FORWARDED_FOR: '127.0.0.1'} + mock.get(url='https://client.perimeterx.net/PXfake_app_id/main.min.js', text=content, request_headers=headers, + status_code=200, reason='OK') + px_proxy = PXProxy(config) + response_body = px_proxy.handle_reverse_request(config=config, ctx=ctx, start_response=lambda x, y: x, body='') + self.assertEqual(content, response_body) + @requests_mock.mock() + def test_send_reverse_captcha_request(self, mock): + content = 'captcha js content' + config = PXConfig({'app_id': 'PXfake_app_id'}) + ctx = {'uri': '/fake_app_id/captcha/captcha.js', + 'headers': {'X-FORWARDED-FOR': '127.0.0.1'}, + 'ip': '127.0.0.1', + 'query_params': 'a=c&u=cfe74220-f484-11e8-9b14-d7280325a290&v=0701bb80-f482-11e8-8a31-a37cf9620569&m=0'} + headers = {'host': px_constants.CAPTCHA_HOST, + px_constants.FIRST_PARTY_HEADER: '1', + px_constants.ENFORCER_TRUE_IP_HEADER: ctx.get('ip'), + px_constants.FIRST_PARTY_FORWARDED_FOR: '127.0.0.1'} + mock.get( + url='https://captcha.px-cdn.net/PXfake_app_id/captcha.js?a=c&u=cfe74220-f484-11e8-9b14-d7280325a290&v=0701bb80-f482-11e8-8a31-a37cf9620569&m=0', + text=content, request_headers=headers, status_code=200, reason='OK') + px_proxy = PXProxy(config) + response_body = px_proxy.handle_reverse_request(config=config, ctx=ctx, start_response=lambda x, y: x, body='') + self.assertEqual(content, response_body) + @requests_mock.mock() + def test_send_reverse_xhr_request(self, mock): + content = 'captcha content' + config = PXConfig({'app_id': 'PXfake_app_id'}) + ctx = {'uri': '/fake_app_id/xhr/api/v1/collector', + 'headers': {'X-FORWARDED-FOR': '127.0.0.1'}, + 'ip': '127.0.0.1', + 'http_method': 'POST'} + headers = {'host': config.collector_host, + px_constants.FIRST_PARTY_HEADER: '1', + px_constants.ENFORCER_TRUE_IP_HEADER: ctx.get('ip'), + px_constants.FIRST_PARTY_FORWARDED_FOR: '127.0.0.1'} + mock.post(url='https://collector-pxfake_app_id.perimeterx.net/api/v1/collector', text=content, + request_headers=headers, status_code=200, reason='OK') + px_proxy = PXProxy(config) + response_body = px_proxy.handle_reverse_request(config=config, ctx=ctx, start_response=lambda x, y: x, body='') + self.assertEqual(content, response_body)