Skip to content

Commit 7dc3f27

Browse files
authored
Merge pull request #50 from PerimeterX/dev
Dev
2 parents bbe95fc + b403714 commit 7dc3f27

16 files changed

+725
-607
lines changed

changelog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ 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+
## [2.1.0] - 2020-09-01
9+
### Added
10+
- Added option to set a different px configuration on each request
11+
- Added types validation on configuration fields
12+
13+
### Fixed
14+
- New cookie logic for mobile requests
15+
- Renamed api_connect_timeout to api_timeout_conncection on default configuration
16+
- Removed unsapported configuration fields: max_buffer_len and local_proxy
17+
- Send cookie_origin only if there is a cookie
18+
819
## [2.0.0] - 2020-07-24
920
### Added
1021
- Added fields to Block Activity: simulated_block, http_version, http_method, risk_rtt, px_orig_cookie

lib/perimeter_x.rb

Lines changed: 89 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -11,122 +11,122 @@
1111
require 'perimeterx/internal/clients/perimeter_x_activity_client'
1212
require 'perimeterx/internal/validators/perimeter_x_s2s_validator'
1313
require 'perimeterx/internal/validators/perimeter_x_cookie_validator'
14+
require 'perimeterx/internal/exceptions/px_config_exception'
1415

1516
module PxModule
1617
# Module expose API
17-
def px_verify_request
18-
px_ctx = PerimeterX.instance.verify(request.env)
19-
px_config = PerimeterX.instance.px_config
20-
msg_title = 'PxModule[px_verify_request]'
21-
22-
# In case custom verification handler is in use
23-
if px_config.key?(:custom_verification_handler)
24-
px_config[:logger].debug("#{msg_title}: custom_verification_handler triggered")
25-
return instance_exec(px_ctx, &px_config[:custom_verification_handler])
26-
end
18+
def px_verify_request(request_config={})
19+
begin
20+
px_instance = PerimeterX.new(request_config)
21+
px_ctx = px_instance.verify(request.env)
22+
px_config = px_instance.px_config
23+
24+
msg_title = 'PxModule[px_verify_request]'
25+
26+
# In case custom verification handler is in use
27+
if px_config.key?(:custom_verification_handler)
28+
px_config[:logger].debug("#{msg_title}: custom_verification_handler triggered")
29+
return instance_exec(px_ctx, &px_config[:custom_verification_handler])
30+
end
2731

28-
unless px_ctx.nil? || px_ctx.context[:verified] || (px_config[:module_mode] == PxModule::MONITOR_MODE && !px_ctx.context[:should_bypass_monitor])
29-
# In case custom block handler exists (soon to be deprecated)
30-
if px_config.key?(:custom_block_handler)
31-
px_config[:logger].debug("#{msg_title}: custom_block_handler triggered")
32-
px_config[:logger].debug(
33-
"#{msg_title}: Please note that custom_block_handler is deprecated. Use custom_verification_handler instead.")
34-
return instance_exec(px_ctx, &px_config[:custom_block_handler])
35-
else
36-
if px_ctx.context[:block_action]== 'rate_limit'
37-
px_config[:logger].debug("#{msg_title}: sending rate limit page")
38-
response.status = 429
32+
unless px_ctx.nil? || px_ctx.context[:verified] || (px_config[:module_mode] == PxModule::MONITOR_MODE && !px_ctx.context[:should_bypass_monitor])
33+
# In case custom block handler exists (soon to be deprecated)
34+
if px_config.key?(:custom_block_handler)
35+
px_config[:logger].debug("#{msg_title}: custom_block_handler triggered")
36+
px_config[:logger].debug(
37+
"#{msg_title}: Please note that custom_block_handler is deprecated. Use custom_verification_handler instead.")
38+
return instance_exec(px_ctx, &px_config[:custom_block_handler])
3939
else
40-
px_config[:logger].debug("#{msg_title}: sending default block page")
41-
response.status = 403
42-
end
43-
44-
is_mobile = px_ctx.context[:cookie_origin] == 'header' ? '1' : '0'
45-
action = px_ctx.context[:block_action][0,1]
46-
47-
px_template_object = {
48-
block_script: "//#{PxModule::CAPTCHA_HOST}/#{px_config[:app_id]}/captcha.js?a=#{action}&u=#{px_ctx.context[:uuid]}&v=#{px_ctx.context[:vid]}&m=#{is_mobile}",
49-
js_client_src: "//#{PxModule::CLIENT_HOST}/#{px_config[:app_id]}/main.min.js"
50-
}
51-
52-
html = PxTemplateFactory.get_template(px_ctx, px_config, px_template_object)
40+
if px_ctx.context[:block_action]== 'rate_limit'
41+
px_config[:logger].debug("#{msg_title}: sending rate limit page")
42+
response.status = 429
43+
else
44+
px_config[:logger].debug("#{msg_title}: sending default block page")
45+
response.status = 403
46+
end
5347

54-
# Web handler
55-
if px_ctx.context[:cookie_origin] == 'cookie'
48+
is_mobile = px_ctx.context[:cookie_origin] == 'header' ? '1' : '0'
49+
action = px_ctx.context[:block_action][0,1]
5650

57-
accept_header_value = request.headers['accept'] || request.headers['content-type'];
58-
is_json_response = px_ctx.context[:block_action] != 'rate_limit' && accept_header_value && accept_header_value.split(',').select {|e| e.downcase.include? 'application/json'}.length > 0;
51+
px_template_object = {
52+
block_script: "//#{PxModule::CAPTCHA_HOST}/#{px_config[:app_id]}/captcha.js?a=#{action}&u=#{px_ctx.context[:uuid]}&v=#{px_ctx.context[:vid]}&m=#{is_mobile}",
53+
js_client_src: "//#{PxModule::CLIENT_HOST}/#{px_config[:app_id]}/main.min.js"
54+
}
5955

60-
if (is_json_response)
61-
px_config[:logger].debug("#{msg_title}: advanced blocking response response")
56+
html = PxTemplateFactory.get_template(px_ctx, px_config, px_template_object)
57+
58+
# Web handler
59+
if px_ctx.context[:cookie_origin] == 'cookie'
60+
61+
accept_header_value = request.headers['accept'] || request.headers['content-type'];
62+
is_json_response = px_ctx.context[:block_action] != 'rate_limit' && accept_header_value && accept_header_value.split(',').select {|e| e.downcase.include? 'application/json'}.length > 0;
63+
64+
if (is_json_response)
65+
px_config[:logger].debug("#{msg_title}: advanced blocking response response")
66+
response.headers['Content-Type'] = 'application/json'
67+
68+
hash_json = {
69+
:appId => px_config[:app_id],
70+
:jsClientSrc => px_template_object[:js_client_src],
71+
:firstPartyEnabled => false,
72+
:uuid => px_ctx.context[:uuid],
73+
:vid => px_ctx.context[:vid],
74+
:hostUrl => "https://collector-#{px_config[:app_id]}.perimeterx.net",
75+
:blockScript => px_template_object[:block_script],
76+
}
77+
78+
render :json => hash_json
79+
else
80+
px_config[:logger].debug('#{msg_title}: web block')
81+
response.headers['Content-Type'] = 'text/html'
82+
render :html => html
83+
end
84+
else # Mobile SDK
85+
px_config[:logger].debug("#{msg_title}: mobile sdk block")
6286
response.headers['Content-Type'] = 'application/json'
63-
6487
hash_json = {
65-
:appId => px_config[:app_id],
66-
:jsClientSrc => px_template_object[:js_client_src],
67-
:firstPartyEnabled => false,
88+
:action => px_ctx.context[:block_action],
6889
:uuid => px_ctx.context[:uuid],
6990
:vid => px_ctx.context[:vid],
70-
:hostUrl => "https://collector-#{px_config[:app_id]}.perimeterx.net",
71-
:blockScript => px_template_object[:block_script],
91+
:appId => px_config[:app_id],
92+
:page => Base64.strict_encode64(html),
93+
:collectorUrl => "https://collector-#{px_config[:app_id]}.perimeterx.net"
7294
}
73-
7495
render :json => hash_json
75-
else
76-
px_config[:logger].debug('#{msg_title}: web block')
77-
response.headers['Content-Type'] = 'text/html'
78-
render :html => html
7996
end
80-
else # Mobile SDK
81-
px_config[:logger].debug("#{msg_title}: mobile sdk block")
82-
response.headers['Content-Type'] = 'application/json'
83-
hash_json = {
84-
:action => px_ctx.context[:block_action],
85-
:uuid => px_ctx.context[:uuid],
86-
:vid => px_ctx.context[:vid],
87-
:appId => px_config[:app_id],
88-
:page => Base64.strict_encode64(html),
89-
:collectorUrl => "https://collector-#{px_config[:app_id]}.perimeterx.net"
90-
}
91-
render :json => hash_json
9297
end
9398
end
94-
end
9599

96-
# Request was verified
97-
return px_ctx.nil? ? true : px_ctx.context[:verified]
100+
# Request was verified
101+
return px_ctx.nil? ? true : px_ctx.context[:verified]
102+
103+
rescue PxConfigurationException
104+
raise
105+
rescue Exception => e
106+
error_logger = PxLogger.new(true)
107+
error_logger.error("#{e.backtrace.first}: #{e.message} (#{e.class})")
108+
e.backtrace.drop(1).map {|s| error_logger.error("\t#{s}")}
109+
return nil
110+
end
98111
end
99112

100-
def self.configure(params)
101-
@px_instance = PerimeterX.configure(params)
113+
def self.configure(basic_config)
114+
PerimeterX.set_basic_config(basic_config)
102115
end
103116

104117

105118
# PerimeterX Module
106119
class PerimeterX
107-
@@__instance = nil
108-
@@mutex = Mutex.new
109120

110121
attr_reader :px_config
111122
attr_accessor :px_http_client
112123
attr_accessor :px_activity_client
113124

114125
#Static methods
115-
def self.configure(params)
116-
return true if @@__instance
117-
@@mutex.synchronize {
118-
return @@__instance if @@__instance
119-
@@__instance = new(params)
120-
}
121-
return true
126+
def self.set_basic_config(basic_config)
127+
Configuration.set_basic_config(basic_config)
122128
end
123129

124-
def self.instance
125-
return @@__instance if !@@__instance.nil?
126-
raise Exception.new('Please initialize perimeter x first')
127-
end
128-
129-
130130
#Instance Methods
131131
def verify(env)
132132
begin
@@ -155,6 +155,9 @@ def verify(env)
155155
# Cookie phase
156156
cookie_verified, px_ctx = @px_cookie_validator.verify(px_ctx)
157157
if !cookie_verified
158+
if !px_ctx.context[:mobile_error].nil?
159+
px_ctx.context[:s2s_call_reason] = "mobile_error_#{px_ctx.context[:mobile_error]}"
160+
end
158161
@px_s2s_validator.verify(px_ctx)
159162
end
160163

@@ -166,8 +169,9 @@ def verify(env)
166169
end
167170
end
168171

169-
private def initialize(params)
170-
@px_config = Configuration.new(params).configuration
172+
def initialize(request_config)
173+
174+
@px_config = Configuration.new(request_config).configuration
171175
@logger = @px_config[:logger]
172176
@px_http_client = PxHttpClient.new(@px_config)
173177

@@ -219,6 +223,5 @@ def verify(env)
219223
false
220224
end
221225

222-
private_class_method :new
223226
end
224227
end

lib/perimeterx/configuration.rb

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
require 'perimeterx/utils/px_logger'
22
require 'perimeterx/utils/px_constants'
3+
require 'perimeterx/internal/validators/hash_schema_validator'
34

45
module PxModule
56
class Configuration
7+
@@basic_config = nil
8+
@@mutex = Mutex.new
69

710
attr_accessor :configuration
8-
attr_accessor :PX_DEFAULT
911

1012
PX_DEFAULT = {
1113
:app_id => nil,
@@ -16,15 +18,13 @@ class Configuration
1618
:encryption_enabled => true,
1719
:blocking_score => 100,
1820
:sensitive_headers => ["http-cookie", "http-cookies"],
19-
:api_connect_timeout => 1,
21+
:api_timeout_connection => 1,
2022
:api_timeout => 1,
21-
:max_buffer_len => 10,
2223
:send_page_activities => true,
2324
:send_block_activities => true,
2425
:sdk_name => PxModule::SDK_NAME,
2526
:debug => false,
2627
:module_mode => PxModule::MONITOR_MODE,
27-
:local_proxy => false,
2828
:sensitive_routes => [],
2929
:whitelist_routes => [],
3030
:ip_headers => [],
@@ -33,9 +33,54 @@ class Configuration
3333
:risk_cookie_max_iterations => 5000
3434
}
3535

36+
CONFIG_SCHEMA = {
37+
:app_id => {types: [String], required: true},
38+
:cookie_key => {types: [String], required: true},
39+
:auth_token => {types: [String], required: true},
40+
:module_enabled => {types: [FalseClass, TrueClass], required: false},
41+
:challenge_enabled => {types: [FalseClass, TrueClass], required: false},
42+
:encryption_enabled => {types: [FalseClass, TrueClass], required: false},
43+
:blocking_score => {types: [Integer], required: false},
44+
:sensitive_headers => {types: [Array], allowed_element_types: [String], required: false},
45+
:api_timeout_connection => {types: [Integer, Float], required: false},
46+
:api_timeout => {types: [Integer, Float], required: false},
47+
:send_page_activities => {types: [FalseClass, TrueClass], required: false},
48+
:send_block_activities => {types: [FalseClass, TrueClass], required: false},
49+
:sdk_name => {types: [String], required: false},
50+
:debug => {types: [FalseClass, TrueClass], required: false},
51+
:module_mode => {types: [Integer], required: false},
52+
:sensitive_routes => {types: [Array], allowed_element_types: [String], required: false},
53+
:whitelist_routes => {types: [Array], allowed_element_types: [String, Regexp], required: false},
54+
:ip_headers => {types: [Array], allowed_element_types: [String], required: false},
55+
:ip_header_function => {types: [Proc], required: false},
56+
:bypass_monitor_header => {types: [FalseClass, TrueClass], required: false},
57+
:risk_cookie_max_iterations => {types: [Integer], required: false},
58+
:custom_verification_handler => {types: [Proc], required: false},
59+
:additional_activity_handler => {types: [Proc], required: false},
60+
:custom_logo => {types: [String], required: false},
61+
:css_ref => {types: [String], required: false},
62+
:js_ref => {types: [String], required: false},
63+
:custom_uri => {types: [Proc], required: false}
64+
}
65+
66+
def self.set_basic_config(basic_config)
67+
if @@basic_config.nil?
68+
@@mutex.synchronize {
69+
@@basic_config = PX_DEFAULT.merge(basic_config)
70+
}
71+
end
72+
end
73+
3674
def initialize(params)
37-
PX_DEFAULT[:backend_url] = "https://sapi-#{params[:app_id].downcase}.perimeterx.net"
38-
@configuration = PX_DEFAULT.merge(params)
75+
if ! @@basic_config.is_a?(Hash)
76+
raise PxConfigurationException.new('PerimeterX: Please initialize PerimeterX first')
77+
end
78+
79+
# merge request configuration into the basic configuration
80+
@configuration = @@basic_config.merge(params)
81+
validate_hash_schema(@configuration, CONFIG_SCHEMA)
82+
83+
@configuration[:backend_url] = "https://sapi-#{@configuration[:app_id].downcase}.perimeterx.net"
3984
@configuration[:logger] = PxLogger.new(@configuration[:debug])
4085
end
4186
end

lib/perimeterx/internal/clients/perimeter_x_activity_client.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ def send_to_perimeterx(activity_type, px_ctx, details = [])
1717
@px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
1818
end
1919

20+
if !px_ctx.context[:px_cookie].empty?
21+
details[:cookie_origin] = px_ctx.context[:cookie_origin]
22+
end
23+
2024
details[:module_version] = @px_config[:sdk_name]
21-
details[:cookie_origin] = px_ctx.context[:cookie_origin]
2225

2326
px_data = {
2427
:type => activity_type,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class PxConfigurationException < StandardError
2+
def initialize(msg)
3+
super(msg)
4+
end
5+
end
6+

lib/perimeterx/internal/perimeter_x_context.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ def initialize(px_config, req)
3737
if req.headers[PxModule::TOKEN_HEADER]
3838
@context[:cookie_origin] = 'header'
3939
token = force_utf8(req.headers[PxModule::TOKEN_HEADER])
40-
if token.include? ':'
41-
exploded_token = token.split(':', 2)
42-
cookie_sym = "v#{exploded_token[0]}".to_sym
43-
@context[:px_cookie][cookie_sym] = exploded_token[1]
44-
else # TOKEN_HEADER exists yet there's no ':' delimiter - may indicate an error (storing original value)
45-
@context[:px_cookie] = force_utf8(req.headers[PxModule::TOKEN_HEADER])
40+
if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
41+
@context[:px_cookie][:v3] = token[2..-1]
42+
elsif token.match(PxModule::MOBILE_ERROR_REGEX)
43+
@context[:mobile_error] = token
44+
if req.headers[PxModule::ORIGINAL_TOKEN_HEADER]
45+
token = force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
46+
if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
47+
@context[:px_cookie][:v3] = token[2..-1]
48+
end
49+
end
4650
end
4751
elsif !cookies.empty? # Get cookie from jar
4852
# Prepare hashed cookies

0 commit comments

Comments
 (0)