diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2766e7..b26d0d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log
+## [v3.1.0](https://github.com/PerimeterX/perimeterx-python-wsgi) (2019-02-26)
+- Refactor of px_logger to use native python logger
+- Added support for bypass monitor mode header
+
## [v3.0.2](https://github.com/PerimeterX/perimeterx-python-wsgi) (2019-02-13)
- page requested pass_reason alignment
- better error handling for http errors
diff --git a/README.md b/README.md
index ebbd3cb..c23e22b 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,9 @@
[PerimeterX](http://www.perimeterx.com) Python Middleware
=============================================================
-> Latest stable version: [v3.0.2](https://pypi.org/project/perimeterx-python-wsgi/)
+> Latest stable version: [v3.1.0](https://pypi.org/project/perimeterx-python-wsgi/)
-> Latest GAE stable version: [v3.0.2](https://pypi.org/project/perimeterx-python-wsgi-gae/)
+> Latest GAE stable version: [v3.1.0](https://pypi.org/project/perimeterx-python-wsgi-gae/)
Table of Contents
-----------------
@@ -28,6 +28,7 @@ Table of Contents
* [Custom Request Handler](#custom_request_handler)
* [Additional Activity Handler](#additional_activity_handler)
* [Px Disable Request](#px_disable_request)
+ * [Test Block Flow on Monitoring Mode](#bypass_monitor_header)
## Installation
@@ -254,3 +255,21 @@ environ['px_disable_request'] = True #The enforcer shall be disabled for that re
```
+#### Test Block Flow on Monitoring Mode
+
+Allows you to test an enforcer’s blocking flow while you are still in Monitor Mode.
+
+When the header name is set(eg. `x-px-block`) and the value is set to `1`, when there is a block response (for example from using a User-Agent header with the value of `PhantomJS/1.0`) the Monitor Mode is bypassed and full block mode is applied. If one of the conditions is missing you will stay in Monitor Mode. This is done per request.
+To stay in Monitor Mode, set the header value to `0`.
+
+The Header Name is configurable using the `bypass_monitor_header` property.
+
+**Default:** Empty
+
+```python
+config = {
+ ...
+ bypass_monitor_header: 'x-px-block',
+ ...
+}
+```
diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py
index 745eaf9..122cf69 100644
--- a/perimeterx/px_blocker.py
+++ b/perimeterx/px_blocker.py
@@ -35,18 +35,18 @@ def handle_blocking(self, ctx, config):
headers = {'Content-Type': content_type}
if action is px_constants.ACTION_CHALLENGE:
- logger.debug('Challenge page is served')
+ logger.debug('Enforcing action: Challenge page is served')
blocking_props = ctx.block_action_data
blocking_response = blocking_props
elif action is px_constants.ACTION_RATELIMIT:
- logger.debug('Rate limit page is served')
+ logger.debug('Enforcing action: Rate limit page is served')
blocking_props = None
blocking_response = self.ratelimit_rendered_page
status = '429 Too Many Requests'
else: # block
- logger.debug('Block page is served')
+ logger.debug('Enforcing action: Block page is served')
blocking_props = self.prepare_properties(ctx, config)
blocking_response = self.mustache_renderer.render(px_template.get_template(px_constants.BLOCK_TEMPLATE),
blocking_props)
@@ -63,6 +63,7 @@ def handle_blocking(self, ctx, config):
return page_response, headers, status
if is_json_response:
+ logger.debug('Serving advanced blocking response')
blocking_response = json.dumps(blocking_props)
blocking_response = str(blocking_response)
diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py
index e430945..caa90ff 100644
--- a/perimeterx/px_config.py
+++ b/perimeterx/px_config.py
@@ -37,6 +37,7 @@ def __init__(self, config_dict):
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._bypass_monitor_header = config_dict.get('bypass_monitor_header','')
sensitive_routes = config_dict.get('sensitive_routes', [])
if not isinstance(sensitive_routes, list):
@@ -191,6 +192,10 @@ def enrich_custom_parameters(self):
def testing_mode(self):
return self._testing_mode
+ @property
+ def bypass_monitor_header(self):
+ return self._bypass_monitor_header
+
def __instantiate_user_defined_handlers(self, config_dict):
self._custom_request_handler = self.__set_handler('custom_request_handler', config_dict)
self._get_user_ip = self.__set_handler('get_user_ip', config_dict)
diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py
index 97a6552..4505e17 100644
--- a/perimeterx/px_constants.py
+++ b/perimeterx/px_constants.py
@@ -30,7 +30,7 @@
EMPTY_GIF_B64 = 'R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
COLLECTOR_HOST = 'collector.perimeterx.net'
FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR'
-MODULE_VERSION = 'Python WSGI Module{} v3.0.2'
+MODULE_VERSION = 'Python WSGI Module{} v3.1.0'
API_RISK = '/api/v3/risk'
PAGE_REQUESTED_ACTIVITY = 'page_requested'
BLOCK_ACTIVITY = 'block'
diff --git a/perimeterx/px_cookie_validator.py b/perimeterx/px_cookie_validator.py
index 8d1458d..f178094 100644
--- a/perimeterx/px_cookie_validator.py
+++ b/perimeterx/px_cookie_validator.py
@@ -19,7 +19,7 @@ def verify(ctx, config):
logger = config.logger
try:
if not ctx.px_cookies.keys():
- logger.debug('No risk cookie on the request')
+ logger.debug('Cookie is missing')
ctx.s2s_call_reason = 'no_cookie'
return False
@@ -46,7 +46,7 @@ def verify(ctx, config):
if not px_cookie.deserialize():
cookie = px_cookie._hmac + ":" + px_cookie._raw_cookie
- logger.error('Cookie decryption failed, value: {}'.format(cookie))
+ logger.debug('Cookie decryption failed, value: {}'.format(cookie))
ctx.px_cookie_raw = cookie_version + "=" + cookie
ctx.s2s_call_reason = 'cookie_decryption_failed'
return False
@@ -67,7 +67,7 @@ def verify(ctx, config):
if px_cookie.is_cookie_expired():
ctx.s2s_call_reason = 'cookie_expired'
- msg = 'Cookie TTL expired, value: {}, age: {}'
+ msg = 'Cookie TTL is expired, value: {}, age: {}'
logger.debug(msg.format(px_cookie.decoded_cookie, px_cookie.get_age()))
return False
@@ -86,7 +86,7 @@ def verify(ctx, config):
return True
except Exception, err:
traceback.print_exc()
- logger.debug('Unexpected exception while evaluating Risk cookie. Error: {}'.format(err))
+ logger.error('Unexpected exception while evaluating Risk cookie. Error: {}'.format(err))
ctx.px_cookie_raw = px_cookie._raw_cookie
ctx.s2s_call_reason = 'cookie_decryption_failed'
return False
diff --git a/perimeterx/px_logger.py b/perimeterx/px_logger.py
index 0de8c6e..244bf50 100644
--- a/perimeterx/px_logger.py
+++ b/perimeterx/px_logger.py
@@ -1,11 +1,21 @@
+import logging
+
class Logger(object):
def __init__(self, debug, app_id):
self.debug_mode = debug
self.app_id = app_id
+ # Setup logger
+ self.logger = logging.getLogger(__name__)
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter('[PerimeterX %(levelname)s][{}]: %(message)s'.format(self.app_id))
+ handler.setFormatter(formatter)
+ self.logger.addHandler(handler)
+ self.logger.setLevel(logging.DEBUG)
+
def debug(self, message):
- if self.debug_mode:
- print '[PerimeterX DEBUG][{}]: '.format(self.app_id) + message
+ if self.debug_mode:
+ self.logger.debug(message)
def error(self, message):
- print '[PerimeterX ERROR][{}]: '.format(self.app_id) + message
+ self.logger.error(message)
diff --git a/perimeterx/px_original_token_validator.py b/perimeterx/px_original_token_validator.py
index 507a5d0..a3bb60e 100644
--- a/perimeterx/px_original_token_validator.py
+++ b/perimeterx/px_original_token_validator.py
@@ -17,7 +17,7 @@ def verify(ctx, config):
cookie_version, px_cookie = px_cookie_builder.build_px_cookie({version: no_version_token}, '')
if not px_cookie.deserialize():
- logger.error('Original token decryption failed, value: {}'.format(px_cookie.raw_cookie))
+ logger.debug('Original token decryption failed, value: {}'.format(px_cookie.raw_cookie))
ctx.original_token_error = 'decryption_failed'
return False
diff --git a/perimeterx/px_request_verifier.py b/perimeterx/px_request_verifier.py
index 4ca19ca..6e7f01e 100644
--- a/perimeterx/px_request_verifier.py
+++ b/perimeterx/px_request_verifier.py
@@ -50,9 +50,10 @@ def handle_verification(self, ctx, request):
else:
logger.debug('Risk score is higher or equal than blocking score')
self.report_block_traffic(ctx)
+ should_bypass_monitor = config.bypass_monitor_header and ctx.headers.get(config.bypass_monitor_header) == '1';
if config.additional_activity_handler:
config.additional_activity_handler(ctx, config)
- if config.module_mode == px_constants.MODULE_MODE_BLOCKING:
+ if config.module_mode == px_constants.MODULE_MODE_BLOCKING or should_bypass_monitor:
data, headers, status = self.px_blocker.handle_blocking(ctx=ctx, config=config)
response_function = generate_blocking_response(data, headers, status)
else:
diff --git a/setup-gae.py b/setup-gae.py
index 7d282fe..23cbc70 100644
--- a/setup-gae.py
+++ b/setup-gae.py
@@ -2,7 +2,7 @@
from setuptools import setup, find_packages
-version = 'v3.0.2'
+version = 'v3.1.0'
setup(name='perimeterx-python-wsgi-gae',
version=version,
license='MIT',
diff --git a/setup.py b/setup.py
index ff7307e..d777682 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
from setuptools import setup, find_packages
-version = 'v3.0.2'
+version = 'v3.1.0'
setup(name='perimeterx-python-wsgi',
version=version,
license='MIT',
diff --git a/test/test_px_request_validator.py b/test/test_px_request_validator.py
index ac5dc33..410624c 100644
--- a/test/test_px_request_validator.py
+++ b/test/test_px_request_validator.py
@@ -71,3 +71,92 @@ def test_handle_verification_failed(self):
context.score = 100
response = self.request_handler.handle_verification(context, request)
self.assertEqual(response.status, '403 Forbidden')
+
+ def test_handle_monitor(self):
+ config = PxConfig({'app_id': 'PXfake_app_id',
+ 'auth_token': '',
+ 'module_mode': px_constants.MODULE_MODE_MONITORING
+ });
+ request_handler = PxRequestVerifier(config)
+ builder = EnvironBuilder(headers=self.headers, path='/')
+ env = builder.get_environ()
+ request = Request(env)
+ context = PxContext(request, request_handler.config)
+ context.score = 100
+ response = request_handler.handle_verification(context, request)
+ self.assertEqual(response, True)
+
+ def test_bypass_monitor_header_enabled(self):
+ config = PxConfig({'app_id': 'PXfake_app_id',
+ 'auth_token': '',
+ 'module_mode': px_constants.MODULE_MODE_MONITORING,
+ 'bypass_monitor_header': 'x-px-block'
+ });
+ headers = {'X-FORWARDED-FOR': '127.0.0.1',
+ 'remote-addr': '127.0.0.1',
+ 'x-px-block': '1',
+ 'content_length': '100'}
+ request_handler = PxRequestVerifier(config)
+ builder = EnvironBuilder(headers=headers, path='/')
+ env = builder.get_environ()
+ request = Request(env)
+ context = PxContext(request, request_handler.config)
+ context.score = 100
+ response = request_handler.handle_verification(context, request)
+ self.assertEqual(response.status, '403 Forbidden')
+
+ def test_bypass_monitor_header_disabled(self):
+ config = PxConfig({'app_id': 'PXfake_app_id',
+ 'auth_token': '',
+ 'module_mode': px_constants.MODULE_MODE_MONITORING,
+ 'bypass_monitor_header': 'x-px-block'
+ });
+ headers = {'X-FORWARDED-FOR': '127.0.0.1',
+ 'remote-addr': '127.0.0.1',
+ 'x-px-block': '0',
+ 'content_length': '100'}
+ request_handler = PxRequestVerifier(config)
+ builder = EnvironBuilder(headers=headers, path='/')
+ env = builder.get_environ()
+ request = Request(env)
+ context = PxContext(request, request_handler.config)
+ context.score = 100
+ response = request_handler.handle_verification(context, request)
+ self.assertEqual(response, True)
+
+ def test_bypass_monitor_header_configured_but_missing(self):
+ config = PxConfig({'app_id': 'PXfake_app_id',
+ 'auth_token': '',
+ 'module_mode': px_constants.MODULE_MODE_MONITORING,
+ 'bypass_monitor_header': 'x-px-block'
+ });
+ headers = {'X-FORWARDED-FOR': '127.0.0.1',
+ 'remote-addr': '127.0.0.1',
+ 'content_length': '100'}
+ request_handler = PxRequestVerifier(config)
+ builder = EnvironBuilder(headers=headers, path='/')
+ env = builder.get_environ()
+ request = Request(env)
+ context = PxContext(request, request_handler.config)
+ context.score = 100
+ response = request_handler.handle_verification(context, request)
+ self.assertEqual(response, True)
+
+ def test_bypass_monitor_header_on_valid_request(self):
+ config = PxConfig({'app_id': 'PXfake_app_id',
+ 'auth_token': '',
+ 'module_mode': px_constants.MODULE_MODE_MONITORING,
+ 'bypass_monitor_header': 'x-px-block'
+ });
+ headers = {'X-FORWARDED-FOR': '127.0.0.1',
+ 'remote-addr': '127.0.0.1',
+ 'x-px-block': '1',
+ 'content_length': '100'}
+ request_handler = PxRequestVerifier(config)
+ builder = EnvironBuilder(headers=headers, path='/')
+ env = builder.get_environ()
+ request = Request(env)
+ context = PxContext(request, request_handler.config)
+ context.score = 0
+ response = request_handler.handle_verification(context, request)
+ self.assertEqual(response, True)
\ No newline at end of file