From 1159bbb15ec6b933aeed021400ba80d9f674afd8 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sat, 24 Nov 2018 13:55:22 +0200 Subject: [PATCH 01/34] Fxied errors --- perimeterx/middleware.py | 1 - perimeterx/px_context.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 499ed9a..97ff988 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -5,7 +5,6 @@ import px_httpc import px_blocker import px_api -import px_template from px_proxy import PXProxy import Cookie import px_constants diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index a55e75a..2fd1e7b 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -63,7 +63,7 @@ def build_context(environ, config): 'hostname': hostname, 'px_cookies': px_cookies, 'cookie_names': request_cookie_names, - 'risk_rtt': 0 + 'risk_rtt': 0, 'ip': extract_ip(config, environ), 'vid': vid, 'query_params': environ['QUERY_STRING'] From 8e2a9d4828abb8ec9421f796bee610fb70260792 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sat, 24 Nov 2018 18:00:16 +0200 Subject: [PATCH 02/34] Config refactoring --- perimeterx/middleware.py | 87 +++++++++----------- perimeterx/px_activities_client.py | 15 ++-- perimeterx/px_api.py | 13 +-- perimeterx/px_blocker.py | 12 +-- perimeterx/px_config.py | 126 +++++++++++++++++++++++++++++ perimeterx/px_constants.py | 4 +- perimeterx/px_context.py | 29 ++++--- perimeterx/px_cookie.py | 40 ++++----- perimeterx/px_cookie_v1.py | 8 +- perimeterx/px_cookie_v3.py | 9 ++- perimeterx/px_cookie_validator.py | 7 +- perimeterx/px_httpc.py | 10 +-- perimeterx/px_proxy.py | 30 +++---- perimeterx/px_utils.py | 12 +-- 14 files changed, 256 insertions(+), 146 deletions(-) create mode 100644 perimeterx/px_config.py diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 97ff988..1495771 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -1,85 +1,66 @@ -from px_logger import Logger import px_context import px_activities_client import px_cookie_validator import px_httpc import px_blocker import px_api -from px_proxy import PXProxy -import Cookie import px_constants +from px_proxy import PXProxy +from px_config import PXConfig class PerimeterX(object): def __init__(self, app, config=None): self.app = app # merging user's defined configurations with the default one - self.config = { - 'blocking_score': 60, - 'debug_mode': False, - 'module_version': 'Python SDK v1.0.3', - 'module_mode': 'monitor', - 'perimeterx_server_host': 'sapi.perimeterx.net', - 'captcha_enabled': True, - 'server_calls_enabled': True, - 'encryption_enabled': True, - 'sensitive_headers': ['cookie', 'cookies'], - 'send_page_activities': True, - 'api_timeout': 1, - 'custom_logo': None, - 'css_ref': None, - 'js_ref': None, - 'monitor_mode': px_constants.MODULE_MODE_MONITORING, - 'is_mobile': False, - 'first_party': True, - 'first_party_xhr_enabled': True, - } - - self.config = dict(self.config.items() + config.items()) - self.config['logger'] = logger = Logger(self.config['debug_mode']) - if not config['app_id']: + px_config = PXConfig(config) + logger = px_config.logger + if not px_config.app_id: logger.error('PX App ID is missing') raise ValueError('PX App ID is missing') url = px_constants.COLLECTOR_URL - self.config['collector_url'] = url.format(config.get('app_id').lower()) # if APP_ID is not set, use the deafult perimeterx server - else, use the appid specific sapi. - self.config['perimeterx_server_host'] = 'sapi.perimeterx.net' if self.config['app_id'] == 'PX_APP_ID' else 'sapi-' + self.config['app_id'].lower() + '.perimeterx.net' - if not config['auth_token']: + if not px_config.auth_token: logger.error('PX Auth Token is missing') raise ValueError('PX Auth Token is missing') - if not config['cookie_key']: + if not px_config.cookie_key: logger.error('PX Cookie Key is missing') raise ValueError('PX Cookie Key is missing') - self.reverse_proxy_prefix = self.config['app_id'][2:].lower() - - self.PXBlocker = px_blocker.PXBlocker() - px_httpc.init(self.config) + self.reverse_proxy_prefix = px_config.app_id[2:].lower() + # if px_config.custom_request_handler: + # self.handle_verification = px_config.custom_request_handler.__get__(self, PerimeterX) + self._PXBlocker = px_blocker.PXBlocker() + self._config = px_config + px_httpc.init(px_config) def __call__(self, environ, start_response): return self._verify(environ, start_response) def _verify(self, environ, start_response): - logger = self.config['logger'] + config = self.config + logger = config.logger try: - ctx = px_context.build_context(environ, self.config) + ctx = px_context.build_context(environ, config) uri = ctx.get('uri') - px_proxy = PXProxy(self.config, ctx) - if px_proxy.should_reverse_request(uri): + px_proxy = PXProxy(config) + if px_proxy.should_reverse_request(uri): return px_proxy.handle_reverse_request(self.config, ctx, start_response) - if ctx.get('module_mode') == 'inactive' or is_static_file(ctx): + if is_static_file(ctx): logger.debug('Filter static file request. uri: ' + uri) return self.app(environ, start_response) - - cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE')) + if not self._config._module_enabled: + logger.debug('Module is disabled, request will not be verified') + return self.app(environ, start_response) # PX Cookie verification - if not px_cookie_validator.verify(ctx, self.config) and self.config.get('server_calls_enabled', True): + if not px_cookie_validator.verify(ctx, config): # Server-to-Server verification fallback if not px_api.verify(ctx, self.config): return self.app(environ, start_response) - + if config.custom_request_handler: + return config.custom_request_handler(ctx, self.config, environ, start_response) return self.handle_verification(ctx, self.config, environ, start_response) except: logger.error("Cought exception, passing request") @@ -88,14 +69,14 @@ def _verify(self, environ, start_response): def handle_verification(self, ctx, config, environ, start_response): score = ctx.get('risk_score', -1) - if score < config['blocking_score']: + if score < config.blocking_score: return self.pass_traffic(environ, start_response, ctx) - if config.get('custom_block_handler', False): + if config.custom_block_handler: px_activities_client.send_block_activity(ctx, config) - return config['custom_block_handler'](ctx, start_response) - elif config.get('module_mode') == px_constants.MODULE_MODE_BLOCKING: - return self.PXBlocker.handle_blocking(ctx=ctx, config=config, start_response=start_response) + return config.custom_block_handler(ctx, start_response) + elif config.module_mode == px_constants.MODULE_MODE_BLOCKING: + return self.px_blocker.handle_blocking(ctx=ctx, config=config, start_response=start_response) else: return self.pass_traffic(environ, start_response, ctx) @@ -106,6 +87,14 @@ def pass_traffic(self, environ, start_response, ctx): px_activities_client.send_to_perimeterx('page_requested', ctx, self.config, details) return self.app(environ, start_response) + @property + def config(self): + return self._config + + @property + def px_blocker(self): + return self._PXBlocker + def is_static_file(ctx): uri = ctx.get('uri', '') diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 3e06d64..ae09beb 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -26,21 +26,18 @@ def send_activities(): def send_to_perimeterx(activity_type, ctx, config, detail): global CONFIG try: - if not config.get('server_calls_enabled', True): - return - - if activity_type == 'page_requested' and not config.get('send_page_activities', False): + if activity_type == 'page_requested' and not config.send_page_activities: print 'Page activities disabled in config - skipping.' return - if len(CONFIG.keys()) == 0: + if not CONFIG: CONFIG = config _details = { 'http_method': ctx.get('http_method', ''), 'http_version': ctx.get('http_version', ''), - 'module_version': config.get('module_version', ''), - 'risk_mode': config.get('module_mode', '') + 'module_version': config.module_version, + 'risk_mode': config.module_mode, } if len(detail.keys()) > 0: @@ -51,7 +48,7 @@ def send_to_perimeterx(activity_type, ctx, config, detail): 'headers': ctx.get('headers'), 'timestamp': int(round(time.time() * 1000)), 'socket_ip': ctx.get('socket_ip'), - 'px_app_id': config.get('app_id'), + 'px_app_id': config.app_id, 'url': ctx.get('full_url'), 'details': _details, 'vid': ctx.get('vid', ''), @@ -68,7 +65,7 @@ def send_block_activity(ctx, config): 'block_score': ctx.get('risk_score'), 'client_uuid': ctx.get('uuid'), 'block_reason': ctx.get('block_reason'), - 'http_method' : ctx.get('http_method'), + 'http_method': ctx.get('http_method'), 'http_version': ctx.get('http_version'), 'px_cookie': ctx.get('decoded_cookie'), 'risk_rtt': ctx.get('risk_rtt'), diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index f19b8ef..36cb0c0 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -1,16 +1,17 @@ import sys import px_httpc import time +import px_constants def send_risk_request(ctx, config): body = prepare_risk_body(ctx, config) - return px_httpc.send('/api/v2/risk', body, config) + return px_httpc.send(px_constants.API_RISK, body, config) def verify(ctx, config): - logger = config['logger'] + logger = config.logger logger.debug("PXVerify") try: start = time.time() @@ -24,7 +25,7 @@ def verify(ctx, config): ctx['uuid'] = response['uuid'] ctx['block_action'] = response['action'] ctx['risk_rtt'] = risk_rtt - if score >= config['blocking_score']: + if score >= config.blocking_score: logger.debug("PXVerify block score threshold reached, will initiate blocking") ctx['block_reason'] = 's2s_high_score' elif response['action'] is 'j' and response.get('action_data') is not None and response.get('action_data').get('body') is not None: @@ -47,7 +48,7 @@ def verify(ctx, config): def prepare_risk_body(ctx, config): - logger = config['logger'] + logger = config.logger logger.debug("PxAPI[send_risk_request]") body = { 'request': { @@ -62,8 +63,8 @@ def prepare_risk_body(ctx, config): 's2s_call_reason': ctx.get('s2s_call_reason', ''), 'http_method': ctx.get('http_method', ''), 'http_version': ctx.get('http_version', ''), - 'module_version': config.get('module_version', ''), - 'risk_mode': config.get('module_mode', ''), + 'module_version': config.module_version, + 'risk_mode': config.module_mode, 'px_cookie_hmac': ctx.get('cookie_hmac', ''), 'request_cookie_names': ctx.get('cookie_names', '') } diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py index 5bc574e..add48f9 100644 --- a/perimeterx/px_blocker.py +++ b/perimeterx/px_blocker.py @@ -37,14 +37,14 @@ def handle_blocking(self, ctx, config, start_response): return str(blocking_response) def prepare_properties(self, ctx, config): - app_id = config.get('app_id').lower() + app_id = config.app_id.lower() vid = ctx.get('vid') if ctx.get('vid') is not None else '' uuid = ctx.get('uuid') - custom_logo = config.get('CUSTOM_LOGO') if config.get('CUSTOM_LOGO') is not None else '' + custom_logo = config.custom_logo is_mobile_num = 1 if ctx.get('is_mobile') else 0 captcha_uri = 'captcha.js?a={}&u={}&v={}&m={}'.format(ctx.get('block_action'), uuid, vid, is_mobile_num) - if config.get('first_party') and not ctx.get('is_mobile'): + if config.first_party and not ctx.get('is_mobile'): prefix = app_id[2:] js_client_src = '/{}/{}'.format(prefix, px_constants.CLIENT_FP_PATH) captcha_src = '/{}/{}/{}'.format(prefix, px_constants.CAPTCHA_FP_PATH, captcha_uri) @@ -60,12 +60,12 @@ def prepare_properties(self, ctx, config): 'vid': vid, 'uuid': uuid, 'customLogo': custom_logo, - 'cssRef': config.get('css_ref'), - 'jsRef': config.get('js_ref'), + 'cssRef': config.css_ref, + 'jsRef': config.js_ref, 'logoVisibility': 'visible' if custom_logo is not None else 'hidden', 'hostUrl': host_url, 'jsClientSrc': js_client_src, - 'firstPartyEnabled': config.get('first_party'), + 'firstPartyEnabled': config.first_party, 'blockScript': captcha_src } diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py new file mode 100644 index 0000000..bb5d113 --- /dev/null +++ b/perimeterx/px_config.py @@ -0,0 +1,126 @@ +import px_constants +from px_logger import Logger + +class PXConfig(object): + def __init__(self, config_dict): + app_id = config_dict.get('app_id') + debug_mode = config_dict.get('debug_mode', False) + module_mode = config_dict.get('module_mode', px_constants.MODULE_MODE_MONITORING) + self._app_id = app_id + self._blocking_score = config_dict.get('blocking_score', 100) + self._debug_mode = debug_mode + self._module_version = config_dict.get('module_version', px_constants.MODULE_VERSION) + self._module_mode = module_mode + self._server_host = 'sapi.perimeterx.net' if app_id is None else px_constants.SERVER_URL.format(app_id.lower()) + self._collector_host = 'collector.perimeterx.net' if app_id is None else px_constants.COLLECTOR_URL.format(app_id.lower()) + self._encryption_enabled = config_dict.get('encryption_enabled', True) + self._sensitive_headers = config_dict.get('sensitive_headers', ['cookie', 'cookies']) + self._send_page_activities = config_dict.get('send_page_activities', True) + self._api_timeout = config_dict.get('api_timeout', 500) + self._custom_logo = config_dict.get('custom_logo', '') + self._css_ref = config_dict.get('_custom_logo', '') + self._js_ref = config_dict.get('js_ref', '') + self._is_mobile = config_dict.get('is_mobile', False) + self._monitor_mode = 0 if module_mode is px_constants.MODULE_MODE_MONITORING else 1 + self._module_enabled = config_dict.get('module_enabled', True) + self._cookie_key = config_dict.get('cookie_key', None) + self._auth_token = config_dict.get('auth_token', None) + self._is_mobile = config_dict.get('is_mobile', False) + self._first_party = config_dict.get('first_party', True) + self._first_party_xhr_enabled = config_dict.get('first_party_xhr_enabled', True) + self._logger = Logger(debug_mode) + self._ip_headers = config_dict.get('ip_headers', []) + self._proxy_url = config_dict.get('proxy_url', None) + self._custom_request_handler = config_dict.get('custom_request_handler', None) + self._custom_block_handler = config_dict.get('custom_block_handler', None) + + @property + def module_mode(self): + return self._module_mode + + @property + def app_id(self): + return self._app_id + + @property + def logger(self): + return self._logger + + @property + def auth_token(self): + return self._auth_token + + @property + def cookie_key(self): + return self._cookie_key + + @property + def server_host(self): + return self._server_host + + @property + def api_timeout(self): + return self._api_timeout + + @property + def module_enabled(self): + return self._module_enabled + + @property + def ip_headers(self): + return self._ip_headers + + @property + def sensitive_headers(self): + return self._sensitive_headers + + @property + def proxy_url(self): + return self._proxy_url + + @property + def custom_request_handler(self): + return self._custom_request_handler + + @property + def custom_block_handler(self): + return self._custom_block_handler + + @property + def blocking_score(self): + return self._blocking_score + + @property + def encryption_enabled(self): + return self._encryption_enabled + + @property + def module_version(self): + return self._module_version + + @property + def send_page_activities(self): + return self._send_page_activities + + @property + def custom_logo(self): + return self._custom_logo + + @property + def css_ref(self): + return self._css_ref + + @property + def js_ref(self): + return self._js_ref + + @property + def first_party(self): + return self._first_party + + @property + def first_party_xhr_enabled(self): + return self._first_party_xhr_enabled + + def collector_host(self): + return self._collector_host diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index c7873d5..cedc79e 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -12,7 +12,8 @@ BLOCK_ACTION_RATE = 'r' CLIENT_HOST = 'client.perimeterx.net' CAPTCHA_HOST = 'captcha.px-cdn.net' -COLLECTOR_URL = 'https://collector-{}.perimeterx.net' +COLLECTOR_URL = 'collector-{}.perimeterx.net' +SERVER_URL = 'sapi-{}.perimeterx.net' CLIENT_FP_PATH = 'init.js' CAPTCHA_FP_PATH = 'captcha' XHR_FP_PATH = 'xhr' @@ -25,3 +26,4 @@ COLLECTOR_HOST = 'collector.perimeterx.net' FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR' MODULE_VERSION = 'Python WSGI Module v2.0.0' +API_RISK = '/api/v2/risk' diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index 2fd1e7b..39472df 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -3,7 +3,7 @@ def build_context(environ, config): - logger = config['logger'] + logger = config.logger headers = {} # Default values @@ -13,17 +13,12 @@ def build_context(environ, config): px_cookies = {} request_cookie_names = list() - # IP Extraction - if config.get('ip_handler'): - socket_ip = config.get('ip_handler')(environ) - else: - socket_ip = environ.get('REMOTE_ADDR') # Extracting: Headers, user agent, http method, http version for key in environ.keys(): if key.startswith('HTTP_') and environ.get(key): header_name = key.split('HTTP_')[1].replace('_', '-').lower() - if header_name not in config.get('sensitive_headers'): + if header_name not in config.sensitive_headers: headers[header_name] = environ.get(key) if key == 'REQUEST_METHOD': http_method = environ.get(key) @@ -57,7 +52,6 @@ def build_context(environ, config): 'http_method': http_method, 'http_version': http_version, 'user_agent': user_agent, - 'socket_ip': socket_ip, 'full_url': full_url, 'uri': uri, 'hostname': hostname, @@ -71,9 +65,18 @@ def build_context(environ, config): return ctx def extract_ip(config, environ): - ip = environ.get('HTTP_X_FORWARDED_FOR') - if not ip == None: - return ip.split(',')[-1].strip() - else: - return '' + ip_default = environ.get('HTTP_X_FORWARDED_FOR') + ip_headers = config.ip_headers + logger = config.logger + if not ip_headers: + try: + for ip_header in ip_headers: + ip_header_name = 'HTTP_' + ip_header.upper() + if environ.get(ip_header_name): + return environ.get(ip_header_name) + except: + logger.debug('Failed to use IP_HEADERS from config') + return ip_default + + diff --git a/perimeterx/px_cookie.py b/perimeterx/px_cookie.py index 005e967..8f14128 100644 --- a/perimeterx/px_cookie.py +++ b/perimeterx/px_cookie.py @@ -13,14 +13,14 @@ class PxCookie: - def __init__(self): - pass + def __init__(self, config): + self._config = config + self._logger = config.logger - @staticmethod - def build_px_cookie(ctx, config): - config["logger"].debug("PxCookie[build_px_cookie]") - px_cookies = ctx['px_cookies'].keys() + def build_px_cookie(self, ctx): + self._logger.debug("PxCookie[build_px_cookie]") + px_cookies = ctx['px_cookies'].keys() # Check that its not empty if not px_cookies: return None @@ -28,17 +28,17 @@ def build_px_cookie(ctx, config): px_cookies.sort(reverse=True) prefix = px_cookies[0] if prefix == PREFIX_PX_COOKIE_V1: - config["logger"].debug("PxCookie[build_px_cookie] using cookie v1") + self._logger.debug("PxCookie[build_px_cookie] using cookie v1") from px_cookie_v1 import PxCookieV1 - return PxCookieV1(ctx, config) + return PxCookieV1(ctx, self._config) if prefix == PREFIX_PX_COOKIE_V3: - config["logger"].debug("PxCookie[build_px_cookie] using cookie v3") + self._logger.debug("PxCookie[build_px_cookie] using cookie v3") from px_cookie_v3 import PxCookieV3 - return PxCookieV3(ctx, config) + return PxCookieV3(ctx, self._config) def decode_cookie(self): - self.config['logger'].debug("PxCookie[decode_cookie]") + self._logger.debug("PxCookie[decode_cookie]") return base64.b64decode(self.raw_cookie) def pbkdf2_hmac(self, hash_name, password, salt, iterations, dklen=None): @@ -103,7 +103,7 @@ def decrypt_cookie(self): :return: Returns decrypted value if valid and False if not :rtype: Bool|String """ - self.config['logger'].debug("PxCookie[decrypt_cookie]") + self._logger.debug("PxCookie[decrypt_cookie]") try: parts = self.raw_cookie.split(':', 3) if len(parts) != 3: @@ -113,13 +113,13 @@ def decrypt_cookie(self): if iterations < 1 or iterations > 10000: return False data = base64.b64decode(parts[2]) - dk = self.pbkdf2_hmac('sha256', self.config['cookie_key'], salt, iterations, dklen=48) + dk = self.pbkdf2_hmac('sha256', self._config.cookie_key, salt, iterations, dklen=48) key = dk[:32] iv = dk[32:] cipher = AES.new(key, AES.MODE_CBC, iv) unpad = lambda s: s[0:-ord(s[-1])] plaintext = unpad(cipher.decrypt(data)) - self.config['logger'].debug("PxCookie[decrypt_cookie] cookie decrypted") + self._logger.debug("PxCookie[decrypt_cookie] cookie decrypted") return plaintext except: print traceback.format_exception(*sys.exc_info()) @@ -142,15 +142,15 @@ def is_cookie_valid(self, str_to_hmac): :rtype: Bool """ try: - calculated_digest = hmac.new(self.config['cookie_key'], str_to_hmac, hashlib.sha256).hexdigest() + calculated_digest = hmac.new(self._config.cookie_key, str_to_hmac, hashlib.sha256).hexdigest() return self.get_hmac() == calculated_digest except: - self.config["logger"].debug("failed to calculate hmac") + self._logger.debug("failed to calculate hmac") return False def deserialize(self): - self.config['logger'].debug("PxCookie[deserialize]") - if self.config.get("encryption_enabled", False): + self._logger.debug("PxCookie[deserialize]") + if self._config.encryption_enabled: cookie = self.decrypt_cookie() else: cookie = self.decode_cookie() @@ -158,12 +158,12 @@ def deserialize(self): if not cookie: return False - self.config['logger'].debug("PxCookie[deserialize] decoded cookie: " + cookie) + self._logger.debug("PxCookie[deserialize] decoded cookie: " + cookie) self.decoded_cookie = json.loads(cookie) return self.is_cookie_format_valid() def is_high_score(self): - return self.get_score() >= self.config['blocking_score'] + return self.get_score() >= self._config.blocking_score def get_timestamp(self): return self.decoded_cookie['t'] diff --git a/perimeterx/px_cookie_v1.py b/perimeterx/px_cookie_v1.py index 959602f..fa5e4de 100644 --- a/perimeterx/px_cookie_v1.py +++ b/perimeterx/px_cookie_v1.py @@ -5,8 +5,8 @@ class PxCookieV1(PxCookie): def __init__(self, ctx, config): - self.ctx = ctx - self.config = config + self._ctx = ctx + self._config = config self.raw_cookie = ctx['px_cookies'].get(PREFIX_PX_COOKIE_V1, '') def get_score(self): @@ -24,8 +24,8 @@ def is_cookie_format_valid(self): def is_secured(self): c = self.decoded_cookie - user_agent = self.ctx.get('user_agent', '') - ip = self.ctx.get('ip', '') + 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 00226a8..ab70e3a 100644 --- a/perimeterx/px_cookie_v3.py +++ b/perimeterx/px_cookie_v3.py @@ -5,9 +5,10 @@ class PxCookieV3(PxCookie): def __init__(self, ctx, config): - self.ctx = ctx - self.config = config - spliced_cookie = ctx['px_cookies'].get(PREFIX_PX_COOKIE_V3, '').split(":", 1) + self._config = config + self._logger = config.logger + self._ctx = ctx + spliced_cookie = self._ctx['px_cookies'].get(PREFIX_PX_COOKIE_V3, '').split(":", 1) if spliced_cookie.count > 1: self.hmac = spliced_cookie[0] self.raw_cookie = spliced_cookie[1] @@ -26,7 +27,7 @@ def is_cookie_format_valid(self): 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._ctx.get('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 2aedda6..c46da7d 100644 --- a/perimeterx/px_cookie_validator.py +++ b/perimeterx/px_cookie_validator.py @@ -1,4 +1,5 @@ import traceback +from px_cookie import PxCookie def verify(ctx, config): @@ -11,15 +12,15 @@ def verify(ctx, config): :return: Returns True if verification succeeded and False if not :rtype: Bool """ - logger = config['logger'] + logger = config.logger try: if not ctx["px_cookies"].keys(): logger.debug('No risk cookie on the request') ctx['s2s_call_reason'] = 'no_cookie' return False - from px_cookie import PxCookie - px_cookie = PxCookie.build_px_cookie(ctx, config) + px_cookie_builder = PxCookie(config) + px_cookie = px_cookie_builder.build_px_cookie(ctx) if not px_cookie.deserialize(): logger.error('Cookie decryption failed') diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index 0ca7e1c..b34f56f 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -7,13 +7,13 @@ def init(config): global http_client - http_client = httplib.HTTPConnection(config.get('perimeterx_server_host'), timeout=config.get('api_timeout', 1)) + http_client = httplib.HTTPConnection(host=config.server_host, timeout=config.api_timeout) def send(uri, body, config): - logger = config['logger'] + logger = config.logger headers = { - 'Authorization': 'Bearer ' + config.get('auth_token', ''), + 'Authorization': 'Bearer ' + config.auth_token, 'Content-Type': 'application/json' } try: @@ -32,10 +32,10 @@ def send(uri, body, config): return False def send_reverse(url, path, body, headers, config, method): - logger = config['logger'] + logger = config.logger try: start = time.time() - http_client = httplib.HTTPSConnection(url, timeout=config.get('api_timeout', 1)) + http_client = httplib.HTTPSConnection(url, timeout=config.api_timeout) http_client.request(method, path, body, headers=headers) response = http_client.getresponse() diff --git a/perimeterx/px_proxy.py b/perimeterx/px_proxy.py index b3bb3c7..c9dac3f 100644 --- a/perimeterx/px_proxy.py +++ b/perimeterx/px_proxy.py @@ -11,10 +11,10 @@ class PXProxy(object): - def __init__(self, px_config): - self.logger = px_config['logger'] + def __init__(self, config): + self._logger = config.logger - reverse_app_id = px_config['app_id'][2:] + reverse_app_id = config.app_id[2:] self.client_reverse_prefix = '/{}/{}'.format(reverse_app_id, px_constants.CLIENT_FP_PATH).lower() self.xhr_reverse_prefix = '/{}/{}'.format(reverse_app_id, px_constants.XHR_FP_PATH).lower() self.captcha_reverse_prefix = '/{}/{}'.format(reverse_app_id, px_constants.CAPTCHA_FP_PATH).lower() @@ -37,18 +37,18 @@ def handle_reverse_request(self, config, ctx, start_response): return self.send_reverse_captcha_request(config=config, context=ctx, start_response=start_response) def send_reverse_client_request(self, config, context, start_response): - if not config['first_party']: + if not config.first_party: headers = [('Content-Type', 'application/javascript')] start_response("200 OK", headers) 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)) + 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)) 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.filter_sensitive_headers(context['headers'], config) + filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), context.get('ip')) filtered_headers = px_utils.merge_two_dicts(filtered_headers, headers) response = px_httpc.send_reverse(url=px_constants.CLIENT_HOST, path=client_request_uri, body='', headers=filtered_headers, config=config, method='GET') @@ -59,7 +59,7 @@ def send_reverse_client_request(self, config, context, start_response): def send_reverse_xhr_request(self, config, context, start_response): uri = context.get('uri') - if not config.get('first_party') or not config.get('first_party_xhr_enabled'): + if not config.first_party or not config.first_party_xhr_enabled: body, content_type = self.return_default_response(uri) start_response('200 OK', [content_type]) @@ -68,7 +68,7 @@ def send_reverse_xhr_request(self, config, context, start_response): xhr_path_index = uri.find('/' + px_constants.XHR_FP_PATH) suffix_uri = uri[xhr_path_index + 4:] - host = config.get('collector_url').replace('https://', '') + host = config.collector_host headers = {'host': host, px_constants.FIRST_PARTY_HEADER: 1, px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} @@ -76,9 +76,9 @@ def send_reverse_xhr_request(self, config, context, start_response): if context.get('vid') is not None: headers['Cookies'] = '_pxvid=' + context.get('vid') - filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), config, 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) - 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_reverse(url=host, path=suffix_uri, body='', headers=filtered_headers, config=config, method=context.get('http_method')) @@ -102,20 +102,20 @@ def return_default_response(self, uri): return body, content_type def send_reverse_captcha_request(self, config, context, start_response): - if not config['first_party']: + if not config.first_party: status = '200 OK' 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, px_constants.FIRST_PARTY_HEADER: 1, px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} - filtered_headers = px_utils.filter_sensitive_headers(context['headers'], config) + 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, uri)) + self._logger.debug('Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(), host, uri)) response = px_httpc.send_reverse(url=host, path=uri, body='', headers=filtered_headers, config=config, method='GET') headers = filter(lambda x: x[0] not in hoppish, response.getheaders()) diff --git a/perimeterx/px_utils.py b/perimeterx/px_utils.py index 6d1d572..021a225 100644 --- a/perimeterx/px_utils.py +++ b/perimeterx/px_utils.py @@ -1,23 +1,13 @@ import px_constants -def filter_sensitive_headers(headers, config): - sensitive_keys = config.get('sensitive_headers') - if not sensitive_keys is not None: - return {header_name: sensitive_keys[header_name] for header_name in headers if - header_name not in sensitive_keys} - else: - return headers - - def merge_two_dicts(x, y): z = x.copy() # start with x's keys and values z.update(y) # modifies z with y's keys and values & returns None return z -def handle_proxy_headers(headers, config, ip): - filtered_headers = filter_sensitive_headers(headers, config) +def handle_proxy_headers(filtered_headers, ip): for item in filtered_headers.keys(): if item.upper() == px_constants.FIRST_PARTY_FORWARDED_FOR: filtered_headers[item] = ip From 36e5e6e98d52b1889879e413aacd7f1e955a2792 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 08:55:41 +0200 Subject: [PATCH 03/34] Config --- perimeterx/px_activities_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index ae09beb..cdbe40d 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -71,6 +71,5 @@ def send_block_activity(ctx, config): 'risk_rtt': ctx.get('risk_rtt'), #'cookie_origin':, 'module_version': px_constants.MODULE_VERSION, - 'simulated_block': config.get('monitor_mode') is px_constants.MODULE_MODE_MONITORING - + 'simulated_block': config.monitor_mode is 0, }) From c811d140bf156d3190a1760a8df0a1bd9d315e0d Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 09:16:07 +0200 Subject: [PATCH 04/34] Deleted Notes --- perimeterx/middleware.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 1495771..8940e6b 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -18,7 +18,6 @@ def __init__(self, app, config=None): if not px_config.app_id: logger.error('PX App ID is missing') raise ValueError('PX App ID is missing') - url = px_constants.COLLECTOR_URL # if APP_ID is not set, use the deafult perimeterx server - else, use the appid specific sapi. if not px_config.auth_token: @@ -29,8 +28,8 @@ def __init__(self, app, config=None): logger.error('PX Cookie Key is missing') raise ValueError('PX Cookie Key is missing') self.reverse_proxy_prefix = px_config.app_id[2:].lower() - # if px_config.custom_request_handler: - # self.handle_verification = px_config.custom_request_handler.__get__(self, PerimeterX) + if px_config.custom_request_handler: + self.handle_verification = px_config.custom_request_handler.__get__(self, PerimeterX) self._PXBlocker = px_blocker.PXBlocker() self._config = px_config px_httpc.init(px_config) From 12e2efb6eef05e40d5bc4eb851a75ba2c4ad90f7 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 09:18:31 +0200 Subject: [PATCH 05/34] Feactored static files check --- perimeterx/middleware.py | 13 ++----------- perimeterx/px_utils.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 8940e6b..7389e79 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -5,6 +5,7 @@ import px_blocker import px_api import px_constants +import px_utils from px_proxy import PXProxy from px_config import PXConfig @@ -46,7 +47,7 @@ def _verify(self, environ, start_response): px_proxy = PXProxy(config) if px_proxy.should_reverse_request(uri): return px_proxy.handle_reverse_request(self.config, ctx, start_response) - if is_static_file(ctx): + if px_utils.is_static_file(ctx): logger.debug('Filter static file request. uri: ' + uri) return self.app(environ, start_response) if not self._config._module_enabled: @@ -95,14 +96,4 @@ def px_blocker(self): return self._PXBlocker -def is_static_file(ctx): - uri = ctx.get('uri', '') - static_extensions = ['.css', '.bmp', '.tif', '.ttf', '.docx', '.woff2', '.js', '.pict', '.tiff', '.eot', - '.xlsx', '.jpg', '.csv', '.eps', '.woff', '.xls', '.jpeg', '.doc', '.ejs', '.otf', '.pptx', - '.gif', '.pdf', '.swf', '.svg', '.ps', '.ico', '.pls', '.midi', '.svgz', '.class', '.png', - '.ppt', '.mid', 'webp', '.jar'] - for ext in static_extensions: - if uri.endswith(ext): - return True - return False diff --git a/perimeterx/px_utils.py b/perimeterx/px_utils.py index 021a225..3fc9b73 100644 --- a/perimeterx/px_utils.py +++ b/perimeterx/px_utils.py @@ -14,3 +14,16 @@ def handle_proxy_headers(filtered_headers, ip): else: filtered_headers[px_constants.FIRST_PARTY_FORWARDED_FOR] = ip return filtered_headers + + +def is_static_file(ctx): + uri = ctx.get('uri', '') + static_extensions = ['.css', '.bmp', '.tif', '.ttf', '.docx', '.woff2', '.js', '.pict', '.tiff', '.eot', + '.xlsx', '.jpg', '.csv', '.eps', '.woff', '.xls', '.jpeg', '.doc', '.ejs', '.otf', '.pptx', + '.gif', '.pdf', '.swf', '.svg', '.ps', '.ico', '.pls', '.midi', '.svgz', '.class', '.png', + '.ppt', '.mid', 'webp', '.jar'] + + for ext in static_extensions: + if uri.endswith(ext): + return True + return False \ No newline at end of file From 47f4b0edfa9ebe0835e2dd386c0e17599491c367 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 09:22:13 +0200 Subject: [PATCH 06/34] Refactored literals --- perimeterx/middleware.py | 2 +- perimeterx/px_activities_client.py | 2 +- perimeterx/px_constants.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 7389e79..820a4dc 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -84,7 +84,7 @@ def pass_traffic(self, environ, start_response, ctx): details = {} if ctx.get('decoded_cookie', ''): details = {"px_cookie": ctx['decoded_cookie']} - px_activities_client.send_to_perimeterx('page_requested', ctx, self.config, details) + px_activities_client.send_to_perimeterx(px_constants.PAGE_REQUESTED_ACTIVITY, ctx, self.config, details) return self.app(environ, start_response) @property diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index cdbe40d..bffb4c1 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -61,7 +61,7 @@ def send_to_perimeterx(activity_type, ctx, config, detail): def send_block_activity(ctx, config): - send_to_perimeterx('block', ctx, config, { + send_to_perimeterx(px_constants.BLOCK, ctx, config, { 'block_score': ctx.get('risk_score'), 'client_uuid': ctx.get('uuid'), 'block_reason': ctx.get('block_reason'), diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index cedc79e..8afe2ad 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -27,3 +27,5 @@ FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR' MODULE_VERSION = 'Python WSGI Module v2.0.0' API_RISK = '/api/v2/risk' +PAGE_REQUESTED_ACTIVITY = 'page_requested' +BLOCK_ACTIVITY = 'block' From 694ce36cf6d75995e248f7151df6c37fa67b9d85 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 09:22:48 +0200 Subject: [PATCH 07/34] Block activity --- perimeterx/px_activities_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index bffb4c1..75fe8b6 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -61,7 +61,7 @@ def send_to_perimeterx(activity_type, ctx, config, detail): def send_block_activity(ctx, config): - send_to_perimeterx(px_constants.BLOCK, ctx, config, { + send_to_perimeterx(px_constants.BLOCK_ACTIVITY, ctx, config, { 'block_score': ctx.get('risk_score'), 'client_uuid': ctx.get('uuid'), 'block_reason': ctx.get('block_reason'), From e0f90920114bccf2ba2a5f66639e74585385f892 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 09:24:32 +0200 Subject: [PATCH 08/34] Block action added to block acitivity --- perimeterx/px_activities_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 75fe8b6..84f5c2d 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -70,6 +70,7 @@ def send_block_activity(ctx, config): 'px_cookie': ctx.get('decoded_cookie'), 'risk_rtt': ctx.get('risk_rtt'), #'cookie_origin':, + 'block_action': ctx.get('block_action',''), 'module_version': px_constants.MODULE_VERSION, 'simulated_block': config.monitor_mode is 0, }) From a89913ab383cad338e7f066e0f390f06848cbc60 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 09:29:56 +0200 Subject: [PATCH 09/34] Removed socket_ip from context --- perimeterx/px_activities_client.py | 2 +- perimeterx/px_api.py | 2 +- perimeterx/px_context.py | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 84f5c2d..b043eeb 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -47,7 +47,7 @@ def send_to_perimeterx(activity_type, ctx, config, detail): 'type': activity_type, 'headers': ctx.get('headers'), 'timestamp': int(round(time.time() * 1000)), - 'socket_ip': ctx.get('socket_ip'), + 'socket_ip': ctx.get('ip'), 'px_app_id': config.app_id, 'url': ctx.get('full_url'), 'details': _details, diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index 36cb0c0..9ed3b47 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -52,7 +52,7 @@ def prepare_risk_body(ctx, config): logger.debug("PxAPI[send_risk_request]") body = { 'request': { - 'ip': ctx.get('socket_ip'), + 'ip': ctx.get('ip'), 'headers': format_headers(ctx.get('headers')), 'uri': ctx.get('uri'), 'url': ctx.get('full_url', '') diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index 39472df..b01f46d 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -13,7 +13,6 @@ def build_context(environ, config): px_cookies = {} request_cookie_names = list() - # Extracting: Headers, user agent, http method, http version for key in environ.keys(): if key.startswith('HTTP_') and environ.get(key): @@ -64,6 +63,7 @@ def build_context(environ, config): } return ctx + def extract_ip(config, environ): ip_default = environ.get('HTTP_X_FORWARDED_FOR') ip_headers = config.ip_headers @@ -77,6 +77,3 @@ def extract_ip(config, environ): except: logger.debug('Failed to use IP_HEADERS from config') return ip_default - - - From 4ca2ee2470c4269a0b204cb91820ea59703886a6 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 10:38:40 +0200 Subject: [PATCH 10/34] property --- perimeterx/px_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index bb5d113..6ffb1d0 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -122,5 +122,6 @@ def first_party(self): def first_party_xhr_enabled(self): return self._first_party_xhr_enabled + @property def collector_host(self): return self._collector_host From c69dd9eb3338830d5bb1425e94b3ad2fabef250c Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 11:28:26 +0200 Subject: [PATCH 11/34] Telemetry --- perimeterx/px_activities_client.py | 18 +++++++++++++++++- perimeterx/px_config.py | 5 +++++ perimeterx/px_constants.py | 3 +++ perimeterx/px_httpc.py | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index b043eeb..8750305 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -14,7 +14,7 @@ def send_activities(): if len(ACTIVITIES_BUFFER) > 0: chunk = ACTIVITIES_BUFFER[:10] ACTIVITIES_BUFFER = ACTIVITIES_BUFFER[10:] - px_httpc.send('/api/v1/collector/s2s', chunk, CONFIG) + px_httpc.send(px_constants.API_ACTIVITIES, chunk, CONFIG) time.sleep(1) @@ -74,3 +74,19 @@ def send_block_activity(ctx, config): 'module_version': px_constants.MODULE_VERSION, 'simulated_block': config.monitor_mode is 0, }) + +def send_enforcer_telemetry_activity(ctx, config): + details = { + 'enforcer_configs': config.get_telemetry_config(), + 'node_name': os.hostname(), + 'os_name': os.platform(), + 'update_reason': updateReason, + 'module_version': config.module_version + } + body = { + 'type': px_constants.TELEMETRY_ACTIVITY, + 'timestamp': time.time(), + 'px_app_id': config.app_id, + 'details': details + } + diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 6ffb1d0..bd7f2d8 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -125,3 +125,8 @@ def first_party_xhr_enabled(self): @property def collector_host(self): return self._collector_host + + def get_telemetry_config(self): + + + diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 8afe2ad..9043ee0 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -29,3 +29,6 @@ API_RISK = '/api/v2/risk' PAGE_REQUESTED_ACTIVITY = 'page_requested' BLOCK_ACTIVITY = 'block' +API_ENFORCER_TELEMETRY = '/api/v2/risk/telemetry' +API_ACTIVITIES = '/api/v1/collector/s2s' +TELEMETRY_ACTIVITY = 'telemetry' diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index b34f56f..469fce5 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -49,3 +49,7 @@ def send_reverse(url, path, body, headers, config, method): init(config) return False +def send_activity(url, path, body, headers, config): + method = 'POST' + + From 0385fe5d5f43eb6085f2048070411b3c3cf2c937 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Sun, 25 Nov 2018 12:09:05 +0200 Subject: [PATCH 12/34] Whitelist and sensitive route capabilities --- perimeterx/middleware.py | 4 ++++ perimeterx/px_config.py | 34 ++++++++++++++++++++++++++++--- perimeterx/px_context.py | 13 ++++++++---- perimeterx/px_cookie_validator.py | 5 +++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 820a4dc..389aaa0 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -54,6 +54,10 @@ def _verify(self, environ, start_response): logger.debug('Module is disabled, request will not be verified') return self.app(environ, start_response) + if ctx.get('whitelist'): + logger.debug('The requested uri is whitelisted, passing request') + return self.app(environ, start_response) + # PX Cookie verification if not px_cookie_validator.verify(ctx, config): # Server-to-Server verification fallback diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 6ffb1d0..090e650 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -1,6 +1,7 @@ import px_constants from px_logger import Logger + class PXConfig(object): def __init__(self, config_dict): app_id = config_dict.get('app_id') @@ -12,7 +13,8 @@ def __init__(self, config_dict): self._module_version = config_dict.get('module_version', px_constants.MODULE_VERSION) self._module_mode = module_mode self._server_host = 'sapi.perimeterx.net' if app_id is None else px_constants.SERVER_URL.format(app_id.lower()) - self._collector_host = 'collector.perimeterx.net' if app_id is None else px_constants.COLLECTOR_URL.format(app_id.lower()) + self._collector_host = 'collector.perimeterx.net' if app_id is None else px_constants.COLLECTOR_URL.format( + app_id.lower()) self._encryption_enabled = config_dict.get('encryption_enabled', True) self._sensitive_headers = config_dict.get('sensitive_headers', ['cookie', 'cookies']) self._send_page_activities = config_dict.get('send_page_activities', True) @@ -31,8 +33,10 @@ def __init__(self, config_dict): self._logger = Logger(debug_mode) self._ip_headers = config_dict.get('ip_headers', []) self._proxy_url = config_dict.get('proxy_url', None) - self._custom_request_handler = config_dict.get('custom_request_handler', None) - self._custom_block_handler = config_dict.get('custom_block_handler', None) + self._max_buffer_len = config_dict.get('max_buffer_len', 30) + self._sensitive_routes = config_dict.get('sensitive_routes', []) + self._whitelist_routes = config_dict.get('whitelist_routes', []) + self.instantiate_user_defined_handlers(config_dict) @property def module_mode(self): @@ -125,3 +129,27 @@ def first_party_xhr_enabled(self): @property def collector_host(self): return self._collector_host + + @property + def get_user_ip(self): + return self._get_user_ip + + @property + def sensitive_routes(self): + return self._sensitive_routes + + @property + def whitelist_routes(self): + return self._whitelist_routes + + + def instantiate_user_defined_handlers(self, config_dict): + self._custom_request_handler = self.set_handler('custom_request_handler', config_dict) + self._custom_block_handler = self.set_handler('custom_block_handler', config_dict) + self._get_user_ip = self.set_handler('get_user_ip', config_dict) + self._additional_activity_handler = self.set_handler('additional_activity_handler', config_dict) + + + def set_handler(self, function_name, config_dict): + return config_dict.get(function_name) if config_dict.get(function_name) and callable( + config_dict.get(function_name)) else None diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index b01f46d..ed0f16e 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -41,11 +41,12 @@ def build_context(environ, config): vid = cookies.get('_pxvid').value else: vid = '' - 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') + sensitive_route = uri in config.sensitive_routes + whitelist_route = uri in config.whitelist_routes ctx = { 'headers': headers, 'http_method': http_method, @@ -59,13 +60,15 @@ def build_context(environ, config): 'risk_rtt': 0, 'ip': extract_ip(config, environ), 'vid': vid, - 'query_params': environ['QUERY_STRING'] + 'query_params': environ['QUERY_STRING'], + 'sensitive_route': sensitive_route, + 'whitelist_route': whitelist_route, } return ctx def extract_ip(config, environ): - ip_default = environ.get('HTTP_X_FORWARDED_FOR') + ip = environ.get('HTTP_X_FORWARDED_FOR') ip_headers = config.ip_headers logger = config.logger if not ip_headers: @@ -76,4 +79,6 @@ def extract_ip(config, environ): return environ.get(ip_header_name) except: logger.debug('Failed to use IP_HEADERS from config') - return ip_default + if config.get_user_ip: + ip = config.get_user_ip(environ) + return ip diff --git a/perimeterx/px_cookie_validator.py b/perimeterx/px_cookie_validator.py index c46da7d..4d30893 100644 --- a/perimeterx/px_cookie_validator.py +++ b/perimeterx/px_cookie_validator.py @@ -50,6 +50,11 @@ def verify(ctx, config): ctx['s2s_call_reason'] = 'cookie_validation_failed' return False + if ctx.get('sensitive_route'): + logger.debug('Sensitive route match, sending Risk API. path: {}'.format(ctx.get('uri'))) + ctx['s2s_call_reason'] = 'sensitive_route' + return False + logger.debug('Cookie validation passed with good score: ' + str(ctx['risk_score'])) return True except Exception, e: From 07c26f9d49085b8b24613f240515b25225ac7b7c Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Mon, 26 Nov 2018 10:24:18 +0200 Subject: [PATCH 13/34] Changes --- perimeterx/middleware.py | 1 + perimeterx/px_activities_client.py | 20 ++++++--- perimeterx/px_config.py | 68 ++++++++++++++++++++++++++---- perimeterx/px_constants.py | 2 +- perimeterx/px_httpc.py | 10 ++--- perimeterx/px_proxy.py | 12 +++--- tests/px_blocker.py | 7 +++ 7 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 tests/px_blocker.py diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 389aaa0..dbea496 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -34,6 +34,7 @@ def __init__(self, app, config=None): self._PXBlocker = px_blocker.PXBlocker() self._config = px_config px_httpc.init(px_config) + px_activities_client.send_enforcer_telemetry_activity(config=px_config, update_reason='initial_config') def __call__(self, environ, start_response): return self._verify(environ, start_response) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 8750305..442af22 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -3,6 +3,7 @@ import threading import traceback, sys import px_constants +import socket ACTIVITIES_BUFFER = [] CONFIG = {} @@ -75,12 +76,12 @@ def send_block_activity(ctx, config): 'simulated_block': config.monitor_mode is 0, }) -def send_enforcer_telemetry_activity(ctx, config): +def send_enforcer_telemetry_activity(config, update_reason): details = { - 'enforcer_configs': config.get_telemetry_config(), - 'node_name': os.hostname(), - 'os_name': os.platform(), - 'update_reason': updateReason, + 'enforcer_configs': config.telemetry_config, + 'node_name': socket.gethostname(), + 'os_name': sys.platform, + 'update_reason': update_reason, 'module_version': config.module_version } body = { @@ -89,4 +90,13 @@ def send_enforcer_telemetry_activity(ctx, config): 'px_app_id': config.app_id, 'details': details } + headers = { + 'Authorization': 'Bearer ' + config.auth_token, + 'Content-Type': 'application/json' + } + config.logger.debug('Sending telemetry activity to PerimeterX servers') + px_httpc.send_https(url=config.server_host, path=px_constants.API_ENFORCER_TELEMETRY, body=body, + headers=headers, config=config, method='POST') + + diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 14f08c3..503466f 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -7,6 +7,7 @@ def __init__(self, config_dict): app_id = config_dict.get('app_id') debug_mode = config_dict.get('debug_mode', False) module_mode = config_dict.get('module_mode', px_constants.MODULE_MODE_MONITORING) + custom_logo = config_dict.get('custom_logo', None) self._app_id = app_id self._blocking_score = config_dict.get('blocking_score', 100) self._debug_mode = debug_mode @@ -19,7 +20,7 @@ def __init__(self, config_dict): self._sensitive_headers = config_dict.get('sensitive_headers', ['cookie', 'cookies']) self._send_page_activities = config_dict.get('send_page_activities', True) self._api_timeout = config_dict.get('api_timeout', 500) - self._custom_logo = config_dict.get('custom_logo', '') + self._custom_logo = custom_logo self._css_ref = config_dict.get('_custom_logo', '') self._js_ref = config_dict.get('js_ref', '') self._is_mobile = config_dict.get('is_mobile', False) @@ -36,7 +37,10 @@ def __init__(self, config_dict): self._max_buffer_len = config_dict.get('max_buffer_len', 30) self._sensitive_routes = config_dict.get('sensitive_routes', []) self._whitelist_routes = config_dict.get('whitelist_routes', []) - self.instantiate_user_defined_handlers(config_dict) + self._block_html = 'BLOCK' + self._logo_visibility = 'visible' if custom_logo is not None else 'hidden' + self.__instantiate_user_defined_handlers(config_dict) + self._telemetry_config = self.__create_telemetry_config() @property def module_mode(self): @@ -142,19 +146,65 @@ def sensitive_routes(self): def whitelist_routes(self): return self._whitelist_routes + @property + def block_html(self): + return self._block_html + + @property + def logo_visibility(self): + return self._logo_visibility + + @property + def additional_activity_handler(self): + return self._additional_activity_handler + + @property + def debug_mode(self): + return self._debug_mode + + @property + def max_buffer_len(self): + return self._max_buffer_len + + @property + def telemetry_config(self): + return self._telemetry_config - def instantiate_user_defined_handlers(self, config_dict): - self._custom_request_handler = self.set_handler('custom_request_handler', config_dict) - self._custom_block_handler = self.set_handler('custom_block_handler', config_dict) - self._get_user_ip = self.set_handler('get_user_ip', config_dict) - self._additional_activity_handler = self.set_handler('additional_activity_handler', config_dict) + def __instantiate_user_defined_handlers(self, config_dict): + self._custom_request_handler = self.__set_handler('custom_request_handler', config_dict) + self._custom_block_handler = self.__set_handler('custom_block_handler', config_dict) + self._get_user_ip = self.__set_handler('get_user_ip', config_dict) + self._additional_activity_handler = self.__set_handler('additional_activity_handler', config_dict) - def set_handler(self, function_name, config_dict): + + def __set_handler(self, function_name, config_dict): return config_dict.get(function_name) if config_dict.get(function_name) and callable( config_dict.get(function_name)) else None - def get_telemetry_config(self): + def __create_telemetry_config(self): + return { + 'PX_APP_ID': self.app_id, + 'ENABLE_MODULE': self.module_enabled, + 'API_TIMEOUT_MS': self.api_timeout, + 'BLOCKING_SCORE': self.blocking_score, + 'IP_HEADERS': self.ip_headers, + 'BLOCK_HTML': self.block_html, + 'SENSITIVE_HEADERS': self.sensitive_headers, + 'PROXY_URL': self.proxy_url, + 'SEND_PAGE_ACTIVITIES': self.send_page_activities, + 'DEBUG_MODE': self.debug_mode, + 'MAX_BUFFER_LEN': self.max_buffer_len, + 'GET_USER_IP': self.get_user_ip, + 'CSS_REF': self.css_ref, + 'JS_REF': self.js_ref, + 'CUSTOM_LOGO': self.custom_logo, + 'LOGO_VISIBILITY': self.logo_visibility, + 'SENSITIVE_ROUTES': self.sensitive_routes, + 'MODULE_MODE': self.module_mode, + } + + diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 9043ee0..7ef8fb3 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -31,4 +31,4 @@ BLOCK_ACTIVITY = 'block' API_ENFORCER_TELEMETRY = '/api/v2/risk/telemetry' API_ACTIVITIES = '/api/v1/collector/s2s' -TELEMETRY_ACTIVITY = 'telemetry' +TELEMETRY_ACTIVITY = 'enforcer_telemetry' diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index 469fce5..56e8ab4 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -31,15 +31,16 @@ def send(uri, body, config): init(config) return False -def send_reverse(url, path, body, headers, config, method): +def send_https(url, path, body, headers, config, method): logger = config.logger try: start = time.time() - http_client = httplib.HTTPSConnection(url, timeout=config.api_timeout) - http_client.request(method, path, body, headers=headers) + http_client = httplib.HTTPConnection(url, timeout=config.api_timeout) + http_client.request(method=method, url=path, body=json.dumps(body), headers=headers) response = http_client.getresponse() if response.status >= 400: + logger.debug('PerimeterX server call failed') return False logger.debug('Server call took ' + str(time.time() - start) + 'ms') @@ -49,7 +50,6 @@ def send_reverse(url, path, body, headers, config, method): init(config) return False -def send_activity(url, path, body, headers, config): - method = 'POST' + diff --git a/perimeterx/px_proxy.py b/perimeterx/px_proxy.py index c9dac3f..e0e1be9 100644 --- a/perimeterx/px_proxy.py +++ b/perimeterx/px_proxy.py @@ -50,8 +50,8 @@ def send_reverse_client_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) - response = px_httpc.send_reverse(url=px_constants.CLIENT_HOST, path=client_request_uri, body='', - headers=filtered_headers, config=config, method='GET') + response = px_httpc.send_https(url=px_constants.CLIENT_HOST, path=client_request_uri, body='', + headers=filtered_headers, config=config, method='GET') headers = filter(lambda x: x[0] not in hoppish, response.getheaders()) start_response(str(response.status) + ' ' + response.reason, headers) @@ -79,8 +79,8 @@ def send_reverse_xhr_request(self, config, context, start_response): 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)) - response = px_httpc.send_reverse(url=host, path=suffix_uri, body='', - headers=filtered_headers, config=config, method=context.get('http_method')) + response = px_httpc.send_https(url=host, path=suffix_uri, body='', + headers=filtered_headers, config=config, method=context.get('http_method')) if response.status >= 400: body, content_type = self.return_default_response(uri) @@ -116,8 +116,8 @@ def send_reverse_captcha_request(self, config, context, start_response): 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, uri)) - response = px_httpc.send_reverse(url=host, path=uri, body='', - headers=filtered_headers, config=config, method='GET') + response = px_httpc.send_https(url=host, path=uri, body='', + headers=filtered_headers, config=config, method='GET') headers = filter(lambda x: x[0] not in hoppish, response.getheaders()) start_response(str(response.status) + ' ' + response.reason, headers) return response.read() diff --git a/tests/px_blocker.py b/tests/px_blocker.py new file mode 100644 index 0000000..4c613da --- /dev/null +++ b/tests/px_blocker.py @@ -0,0 +1,7 @@ +# from perimeterx.px_blocker import PXBlocker +# +# +# import unittest +# +# class Test_PXBlocker(unittest.TestCase): +# From 63a43b5ec67c6f82428ded220518a96ac19f0ff3 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Mon, 26 Nov 2018 11:25:55 +0200 Subject: [PATCH 14/34] px_blocker tests --- tests/px_blocker.py | 105 +++++++++++++- tests/px_blocking_messages/blocking.txt | 169 +++++++++++++++++++++++ tests/px_blocking_messages/ratelimit.txt | 9 ++ 3 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 tests/px_blocking_messages/blocking.txt create mode 100644 tests/px_blocking_messages/ratelimit.txt diff --git a/tests/px_blocker.py b/tests/px_blocker.py index 4c613da..34a081d 100644 --- a/tests/px_blocker.py +++ b/tests/px_blocker.py @@ -1,7 +1,98 @@ -# from perimeterx.px_blocker import PXBlocker -# -# -# import unittest -# -# class Test_PXBlocker(unittest.TestCase): -# +from perimeterx.px_blocker import PXBlocker + + +import unittest +from perimeterx.px_config import PXConfig + + +class Test_PXBlocker(unittest.TestCase): + + def test_is_json_response(self): + px_blocker = PXBlocker() + ctx = { + 'headers': {'Accept': 'text/html'} + } + self.assertFalse(px_blocker.is_json_response(ctx)) + ctx['headers']['Accept'] = 'application/json' + self.assertTrue(px_blocker.is_json_response(ctx)) + + def test_handle_blocking(self): + px_blocker = PXBlocker() + vid = 'bf619be8-94be-458a-b6b1-ee81f154c282' + px_uuid = '8712cef7-bcfa-4bb6-ae99-868025e1908a' + ctx = { + 'headers': {'Accept': 'text/html'}, + 'vid': vid, + 'uuid': px_uuid + } + px_config = PXConfig({'app_id': 'PXfake_app_ip'}) + message = px_blocker.handle_blocking(ctx, px_config, lambda x, y : None) + blocking_message = None + with open('./px_blocking_messages/blocking.txt', 'r') as myfile: + blocking_message = myfile.read() + self.assertEqual(message, blocking_message) + + def test_handle_ratelimit(self): + px_blocker = PXBlocker() + vid = 'bf619be8-94be-458a-b6b1-ee81f154c282' + px_uuid = '8712cef7-bcfa-4bb6-ae99-868025e1908a' + ctx = { + 'headers': {'Accept': 'text/html'}, + 'vid': vid, + 'uuid': px_uuid, + 'block_action': 'r' + } + px_config = PXConfig({'app_id': 'PXfake_app_ip'}) + message = px_blocker.handle_blocking(ctx, px_config, lambda x, y : None) + blocking_message = None + with open('./px_blocking_messages/ratelimit.txt', 'r') as myfile: + blocking_message = myfile.read() + self.assertEqual(message, blocking_message) + + def test_handle_challenge(self): + px_blocker = PXBlocker() + vid = 'bf619be8-94be-458a-b6b1-ee81f154c282' + px_uuid = '8712cef7-bcfa-4bb6-ae99-868025e1908a' + ctx = { + 'headers': {'Accept': 'text/html'}, + 'vid': vid, + 'uuid': px_uuid, + 'block_action': 'j', + 'block_action_data': 'Bla' + } + px_config = PXConfig({'app_id': 'PXfake_app_ip'}) + message = px_blocker.handle_blocking(ctx, px_config, lambda x, y : None) + blocking_message = 'Bla' + self.assertEqual(message, blocking_message) + + def test_prepare_properties(self): + px_blocker = PXBlocker() + vid = 'bf619be8-94be-458a-b6b1-ee81f154c282' + px_uuid = '8712cef7-bcfa-4bb6-ae99-868025e1908a' + ctx = { + 'headers': {'Accept': 'text/html'}, + 'vid': vid, + 'uuid': px_uuid, + } + px_config = PXConfig({'app_id': 'PXfake_app_ip'}) + message = px_blocker.prepare_properties(ctx, px_config) + expected_message = {'blockScript': '/fake_app_ip/captcha/captcha.js?a=None&u=8712cef7-bcfa-4bb6-ae99-868025e1908a&v=bf619be8-94be-458a-b6b1-ee81f154c282&m=0', + 'vid': 'bf619be8-94be-458a-b6b1-ee81f154c282', + 'jsRef': '', + 'hostUrl': '/fake_app_ip/xhr', + 'customLogo': None, + 'appId': 'pxfake_app_ip', + 'uuid': '8712cef7-bcfa-4bb6-ae99-868025e1908a', + 'logoVisibility': 'hidden', + 'jsClientSrc': '/fake_app_ip/init.js', + 'firstPartyEnabled': True, + 'refId': '8712cef7-bcfa-4bb6-ae99-868025e1908a', + 'cssRef': ''} + self.assertDictEqual(message, expected_message) + expected_message['blockScript'] = '/fake_app/captcha/captcha.js?a=None&u=8712cef7-bcfa-4bb6-ae99-868025e1908a&v=bf619be8-94be-458a-b6b1-ee81f154c282&m=0' + self.assertNotEqual(message, expected_message) + + + + + diff --git a/tests/px_blocking_messages/blocking.txt b/tests/px_blocking_messages/blocking.txt new file mode 100644 index 0000000..50a2790 --- /dev/null +++ b/tests/px_blocking_messages/blocking.txt @@ -0,0 +1,169 @@ + + + + + + Access to this page has been denied. + + + + + + +
+
+ +
+
+
+

Please verify you are a human

+
+
+
+
+ +
+
+

+ Access to this page has been denied because we believe you are using automation tools to browse the + website. +

+

+ This may happen as a result of the following: +

+
    +
  • + Javascript is disabled or blocked by an extension (ad blockers for example) +
  • +
  • + Your browser does not support cookies +
  • +
+

+ Please make sure that Javascript and cookies are enabled on your browser and that you are not blocking + them from loading. +

+

+ Reference ID: #8712cef7-bcfa-4bb6-ae99-868025e1908a +

+
+
+ +
+ + + + + + + diff --git a/tests/px_blocking_messages/ratelimit.txt b/tests/px_blocking_messages/ratelimit.txt new file mode 100644 index 0000000..36fd393 --- /dev/null +++ b/tests/px_blocking_messages/ratelimit.txt @@ -0,0 +1,9 @@ + + + Too Many Requests + + +

Too Many Requests

+

Reached maximum requests limitation, try again soon.

+ + \ No newline at end of file From b685b68e55e0a0cf7ce7b17c5575f1ed8e1e5211 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Mon, 26 Nov 2018 13:26:30 +0200 Subject: [PATCH 15/34] px_utils tests --- tests/px_httpc.py | 20 ++++++++++++++++++++ tests/px_utils.py | 26 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/px_httpc.py create mode 100644 tests/px_utils.py diff --git a/tests/px_httpc.py b/tests/px_httpc.py new file mode 100644 index 0000000..16c0cc6 --- /dev/null +++ b/tests/px_httpc.py @@ -0,0 +1,20 @@ +from perimeterx import px_httpc +import unittest +from mock import MagicMock,patch +from perimeterx.px_config import PXConfig +import httplib + + +class Test_PXHTTPC(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) + + + print 'f' \ No newline at end of file diff --git a/tests/px_utils.py b/tests/px_utils.py new file mode 100644 index 0000000..1d429c7 --- /dev/null +++ b/tests/px_utils.py @@ -0,0 +1,26 @@ +from perimeterx import px_utils +import unittest +from perimeterx import px_constants + + +class Test_PXUtils(unittest.TestCase): + + def test_merge_two_dicts(self): + dict1 = {'a': '1'} + dict2 = {'b': '2'} + merged_dict = px_utils.merge_two_dicts(dict1, dict2) + self.assertDictEqual(merged_dict, {'a': '1', 'b': '2'}) + + def test_handle_proxy_headers(self): + headers_sample = {'ddd': 'not_proxy_url', px_constants.FIRST_PARTY_FORWARDED_FOR: 'proxy_url'} + headers_sample = px_utils.handle_proxy_headers(headers_sample, '127.0.0.1') + self.assertEqual(headers_sample[px_constants.FIRST_PARTY_FORWARDED_FOR], '127.0.0.1') + headers_sample = {'ddd': 'not_proxy_url'} + headers_sample = px_utils.handle_proxy_headers(headers_sample, '127.0.0.1') + self.assertEqual(headers_sample[px_constants.FIRST_PARTY_FORWARDED_FOR], '127.0.0.1') + + def test_is_static_file(self): + ctx = {'uri': '/sample.css'} + self.assertTrue(px_utils.is_static_file(ctx)) + ctx = {'uri': '/sample.html'} + self.assertFalse(px_utils.is_static_file(ctx)) From 2e58395e530c63bbef89371ae368360f4e85eb1a Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Mon, 26 Nov 2018 15:34:17 +0200 Subject: [PATCH 16/34] px_validator tests --- perimeterx/px_cookie_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perimeterx/px_cookie_validator.py b/perimeterx/px_cookie_validator.py index 4d30893..3a6a00a 100644 --- a/perimeterx/px_cookie_validator.py +++ b/perimeterx/px_cookie_validator.py @@ -45,7 +45,7 @@ def verify(ctx, config): logger.debug('Cookie expired') return False - if px_cookie.is_secured(): + if not px_cookie.is_secured(): logger.debug('Cookie validation failed') ctx['s2s_call_reason'] = 'cookie_validation_failed' return False From 63332ddda03fa1f0ec2862b00443d9eed7be66f2 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Mon, 26 Nov 2018 15:43:27 +0200 Subject: [PATCH 17/34] px_validator tests --- tests/px_cookie_validator.py | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/px_cookie_validator.py diff --git a/tests/px_cookie_validator.py b/tests/px_cookie_validator.py new file mode 100644 index 0000000..8f984ab --- /dev/null +++ b/tests/px_cookie_validator.py @@ -0,0 +1,66 @@ +from perimeterx import px_cookie_validator +import unittest +from perimeterx.px_config import PXConfig + + +class Test_PXCookieValidator(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.cookie_key = 'Pyth0nS3crE7K3Y' + + def test_verify_no_cookie(self): + config = PXConfig({'app_id': 'app_id'}) + ctx = {'px_cookies': {}} + 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': { + '_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': { + '_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': { + '_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': { + '_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': { + '_px3': '0d67bdf4a58c524b55b9cf0f703e4f0f3cbe23a10bd2671530d3c7e0cfa509eb:HOiYSw11ICB2A+HYx+C+l5Naxcl7hMeEo67QNghCQByyHlhWZT571ZKfqV98JFWg7TvbV9QtlrQtXakPYeIEjQ==:1000:+kuXS/iJUoEqrm8Fo4K0cTebsc4YQZu+f5bRGX0lC1T+l0g1gzRUuKiCtWTar28Y0wjch1ZQvkNy523Pxr07agVi/RL0SUktmEl59qGor+m4FLewZBVdcgx/Ya9kU0riis98AAR0zdTpTtoN5wpNbmztIpOZ0YejeD0Esk3vagU='}} + verified = px_cookie_validator.verify(ctx, config) + self.assertFalse(verified) + self.assertEqual(None, ctx.get('cookie_expired')) + + + + From 4c8f7aefcd99133b716c97a3894b3cbb2039966b Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 13:24:19 +0200 Subject: [PATCH 18/34] Fixed a few files after running automation --- perimeterx/middleware.py | 54 +++++++++++++++++------------- perimeterx/px_activities_client.py | 8 ++++- perimeterx/px_blocker.py | 5 ++- perimeterx/px_config.py | 5 --- perimeterx/px_context.py | 10 +++--- perimeterx/px_cookie_v1.py | 1 + perimeterx/px_cookie_v3.py | 3 +- 7 files changed, 49 insertions(+), 37 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index dbea496..ddc49e9 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -29,8 +29,6 @@ def __init__(self, app, config=None): logger.error('PX Cookie Key is missing') raise ValueError('PX Cookie Key is missing') self.reverse_proxy_prefix = px_config.app_id[2:].lower() - if px_config.custom_request_handler: - self.handle_verification = px_config.custom_request_handler.__get__(self, PerimeterX) self._PXBlocker = px_blocker.PXBlocker() self._config = px_config px_httpc.init(px_config) @@ -64,33 +62,46 @@ def _verify(self, environ, start_response): # Server-to-Server verification fallback if not px_api.verify(ctx, self.config): return self.app(environ, start_response) - if config.custom_request_handler: - return config.custom_request_handler(ctx, self.config, environ, start_response) return self.handle_verification(ctx, self.config, environ, start_response) except: logger.error("Cought exception, passing request") - self.pass_traffic(environ, start_response, ctx) + self.pass_traffic(ctx) + return self.app(environ, start_response) def handle_verification(self, ctx, config, environ, start_response): score = ctx.get('risk_score', -1) - + result = None + headers = None + status = None + pass_request = True if score < config.blocking_score: - return self.pass_traffic(environ, start_response, ctx) - - if config.custom_block_handler: - px_activities_client.send_block_activity(ctx, config) - return config.custom_block_handler(ctx, start_response) - elif config.module_mode == px_constants.MODULE_MODE_BLOCKING: - return self.px_blocker.handle_blocking(ctx=ctx, config=config, start_response=start_response) + self.pass_traffic(ctx) else: - return self.pass_traffic(environ, start_response, ctx) + pass_request = False + self.block_traffic(ctx) + + if config.additional_activity_handler: + config.additional_activity_handler(ctx, config) + + if config.module_mode == px_constants.MODULE_MODE_BLOCKING and result is None and not pass_request: + result, headers, status = self.px_blocker.handle_blocking(ctx=ctx, config=config) + if config.custom_request_handler: + custom_body, custom_headers, custom_status = config.custom_request_handler(ctx, self.config, environ) + if (custom_body is not None): + start_response(custom_status, custom_headers) + return custom_body + + if headers is not None: + start_response(status, headers) + return result + else: + return self.app(environ, start_response) + + def pass_traffic(self, ctx): + px_activities_client.send_page_requested_activity( ctx, self.config) - def pass_traffic(self, environ, start_response, ctx): - details = {} - if ctx.get('decoded_cookie', ''): - details = {"px_cookie": ctx['decoded_cookie']} - px_activities_client.send_to_perimeterx(px_constants.PAGE_REQUESTED_ACTIVITY, ctx, self.config, details) - return self.app(environ, start_response) + def block_traffic(self, ctx): + px_activities_client.send_block_activity(ctx, self.config) @property def config(self): @@ -99,6 +110,3 @@ def config(self): @property def px_blocker(self): return self._PXBlocker - - - diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 442af22..944a9e9 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -73,9 +73,15 @@ def send_block_activity(ctx, config): #'cookie_origin':, 'block_action': ctx.get('block_action',''), 'module_version': px_constants.MODULE_VERSION, - 'simulated_block': config.monitor_mode is 0, + 'simulated_block': config.module_mode is px_constants.MODULE_MODE_MONITORING, }) +def send_page_requested_activity(ctx, config): + details = {} + if ctx.get('decoded_cookie', ''): + details = {"px_cookie": ctx['decoded_cookie']} + send_to_perimeterx(px_constants.PAGE_REQUESTED_ACTIVITY, ctx, config, details) + def send_enforcer_telemetry_activity(config, update_reason): details = { 'enforcer_configs': config.telemetry_config, diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py index add48f9..68d902c 100644 --- a/perimeterx/px_blocker.py +++ b/perimeterx/px_blocker.py @@ -10,7 +10,7 @@ def __init__(self): self.ratelimit_rendered_page = self.mustache_renderer.render( px_template.get_template(px_constants.RATELIMIT_TEMPLATE), {}) - def handle_blocking(self, ctx, config, start_response): + def handle_blocking(self, ctx, config): action = ctx.get('block_action') status = '403 Forbidden' @@ -31,10 +31,9 @@ def handle_blocking(self, ctx, config, start_response): blocking_props = self.prepare_properties(ctx, config) blocking_response = self.mustache_renderer.render(px_template.get_template(px_constants.BLOCK_TEMPLATE), blocking_props) - start_response(status, headers) if is_json_response: blocking_response = json.dumps(blocking_props) - return str(blocking_response) + return str(blocking_response), headers, status def prepare_properties(self, ctx, config): app_id = config.app_id.lower() diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 503466f..e76c9d2 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -90,10 +90,6 @@ def proxy_url(self): def custom_request_handler(self): return self._custom_request_handler - @property - def custom_block_handler(self): - return self._custom_block_handler - @property def blocking_score(self): return self._blocking_score @@ -173,7 +169,6 @@ def telemetry_config(self): def __instantiate_user_defined_handlers(self, config_dict): self._custom_request_handler = self.__set_handler('custom_request_handler', config_dict) - self._custom_block_handler = self.__set_handler('custom_block_handler', config_dict) self._get_user_ip = self.__set_handler('get_user_ip', config_dict) self._additional_activity_handler = self.__set_handler('additional_activity_handler', config_dict) diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index ed0f16e..9f541a1 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -41,12 +41,12 @@ def build_context(environ, config): vid = cookies.get('_pxvid').value else: vid = '' - user_agent = headers.get('user-agent') + user_agent = environ.get('HTTP_USER_AGENT', '') uri = environ.get('PATH_INFO') or '' - full_url = http_protocol + headers.get('host') or environ.get('SERVER_NAME') or '' + uri + full_url = http_protocol + (headers.get('host') or environ.get('SERVER_NAME') or '') + uri hostname = headers.get('host') - sensitive_route = uri in config.sensitive_routes - whitelist_route = uri in config.whitelist_routes + sensitive_route = len(filter(lambda sensitive_route_item : uri.startswith(sensitive_route_item), config.sensitive_routes)) > 0 + whitelist_route = len(filter(lambda whitelist_route_item : uri.startswith(whitelist_route_item), config.whitelist_routes)) > 0 ctx = { 'headers': headers, 'http_method': http_method, @@ -63,6 +63,8 @@ def build_context(environ, config): 'query_params': environ['QUERY_STRING'], 'sensitive_route': sensitive_route, 'whitelist_route': whitelist_route, + 's2s_call_reason': 'none', + 'cookie_origin': 'cookie' } return ctx diff --git a/perimeterx/px_cookie_v1.py b/perimeterx/px_cookie_v1.py index fa5e4de..3c3cd4d 100644 --- a/perimeterx/px_cookie_v1.py +++ b/perimeterx/px_cookie_v1.py @@ -7,6 +7,7 @@ class PxCookieV1(PxCookie): def __init__(self, ctx, config): self._ctx = ctx self._config = config + self._logger = config.logger self.raw_cookie = ctx['px_cookies'].get(PREFIX_PX_COOKIE_V1, '') def get_score(self): diff --git a/perimeterx/px_cookie_v3.py b/perimeterx/px_cookie_v3.py index ab70e3a..9b4f8b0 100644 --- a/perimeterx/px_cookie_v3.py +++ b/perimeterx/px_cookie_v3.py @@ -8,8 +8,9 @@ def __init__(self, ctx, config): 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 spliced_cookie.count > 1: + if len(spliced_cookie) > 1: self.hmac = spliced_cookie[0] self.raw_cookie = spliced_cookie[1] From 8b04726c57f2b8515654c857d71111ea2ec8549e Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 13:52:50 +0200 Subject: [PATCH 19/34] Fixed unittests --- tests/px_blocker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/px_blocker.py b/tests/px_blocker.py index 34a081d..631af7a 100644 --- a/tests/px_blocker.py +++ b/tests/px_blocker.py @@ -26,7 +26,7 @@ def test_handle_blocking(self): 'uuid': px_uuid } px_config = PXConfig({'app_id': 'PXfake_app_ip'}) - message = px_blocker.handle_blocking(ctx, px_config, lambda x, y : None) + message, _, _ = px_blocker.handle_blocking(ctx, px_config) blocking_message = None with open('./px_blocking_messages/blocking.txt', 'r') as myfile: blocking_message = myfile.read() @@ -43,7 +43,7 @@ def test_handle_ratelimit(self): 'block_action': 'r' } px_config = PXConfig({'app_id': 'PXfake_app_ip'}) - message = px_blocker.handle_blocking(ctx, px_config, lambda x, y : None) + message, _, _ = px_blocker.handle_blocking(ctx, px_config) blocking_message = None with open('./px_blocking_messages/ratelimit.txt', 'r') as myfile: blocking_message = myfile.read() @@ -61,7 +61,7 @@ def test_handle_challenge(self): 'block_action_data': 'Bla' } px_config = PXConfig({'app_id': 'PXfake_app_ip'}) - message = px_blocker.handle_blocking(ctx, px_config, lambda x, y : None) + message, _, _ = px_blocker.handle_blocking(ctx, px_config) blocking_message = 'Bla' self.assertEqual(message, blocking_message) From 35efd5a752619205ecf3e688239312fa28c35766 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 14:30:25 +0200 Subject: [PATCH 20/34] Fxied j challenge response --- perimeterx/px_api.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index 9ed3b47..ab7aa3b 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -26,15 +26,16 @@ def verify(ctx, config): ctx['block_action'] = response['action'] ctx['risk_rtt'] = risk_rtt if score >= config.blocking_score: - logger.debug("PXVerify block score threshold reached, will initiate blocking") - ctx['block_reason'] = 's2s_high_score' - elif response['action'] is 'j' and response.get('action_data') is not None and response.get('action_data').get('body') is not None: - logger.debug("PXVerify received javascript challenge action") - ctx['block_action_data'] = response.get('action_data').get('body') - ctx['block_reason'] = 'challenge' - elif response['action'] is 'r': - logger.debug("PXVerify received javascript ratelimit action") - ctx['block_reason'] = 'exceeded_rate_limit' + if response['action'] == 'j' and response.get('action_data') is not None and response.get('action_data').get('body') is not None: + logger.debug("PXVerify received javascript challenge action") + ctx['block_action_data'] = response.get('action_data').get('body') + ctx['block_reason'] = 'challenge' + elif response['action'] is 'r': + logger.debug("PXVerify received javascript ratelimit action") + ctx['block_reason'] = 'exceeded_rate_limit' + else: + logger.debug("PXVerify block score threshold reached, will initiate blocking") + ctx['block_reason'] = 's2s_high_score' else: ctx['pass_reason'] = 's2s' From d89edaf02c167580a677906835738288bee769db Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 16:22:00 +0200 Subject: [PATCH 21/34] Added px_proxy tests --- perimeterx/middleware.py | 2 +- .../{px_proxy.py => px_proxy_handler.py} | 0 tests/px_proxy_handler.py | 33 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) rename perimeterx/{px_proxy.py => px_proxy_handler.py} (100%) create mode 100644 tests/px_proxy_handler.py diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index ddc49e9..10319c5 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -6,7 +6,7 @@ import px_api import px_constants import px_utils -from px_proxy import PXProxy +from perimeterx.px_proxy_handler import PXProxy from px_config import PXConfig diff --git a/perimeterx/px_proxy.py b/perimeterx/px_proxy_handler.py similarity index 100% rename from perimeterx/px_proxy.py rename to perimeterx/px_proxy_handler.py diff --git a/tests/px_proxy_handler.py b/tests/px_proxy_handler.py new file mode 100644 index 0000000..8e14db5 --- /dev/null +++ b/tests/px_proxy_handler.py @@ -0,0 +1,33 @@ +import unittest + +from httplib import HTTPResponse + +from perimeterx.px_proxy_handler import PXProxy +from perimeterx.px_config import PXConfig +from mock import MagicMock,patch + +class Test_PXProxy(unittest.TestCase): + + def test_should_reverse_request(self): + config = PXConfig({'app_id': 'PXfake_app_id'}) + px_proxy = PXProxy(config) + should_reverse = px_proxy.should_reverse_request('/fake_app_id/init.js') + self.assertTrue(should_reverse) + should_reverse = px_proxy.should_reverse_request('/fake_app_id/xhr') + self.assertTrue(should_reverse) + 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 '' + + + From c0abed6a3842d2f1022ded79abbec60eab744f17 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 16:48:10 +0200 Subject: [PATCH 22/34] Fxied building process of telemetry config --- perimeterx/px_config.py | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index e76c9d2..7f9de19 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -1,4 +1,5 @@ import px_constants +import json from px_logger import Logger @@ -26,12 +27,10 @@ def __init__(self, config_dict): self._is_mobile = config_dict.get('is_mobile', False) self._monitor_mode = 0 if module_mode is px_constants.MODULE_MODE_MONITORING else 1 self._module_enabled = config_dict.get('module_enabled', True) - self._cookie_key = config_dict.get('cookie_key', None) self._auth_token = config_dict.get('auth_token', None) self._is_mobile = config_dict.get('is_mobile', False) self._first_party = config_dict.get('first_party', True) self._first_party_xhr_enabled = config_dict.get('first_party_xhr_enabled', True) - self._logger = Logger(debug_mode) self._ip_headers = config_dict.get('ip_headers', []) self._proxy_url = config_dict.get('proxy_url', None) self._max_buffer_len = config_dict.get('max_buffer_len', 30) @@ -39,9 +38,14 @@ def __init__(self, config_dict): self._whitelist_routes = config_dict.get('whitelist_routes', []) self._block_html = 'BLOCK' self._logo_visibility = 'visible' if custom_logo is not None else 'hidden' - self.__instantiate_user_defined_handlers(config_dict) self._telemetry_config = self.__create_telemetry_config() + self._auth_token = config_dict.get('auth_token', None) + self._cookie_key = config_dict.get('cookie_key', None) + self.__instantiate_user_defined_handlers(config_dict) + self._logger = Logger(debug_mode) + + @property def module_mode(self): return self._module_mode @@ -178,26 +182,13 @@ def __set_handler(self, function_name, config_dict): config_dict.get(function_name)) else None def __create_telemetry_config(self): - return { - 'PX_APP_ID': self.app_id, - 'ENABLE_MODULE': self.module_enabled, - 'API_TIMEOUT_MS': self.api_timeout, - 'BLOCKING_SCORE': self.blocking_score, - 'IP_HEADERS': self.ip_headers, - 'BLOCK_HTML': self.block_html, - 'SENSITIVE_HEADERS': self.sensitive_headers, - 'PROXY_URL': self.proxy_url, - 'SEND_PAGE_ACTIVITIES': self.send_page_activities, - 'DEBUG_MODE': self.debug_mode, - 'MAX_BUFFER_LEN': self.max_buffer_len, - 'GET_USER_IP': self.get_user_ip, - 'CSS_REF': self.css_ref, - 'JS_REF': self.js_ref, - 'CUSTOM_LOGO': self.custom_logo, - 'LOGO_VISIBILITY': self.logo_visibility, - 'SENSITIVE_ROUTES': self.sensitive_routes, - 'MODULE_MODE': self.module_mode, - } + config = self.__dict__ + mutated_config = {} + for key, value in config.iteritems(): + mutated_config[key[1:]] = value + config_dict = json.dumps(mutated_config) + + From 6a694ff9f87bb1b805f55ebdc1433cdbd54764c6 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 17:15:00 +0200 Subject: [PATCH 23/34] Changed telemtry uri --- perimeterx/px_config.py | 12 ++++++------ perimeterx/px_constants.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 7f9de19..4681f27 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -9,7 +9,7 @@ def __init__(self, config_dict): debug_mode = config_dict.get('debug_mode', False) module_mode = config_dict.get('module_mode', px_constants.MODULE_MODE_MONITORING) custom_logo = config_dict.get('custom_logo', None) - self._app_id = app_id + self._px_app_id = app_id self._blocking_score = config_dict.get('blocking_score', 100) self._debug_mode = debug_mode self._module_version = config_dict.get('module_version', px_constants.MODULE_VERSION) @@ -20,7 +20,7 @@ def __init__(self, config_dict): self._encryption_enabled = config_dict.get('encryption_enabled', True) self._sensitive_headers = config_dict.get('sensitive_headers', ['cookie', 'cookies']) self._send_page_activities = config_dict.get('send_page_activities', True) - self._api_timeout = config_dict.get('api_timeout', 500) + self._api_timeout_ms = config_dict.get('api_timeout', 500) self._custom_logo = custom_logo self._css_ref = config_dict.get('_custom_logo', '') self._js_ref = config_dict.get('js_ref', '') @@ -52,7 +52,7 @@ def module_mode(self): @property def app_id(self): - return self._app_id + return self._px_app_id @property def logger(self): @@ -72,7 +72,7 @@ def server_host(self): @property def api_timeout(self): - return self._api_timeout + return self._api_timeout_ms @property def module_enabled(self): @@ -185,8 +185,8 @@ def __create_telemetry_config(self): config = self.__dict__ mutated_config = {} for key, value in config.iteritems(): - mutated_config[key[1:]] = value - config_dict = json.dumps(mutated_config) + mutated_config[key[1:].upper()] = value + return mutated_config diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index e0fb006..01e634b 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -29,6 +29,6 @@ API_RISK = '/api/v3/risk' PAGE_REQUESTED_ACTIVITY = 'page_requested' BLOCK_ACTIVITY = 'block' -API_ENFORCER_TELEMETRY = '/api/v3/risk/telemetry' +API_ENFORCER_TELEMETRY = '/api/v2/risk/telemetry' API_ACTIVITIES = '/api/v1/collector/s2s' TELEMETRY_ACTIVITY = 'enforcer_telemetry' From 009e13cc65438b49b8fc2e5bcf49804a38a024a7 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 17:19:50 +0200 Subject: [PATCH 24/34] Empty tests --- tests/px_activities_client.py | 0 tests/px_api.py | 0 tests/px_config.py | 0 tests/px_context.py | 0 tests/px_cookie.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/px_activities_client.py create mode 100644 tests/px_api.py create mode 100644 tests/px_config.py create mode 100644 tests/px_context.py create mode 100644 tests/px_cookie.py diff --git a/tests/px_activities_client.py b/tests/px_activities_client.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/px_api.py b/tests/px_api.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/px_config.py b/tests/px_config.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/px_context.py b/tests/px_context.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/px_cookie.py b/tests/px_cookie.py new file mode 100644 index 0000000..e69de29 From 7581c84399e76f2750f16669a4a30f089d4d56a9 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Tue, 27 Nov 2018 19:15:44 +0200 Subject: [PATCH 25/34] Empty tests --- perimeterx/px_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 4681f27..b492d75 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -194,3 +194,4 @@ def __create_telemetry_config(self): + From 8b9922a0d3d6b1b9437e8f1308e54356c24b7081 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Wed, 28 Nov 2018 15:53:46 +0200 Subject: [PATCH 26/34] Fxied captcha loop --- perimeterx/px_blocker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py index 68d902c..c0759df 100644 --- a/perimeterx/px_blocker.py +++ b/perimeterx/px_blocker.py @@ -36,7 +36,7 @@ def handle_blocking(self, ctx, config): return str(blocking_response), headers, status def prepare_properties(self, ctx, config): - app_id = config.app_id.lower() + app_id = config.app_id vid = ctx.get('vid') if ctx.get('vid') is not None else '' uuid = ctx.get('uuid') custom_logo = config.custom_logo @@ -64,7 +64,7 @@ def prepare_properties(self, ctx, config): 'logoVisibility': 'visible' if custom_logo is not None else 'hidden', 'hostUrl': host_url, 'jsClientSrc': js_client_src, - 'firstPartyEnabled': config.first_party, + 'firstPartyEnabled': 'true' if config.first_party else 'false', 'blockScript': captcha_src } From febc41de4e0322bb913f15705fe87ed020776e31 Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Wed, 28 Nov 2018 17:59:19 +0200 Subject: [PATCH 27/34] Fixed captcha loops --- perimeterx/middleware.py | 6 ++-- perimeterx/px_activities_client.py | 26 ++++++++------- perimeterx/px_api.py | 9 +++-- perimeterx/px_constants.py | 8 ++--- perimeterx/px_context.py | 9 +++-- perimeterx/px_httpc.py | 53 ++++++------------------------ perimeterx/px_proxy_handler.py | 47 +++++++++++++------------- 7 files changed, 70 insertions(+), 88 deletions(-) diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 10319c5..2e016ed 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -31,7 +31,7 @@ def __init__(self, app, config=None): self.reverse_proxy_prefix = px_config.app_id[2:].lower() self._PXBlocker = px_blocker.PXBlocker() self._config = px_config - px_httpc.init(px_config) + px_activities_client.init_activities_configuration(px_config) px_activities_client.send_enforcer_telemetry_activity(config=px_config, update_reason='initial_config') def __call__(self, environ, start_response): @@ -45,7 +45,7 @@ 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) + return px_proxy.handle_reverse_request(self.config, ctx, start_response, environ) if px_utils.is_static_file(ctx): logger.debug('Filter static file request. uri: ' + uri) return self.app(environ, start_response) @@ -69,7 +69,7 @@ def _verify(self, environ, start_response): 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_activities_client.py b/perimeterx/px_activities_client.py index 02327e4..4037464 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -4,36 +4,40 @@ import traceback, sys import px_constants import socket +import json ACTIVITIES_BUFFER = [] CONFIG = {} +def init_activities_configuration(config): + global CONFIG + CONFIG = config + t1 = threading.Thread(target=send_activities) + t1.daemon = True + t1.start() def send_activities(): global ACTIVITIES_BUFFER + default_headers = { + 'Authorization': 'Bearer ' + CONFIG.auth_token, + 'Content-Type': 'application/json' + } + full_url = CONFIG.server_host + px_constants.API_ACTIVITIES while True: if len(ACTIVITIES_BUFFER) > 0: chunk = ACTIVITIES_BUFFER[:10] ACTIVITIES_BUFFER = ACTIVITIES_BUFFER[10:] - px_httpc.send(px_constants.API_ACTIVITIES, chunk, CONFIG) + px_httpc.send(full_url=full_url, body=json.dumps(chunk), headers=default_headers, config=CONFIG, method='POST') time.sleep(1) -t1 = threading.Thread(target=send_activities) -t1.daemon = True -t1.start() - def send_to_perimeterx(activity_type, ctx, config, detail): - global CONFIG try: if activity_type == 'page_requested' and not config.send_page_activities: print 'Page activities disabled in config - skipping.' return - if not CONFIG: - CONFIG = config - _details = { 'http_method': ctx.get('http_method', ''), 'http_version': ctx.get('http_version', ''), @@ -101,8 +105,8 @@ def send_enforcer_telemetry_activity(config, update_reason): 'Content-Type': 'application/json' } config.logger.debug('Sending telemetry activity to PerimeterX servers') - px_httpc.send_https(url=config.server_host, path=px_constants.API_ENFORCER_TELEMETRY, body=body, - headers=headers, config=config, method='POST') + px_httpc.send(full_url=config.server_host + px_constants.API_ENFORCER_TELEMETRY, body=json.dumps(body), + headers=headers, config=config, method='POST') diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index ab7aa3b..8ef7778 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -2,13 +2,18 @@ import px_httpc import time import px_constants +import json def send_risk_request(ctx, config): body = prepare_risk_body(ctx, config) - return px_httpc.send(px_constants.API_RISK, body, config) - + default_headers = { + 'Authorization': 'Bearer ' + config.auth_token, + 'Content-Type': 'application/json' + } + response = px_httpc.send(full_url=config.server_host + px_constants.API_RISK,body=json.dumps(body),config=config, headers=default_headers,method='POST') + return json.loads(response.content) def verify(ctx, config): logger = config.logger diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 01e634b..95722ea 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -10,10 +10,10 @@ BLOCK_ACTION_CAPTCHA = 'b' BLOCK_ACTION_CHALLENGE = 'j' BLOCK_ACTION_RATE = 'r' -CLIENT_HOST = 'client.perimeterx.net' -CAPTCHA_HOST = 'captcha.px-cdn.net' -COLLECTOR_URL = 'collector-{}.perimeterx.net' -SERVER_URL = 'sapi-{}.perimeterx.net' +CLIENT_HOST = 'https://client.perimeterx.net' +CAPTCHA_HOST = 'https://captcha.px-cdn.net' +COLLECTOR_URL = 'https://collector-{}.perimeterx.net' +SERVER_URL = 'https://sapi-{}.perimeterx.net' CLIENT_FP_PATH = 'init.js' CAPTCHA_FP_PATH = 'captcha' XHR_FP_PATH = 'xhr' diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index 9f541a1..e21da96 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -27,6 +27,9 @@ def build_context(environ, config): http_protocol = protocol_split[0].lower() + '://' if len(protocol_split) > 1: http_version = protocol_split[1] + if key == 'CONTENT_TYPE' or key == 'CONTENT_LENGTH': + headers['Content-type'.replace('_', '-')] = environ.get(key) + cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE', '')) cookie_keys = cookies.keys() @@ -70,13 +73,13 @@ def build_context(environ, config): def extract_ip(config, environ): - ip = environ.get('HTTP_X_FORWARDED_FOR') + ip = environ.get('HTTP_X_FORWARDED_FOR') if environ.get('HTTP_X_FORWARDED_FOR') else environ.get('REMOTE_ADDR') ip_headers = config.ip_headers logger = config.logger - if not ip_headers: + if ip_headers: try: for ip_header in ip_headers: - ip_header_name = 'HTTP_' + ip_header.upper() + ip_header_name = 'HTTP_' + ip_header.replace('-', '_').upper() if environ.get(ip_header_name): return environ.get(ip_header_name) except: diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index 56e8ab4..44fb218 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -1,55 +1,22 @@ -import httplib -import json import time +import requests -http_client = None - -def init(config): - global http_client - http_client = httplib.HTTPConnection(host=config.server_host, timeout=config.api_timeout) - - -def send(uri, body, config): +def send(full_url, body, headers, config, method): logger = config.logger - headers = { - 'Authorization': 'Bearer ' + config.auth_token, - 'Content-Type': 'application/json' - } try: start = time.time() - http_client.request('POST', uri, body=json.dumps(body), headers=headers) - r = http_client.getresponse() - if r.status != 200: - logger.error('error posting server to server call ' + r.reason) - return False + if method == 'GET': + response = requests.get(url=full_url, headers=headers, timeout=config.api_timeout) + else: + response = requests.post(url=full_url, headers=headers, data=body, timeout=config.api_timeout) - logger.debug('Server call took ' + str(time.time() - start) + 'ms') - response_body = r.read() - return json.loads(response_body) - except httplib.HTTPException: - init(config) - return False - -def send_https(url, path, body, headers, config, method): - logger = config.logger - try: - start = time.time() - http_client = httplib.HTTPConnection(url, timeout=config.api_timeout) - http_client.request(method=method, url=path, body=json.dumps(body), headers=headers) - response = http_client.getresponse() - - if response.status >= 400: + if response.status_code >= 400: logger.debug('PerimeterX server call failed') return False - logger.debug('Server call took ' + str(time.time() - start) + 'ms') + logger.debug('PerimeterX server call took ' + str(time.time() - start) + 'ms') return response - - except httplib.HTTPException: - init(config) + except requests.exceptions.RequestException as e: + print e return False - - - - diff --git a/perimeterx/px_proxy_handler.py b/perimeterx/px_proxy_handler.py index e0e1be9..f0169fb 100644 --- a/perimeterx/px_proxy_handler.py +++ b/perimeterx/px_proxy_handler.py @@ -26,13 +26,13 @@ def should_reverse_request(self, uri): return True return False - def handle_reverse_request(self, config, ctx, start_response): + def handle_reverse_request(self, config, ctx, start_response, environ): 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) + 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')))) if uri.startswith(self.captcha_reverse_prefix): return self.send_reverse_captcha_request(config=config, context=ctx, start_response=start_response) @@ -46,18 +46,17 @@ def send_reverse_client_request(self, config, context, start_response): 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.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) - response = px_httpc.send_https(url=px_constants.CLIENT_HOST, path=client_request_uri, body='', - headers=filtered_headers, config=config, method='GET') + response = px_httpc.send(full_url=px_constants.CLIENT_HOST + client_request_uri, body='', + headers=filtered_headers, config=config, method='GET') - headers = filter(lambda x: x[0] not in hoppish, response.getheaders()) - start_response(str(response.status) + ' ' + response.reason, headers) - return response.read() + self.handle_proxy_response(response, start_response) + return response.content - def send_reverse_xhr_request(self, config, context, start_response): + def send_reverse_xhr_request(self, config, context, start_response, body): uri = context.get('uri') if not config.first_party or not config.first_party_xhr_enabled: body, content_type = self.return_default_response(uri) @@ -70,7 +69,7 @@ def send_reverse_xhr_request(self, config, context, start_response): host = config.collector_host headers = {'host': host, - px_constants.FIRST_PARTY_HEADER: 1, + px_constants.FIRST_PARTY_HEADER: '1', px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} if context.get('vid') is not None: @@ -79,18 +78,23 @@ def send_reverse_xhr_request(self, config, context, start_response): 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)) - response = px_httpc.send_https(url=host, path=suffix_uri, body='', - headers=filtered_headers, config=config, method=context.get('http_method')) + response = px_httpc.send(full_url=host + suffix_uri, body=body, + headers=filtered_headers, config=config, method=context.get('http_method')) - if response.status >= 400: + if response.status_code >= 400: body, content_type = self.return_default_response(uri) px_logger.Logger.debug('error reversing the http call ' + response.reason) start_response('200 OK', [content_type]) return body - response_headers = filter(lambda x: x[0] not in hoppish, response.getheaders()) - start_response(str(response.status) + ' ' + response.reason, response_headers) - return response.read() + self.handle_proxy_response(response, start_response) + return response.content + def handle_proxy_response(self, response, start_response): + headers = [] + for header in response.headers: + if header.lower() not in hoppish: + headers.append((header, response.headers[header])) + start_response(str(response.status_code) + ' ' + response.reason, headers) def return_default_response(self, uri): if 'gif' in uri.lower(): @@ -111,16 +115,15 @@ def send_reverse_captcha_request(self, config, context, start_response): host = px_constants.CAPTCHA_HOST headers = {'host': px_constants.CAPTCHA_HOST, - px_constants.FIRST_PARTY_HEADER: 1, + 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) self._logger.debug('Forwarding request from {} to client at {}{}'.format(context.get('uri').lower(), host, uri)) - response = px_httpc.send_https(url=host, path=uri, body='', - headers=filtered_headers, config=config, method='GET') - headers = filter(lambda x: x[0] not in hoppish, response.getheaders()) - start_response(str(response.status) + ' ' + response.reason, headers) - return response.read() + 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.content From 757745e40426531b905e37c54c101bf441563e0f Mon Sep 17 00:00:00 2001 From: Alex Bluvstein Date: Wed, 28 Nov 2018 18:02:32 +0200 Subject: [PATCH 28/34] fixed unittests --- tests/px_blocker.py | 5 ++--- tests/px_blocking_messages/blocking.txt | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/px_blocker.py b/tests/px_blocker.py index 631af7a..9e9b9ee 100644 --- a/tests/px_blocker.py +++ b/tests/px_blocker.py @@ -27,7 +27,6 @@ def test_handle_blocking(self): } px_config = PXConfig({'app_id': 'PXfake_app_ip'}) message, _, _ = px_blocker.handle_blocking(ctx, px_config) - blocking_message = None with open('./px_blocking_messages/blocking.txt', 'r') as myfile: blocking_message = myfile.read() self.assertEqual(message, blocking_message) @@ -81,11 +80,11 @@ def test_prepare_properties(self): 'jsRef': '', 'hostUrl': '/fake_app_ip/xhr', 'customLogo': None, - 'appId': 'pxfake_app_ip', + 'appId': 'PXfake_app_ip', 'uuid': '8712cef7-bcfa-4bb6-ae99-868025e1908a', 'logoVisibility': 'hidden', 'jsClientSrc': '/fake_app_ip/init.js', - 'firstPartyEnabled': True, + 'firstPartyEnabled': 'true', 'refId': '8712cef7-bcfa-4bb6-ae99-868025e1908a', 'cssRef': ''} self.assertDictEqual(message, expected_message) diff --git a/tests/px_blocking_messages/blocking.txt b/tests/px_blocking_messages/blocking.txt index 50a2790..b42ba1b 100644 --- a/tests/px_blocking_messages/blocking.txt +++ b/tests/px_blocking_messages/blocking.txt @@ -141,9 +141,9 @@