diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index b97d8b8..389aaa0 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -1,83 +1,70 @@ -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 -import px_template -from px_proxy import PXProxy -import Cookie import px_constants +import px_utils +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, - '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, - 'is_mobile': False, - 'first_party': True, - 'first_party_xhr_enabled': True, - 'monitor_mode': px_constants.MODULE_MODE_MONITORING, - } - - 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 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: + logger.debug('Module is disabled, request will not be verified') + return self.app(environ, start_response) - cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE')) + 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, 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") @@ -86,14 +73,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) @@ -101,18 +88,16 @@ 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 + def config(self): + return self._config + + @property + 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_activities_client.py b/perimeterx/px_activities_client.py index 3e06d64..9ac6b60 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: @@ -50,8 +47,8 @@ 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'), - 'px_app_id': config.get('app_id'), + 'socket_ip': ctx.get('ip'), + 'px_app_id': config.app_id, 'url': ctx.get('full_url'), 'details': _details, 'vid': ctx.get('vid', ''), @@ -64,16 +61,16 @@ 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_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'), #'cookie_origin':, + 'block_action': ctx.get('block_action', ''), '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, }) diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index f19b8ef..9ed3b47 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,11 +48,11 @@ def verify(ctx, config): def prepare_risk_body(ctx, config): - logger = config['logger'] + logger = config.logger 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', '') @@ -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..fb6b98c --- /dev/null +++ b/perimeterx/px_config.py @@ -0,0 +1,154 @@ +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._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) + + @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 + + @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_constants.py b/perimeterx/px_constants.py index c68728b..648a3b5 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' @@ -24,5 +25,7 @@ EMPTY_GIF_B64 = 'R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' COLLECTOR_HOST = 'collector.perimeterx.net' FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR' - MODULE_VERSION = 'Python WSGI Module v2.0.0' +API_RISK = '/api/v3/risk' +PAGE_REQUESTED_ACTIVITY = 'page_requested' +BLOCK_ACTIVITY = 'block' diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index dc49c49..ed0f16e 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,11 @@ 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) @@ -47,18 +41,17 @@ 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, 'http_version': http_version, 'user_agent': user_agent, - 'socket_ip': socket_ip, 'full_url': full_url, 'uri': uri, 'hostname': hostname, @@ -67,14 +60,25 @@ 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 = environ.get('HTTP_X_FORWARDED_FOR') - if not ip == None: - return ip.split(',')[-1].strip() - else: - return '' - + 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') + if config.get_user_ip: + ip = config.get_user_ip(environ) + return ip 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..4d30893 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') @@ -49,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: diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index c44397c..b34f56f 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -7,37 +7,35 @@ 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: 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): - 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..3fc9b73 100644 --- a/perimeterx/px_utils.py +++ b/perimeterx/px_utils.py @@ -1,26 +1,29 @@ 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 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