diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 389aaa0..2e016ed 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 @@ -29,11 +29,10 @@ 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) + 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): return self._verify(environ, start_response) @@ -46,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) @@ -63,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) - + score = ctx.get('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): @@ -98,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 9ac6b60..4037464 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -3,36 +3,41 @@ import threading 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('/api/v1/collector/s2s', 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', ''), @@ -72,5 +77,36 @@ 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, + 'node_name': socket.gethostname(), + 'os_name': sys.platform, + 'update_reason': update_reason, + 'module_version': config.module_version + } + body = { + 'type': px_constants.TELEMETRY_ACTIVITY, + 'timestamp': time.time(), + '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(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 9ed3b47..efb7f83 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -1,14 +1,30 @@ -import sys import px_httpc import time import px_constants +import json +import re - +custom_params = { + 'custom_param1': '', + 'custom_param2': '', + 'custom_param3': '', + 'custom_param4': '', + 'custom_param5': '', + 'custom_param6': '', + 'custom_param7': '', + 'custom_param8': '', + 'custom_param9': '', + 'custom_param10': '' +} 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 @@ -26,15 +42,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' @@ -70,6 +87,13 @@ def prepare_risk_body(ctx, config): } } + if config.enrich_custom_parameters: + risk_custom_params = config.enrich_custom_parameters(custom_params) + for param in risk_custom_params: + if re.match('^custom_param\d$',param) and risk_custom_params[param]: + body['additional'][param] = risk_custom_params[param] + + if ctx['s2s_call_reason'] == 'cookie_decryption_failed': logger.debug('attaching orig_cookie to request') body['additional']['px_cookie_orig'] = ctx.get('px_orig_cookie') diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py index add48f9..c0759df 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,13 +31,12 @@ 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() + 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 @@ -65,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 } diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index fb6b98c..247859e 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 @@ -7,7 +8,8 @@ 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 + custom_logo = config_dict.get('custom_logo', None) + 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) @@ -18,24 +20,30 @@ 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._custom_logo = config_dict.get('custom_logo', '') + 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', '') 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) 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._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): @@ -43,7 +51,7 @@ def module_mode(self): @property def app_id(self): - return self._app_id + return self._px_app_id @property def logger(self): @@ -63,7 +71,7 @@ def server_host(self): @property def api_timeout(self): - return self._api_timeout + return self._api_timeout_ms / 1000.000 @property def module_enabled(self): @@ -85,10 +93,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 @@ -141,14 +145,47 @@ def sensitive_routes(self): def whitelist_routes(self): return self._whitelist_routes + @property + def block_html(self): + return self._block_html - 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) + @property + def logo_visibility(self): + return self._logo_visibility + @property + def additional_activity_handler(self): + return self._additional_activity_handler - def set_handler(self, function_name, config_dict): + @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 + + @property + def enrich_custom_parameters(self): + return self._enrich_custom_parameters + + def __instantiate_user_defined_handlers(self, config_dict): + self._custom_request_handler = self.__set_handler('custom_request_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) + self._enrich_custom_parameters = self.__set_handler('enrich_custom_parameters', 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 __create_telemetry_config(self): + config = self.__dict__ + mutated_config = {} + for key, value in config.iteritems(): + mutated_config[key[1:].upper()] = value + return mutated_config diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 648a3b5..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' @@ -29,3 +29,6 @@ API_RISK = '/api/v3/risk' PAGE_REQUESTED_ACTIVITY = 'page_requested' BLOCK_ACTIVITY = 'block' +API_ENFORCER_TELEMETRY = '/api/v2/risk/telemetry' +API_ACTIVITIES = '/api/v1/collector/s2s' +TELEMETRY_ACTIVITY = 'enforcer_telemetry' diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index ed0f16e..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() @@ -41,12 +44,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,18 +66,20 @@ 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 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_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] 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 diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index b34f56f..c31fbb6 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -1,51 +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): - 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 - - 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_reverse(url, path, body, headers, config, method): +def send(full_url, 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) - response = http_client.getresponse() + 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) - 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: + logger.debug('Received RequestException, message: ' + e.message) return False - diff --git a/perimeterx/px_proxy.py b/perimeterx/px_proxy_handler.py similarity index 76% rename from perimeterx/px_proxy.py rename to perimeterx/px_proxy_handler.py index c9dac3f..f0169fb 100644 --- a/perimeterx/px_proxy.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_reverse(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_reverse(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_reverse(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 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..d7bc19f --- /dev/null +++ b/tests/px_api.py @@ -0,0 +1,20 @@ +import unittest +from perimeterx import px_api +from perimeterx.px_config import PXConfig + +class Test_PXApi(unittest.TestCase): + + def enrich_custom_parameters(self, params): + params['custom_param1'] = '1' + params['custom_param2'] = '5' + params['custom'] = '6' + return params + + def test_prepare_risk_body(self): + config = PXConfig({'app_id': 'app_id', 'enrich_custom_parameters': self.enrich_custom_parameters}) + ctx = {'headers': {}, 's2s_call_reason': 'no_cookie'} + body = px_api.prepare_risk_body(ctx, config) + self.assertEqual(body['additional'].get('custom_param1'), '1') + self.assertEqual(body['additional'].get('custom_param2'), '5') + self.assertFalse(body['additional'].get('custom')) + print diff --git a/tests/px_blocker.py b/tests/px_blocker.py new file mode 100644 index 0000000..9e9b9ee --- /dev/null +++ b/tests/px_blocker.py @@ -0,0 +1,97 @@ +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) + 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) + 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) + 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..b42ba1b --- /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 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 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')) + + + + 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_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 '' + + + 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))