diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dcbc65f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Change Log + +## [v2.0.0](https://github.com/PerimeterX/perimeterx-python-wsgi/compare/v1.0.17...HEAD) (2018-12-03) +- Added Major Enforcer functionalities: Mobile SDK, FirstParty, CaptchaV2, Block handling +- Added unit tests diff --git a/README.md b/README.md index 755327a..3b781cf 100644 --- a/README.md +++ b/README.md @@ -1,329 +1,226 @@ -![image](http://media.marketwire.com/attachments/201604/34215_PerimeterX_logo.jpg) +[![Build Status](https://travis-ci.org/PerimeterX/perimeterx-python-wsgi.svg?branch=master)](https://travis-ci.org/PerimeterX/perimeterx-python-wsgi) -[PerimeterX](http://www.perimeterx.com) Python WSGI Middleware +![image](https://s.perimeterx.net/logo.png) + +[PerimeterX](http://www.perimeterx.com) Python Middleware ============================================================= -> The PerimeterX Python Middleware is supported by all [WSGI based frameworks](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface#WSGI-compatible_applications_and_frameworks). + +> Latest stable version: [v2.0.0](link to package) Table of Contents ----------------- - -- [Usage](#usage) - * [Dependencies](#dependencies) - * [Installation](#installation) - * [Basic Usage Example](#basic-usage) -- [Configuration](#configuration) - * [Blocking Score](#blocking-score) - * [Customizing Block page](#custom-block-page) - * [Custom Block Action](#custom-block) - * [Enable/Disable Server Calls](#server-calls) - * [Enable/Disable Captcha](#captcha-support) - * [Extracting Real IP Address](#real-ip) - * [Filter Sensitive Headers](#sensitive-headers) - * [API Timeouts](#api-timeout) - * [Send Page Activities](#send-page-activities) - * [Debug Mode](#debug-mode) -- [Contributing](#contributing) - * [Tests](#tests) - - - - Dependencies ----------------------------------------- - -- [Python v2.7](https://www.python.org/download/releases/2.7/) -- [pycrypto v2.6](https://pypi.python.org/pypi/pycrypto) - - Note: pycrypto is a python core module, this need to be manually added to dependencies when using GAE - - - Installation ----------------------------------------- - -Installation can be done using Composer. - -```sh -$ pip install perimeterx-python-wsgi -``` - -### Basic Usage Example -##### Django: +- [Installation](#installation) +- [Basic Usage Example](#basic_usage) +- [Advanced Blocking Response](#advanced_blocking_response) +- [Advanced Configuration Options](#configuration) + * [Module Enabled](#module_enabled) + * [Module Mode](#module_mode) + * [Blocking Score](#blocking_score) + * [Send Page Activities](#send_page_activities) + * [Debug Mode](#debug_mode) + * [Sensitive Routes](#sensitive_routes) + * [Whitelist Routes](#whitelist_routes) + * [Sensitive Headers](#sensitive_headers) + * [IP Headers](#ip_headers) + * [First Party Enabled](#first_party_enabled) + * [Custom Request Handler](#custom_request_handler) + * [Additional Activity Handler](#additional_activity_handler) + +## Installation +PerimeterX python middleware is installed via PIP: +`$ pip install perimeterx-python-wsgi` + +## Basic Usage Example +To use PerimeterX middleware on a specific route follow this example: ```python -from perimeterx.middleware import PerimeterX - px_config = { - 'app_id': 'APP_ID', - 'cookie_key': 'COOKIE_KEY', - 'auth_token': 'AUTH_TOKEN', - 'blocking_score': 70 + 'app_id': 'APP_ID', + 'cookie_key': 'COOKIE_KEY', + 'auth_token': 'AUTH_TOKEN', } application = get_wsgi_application() application = PerimeterX(application, px_config) -``` -##### Google App Engine: -app.yaml: - -```yaml -libraries: -- name: pycrypto - version: 2.6 -``` - -```python -import webapp2 -from perimeterx.middleware import PerimeterX -app = webapp2.WSGIApplication([ - ('/', MainPage), -], debug=True) +**Note:** app id, cookie secret and auth token are required fields. -px_config = { - 'app_id': 'APP_ID', - 'cookie_key': 'COOKIE_KEY', - 'auth_token': 'AUTH_TOKEN', - 'blocking_score': 70 -} -app = PerimeterX(app, px_config) ``` -### Configuration Options -#### Configuring Required Parameters -Configuration options are set in the `px_config` variable. +For details on how to create a custom Captcha page, refer to the [documentation](https://console.perimeterx.com/docs/server_integration_new.html#custom-captcha-section) -#### Required parameters: +## Advanced Configuration Options -- app_id -- cookie_key -- auth_token +In addition to the basic installation configuration [above](#basicUsage), the following configurations options are available: -#### Changing the Minimum Score for Blocking Requests +#### Module Enabled +A boolean flag to enable/disable the PerimeterX Enforcer. -**default:** 70 +**Default:** true ```python -px_config = { - .. - 'blocking_score': 75 - .. +config = { + ... + module_enabled: False + ... } ``` -### Customizing Block Page -#### Customizing logo -Adding a custom logo to the blocking page is by providing the pxConfig a key ```custom_logo``` , the logo will be displayed at the top div of the the block page The logo's ```max-heigh``` property would be 150px and width would be set to ``auto`` +#### Module Mode +Sets the working mode of the Enforcer. -The key customLogo expects a valid URL address such as https://s.perimeterx.net/logo.png +Possible values: -Example below: -```python -px_config = { - .. - 'custom_logo': 'https://s.perimeterx.net/logo.png' - .. -} -``` - -#### Custom JS/CSS - -The block page can be modified with a custom CSS by adding to the pxConfig the key ```css_ref``` and providing a valid URL to the css In addition there is also the option to add a custom JS file by adding ```js_ref``` key to the pxConfig and providing the JS file that will be loaded with the block page, this key also expects a valid URL +* `active_blocking` - Blocking Mode +* `monitor` - Monitoring Mode -On both cases if the URL is not a valid format an exception will be thrown -Example below: +**Default:** `monitor` - Monitor Mode -Example below: ```python -px_config = { - .. - 'js_ref': 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js' - 'css_ref': 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' - .. +config = { + ... + module_mode: 'active_blocking' + ... } ``` -#### Custom Blocking Actions -Defining a custom block handler is done by setting the value of `custom_block_handler` to a user-defined function, on the `px_config` variable. - -The custom block handler should contain the action to take when a visitng user is given a high score. Common customizations are to present a reCAPTHA or a custom branded Block Page. +#### Blocking Score +Sets the minimum blocking score of a request. -**default:** return HTTP status code 403 and serve the PerimeterX block page. +Possible values: -```python -def custom_block_handler(ctx, start_response): - start_response('403 Forbidden', [('Content-Type', 'text/html')]) - return ['You have been blocked'] +* Any integer between 0 and 100. +**Default:** 100 -px_config = { - .. - 'custom_block_handler': custom_block_handler, - .. +```python +config = { + ... + blocking_score: 100 + ... } +``` -application = get_wsgi_application() -application = PerimeterX(application, px_config) -``` - -###### Examples +#### Send Page Activities +A boolean flag to enable/disable sending activities and metrics to PerimeterX with each request.
+Enabling this feature allows data to populate the PerimeterX Portal with valuable information, such as the number of requests blocked and additional API usage statistics. -**Serve a Custom HTML Page** +**Default:** true ```python -def custom_block_handler(ctx, start_response): - block_score = ctx.get('risk_score') - block_uuid = ctx.get('uuid') - full_url = ctx.get('full_url') - - html = '
Access to ' + full_url + ' has been blocked.
' \ - '
Block reference - ' + uuid + '
' \ - '
Block score - ' + block_score + '
' - - start_response('403 Forbidden', [('Content-Type', 'text/html')]) - return [html] -}; - -px_config = { - .. - 'custom_block_handler': custom_block_handler, - .. +config = { + ... + send_page_activities: True + ... } - -application = get_wsgi_application() -application = PerimeterX(application, px_config) ``` -#### Module Mode -**default:** `active_monitoring` +#### Debug Mode +A boolean flag to enable/disable the debug log messages. -**Applicable Values:** - `['active_monitoring', 'active_blocking', 'inactive']` +**Default:** False ```python -px_config = { - .. - 'module_mode': 'active_blocking' - .. +config = { + ... + debug_mode: True + ... } ``` -#### Enable/Disable Server Calls +#### Sensitive Routes +An array of route prefixes that trigger a server call to PerimeterX servers every time the page is viewed, regardless of viewing history. -By disabling server calls, the module will only evaluate users by their cookie. Users without a cookie will not generate a request to the PerimeterX servers. - -**default:** `True` +**Default:** Empty ```python -px_config = { - .. - 'server_calls_enabled': False - .. +const config = { + ... + sensitive_routes: ['/login', '/user/checkout'] + ... } ``` -#### Enable/Disable CAPTCHA on the block page +#### Whitelist Routes +An array of route prefixes which will bypass enforcement (will never get scored). -By enabling CAPTCHA support, a CAPTCHA will be served as part of the block page, giving real users the ability to answer, get their score cleaned up and navigate to the requested page. - -**default: True** +**Default:** Empty ```python -px_config = { - .. - 'captcha_enabled': True - .. +config = { + ... + whitelist_routes: ['/about-us', '/careers'] + ... } ``` -#### Extracting the Real User IP Address - -> Note: IP extraction, according to your network setup, is important. It is common to have a load balancer/proxy on top of your applications, in this case the PerimeterX module will send an internal IP as the user's. In order to perform processing and detection for server-to-server calls, PerimeterX's module requires the real user's IP. - -The user's IP can be returned to the PerimeterX module, using a custom user defined function on the `px_config` variable. +#### Sensitive Headers +An array of headers that are not sent to PerimeterX servers on API calls. -**default value:** `environ.get('REMOTE_ADDR')` +**Default:** ['cookie', 'cookies'] ```python -def ip_handler(environ): - for key in environ.keys(): - if key == 'HTTP_X_FORWARDED_FOR': - xff = environ[key].split(' ')[1] - return xff - return '1.2.3.4' - -px_config = { - .. - 'ip_handler': ip_handler, - .. +config = { + ... + sensitive_headers: ['cookie', 'cookies', 'x-sensitive-header'] + ... } - - -application = get_wsgi_application() -application = PerimeterX(application, px_config) ``` -#### Filter sensitive headers - -A user can define a list of sensitive headers that will be excluded from any message sent to PerimeterX's servers (lowere case header names). Filtering the 'cookie' header is set by default (for privacy) and will be overridden if a user specifies otherwise in the configuration. +#### IP Headers +An array of trusted headers that specify an IP to be extracted. -**default value:** `['cookie', 'cookies']` +**Default:** Empty ```python -px_config = { - .. - 'sensitive_headers': ['cookie', 'cookies', 'secret-header'] - .. +config = { + ... + ip_headers: ['x-user-real-ip'] + ... } ``` -#### API Timeouts - -Controls the timeouts for PerimeterX requests. The API is called when the risk cookie does not exist, is expired or is invalid. - -API Timeout in seconds (float) to wait for the PerimeterX servers' API response. - +#### First Party Enabled +A boolean flag to enable/disable first party mode. -**default:** 1 +**Default:** True ```python -px_config = { - .. - 'api_timeout': 2 - .. +const pxConfig = { + ... + first_party_enabled: False + ... } ``` - -#### Send Page Activities - -A boolean flag to determine whether or not to send activities and metrics to -PerimeterX, on each page request. Disabling this feature will prevent PerimeterX from receiving data -populating the PerimeterX portal, containing valuable information such as -the amount of requests blocked and other API usage statistics. - -**default:** True +#### Custom Request Handler +A Python function that adds a custom response handler to the request. +Do not forget to declare the function before using it in the config. +Custom request handler is triggered after PerimeterX's verification. +The custom function should handle the response (probably create a new one) +**Default:** Empty ```python -px_config = { - .. - 'send_page_activities': False - .. +config = { + ... + custom_request_handler: custom_request_handler_function, + ... } ``` -#### Debug Mode - -Enables debug logging. +#### Additional Activity Handler +A Python function that allows interaction with the request data collected by PerimeterX before the data is returned to the PerimeterX servers. Does not alter the response. -**default:** false +**Default:** Empty ```python -px_config = { - .. - 'debug_mode': True - .. +config = { + ... + additional_activity_handler: additional_activity_handler_function, + ... } ``` - Contributing ----------------------------------------- diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index e98f89d..bc7171c 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -1,20 +1,19 @@ -import px_context import px_activities_client import px_cookie_validator -import px_httpc +from px_context import PxContext import px_blocker import px_api import px_constants import px_utils from perimeterx.px_proxy_handler import PXProxy -from px_config import PXConfig +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 - px_config = PXConfig(config) + px_config = PxConfig(config) logger = px_config.logger if not px_config.app_id: logger.error('PX App ID is missing') @@ -41,11 +40,12 @@ def _verify(self, environ, start_response): config = self.config logger = config.logger try: - ctx = px_context.build_context(environ, config) - uri = ctx.get('uri') + ctx = PxContext(environ, config) + uri = ctx.uri px_proxy = PXProxy(config) if px_proxy.should_reverse_request(uri): - body = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH'))) if environ.get('CONTENT_LENGTH') else '' + body = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH'))) if environ.get( + 'CONTENT_LENGTH') else '' return px_proxy.handle_reverse_request(self.config, ctx, start_response, body) if px_utils.is_static_file(ctx): logger.debug('Filter static file request. uri: ' + uri) @@ -54,7 +54,7 @@ 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'): + if ctx.whitelist_route: logger.debug('The requested uri is whitelisted, passing request') return self.app(environ, start_response) @@ -65,12 +65,12 @@ def _verify(self, environ, start_response): return self.app(environ, start_response) return self.handle_verification(ctx, self.config, environ, start_response) except: - logger.error("Cought exception, passing request") - self.pass_traffic({}) + logger.error("Caught exception, passing request") + self.pass_traffic(PxContext({}, config)) return self.app(environ, start_response) def handle_verification(self, ctx, config, environ, start_response): - score = ctx.get('score', -1) + score = ctx.score result = None headers = None status = None @@ -88,7 +88,7 @@ def handle_verification(self, ctx, config, environ, start_response): 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): + if custom_body is not None: start_response(custom_status, custom_headers) return custom_body @@ -99,7 +99,7 @@ def handle_verification(self, ctx, config, environ, start_response): return self.app(environ, start_response) def pass_traffic(self, ctx): - px_activities_client.send_page_requested_activity( ctx, self.config) + px_activities_client.send_page_requested_activity(ctx, self.config) def block_traffic(self, ctx): px_activities_client.send_block_activity(ctx, self.config) diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 4037464..95f09b4 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -9,6 +9,7 @@ ACTIVITIES_BUFFER = [] CONFIG = {} + def init_activities_configuration(config): global CONFIG CONFIG = config @@ -16,6 +17,7 @@ def init_activities_configuration(config): t1.daemon = True t1.start() + def send_activities(): global ACTIVITIES_BUFFER default_headers = { @@ -27,11 +29,11 @@ def send_activities(): if len(ACTIVITIES_BUFFER) > 0: chunk = ACTIVITIES_BUFFER[:10] ACTIVITIES_BUFFER = ACTIVITIES_BUFFER[10:] - px_httpc.send(full_url=full_url, body=json.dumps(chunk), headers=default_headers, config=CONFIG, method='POST') + px_httpc.send(full_url=full_url, body=json.dumps(chunk), headers=default_headers, config=CONFIG, + method='POST') time.sleep(1) - def send_to_perimeterx(activity_type, ctx, config, detail): try: if activity_type == 'page_requested' and not config.send_page_activities: @@ -39,8 +41,8 @@ def send_to_perimeterx(activity_type, ctx, config, detail): return _details = { - 'http_method': ctx.get('http_method', ''), - 'http_version': ctx.get('http_version', ''), + 'http_method': ctx.http_method, + 'http_version': ctx.http_version, 'module_version': config.module_version, 'risk_mode': config.module_mode, } @@ -50,14 +52,14 @@ def send_to_perimeterx(activity_type, ctx, config, detail): data = { 'type': activity_type, - 'headers': ctx.get('headers'), + 'headers': ctx.headers, 'timestamp': int(round(time.time() * 1000)), - 'socket_ip': ctx.get('ip'), + 'socket_ip': ctx.ip, 'px_app_id': config.app_id, - 'url': ctx.get('full_url'), + 'url': ctx.full_url, 'details': _details, - 'vid': ctx.get('vid', ''), - 'uuid': ctx.get('uuid', '') + 'vid': ctx.vid, + 'uuid': ctx.uuid } ACTIVITIES_BUFFER.append(data) except: @@ -67,25 +69,27 @@ def send_to_perimeterx(activity_type, ctx, config, detail): def send_block_activity(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_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', ''), + 'block_score': ctx.score, + 'client_uuid': ctx.uuid, + 'block_reason': ctx.block_reason, + 'http_method': ctx.http_method, + 'http_version': ctx.http_version, + 'px_cookie': ctx.decoded_cookie, + 'risk_rtt': ctx.risk_rtt, + 'cookie_origin': ctx.cookie_origin, + 'block_action': ctx.block_action, 'module_version': px_constants.MODULE_VERSION, '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']} + if ctx.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, @@ -107,6 +111,3 @@ def send_enforcer_telemetry_activity(config, update_reason): 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 4f6d16d..87cadac 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -17,15 +17,18 @@ 'custom_param10': '' } + def send_risk_request(ctx, config): body = prepare_risk_body(ctx, 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') + 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 logger.debug("PXVerify") @@ -36,24 +39,24 @@ def verify(ctx, config): logger.debug('Risk call took ' + str(risk_rtt) + 'ms') if response: - score = response['score'] - ctx['score'] = score - ctx['uuid'] = response['uuid'] - ctx['block_action'] = response['action'] - ctx['risk_rtt'] = risk_rtt - if score >= config.blocking_score: - if response['action'] == 'j' and response.get('action_data') is not None and response.get('action_data').get('body') is not None: + ctx.score = response.get('score') + ctx.uuid = response.get('uuid') + ctx.block_action = response.get('action') + ctx.risk_rtt = risk_rtt + if ctx.score >= config.blocking_score: + if response.get('action') == px_constants.ACTION_CHALLENGE 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': + ctx.block_action_data = response.get('action_data').get('body') + ctx.block_reason = 'challenge' + elif response.get('action') is px_constants.ACTION_RATELIMIT: logger.debug("PXVerify received javascript ratelimit action") - ctx['block_reason'] = 'exceeded_rate_limit' + ctx.block_reason = 'exceeded_rate_limit' else: logger.debug("PXVerify block score threshold reached, will initiate blocking") - ctx['block_reason'] = 's2s_high_score' + ctx.block_reason = 's2s_high_score' else: - ctx['pass_reason'] = 's2s' + ctx.pass_reason = 's2s' logger.debug("PxAPI[verify] S2S completed") return True @@ -69,26 +72,29 @@ def prepare_risk_body(ctx, config): logger.debug("PxAPI[send_risk_request]") body = { 'request': { - 'ip': ctx.get('ip'), - 'headers': format_headers(ctx.get('headers')), - 'uri': ctx.get('uri'), - 'url': ctx.get('full_url', ''), + 'ip': ctx.ip, + 'headers': format_headers(ctx.headers), + 'uri': ctx.uri, + 'url': ctx.full_url, 'firstParty': 'true' if config.first_party else 'false' }, - 'vid': ctx.get('vid', ''), - 'uuid': ctx.get('uuid', ''), 'additional': { - 's2s_call_reason': ctx.get('s2s_call_reason', ''), - 'http_method': ctx.get('http_method', ''), - 'http_version': ctx.get('http_version', ''), + 's2s_call_reason': ctx.s2s_call_reason, + 'http_method': ctx.http_method, + 'http_version': ctx.http_version, 'module_version': config.module_version, 'risk_mode': config.module_mode, - 'request_cookie_names': ctx.get('cookie_names', ''), - 'cookie_origin': ctx.get('cookie_origin') + 'cookie_origin': ctx.cookie_origin } } - if ctx.get('cookie_hmac'): - body['additional']['px_cookie_hmac'] = ctx.get('cookie_hmac') + if ctx.vid: + body['vid'] = ctx.vid + if ctx.uuid: + body['uuid'] = ctx.uuid + if ctx.cookie_hmac: + body['additional']['px_cookie_hmac'] = ctx.cookie_hmac + if ctx.cookie_names: + body['additional']['request_cookie_names'] = ctx.cookie_names body = add_original_token_data(ctx, body) @@ -96,32 +102,33 @@ 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]: + 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': + if ctx.s2s_call_reason == 'cookie_decryption_failed': logger.debug('attaching orig_cookie to request') - body['additional']['px_orig_cookie'] = ctx.get('px_orig_cookie') + body['additional']['px_orig_cookie'] = ctx.px_orig_cookie - if ctx['s2s_call_reason'] in ['cookie_expired', 'cookie_validation_failed']: + if ctx.s2s_call_reason in ['cookie_expired', 'cookie_validation_failed']: logger.debug('attaching px_cookie to request') - body['additional']['px_cookie'] = ctx.get('decoded_cookie') + body['additional']['px_cookie'] = ctx.decoded_cookie logger.debug("PxAPI[send_risk_request] request body: " + str(body)) return body + def add_original_token_data(ctx, body): - if ctx.get('original_uuid'): - body['additional']['original_uuid'] = ctx.get('original_uuid') - if ctx.get('original_token_error'): - body['additional']['original_token_error'] = ctx.get('original_token_error') - if ctx.get('original_token'): - body['additional']['original_token'] = ctx.get('original_token') - if ctx.get('decoded_original_token'): - body['additional']['decoded_original_token'] = ctx.get('decoded_original_token') + if ctx.original_uuid: + body['additional']['original_uuid'] = ctx.original_uuid + if ctx.original_token_error: + body['additional']['original_token_error'] = ctx.original_token_error + if ctx.original_token: + body['additional']['original_token'] = ctx.original_token + if ctx.decoded_original_token: + body['additional']['decoded_original_token'] = ctx.decoded_original_token return body + def format_headers(headers): ret_val = [] for key in headers.keys(): diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py index c0759df..315b78a 100644 --- a/perimeterx/px_blocker.py +++ b/perimeterx/px_blocker.py @@ -11,7 +11,7 @@ def __init__(self): px_template.get_template(px_constants.RATELIMIT_TEMPLATE), {}) def handle_blocking(self, ctx, config): - action = ctx.get('block_action') + action = ctx.block_action status = '403 Forbidden' is_json_response = self.is_json_response(ctx) @@ -21,10 +21,10 @@ def handle_blocking(self, ctx, config): content_type = 'text/html' headers = [('Content-Type', content_type)] - if action is 'j': - blocking_props = ctx['block_action_data'] + if action is px_constants.ACTION_CHALLENGE: + blocking_props = ctx.block_action_data blocking_response = blocking_props - elif action is 'r': + elif action is px_constants.ACTION_RATELIMIT: blocking_response = self.ratelimit_rendered_page status = '429 Too Many Requests' else: @@ -37,13 +37,13 @@ def handle_blocking(self, ctx, config): def prepare_properties(self, ctx, config): app_id = config.app_id - vid = ctx.get('vid') if ctx.get('vid') is not None else '' - uuid = ctx.get('uuid') + vid = ctx.vid + uuid = ctx.uuid 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) + is_mobile_num = 1 if ctx.is_mobile else 0 + captcha_uri = 'captcha.js?a={}&u={}&v={}&m={}'.format(ctx.block_action, uuid, vid, is_mobile_num) - if config.first_party and not ctx.get('is_mobile'): + if config.first_party and not ctx.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) @@ -69,10 +69,10 @@ def prepare_properties(self, ctx, config): } def is_json_response(self, ctx): - headers = ctx.get('headers') - if ctx.get('block_action') is not 'r': + headers = ctx.headers + if ctx.block_action is not px_constants.ACTION_RATELIMIT: for item in headers.keys(): - if (item.lower() == 'accept' or item.lower() == 'content-type'): + if item.lower() == 'accept' or item.lower() == 'content-type': item_arr = headers[item].split(',') for header_item in item_arr: if header_item.strip() == 'application/json': diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index 247859e..41d1263 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -1,9 +1,8 @@ import px_constants -import json from px_logger import Logger -class PXConfig(object): +class PxConfig(object): def __init__(self, config_dict): app_id = config_dict.get('app_id') debug_mode = config_dict.get('debug_mode', False) diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 1d99271..5fcc489 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -10,10 +10,6 @@ BLOCK_TEMPLATE = 'block_template.mustache' RATELIMIT_TEMPLATE = 'ratelimit.mustache' -CAPTCHA_ACTION_CAPTCHA = 'c' -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' @@ -36,3 +32,7 @@ API_ENFORCER_TELEMETRY = '/api/v2/risk/telemetry' API_ACTIVITIES = '/api/v1/collector/s2s' TELEMETRY_ACTIVITY = 'enforcer_telemetry' +ACTION_CHALLENGE = 'j' +ACTION_BLOCK = 'b' +ACTION_RATELIMIT = 'r' +ACTION_CAPTCHA = 'c' diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index db9aa84..d37e818 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -1,115 +1,375 @@ import Cookie from px_constants import * -def build_context(environ, config): - logger = config.logger - headers = {} - - # Default values - http_method = 'GET' - http_version = '1.1' - http_protocol = 'http://' - px_cookies = {} - request_cookie_names = list() - cookie_origin = "cookie" - original_token = None - - # Extracting: Headers, user agent, http method, http version - for key in environ.keys(): - if key.startswith('HTTP_') and environ.get(key): - header_name = key.split('HTTP_')[1].replace('_', '-').lower() - if header_name not in config.sensitive_headers: - headers[header_name] = environ.get(key) - if key == 'REQUEST_METHOD': - http_method = environ.get(key) - if key == 'SERVER_PROTOCOL': - protocol_split = environ.get(key, '').split('/') - if protocol_split[0].startswith('HTTP'): - 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[key.replace('_', '-').lower()] = environ.get(key) - if key == 'HTTP_' + MOBILE_SDK_HEADER.replace('-','_').upper(): - headers[MOBILE_SDK_HEADER] = environ.get(key, '') - - mobile_header = headers.get(MOBILE_SDK_HEADER) - vid = None - original_token = None - if mobile_header is None: - cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE', '')) - cookie_keys = cookies.keys() - - for key in cookie_keys: - request_cookie_names.append(key) - if key == PREFIX_PX_COOKIE_V1 or key == PREFIX_PX_COOKIE_V3: - logger.debug('Found cookie prefix:' + key) - px_cookies[key] = cookies.get(key).value - vid = None - if '_pxvid' in cookie_keys: - vid = cookies.get('_pxvid').value + +class PxContext(object): + + def __init__(self, environ, config): + + logger = config.logger + headers = {} + + # Default values + http_method = '' + http_version = '' + http_protocol = '' + px_cookies = {} + request_cookie_names = [] + cookie_origin = "cookie" + vid = '' + + # 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.sensitive_headers: + headers[header_name] = environ.get(key) + if key == 'REQUEST_METHOD': + http_method = environ.get(key) + if key == 'SERVER_PROTOCOL': + protocol_split = environ.get(key, '').split('/') + if protocol_split[0].startswith('HTTP'): + 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[key.replace('_', '-').lower()] = environ.get(key) + if key == 'HTTP_' + MOBILE_SDK_HEADER.replace('-', '_').upper(): + headers[MOBILE_SDK_HEADER] = environ.get(key, '') + + mobile_header = headers.get(MOBILE_SDK_HEADER) + original_token = '' + if mobile_header is None: + cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE', '')) + cookie_keys = cookies.keys() + + for key in cookie_keys: + request_cookie_names.append(key) + if key == PREFIX_PX_COOKIE_V1 or key == PREFIX_PX_COOKIE_V3: + logger.debug('Found cookie prefix:' + key) + px_cookies[key] = cookies.get(key).value + if '_pxvid' in cookie_keys: + vid = cookies.get('_pxvid').value else: - vid = '' - else: - cookie_origin = "header" - original_token = headers.get(MOBILE_SDK_ORIGINAL_HEADER) - logger.debug('Mobile SDK token detected') - cookie_name, cookie = get_token_object(config, mobile_header) - px_cookies[cookie_name] = cookie - - user_agent = headers.get('user-agent','') - uri = environ.get('PATH_INFO') or '' - full_url = http_protocol + (headers.get('host') or environ.get('SERVER_NAME') or '') + uri - hostname = headers.get('host') - 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, - 'http_version': http_version, - 'user_agent': user_agent, - 'full_url': full_url, - 'uri': uri, - 'hostname': hostname, - 'px_cookies': px_cookies, - 'cookie_names': request_cookie_names, - 'risk_rtt': 0, - 'ip': extract_ip(config, environ), - 'vid': vid if vid else None, - 'query_params': environ['QUERY_STRING'], - 'sensitive_route': sensitive_route, - 'whitelist_route': whitelist_route, - 's2s_call_reason': 'none', - 'cookie_origin':cookie_origin, - 'original_token': original_token, - 'is_mobile': cookie_origin == "header" - } - - return ctx - -def get_token_object(config, token): - result = {} - logger = config.logger - sliced_token = token.split(":", 1) - if len(sliced_token) > 1: - key = sliced_token.pop(0) - if key == PREFIX_PX_TOKEN_V1 or key == PREFIX_PX_TOKEN_V3: - logger.debug('Found token prefix:' + key) - return key, sliced_token[0] - return PREFIX_PX_TOKEN_V3, token - -def extract_ip(config, environ): - ip = environ.get('HTTP_X_FORWARDED_FOR') if environ.get('HTTP_X_FORWARDED_FOR') else environ.get('REMOTE_ADDR') - ip_headers = config.ip_headers - logger = config.logger - if ip_headers: - try: - for ip_header in ip_headers: - ip_header_name = 'HTTP_' + ip_header.replace('-', '_').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 + cookie_origin = "header" + original_token = headers.get(MOBILE_SDK_ORIGINAL_HEADER) + logger.debug('Mobile SDK token detected') + cookie_name, cookie = self.get_token_object(config, mobile_header) + px_cookies[cookie_name] = cookie + + user_agent = headers.get('user-agent', '') + uri = environ.get('PATH_INFO') or '' + full_url = http_protocol + (headers.get('host') or environ.get('SERVER_NAME') or '') + uri + hostname = headers.get('host') + 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 + query_params = environ.get('QUERY_STRING') if environ.get('QUERY_STRING') else '' + self._headers = headers + self._http_method = http_method + self._http_version = http_version + self._user_agent = user_agent + self._full_url = full_url + self._uri = uri + self._hostname = hostname + self._px_cookies = px_cookies + self._cookie_names = request_cookie_names + self._risk_rtt = 0 + self._ip = self.extract_ip(config, environ) + self._vid = vid + self._uuid = '' + self._query_params = query_params + self._sensitive_route = sensitive_route + self._whitelist_route = whitelist_route + self._s2s_call_reason = 'none' + self._cookie_origin = cookie_origin + self._is_mobile = cookie_origin == "header" + self._score = -1 + self._block_reason = '' + self._decoded_cookie = '' + self._block_action = '' + self._block_action_data = '' + self._pass_reason = '' + self._cookie_hmac = '' + self._px_orig_cookie = '' + self._original_token_error = '' + self._original_uuid = '' + self._decoded_original_token = '' + self._original_token = original_token + + + def get_token_object(self, config, token): + result = {} + logger = config.logger + sliced_token = token.split(":", 1) + if len(sliced_token) > 1: + key = sliced_token.pop(0) + if key == PREFIX_PX_TOKEN_V1 or key == PREFIX_PX_TOKEN_V3: + logger.debug('Found token prefix:' + key) + return key, sliced_token[0] + return PREFIX_PX_TOKEN_V3, token + + def extract_ip(self, config, environ): + 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 ip_headers: + try: + for ip_header in ip_headers: + ip_header_name = 'HTTP_' + ip_header.replace('-', '_').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 + + @property + def headers(self): + return self._headers + + @headers.setter + def headers(self, headers): + self._headers = headers + + @property + def http_method(self): + return self._http_method + + @http_method.setter + def http_method(self, http_method): + self._http_method = http_method + + @property + def http_version(self): + return self._http_version + + @http_version.setter + def http_version(self, http_version): + self._http_version = http_version + + @property + def user_agent(self): + return self._user_agent + + @user_agent.setter + def user_agent(self, user_agent): + self._user_agent = user_agent + + @property + def full_url(self): + return self._full_url + + @full_url.setter + def full_url(self, full_url): + self._full_url = full_url + + @property + def uri(self): + return self._uri + + @uri.setter + def uri(self, uri): + self._uri = uri + + @property + def hostname(self): + return self._hostname + + @hostname.setter + def hostname(self, hostname): + self._hostname = hostname + + @property + def px_cookies(self): + return self._px_cookies + + @px_cookies.setter + def px_cookies(self, px_cookies): + self._px_cookies = px_cookies + + @property + def cookie_names(self): + return self._cookie_names + + @cookie_names.setter + def cookie_names(self, cookie_names): + self._cookie_names = cookie_names + + @property + def risk_rtt(self): + return self._risk_rtt + + @risk_rtt.setter + def risk_rtt(self, risk_rtt): + self._risk_rtt = risk_rtt + + @property + def ip(self): + return self._ip + + @ip.setter + def ip(self, ip): + self._ip = ip + + @property + def vid(self): + return self._vid + + @vid.setter + def vid(self, vid): + self._vid = vid + + @property + def query_params(self): + return self._query_params + + @query_params.setter + def query_params(self, query_params): + self._query_params = query_params + + @property + def sensitive_route(self): + return self._sensitive_route + + @sensitive_route.setter + def sensitive_route(self, sensitive_route): + self._sensitive_route = sensitive_route + + @property + def whitelist_route(self): + return self._whitelist_route + + @whitelist_route.setter + def whitelist_route(self, whitelist_route): + self._whitelist_route = whitelist_route + + @property + def s2s_call_reason(self): + return self._s2s_call_reason + + @s2s_call_reason.setter + def s2s_call_reason(self, s2s_call_reason): + self._s2s_call_reason = s2s_call_reason + + @property + def cookie_origin(self): + return self._cookie_origin + + @cookie_origin.setter + def cookie_origin(self, cookie_origin): + self._cookie_origin = cookie_origin + + @property + def original_token(self): + return self._original_token + + @original_token.setter + def original_token(self, original_token): + self._original_token = original_token + + @property + def is_mobile(self): + return self._is_mobile + + @is_mobile.setter + def is_mobile(self, is_mobile): + self._is_mobile = is_mobile + + @property + def score(self): + return self._score + + @score.setter + def score(self, score): + self._score = score + + @property + def uuid(self): + return self._uuid + + @uuid.setter + def uuid(self, uuid): + self._uuid = uuid + + @property + def block_reason(self): + return self._block_reason + + @block_reason.setter + def block_reason(self, block_reason): + self._block_reason = block_reason + + @property + def decoded_cookie(self): + return self._decoded_cookie + + @decoded_cookie.setter + def decoded_cookie(self, decoded_cookie): + self._decoded_cookie = decoded_cookie + + @property + def block_action(self): + return self._block_action + + @block_action.setter + def block_action(self, block_action): + self._block_action = block_action + + @property + def block_action_data(self): + return self._block_action_data + + @block_action_data.setter + def block_action_data(self, block_action_data): + self._block_action_data = block_action_data + + @property + def pass_reason(self): + return self._pass_reason + + @pass_reason.setter + def pass_reason(self, pass_reason): + self._pass_reason = pass_reason + + @property + def cookie_hmac(self): + return self._cookie_hmac + + @cookie_hmac.setter + def cookie_hmac(self, cookie_hmac): + self._cookie_hmac = cookie_hmac + + @property + def px_orig_cookie(self): + return self._px_orig_cookie + + @px_orig_cookie.setter + def px_orig_cookie(self, px_orig_cookie): + self._px_orig_cookie = px_orig_cookie + + @property + def original_token_error(self): + return self._original_token_error + + @original_token_error.setter + def original_token_error(self, original_token_error): + self._original_token_error = original_token_error + + @property + def original_uuid(self): + return self._original_uuid + + @original_uuid.setter + def original_uuid(self, original_uuid): + self._original_uuid = original_uuid + + @property + def decoded_original_token(self): + return self._decoded_original_token + + @decoded_original_token.setter + def decoded_original_token(self, decoded_original_token): + self._decoded_original_token = decoded_original_token diff --git a/perimeterx/px_cookie.py b/perimeterx/px_cookie.py index a638548..46738a6 100644 --- a/perimeterx/px_cookie.py +++ b/perimeterx/px_cookie.py @@ -18,7 +18,7 @@ def __init__(self, config): self._logger = config.logger - def build_px_cookie(self, px_cookies, user_agent): + def build_px_cookie(self, px_cookies, user_agent=''): self._logger.debug("PxCookie[build_px_cookie]") # Check that its not empty if not px_cookies: diff --git a/perimeterx/px_cookie_validator.py b/perimeterx/px_cookie_validator.py index 3ff96bc..d62ada8 100644 --- a/perimeterx/px_cookie_validator.py +++ b/perimeterx/px_cookie_validator.py @@ -16,9 +16,9 @@ def verify(ctx, config): """ logger = config.logger try: - if not ctx["px_cookies"].keys(): + if not ctx.px_cookies.keys(): logger.debug('No risk cookie on the request') - ctx['s2s_call_reason'] = 'no_cookie' + ctx.s2s_call_reason = 'no_cookie' return False if not config.cookie_key: @@ -27,55 +27,55 @@ def verify(ctx, config): return False px_cookie_builder = PxCookie(config) - px_cookie = px_cookie_builder.build_px_cookie(px_cookies=ctx.get('px_cookies'), - user_agent=ctx.get('user_agent')) + px_cookie = px_cookie_builder.build_px_cookie(px_cookies=ctx.px_cookies, + user_agent=ctx.user_agent) #Mobile SDK traffic - if px_cookie and ctx['is_mobile']: + if px_cookie and ctx.is_mobile: pattern = re.compile("^\d+$") if re.match(pattern, px_cookie.raw_cookie): - ctx['s2s_call_reason'] = "mobile_error_" + px_cookie.raw_cookie - if ctx['original_token'] is not None: + ctx.s2s_call_reason = "mobile_error_" + px_cookie.raw_cookie + if ctx.original_token is not None: px_original_token_validator.verify(ctx, config) return False if not px_cookie.deserialize(): logger.error('Cookie decryption failed') - ctx['px_orig_cookie'] = px_cookie.raw_cookie - ctx['s2s_call_reason'] = 'cookie_decryption_failed' + ctx.px_orig_cookie = px_cookie.raw_cookie + ctx.s2s_call_reason = 'cookie_decryption_failed' return False - ctx['score'] = px_cookie.get_score() - ctx['uuid'] = px_cookie.get_uuid() - ctx['vid'] = px_cookie.get_vid() - ctx['decoded_cookie'] = px_cookie.decoded_cookie - ctx['cookie_hmac'] = px_cookie.get_hmac() - ctx['block_action'] = px_cookie.get_action() + ctx.score = px_cookie.get_score() + ctx.uuid = px_cookie.get_uuid() + ctx.vid = px_cookie.get_vid() + ctx.decoded_cookie = px_cookie.decoded_cookie + ctx.cookie_hmac = px_cookie.get_hmac() + ctx.block_action = px_cookie.get_action() if px_cookie.is_high_score(): - ctx['block_reason'] = 'cookie_high_score' - logger.debug('Cookie with high score: ' + str(ctx['score'])) + ctx.block_reason = 'cookie_high_score' + logger.debug('Cookie with high score: ' + str(ctx.score)) return True if px_cookie.is_cookie_expired(): - ctx['s2s_call_reason'] = 'cookie_expired' + ctx.s2s_call_reason = 'cookie_expired' logger.debug('Cookie expired') return False if not px_cookie.is_secured(): logger.debug('Cookie validation failed') - ctx['s2s_call_reason'] = 'cookie_validation_failed' + 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' + if ctx.sensitive_route: + logger.debug('Sensitive route match, sending Risk API. path: {}'.format(ctx.uri)) + ctx.s2s_call_reason = 'sensitive_route' return False - logger.debug('Cookie validation passed with good score: ' + str(ctx['score'])) + logger.debug('Cookie validation passed with good score: ' + str(ctx.score)) return True except Exception, e: traceback.print_exc() logger.debug('Could not decrypt cookie, exception was thrown, decryption failed ' + e.message) - ctx['px_orig_cookie'] = px_cookie.raw_cookie - ctx['s2s_call_reason'] = 'cookie_decryption_failed' + ctx.px_orig_cookie = px_cookie.raw_cookie + ctx.s2s_call_reason = 'cookie_decryption_failed' return False diff --git a/perimeterx/px_original_token_validator.py b/perimeterx/px_original_token_validator.py index 9762483..c49e8d0 100644 --- a/perimeterx/px_original_token_validator.py +++ b/perimeterx/px_original_token_validator.py @@ -14,26 +14,26 @@ def verify(ctx, config): logger = config.logger try: logger.debug('Original token found, Evaluating') - original_token = ctx.get('original_token') + original_token = ctx.original_token version, no_version_token = original_token.split(':', 1) px_cookie_builder = PxCookie(config) - px_cookie = px_cookie_builder.build_px_cookie({version: no_version_token}, ctx.get('is_mobile'),'') + px_cookie = px_cookie_builder.build_px_cookie({version: no_version_token},'') if not px_cookie.deserialize(): logger.error('Original token decryption failed, value:' + px_cookie.raw_cookie) - ctx['original_token_error'] = 'decryption_failed' + ctx.original_token_error = 'decryption_failed' return False - ctx['decoded_original_token'] = px_cookie.decoded_cookie - ctx['vid'] = px_cookie.get_vid() - ctx['original_uuid'] = px_cookie.get_uuid() + ctx.decoded_original_token = px_cookie.decoded_cookie + ctx.vid = px_cookie.get_vid() + ctx.original_uuid = px_cookie.get_uuid() if not px_cookie.is_secured(): logger.debug('Original token HMAC validation failed, value: ' + str(px_cookie.decoded_cookie)) - ctx['original_token_error'] = 'validation_failed' + ctx.original_token_error = 'validation_failed' return False return True except Exception, e: logger.debug('Could not decrypt original token, exception was thrown, decryption failed ' + e.message) - ctx['original_token_error'] = 'decryption_failed' + ctx.original_token_error = 'decryption_failed' return False diff --git a/perimeterx/px_proxy_handler.py b/perimeterx/px_proxy_handler.py index 405bc46..cfdb968 100644 --- a/perimeterx/px_proxy_handler.py +++ b/perimeterx/px_proxy_handler.py @@ -34,16 +34,16 @@ def should_reverse_request(self, uri): return False def handle_reverse_request(self, config, ctx, start_response, body): - uri = ctx.get('uri').lower() + uri = ctx.uri.lower() if uri.startswith(self.client_reverse_prefix): - return self.send_reverse_client_request(config=config, context=ctx, start_response=start_response) + return self.send_reverse_client_request(config=config, ctx=ctx, start_response=start_response) if uri.startswith(self.xhr_reverse_prefix): - return self.send_reverse_xhr_request(config=config, context=ctx, start_response=start_response, body=body) + return self.send_reverse_xhr_request(config=config, ctx=ctx, start_response=start_response, body=body) if uri.startswith(self.captcha_reverse_prefix): - return self.send_reverse_captcha_request(config=config, context=ctx, start_response=start_response) + return self.send_reverse_captcha_request(config=config, ctx=ctx, start_response=start_response) - def send_reverse_client_request(self, config, context, start_response): + def send_reverse_client_request(self, config, ctx, start_response): if not config.first_party: headers = [('Content-Type', 'application/javascript')] start_response("200 OK", headers) @@ -51,13 +51,13 @@ def send_reverse_client_request(self, config, context, start_response): 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, + 'Forwarding request from {} to client at {}{}'.format(ctx.uri.lower(), px_constants.CLIENT_HOST, client_request_uri)) headers = {'host': px_constants.CLIENT_HOST, px_constants.FIRST_PARTY_HEADER: '1', - px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} - filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), context.get('ip')) + px_constants.ENFORCER_TRUE_IP_HEADER: ctx.ip} + filtered_headers = px_utils.handle_proxy_headers(ctx.headers, ctx.ip) filtered_headers = px_utils.merge_two_dicts(filtered_headers, headers) delete_extra_headers(filtered_headers) response = px_httpc.send(full_url=px_constants.CLIENT_HOST + client_request_uri, body='', @@ -66,8 +66,8 @@ def send_reverse_client_request(self, config, context, start_response): self.handle_proxy_response(response, start_response) return response.raw.read() - def send_reverse_xhr_request(self, config, context, start_response, body): - uri = context.get('uri') + def send_reverse_xhr_request(self, config, ctx, start_response, body): + uri = ctx.uri if not config.first_party or not config.first_party_xhr_enabled: body, content_type = self.return_default_response(uri) @@ -80,17 +80,17 @@ def send_reverse_xhr_request(self, config, context, start_response, body): host = config.collector_host headers = {'host': host, px_constants.FIRST_PARTY_HEADER: '1', - px_constants.ENFORCER_TRUE_IP_HEADER: context.get('ip')} + px_constants.ENFORCER_TRUE_IP_HEADER: ctx.ip} - if context.get('vid') is not None: - headers['Cookies'] = '_pxvid=' + context.get('vid') + if ctx.vid is not None: + headers['Cookies'] = '_pxvid=' + ctx.vid - filtered_headers = px_utils.handle_proxy_headers(context.get('headers'), context.get('ip')) + filtered_headers = px_utils.handle_proxy_headers(ctx.headers, ctx.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)) + 'Forwarding request from {} to client at {}{}'.format(ctx.uri.lower(), host, suffix_uri)) response = px_httpc.send(full_url=host + suffix_uri, body=body, - headers=filtered_headers, config=config, method=context.get('http_method')) + headers=filtered_headers, config=config, method=ctx.http_method) if response.status_code >= 400: body, content_type = self.return_default_response(uri) @@ -116,23 +116,23 @@ def return_default_response(self, uri): body = {} return body, content_type - def send_reverse_captcha_request(self, config, context, start_response): + def send_reverse_captcha_request(self, config, ctx, start_response): 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, ctx.uri.lower().replace(self.captcha_reverse_prefix, ''), + ctx.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.handle_proxy_headers(context.get('headers'), context.get('ip')) + px_constants.ENFORCER_TRUE_IP_HEADER: ctx.ip} + filtered_headers = px_utils.handle_proxy_headers(ctx.headers, ctx.ip) filtered_headers = px_utils.merge_two_dicts(filtered_headers, headers) delete_extra_headers(filtered_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(ctx.uri.lower(), host, uri)) response = px_httpc.send(full_url=host + uri, body='', headers=filtered_headers, config=config, method='GET') self.handle_proxy_response(response, start_response) diff --git a/perimeterx/px_utils.py b/perimeterx/px_utils.py index 3fc9b73..dd14177 100644 --- a/perimeterx/px_utils.py +++ b/perimeterx/px_utils.py @@ -17,7 +17,7 @@ def handle_proxy_headers(filtered_headers, ip): def is_static_file(ctx): - uri = ctx.get('uri', '') + uri = ctx.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', diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..23ee4f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +pystache==0.5.4 +requests==2.20.1 +setuptools==40.6.2 +requests_mock==1.5.2 +pycrypto==2.6.1 +webapp2==2.5.2 diff --git a/tests/px_api.py b/tests/px_api.py index d7bc19f..d7d53e7 100644 --- a/tests/px_api.py +++ b/tests/px_api.py @@ -1,6 +1,7 @@ import unittest from perimeterx import px_api -from perimeterx.px_config import PXConfig +from perimeterx.px_config import PxConfig +from perimeterx.px_context import PxContext class Test_PXApi(unittest.TestCase): @@ -11,8 +12,9 @@ def enrich_custom_parameters(self, params): 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'} + config = PxConfig({'app_id': 'app_id', 'enrich_custom_parameters': self.enrich_custom_parameters}) + ctx = PxContext({},config) + ctx.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') diff --git a/tests/px_blocker.py b/tests/px_blocker.py index 9e9b9ee..0f3696b 100644 --- a/tests/px_blocker.py +++ b/tests/px_blocker.py @@ -2,30 +2,39 @@ import unittest -from perimeterx.px_config import PXConfig +from perimeterx.px_config import PxConfig +from perimeterx.px_context import PxContext class Test_PXBlocker(unittest.TestCase): def test_is_json_response(self): px_blocker = PXBlocker() - ctx = { - 'headers': {'Accept': 'text/html'} - } + config = PxConfig({'app_id': 'PXfake_app_id'}) + ctx = PxContext({'PATH_INFO': '/fake_app_id/init.js', + 'HTTP_X_FORWARDED_FOR': '127.0.0.1', + 'ip': '127.0.0.1', + 'HTTP_ACCEPT': 'text/html'}, + config) + self.assertFalse(px_blocker.is_json_response(ctx)) - ctx['headers']['Accept'] = 'application/json' + 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'}) + + config = PxConfig({'app_id': 'PXfake_app_id'}) + ctx = PxContext({'PATH_INFO': '/fake_app_id/init.js', + 'HTTP_X_FORWARDED_FOR': '127.0.0.1', + 'ip': '127.0.0.1', + 'HTTP_ACCEPT': 'text/html'}, + config) + ctx.vid = vid + ctx.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() @@ -35,14 +44,16 @@ 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) + config = PxConfig({'app_id': 'PXfake_app_id'}) + ctx = PxContext({'PATH_INFO': '/fake_app_id/init.js', + 'HTTP_X_FORWARDED_FOR': '127.0.0.1', + 'ip': '127.0.0.1', + 'HTTP_ACCEPT': 'text/html'}, + config) + ctx.vid = vid + ctx.uuid = px_uuid + ctx.block_action = 'r' + message, _, _ = px_blocker.handle_blocking(ctx, config) blocking_message = None with open('./px_blocking_messages/ratelimit.txt', 'r') as myfile: blocking_message = myfile.read() @@ -52,15 +63,18 @@ 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) + config = PxConfig({'app_id': 'PXfake_app_id'}) + ctx = PxContext({'PATH_INFO': '/fake_app_id/init.js', + 'HTTP_X_FORWARDED_FOR': '127.0.0.1', + 'ip': '127.0.0.1', + 'HTTP_ACCEPT': 'text/html'}, + config) + ctx.vid = vid + ctx.uuid = px_uuid + ctx.block_action = 'j' + ctx.block_action_data = 'Bla' + + message, _, _ = px_blocker.handle_blocking(ctx, config) blocking_message = 'Bla' self.assertEqual(message, blocking_message) @@ -68,27 +82,29 @@ 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', + config = PxConfig({'app_id': 'PXfake_app_id'}) + ctx = PxContext({'PATH_INFO': '/fake_app_id/xhr', + 'HTTP_X_FORWARDED_FOR': '127.0.0.1', + 'ip': '127.0.0.1', + 'HTTP_ACCEPT': 'text/html'}, + config) + ctx.vid = vid + ctx.uuid = px_uuid + message = px_blocker.prepare_properties(ctx, config) + expected_message = {'blockScript': '/fake_app_id/captcha/captcha.js?a=&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', + 'hostUrl': '/fake_app_id/xhr', 'customLogo': None, - 'appId': 'PXfake_app_ip', + 'appId': 'PXfake_app_id', 'uuid': '8712cef7-bcfa-4bb6-ae99-868025e1908a', 'logoVisibility': 'hidden', - 'jsClientSrc': '/fake_app_ip/init.js', + 'jsClientSrc': '/fake_app_id/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' + expected_message['blockScript'] = '/fake_app/captcha/captcha.js?a=&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 index b42ba1b..21071c1 100644 --- a/tests/px_blocking_messages/blocking.txt +++ b/tests/px_blocking_messages/blocking.txt @@ -150,14 +150,14 @@