Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 44 additions & 59 deletions perimeterx/middleware.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -86,33 +73,31 @@ 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)

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
23 changes: 10 additions & 13 deletions perimeterx/px_activities_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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', ''),
Expand All @@ -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,
})
15 changes: 8 additions & 7 deletions perimeterx/px_api.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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:
Expand All @@ -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', '')
Expand All @@ -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', '')
}
Expand Down
12 changes: 6 additions & 6 deletions perimeterx/px_blocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}

Expand Down
Loading