Skip to content

Commit 07f6112

Browse files
Merge pull request #27 from PerimeterX/release/v1.2.0
Release to master v1.2.0
2 parents 337629a + 345a688 commit 07f6112

14 files changed

+135
-348
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [1.2.0] - 2022-04-11
9+
### Added
10+
- New block page implementation
11+
- Configurable max buffer length
12+
- Configurable px_backend_url
13+
- Sending activities at the end of request cycle rather than beginning of the next one
14+
815
## [1.1.0] - 2020-09-02
916
### Added
1017
- Support for `monitored_specific_routes`

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
[PerimeterX](http://www.perimeterx.com) Python 3 Middleware
77
=============================================================
8-
> Latest stable version: [v1.1.0](https://pypi.org/project/perimeterx-python-3-wsgi/)
8+
> Latest stable version: [v1.2.0](https://pypi.org/project/perimeterx-python-3-wsgi/)
99
1010
Table of Contents
1111
-----------------

dev-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
mock==3.0.5
22
requests_mock==1.7.0
3-
pylint==2.6.1
3+
pylint==2.7.0

perimeterx/middleware.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def __init__(self, app, config=None):
3434
px_activities_client.init_activities_configuration(px_config)
3535

3636
def __call__(self, environ, start_response):
37-
px_activities_client.send_activities_in_thread()
3837
context = None
3938
try:
4039
start = time.time()
@@ -46,6 +45,7 @@ def __call__(self, environ, start_response):
4645
context, verified_response = self.verify(request)
4746
pxhd_callback = create_custom_pxhd_callback(context, start_response)
4847
self._config.logger.debug("PerimeterX Enforcer took: {} ms".format((time.time() - start) * 1000))
48+
px_activities_client.send_activities_in_thread()
4949
if verified_response is True:
5050
return self.app(environ, pxhd_callback)
5151

perimeterx/px_activities_client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ def _send_activities_chunk():
2424
'Content-Type': 'application/json'
2525
}
2626
full_url = CONFIG.server_host + px_constants.API_ACTIVITIES
27-
chunk = ACTIVITIES_BUFFER[:10]
27+
chunk = ACTIVITIES_BUFFER[:CONFIG.max_buffer_len]
2828
for _ in range(len(chunk)):
2929
ACTIVITIES_BUFFER.pop(0)
3030
px_httpc.send(full_url=full_url, body=json.dumps(chunk), headers=default_headers, config=CONFIG, method='POST')
3131

3232
def send_activities_in_thread():
33-
if len(ACTIVITIES_BUFFER) >= 10:
33+
if len(ACTIVITIES_BUFFER) >= CONFIG.max_buffer_len:
3434
CONFIG.logger.debug('Posting {} Activities'.format(len(ACTIVITIES_BUFFER)))
3535
t1 = threading.Thread(target=_send_activities_chunk)
3636
t1.daemon = True

perimeterx/px_blocker.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ def prepare_properties(self, ctx, config):
7373
app_id = config.app_id
7474
vid = ctx.vid
7575
uuid = ctx.uuid
76-
custom_logo = config.custom_logo
76+
custom_logo = config.custom_logo if config.custom_logo else ''
7777
is_mobile_num = 1 if ctx.is_mobile else 0
7878
captcha_uri = 'captcha.js?a={}&u={}&v={}&m={}'.format(ctx.block_action, uuid, vid, is_mobile_num)
79+
alt_captcha_src = '//{}/{}/{}'.format(px_constants.ALT_CAPTCHA_HOST, app_id, captcha_uri)
7980

8081
if config.first_party and not ctx.is_mobile:
8182
prefix = app_id[2:]
@@ -88,18 +89,17 @@ def prepare_properties(self, ctx, config):
8889
host_url = px_constants.COLLECTOR_URL.format(app_id.lower())
8990

9091
return {
91-
'refId': uuid,
9292
'appId': app_id,
9393
'vid': vid,
9494
'uuid': uuid,
9595
'customLogo': custom_logo,
9696
'cssRef': config.css_ref,
9797
'jsRef': config.js_ref,
98-
'logoVisibility': 'visible' if custom_logo is not None else 'hidden',
9998
'hostUrl': host_url,
10099
'jsClientSrc': js_client_src,
101100
'firstPartyEnabled': 'true' if config.first_party else 'false',
102-
'blockScript': captcha_src
101+
'blockScript': captcha_src,
102+
'altBlockScript': alt_captcha_src
103103
}
104104

105105
def is_json_response(self, ctx):

perimeterx/px_config.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ def __init__(self, config_dict):
1010
module_mode = config_dict.get('module_mode', px_constants.MODULE_MODE_MONITORING)
1111
custom_logo = config_dict.get('custom_logo', None)
1212
testing_mode = config_dict.get('testing_mode', False)
13+
px_backend_host = config_dict.get('px_backend_url', None)
14+
max_buffer_len = config_dict.get('max_buffer_len', 30)
1315
self._px_app_id = app_id
1416
self._blocking_score = config_dict.get('blocking_score', 100)
1517
self._debug_mode = debug_mode
1618
self._module_version = config_dict.get('module_version', px_constants.MODULE_VERSION)
1719
self._module_version = px_constants.MODULE_VERSION.format(' GAE') if os.environ.get('SERVER_SOFTWARE','').startswith('Google') else px_constants.MODULE_VERSION.format('')
1820
self._module_mode = module_mode
19-
self._server_host = 'sapi.perimeterx.net' if app_id is None else px_constants.SERVER_URL.format(app_id.lower())
20-
self._collector_host = 'collector.perimeterx.net' if app_id is None else px_constants.COLLECTOR_URL.format(
21-
app_id.lower())
21+
self._server_host = px_backend_host if px_backend_host else 'sapi.perimeterx.net' if app_id is None else px_constants.SERVER_URL.format(app_id.lower())
22+
self._collector_host = px_backend_host if px_backend_host else 'collector.perimeterx.net' if app_id is None else px_constants.COLLECTOR_URL.format(app_id.lower())
2223
self._encryption_enabled = config_dict.get('encryption_enabled', True)
2324
self._sensitive_headers = [*map(lambda header: header.lower(),
2425
config_dict.get('sensitive_headers', ['cookie', 'cookies']))]
@@ -36,7 +37,7 @@ def __init__(self, config_dict):
3637
self._first_party_xhr_enabled = config_dict.get('first_party_xhr_enabled', True)
3738
self._ip_headers = config_dict.get('ip_headers', [])
3839
self._proxy_url = config_dict.get('proxy_url', None)
39-
self._max_buffer_len = config_dict.get('max_buffer_len', 30)
40+
self._max_buffer_len = max_buffer_len if max_buffer_len > 0 else 1
4041
self._bypass_monitor_header = config_dict.get('bypass_monitor_header','')
4142

4243
sensitive_routes = config_dict.get('sensitive_routes', [])
@@ -80,7 +81,6 @@ def __init__(self, config_dict):
8081
self._monitored_specific_routes_regex = monitored_routes_regex
8182

8283
self._block_html = 'BLOCK'
83-
self._logo_visibility = 'visible' if custom_logo is not None else 'hidden'
8484
self._telemetry_config = self.__create_telemetry_config()
8585
self._testing_mode = testing_mode
8686
self._auth_token = config_dict.get('auth_token', None)
@@ -202,10 +202,6 @@ def whitelist_routes_regex(self):
202202
def block_html(self):
203203
return self._block_html
204204

205-
@property
206-
def logo_visibility(self):
207-
return self._logo_visibility
208-
209205
@property
210206
def additional_activity_handler(self):
211207
return self._additional_activity_handler

perimeterx/px_constants.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
EMPTY_GIF_B64 = 'R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
2828
COLLECTOR_HOST = 'collector.perimeterx.net'
2929
FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR'
30-
MODULE_VERSION = 'Python 3 WSGI Module v1.1.0'
30+
MODULE_VERSION = 'Python 3 WSGI Module v1.2.0'
3131
API_RISK = '/api/v3/risk'
3232
PAGE_REQUESTED_ACTIVITY = 'page_requested'
3333
BLOCK_ACTIVITY = 'block'
@@ -38,3 +38,4 @@
3838
ACTION_BLOCK = 'b'
3939
ACTION_RATELIMIT = 'r'
4040
ACTION_CAPTCHA = 'c'
41+
ALT_CAPTCHA_HOST = 'captcha.px-cloud.net'

perimeterx/px_httpc.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ def send(full_url, body, headers, config, method, raise_error = False):
2323
try:
2424
start = time.time()
2525
if method == 'GET':
26-
response = requests.get(url='https://' + full_url, headers=headers, timeout=config.api_timeout, stream=True)
26+
response = requests.get(url=normalize_url(full_url), headers=headers, timeout=config.api_timeout, stream=True)
2727
else:
28-
response = requests.post(url='https://' + full_url, headers=headers, data=body, timeout=config.api_timeout)
28+
response = requests.post(url=normalize_url(full_url), headers=headers, data=body, timeout=config.api_timeout)
2929

3030
if response.status_code >= 400:
3131
logger.debug('PerimeterX server call failed: ' + str(response.status_code))
@@ -38,3 +38,10 @@ def send(full_url, body, headers, config, method, raise_error = False):
3838
logger.debug('PerimeterX Received Request Exception. Error: {}'.format(err))
3939
if raise_error:
4040
raise err
41+
42+
43+
def normalize_url(url):
44+
if url.startswith("http://") or url.startswith("https://"):
45+
return url
46+
else:
47+
return "https://" + url

perimeterx/templates/block_template.mustache

+33-166
Original file line numberDiff line numberDiff line change
@@ -3,175 +3,42 @@
33
<head>
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
6-
<title>Access to this page has been denied.</title>
7-
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
8-
<style>
9-
html, body {
10-
margin: 0;
11-
padding: 0;
12-
font-family: 'Open Sans', sans-serif;
13-
color: #000;
14-
}
15-
16-
a {
17-
color: #c5c5c5;
18-
text-decoration: none;
19-
}
20-
21-
.container {
22-
align-items: center;
23-
display: flex;
24-
flex: 1;
25-
justify-content: space-between;
26-
flex-direction: column;
27-
height: 100%;
28-
}
29-
30-
.container > div {
31-
width: 100%;
32-
display: flex;
33-
justify-content: center;
34-
}
35-
36-
.container > div > div {
37-
display: flex;
38-
width: 80%;
39-
}
40-
41-
.customer-logo-wrapper {
42-
padding-top: 2rem;
43-
flex-grow: 0;
44-
background-color: #fff;
45-
visibility: {{logoVisibility}};
46-
}
47-
48-
.customer-logo {
49-
border-bottom: 1px solid #000;
50-
}
51-
52-
.customer-logo > img {
53-
padding-bottom: 1rem;
54-
max-height: 50px;
55-
max-width: 100%;
56-
}
57-
58-
.page-title-wrapper {
59-
flex-grow: 2;
60-
}
61-
62-
.page-title {
63-
flex-direction: column-reverse;
64-
}
65-
66-
.content-wrapper {
67-
flex-grow: 5;
68-
}
69-
70-
.content {
71-
flex-direction: column;
72-
}
73-
74-
.page-footer-wrapper {
75-
align-items: center;
76-
flex-grow: 0.2;
77-
background-color: #000;
78-
color: #c5c5c5;
79-
font-size: 70%;
80-
}
81-
82-
@media (min-width: 768px) {
83-
html, body {
84-
height: 100%;
85-
}
86-
}
87-
</style>
88-
<!-- Custom CSS -->
6+
<meta name="description" content="px-captcha">
7+
<title>Access to this page has been denied</title>
898
{{#cssRef}}
90-
<link rel="stylesheet" type="text/css" href="{{{cssRef}}}"/>
9+
<link rel="stylesheet" type="text/css" href="{{{cssRef}}}">
9110
{{/cssRef}}
9211
</head>
93-
9412
<body>
95-
<section class="container">
96-
{{#customLogo}}
97-
<div class="customer-logo-wrapper">
98-
<div class="customer-logo">
99-
<img src="{{customLogo}}" alt="Logo"/>
100-
</div>
101-
</div>
102-
{{/customLogo}}
103-
<div class="page-title-wrapper">
104-
<div class="page-title">
105-
<h1>Please verify you are a human</h1>
106-
</div>
107-
</div>
108-
<div class="content-wrapper">
109-
<div class="content">
110-
111-
<div id="px-captcha">
112-
</div>
113-
<p>
114-
Access to this page has been denied because we believe you are using automation tools to browse the
115-
website.
116-
</p>
117-
<p>
118-
This may happen as a result of the following:
119-
</p>
120-
<ul>
121-
<li>
122-
Javascript is disabled or blocked by an extension (ad blockers for example)
123-
</li>
124-
<li>
125-
Your browser does not support cookies
126-
</li>
127-
</ul>
128-
<p>
129-
Please make sure that Javascript and cookies are enabled on your browser and that you are not blocking
130-
them from loading.
131-
</p>
132-
<p>
133-
Reference ID: #{{refId}}
134-
</p>
135-
</div>
136-
</div>
137-
<div class="page-footer-wrapper">
138-
<div class="page-footer">
139-
<p>
140-
Powered by
141-
<a href="https://www.perimeterx.com/whywasiblocked">PerimeterX</a>
142-
, Inc.
143-
</p>
144-
</div>
145-
</div>
146-
</section>
147-
<!-- Px -->
148-
<script>
149-
window._pxAppId = '{{appId}}';
150-
window._pxJsClientSrc = '{{{jsClientSrc}}}';
151-
window._pxFirstPartyEnabled = {{firstPartyEnabled}};
152-
window._pxVid = '{{vid}}';
153-
window._pxUuid = '{{uuid}}';
154-
window._pxHostUrl = '{{{hostUrl}}}';
155-
</script>
156-
<script>
157-
var s = document.createElement('script');
158-
s.src = '{{{blockScript}}}';
159-
var p = document.getElementsByTagName('head')[0];
160-
p.insertBefore(s, null);
161-
if ({{firstPartyEnabled}}) {
162-
s.onerror = function () {
163-
s = document.createElement('script');
164-
var suffixIndex = '{{{blockScript}}}'.indexOf('captcha.js');
165-
var temperedBlockScript = '{{{blockScript}}}'.substring(suffixIndex);
166-
s.src = '//captcha.px-cdn.net/{{appId}}/' + temperedBlockScript;
167-
p.parentNode.insertBefore(s, p);
13+
<script>
14+
window._pxVid = '{{vid}}';
15+
window._pxUuid = '{{uuid}}';
16+
window._pxAppId = '{{appId}}';
17+
window._pxHostUrl = '{{{hostUrl}}}';
18+
window._pxCustomLogo = '{{{customLogo}}}';
19+
window._pxJsClientSrc = '{{{jsClientSrc}}}';
20+
window._pxFirstPartyEnabled = {{firstPartyEnabled}};
21+
var script = document.createElement('script');
22+
script.src = '{{{blockScript}}}';
23+
document.head.appendChild(script);
24+
script.onerror = function () {
25+
script = document.createElement('script');
26+
script.src = '{{{altBlockScript}}}';
27+
script.onerror = window._pxDisplayErrorMessage;
28+
document.head.appendChild(script);
16829
};
169-
}
170-
</script>
171-
172-
<!-- Custom Script -->
173-
{{#jsRef}}
174-
<script src="{{{jsRef}}}"></script>
175-
{{/jsRef}}
30+
window._pxDisplayErrorMessage = function () {
31+
var style = document.createElement('style');
32+
style.innerText = '@import url(https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap);body{background-color:#fafbfc}.px-captcha-error-container{position:fixed;height:340px;background-color:#fff;font-family:Roboto,sans-serif}.px-captcha-error-header{color:#f0f1f2;font-size:29px;margin:67px 0 33px;font-weight:500;line-height:.83;text-align:center}.px-captcha-error-message{color:#f0f1f2;font-size:18px;margin:0 0 29px;line-height:1.33;text-align:center}.px-captcha-error-button{text-align:center;line-height:48px;width:253px;margin:auto;border-radius:50px;border:solid 1px #f0f1f2;font-size:20px;color:#f0f1f2}.px-captcha-error-wrapper{margin:18px 0 0}div.px-captcha-error{margin:auto;text-align:center;width:400px;height:30px;font-size:12px;background-color:#fcf0f2;color:#ce0e2d}img.px-captcha-error{margin:6px 8px -2px 0}.px-captcha-error-refid{border-top:solid 1px #f0eeee;height:27px;margin:13px 0 0;border-radius:0 0 3px 3px;background-color:#fafbfc;font-size:10px;line-height:2.5;text-align:center;color:#b1b5b8}@media (min-width:620px){.px-captcha-error-container{width:530px;top:50%;left:50%;margin-top:-170px;margin-left:-265px;border-radius:3px;box-shadow:0 2px 9px -1px rgba(0,0,0,.13)}}@media (min-width:481px) and (max-width:620px){.px-captcha-error-container{width:85%;top:50%;left:50%;margin-top:-170px;margin-left:-42.5%;border-radius:3px;box-shadow:0 2px 9px -1px rgba(0,0,0,.13)}}@media (max-width:480px){body{background-color:#fff}.px-captcha-error-header{color:#f0f1f2;font-size:29px;margin:55px 0 33px}.px-captcha-error-container{width:530px;top:50%;left:50%;margin-top:-170px;margin-left:-265px}.px-captcha-error-refid{position:fixed;width:100%;left:0;bottom:0;border-radius:0;font-size:14px;line-height:2}}@media (max-width:390px){div.px-captcha-error{font-size:10px}.px-captcha-error-refid{font-size:11px;line-height:2.5}}';
33+
document.head.appendChild(style);
34+
var div = document.createElement('div');
35+
div.className = 'px-captcha-error-container';
36+
div.innerHTML = '<div class="px-captcha-error-header">Before we continue...</div><div class="px-captcha-error-message">Press & Hold to confirm you are<br>a human (and not a bot).</div><div class="px-captcha-error-button">Press & Hold</div><div class="px-captcha-error-wrapper"><div class="px-captcha-error"><img class="px-captcha-error" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAQCAMAAADDGrRQAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAABFUExURUdwTNYELOEGONQILd0AONwALtwEL+AAL9MFLfkJSNQGLdMJLdQJLdQGLdQKLtYFLNcELdUGLdcBL9gFL88OLdUFLNEOLglBhT4AAAAXdFJOUwC8CqgNIRgRoAS1dWWuR4RTjzgryZpYblfkcAAAAI9JREFUGNNdj+sWhCAIhAdvqGVa1r7/oy6RZ7eaH3D4ZACBIed9wlOOMtUnSrEmZ6cHa9YAIfsbCkWrdpi/c50Bk2CO9mNLdMAu03wJA3HpEnfpxbyOg6ruyx8JJi6KNstnslp1dbPd9GnqmuYq7mmcv1zjnbQw8cV0xzkqo+fX1zkjUOO7wnrInUTxJiruC3vtBNRoQQn2AAAAAElFTkSuQmCC">Please check your internet connection or disable your ad-blocker.</div></div><div class="px-captcha-error-refid">Reference ID ' + window._pxUuid + '</div>';
37+
document.body.appendChild(div);
38+
};
39+
</script>
40+
{{#jsRef}}
41+
<script src="{{{jsRef}}}"></script>
42+
{{/jsRef}}
17643
</body>
177-
</html>
44+
</html>

0 commit comments

Comments
 (0)