Skip to content

Commit

Permalink
Merge pull request #70 from PerimeterX/dev
Browse files Browse the repository at this point in the history
Version 3.1.0
  • Loading branch information
Johnny Tordgeman authored Feb 26, 2019
2 parents 3c7a588 + 96e6613 commit acf8db4
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------
Expand All @@ -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)

## <a name="installation"></a> Installation

Expand Down Expand Up @@ -254,3 +255,21 @@ environ['px_disable_request'] = True #The enforcer shall be disabled for that re

```

#### <a name=“bypassMonitorHeader”></a> 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',
...
}
```
7 changes: 4 additions & 3 deletions perimeterx/px_blocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions perimeterx/px_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion perimeterx/px_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 4 additions & 4 deletions perimeterx/px_cookie_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
16 changes: 13 additions & 3 deletions perimeterx/px_logger.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion perimeterx/px_original_token_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion perimeterx/px_request_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion setup-gae.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
89 changes: 89 additions & 0 deletions test/test_px_request_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit acf8db4

Please sign in to comment.