Skip to content

Commit 9fead82

Browse files
authored
Merge pull request #42 from PerimeterX/dev
Async custom params and dynamic module enabling
2 parents 4e115ab + c9deb79 commit 9fead82

37 files changed

+1232
-499
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Change Log
22

3+
## [v2.2.1](https://github.com/PerimeterX/perimeterx-python-wsgi) (2018-01-03)
4+
- Added async custom params
5+
- Added dynamic module enabling/disabling
6+
- Small performance boost
7+
- Major refactoring
8+
9+
## [v2.1.0](https://github.com/PerimeterX/perimeterx-python-wsgi) (2018-12-20)
10+
- Added data enrichment
11+
- Fixed mobile catpcha release
12+
- Added an option to programmatically enable and disable the module
13+
- Async custom params
14+
- Fixed performance issues
15+
- Major refactoring
16+
317
## [v2.0.2](https://github.com/PerimeterX/perimeterx-python-wsgi) (2018-12-05)
418
- Fixed copying resources to package on pypi.
519

README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
[PerimeterX](http://www.perimeterx.com) Python Middleware
66
=============================================================
7-
> Latest stable version: [v2.0.2](https://pypi.org/project/perimeterx-python-wsgi/)
7+
> Latest stable version: [v2.2.1](https://pypi.org/project/perimeterx-python-wsgi/)
88
Table of Contents
99
-----------------
1010
- [Installation](#installation)
11-
- [Required Configuration](#required_config)
1211
- [Upgrading](#upgrading)
12+
- [Required Configuration](#required_config)
1313
- [Advanced Blocking Response](#advanced_blocking_response)
1414
- [Optional Configuration](#configuration)
1515
* [Module Enabled](#module_enabled)
@@ -24,6 +24,7 @@ Table of Contents
2424
* [First-Party Enabled](#first_party_enabled)
2525
* [Custom Request Handler](#custom_request_handler)
2626
* [Additional Activity Handler](#additional_activity_handler)
27+
* [Dynamic Module Disabling](#dynamic_module_disabling)
2728
## <a name="installation"></a> Installation
2829
PerimeterX Python middleware is installed via PIP:
2930
`$ pip install perimeterx-python-wsgi`
@@ -110,7 +111,7 @@ config = {
110111
An array of route prefixes that trigger a server call to PerimeterX servers every time the page is viewed, regardless of viewing history.
111112
**Default:** Empty
112113
```python
113-
const config = {
114+
config = {
114115
...
115116
sensitive_routes: ['/login', '/user/checkout']
116117
...
@@ -150,7 +151,7 @@ config = {
150151
Enable/disable First-Party mode.
151152
**Default:** True
152153
```python
153-
const pxConfig = {
154+
config = {
154155
...
155156
first_party_enabled: False
156157
...
@@ -179,3 +180,25 @@ config = {
179180
...
180181
}
181182
```
183+
184+
#### <a name="pxde"></a>PerimeterX Data Enrichment
185+
This is a cookie we make available for our costumers, that can provide extra data about the request
186+
```python
187+
context.pxde
188+
context.pxde_verified
189+
190+
```
191+
192+
#### <a name="dynamic_module_disabling"></a>Dynamic Module Disabling
193+
These are methods that allow the developer to enable or disable the module programmatically
194+
```python
195+
from perimeterx.middleware import PerimeterX
196+
197+
app = get_wsgi_application()
198+
config = {...}
199+
perimeterX = PerimeterX(app, config)
200+
...
201+
perimeterX.disable_module()
202+
perimeterX.enable_module()
203+
```
204+

perimeterx/middleware.py

Lines changed: 61 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import time
2+
3+
from werkzeug.wrappers import Request
4+
15
import px_activities_client
2-
import px_cookie_validator
3-
from px_context import PxContext
4-
import px_blocker
5-
import px_api
6-
import px_constants
7-
import px_utils
8-
from perimeterx.px_proxy import PXProxy
96
from px_config import PxConfig
7+
from px_context import PxContext
8+
from px_request_verifier import PxRequestVerifier
109

1110

1211
class PerimeterX(object):
@@ -15,99 +14,86 @@ def __init__(self, app, config=None):
1514
# merging user's defined configurations with the default one
1615
px_config = PxConfig(config)
1716
logger = px_config.logger
17+
1818
if not px_config.app_id:
19-
logger.error('PX App ID is missing')
19+
logger.error('Unable to initialize module, missing mandatory configuration: app_id')
2020
raise ValueError('PX App ID is missing')
2121

22-
# if APP_ID is not set, use the deafult perimeterx server - else, use the appid specific sapi.
2322
if not px_config.auth_token:
24-
logger.error('PX Auth Token is missing')
23+
logger.error('Unable to initialize module, missing mandatory configuration: auth_token')
2524
raise ValueError('PX Auth Token is missing')
2625

2726
if not px_config.cookie_key:
28-
logger.error('PX Cookie Key is missing')
27+
logger.error('Unable to initialize module, missing mandatory configuration: px_cookie')
2928
raise ValueError('PX Cookie Key is missing')
29+
3030
self.reverse_proxy_prefix = px_config.app_id[2:].lower()
31-
self._PXBlocker = px_blocker.PXBlocker()
3231
self._config = px_config
32+
self._module_enabled = self._config.module_enabled
33+
self._request_verifier = PxRequestVerifier(px_config)
3334
px_activities_client.init_activities_configuration(px_config)
3435
px_activities_client.send_enforcer_telemetry_activity(config=px_config, update_reason='initial_config')
3536

3637
def __call__(self, environ, start_response):
37-
return self._verify(environ, start_response)
38-
39-
def _verify(self, environ, start_response):
40-
config = self.config
41-
logger = config.logger
4238
try:
43-
ctx = PxContext(environ, config)
44-
uri = ctx.uri
45-
px_proxy = PXProxy(config)
46-
if px_proxy.should_reverse_request(uri):
47-
body = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH'))) if environ.get(
48-
'CONTENT_LENGTH') else ''
49-
return px_proxy.handle_reverse_request(self.config, ctx, start_response, body)
50-
if px_utils.is_static_file(ctx):
51-
logger.debug('Filter static file request. uri: ' + uri)
52-
return self.app(environ, start_response)
53-
if not self._config._module_enabled:
54-
logger.debug('Module is disabled, request will not be verified')
55-
return self.app(environ, start_response)
56-
57-
if ctx.whitelist_route:
58-
logger.debug('The requested uri is whitelisted, passing request')
39+
start = time.time()
40+
context = None
41+
request = Request(environ)
42+
context, verified_response = self.verify(request)
43+
self._config.logger.debug("PerimeterX Enforcer took: {} ms".format((time.time() - start) * 1000))
44+
if verified_response is True:
5945
return self.app(environ, start_response)
6046

61-
# PX Cookie verification
62-
if not px_cookie_validator.verify(ctx, config):
63-
# Server-to-Server verification fallback
64-
if not px_api.verify(ctx, self.config):
65-
return self.app(environ, start_response)
66-
return self.handle_verification(ctx, self.config, environ, start_response)
67-
except:
68-
logger.error("Caught exception, passing request")
69-
self.pass_traffic(PxContext({}, config))
70-
return self.app(environ, start_response)
47+
return verified_response(environ, start_response)
7148

72-
def handle_verification(self, ctx, config, environ, start_response):
73-
score = ctx.score
74-
result = None
75-
headers = None
76-
status = None
77-
pass_request = True
78-
if score < config.blocking_score:
79-
self.pass_traffic(ctx)
80-
else:
81-
pass_request = False
82-
self.block_traffic(ctx)
83-
84-
if config.additional_activity_handler:
85-
config.additional_activity_handler(ctx, config)
86-
87-
if config.module_mode == px_constants.MODULE_MODE_BLOCKING and result is None and not pass_request:
88-
result, headers, status = self.px_blocker.handle_blocking(ctx=ctx, config=config)
89-
if config.custom_request_handler:
90-
custom_body, custom_headers, custom_status = config.custom_request_handler(ctx, self.config, environ)
91-
if custom_body is not None:
92-
start_response(custom_status, custom_headers)
93-
return custom_body
94-
95-
if headers is not None:
96-
start_response(status, headers)
97-
return result
98-
else:
49+
except Exception as err:
50+
self._config.logger.error("Caught exception, passing request. Exception: {}".format(err))
51+
if context:
52+
self.report_pass_traffic(context)
53+
else:
54+
self.report_pass_traffic(PxContext({}, self._config))
9955
return self.app(environ, start_response)
10056

101-
def pass_traffic(self, ctx):
57+
def verify(self, request):
58+
config = self.config
59+
logger = config.logger
60+
logger.debug('Starting request verification')
61+
ctx = None
62+
try:
63+
if not self._module_enabled:
64+
logger.debug('Request will not be verified, module is disabled')
65+
return ctx, True
66+
ctx = PxContext(request, config)
67+
return ctx, self._request_verifier.verify_request(ctx, request)
68+
except Exception as err:
69+
logger.error("Caught exception, passing request. Exception: {}".format(err))
70+
if ctx:
71+
self.report_pass_traffic(ctx)
72+
else:
73+
self.report_pass_traffic(PxContext({}, config))
74+
return True
75+
76+
77+
def report_pass_traffic(self, ctx):
10278
px_activities_client.send_page_requested_activity(ctx, self.config)
10379

104-
def block_traffic(self, ctx):
80+
def report_block_traffic(self, ctx):
10581
px_activities_client.send_block_activity(ctx, self.config)
10682

10783
@property
10884
def config(self):
10985
return self._config
11086

111-
@property
112-
def px_blocker(self):
113-
return self._PXBlocker
87+
def disable_module(self):
88+
if not self._module_enabled:
89+
self._config.logger.debug("Trying to disable the module, module already disabled")
90+
else:
91+
self._module_enabled = False
92+
93+
def enable_module(self):
94+
if self._module_enabled:
95+
self._config.logger.debug("Trying to enable the module, module already enabled")
96+
else:
97+
self._module_enabled = True
98+
99+

perimeterx/px_activities_client.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import time
2-
import px_httpc
1+
import json
2+
import socket
3+
import sys
34
import threading
4-
import traceback, sys
5+
import time
6+
import traceback
7+
58
import px_constants
6-
import socket
7-
import json
9+
import px_httpc
10+
import px_utils
811

912
ACTIVITIES_BUFFER = []
1013
CONFIG = {}
@@ -39,7 +42,6 @@ def send_to_perimeterx(activity_type, ctx, config, detail):
3942
if activity_type == 'page_requested' and not config.send_page_activities:
4043
print 'Page activities disabled in config - skipping.'
4144
return
42-
4345
_details = {
4446
'http_method': ctx.http_method,
4547
'http_version': ctx.http_version,
@@ -52,7 +54,7 @@ def send_to_perimeterx(activity_type, ctx, config, detail):
5254

5355
data = {
5456
'type': activity_type,
55-
'headers': ctx.headers,
57+
'headers': dict(ctx.headers),
5658
'timestamp': int(round(time.time() * 1000)),
5759
'socket_ip': ctx.ip,
5860
'px_app_id': config.app_id,
@@ -61,6 +63,9 @@ def send_to_perimeterx(activity_type, ctx, config, detail):
6163
'vid': ctx.vid,
6264
'uuid': ctx.uuid
6365
}
66+
if activity_type == 'page_requested' or activity_type == 'block':
67+
px_utils.prepare_custom_params(config, _details)
68+
6469
ACTIVITIES_BUFFER.append(data)
6570
except:
6671
print traceback.format_exception(*sys.exc_info())

0 commit comments

Comments
 (0)