perimeter_x 2.1.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d65b91dbf3bdf1829dbfe7b1d44677d86ecde223bc0bd614618d1a9b1a188e84
4
- data.tar.gz: 473dc3f4a54232d59c2f89dcc6a1bddbb6a95de0268fddf53cfa28b47e8653d6
3
+ metadata.gz: 7321ac61fcf5c1b2b8a573eb23744d807e0af5e430a3468cb4704e8d36c7f8a9
4
+ data.tar.gz: 5bd64b4340535a52d2f91db99cabb673693dec8dbf1526dd00d2aa200743a444
5
5
  SHA512:
6
- metadata.gz: a89194401d9a60cd41fdb524724329bee547285742c218ffd157337203090ab66f4992fed517389868c639fea175a9c3561663b81b2463a9b1f3fb0011391e7b
7
- data.tar.gz: 428757d1a7dafc96543a125db63a8a2c7639ba27673f96bd5222b1628bafcd117f26d1fefb8041179f30e89f5ea286454ebbfb897b0d81d54e5be2be24d6fb8e
6
+ metadata.gz: 3037218a40b007fd7a0aaf93c6fe2e367b622042f2653c2e08e3c9a5d53d12d0107374d81e841d46e9320562d95c2ac4957040d7cb3a932c971ab2a1ff16aed2
7
+ data.tar.gz: 5de6563d962cee1d9ba90ac370ecb4a0a8679dfab9a1ab69b2dbcfaafe54d3ce05b12f31b8c57f1a5b129299e215dd0e8f7e488bbadf03308099104ec7f2ab60
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright © 2016 PerimeterX, Inc.
1
+ Copyright © 2022 PerimeterX, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/changelog.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [2.3.0] - 2022-04-10
9
+
10
+ ### Added
11
+
12
+ - Custom logo in block JSON response
13
+
14
+ ### Changed
15
+
16
+ - Updated block page to use new template
17
+
18
+ ## [2.2.1] - 2020-09-27
19
+ ### Fixed
20
+ - bypass_monitor_header type validation
21
+
22
+ ## [2.2.0] - 2020-09-15
23
+ ### Added
24
+ - First Party
25
+
8
26
  ## [2.1.0] - 2020-09-01
9
27
  ### Added
10
28
  - Added option to set a different px configuration on each request
data/lib/perimeter_x.rb CHANGED
@@ -12,13 +12,23 @@ require 'perimeterx/internal/clients/perimeter_x_activity_client'
12
12
  require 'perimeterx/internal/validators/perimeter_x_s2s_validator'
13
13
  require 'perimeterx/internal/validators/perimeter_x_cookie_validator'
14
14
  require 'perimeterx/internal/exceptions/px_config_exception'
15
+ require 'perimeterx/internal/first_party/px_first_party'
15
16
 
16
17
  module PxModule
17
18
  # Module expose API
18
19
  def px_verify_request(request_config={})
19
20
  begin
20
21
  px_instance = PerimeterX.new(request_config)
21
- px_ctx = px_instance.verify(request.env)
22
+ req = ActionDispatch::Request.new(request.env)
23
+
24
+ # handle first party requests
25
+ if px_instance.first_party.is_first_party_request(req)
26
+ render_first_party_response(req, px_instance)
27
+ return true
28
+ end
29
+
30
+ # verify request
31
+ px_ctx = px_instance.verify(req)
22
32
  px_config = px_instance.px_config
23
33
 
24
34
  msg_title = 'PxModule[px_verify_request]'
@@ -47,11 +57,23 @@ module PxModule
47
57
 
48
58
  is_mobile = px_ctx.context[:cookie_origin] == 'header' ? '1' : '0'
49
59
  action = px_ctx.context[:block_action][0,1]
50
-
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
- }
60
+ block_script_uri = "/captcha.js?a=#{action}&u=#{px_ctx.context[:uuid]}&v=#{px_ctx.context[:vid]}&m=#{is_mobile}"
61
+
62
+ if px_config[:first_party_enabled]
63
+ px_template_object = {
64
+ js_client_src: "/#{px_config[:app_id][2..-1]}/init.js",
65
+ block_script: "/#{px_config[:app_id][2..-1]}/captcha/#{px_config[:app_id]}#{block_script_uri}",
66
+ host_url: "/#{px_config[:app_id][2..-1]}/xhr",
67
+ alt_block_script: "//#{PxModule::ALT_CAPTCHA_HOST}/#{px_config[:app_id]}#{block_script_uri}"
68
+ }
69
+ else
70
+ px_template_object = {
71
+ js_client_src: "//#{PxModule::CLIENT_HOST}/#{px_config[:app_id]}/main.min.js",
72
+ block_script: "//#{PxModule::CAPTCHA_HOST}/#{px_config[:app_id]}#{block_script_uri}",
73
+ host_url: "https://collector-#{px_config[:app_id]}.perimeterx.net",
74
+ alt_block_script: "//#{PxModule::ALT_CAPTCHA_HOST}/#{px_config[:app_id]}#{block_script_uri}"
75
+ }
76
+ end
55
77
 
56
78
  html = PxTemplateFactory.get_template(px_ctx, px_config, px_template_object)
57
79
 
@@ -68,11 +90,13 @@ module PxModule
68
90
  hash_json = {
69
91
  :appId => px_config[:app_id],
70
92
  :jsClientSrc => px_template_object[:js_client_src],
71
- :firstPartyEnabled => false,
93
+ :firstPartyEnabled => px_ctx.context[:first_party_enabled],
72
94
  :uuid => px_ctx.context[:uuid],
73
95
  :vid => px_ctx.context[:vid],
74
96
  :hostUrl => "https://collector-#{px_config[:app_id]}.perimeterx.net",
75
97
  :blockScript => px_template_object[:block_script],
98
+ :altBlockScript => px_template_object[:alt_block_script],
99
+ :customLogo => px_config[:custom_logo]
76
100
  }
77
101
 
78
102
  render :json => hash_json
@@ -110,6 +134,29 @@ module PxModule
110
134
  end
111
135
  end
112
136
 
137
+ def render_first_party_response(req, px_instance)
138
+ fp = px_instance.first_party
139
+ px_config = px_instance.px_config
140
+
141
+ if px_config[:first_party_enabled]
142
+ # first party enabled - proxy response
143
+ fp_response = fp.send_first_party_request(req)
144
+ response.status = fp_response.code
145
+ fp_response.to_hash.each do |header_name, header_value_arr|
146
+ if header_name!="content-length"
147
+ response.headers[header_name] = header_value_arr[0]
148
+ end
149
+ end
150
+ res_type = fp.get_response_content_type(req)
151
+ render res_type => fp_response.body
152
+ else
153
+ # first party disabled - return empty response
154
+ response.status = 200
155
+ res_type = fp.get_response_content_type(req)
156
+ render res_type => ""
157
+ end
158
+ end
159
+
113
160
  def self.configure(basic_config)
114
161
  PerimeterX.set_basic_config(basic_config)
115
162
  end
@@ -119,6 +166,7 @@ module PxModule
119
166
  class PerimeterX
120
167
 
121
168
  attr_reader :px_config
169
+ attr_reader :first_party
122
170
  attr_accessor :px_http_client
123
171
  attr_accessor :px_activity_client
124
172
 
@@ -128,7 +176,7 @@ module PxModule
128
176
  end
129
177
 
130
178
  #Instance Methods
131
- def verify(env)
179
+ def verify(req)
132
180
  begin
133
181
 
134
182
  # check module_enabled
@@ -137,13 +185,11 @@ module PxModule
137
185
  @logger.warn('Module is disabled')
138
186
  return nil
139
187
  end
140
-
141
- req = ActionDispatch::Request.new(env)
142
188
 
143
189
  # filter whitelist routes
144
190
  url_path = URI.parse(req.original_url).path
145
191
  if url_path && !url_path.empty?
146
- if check_whitelist_routes(px_config[:whitelist_routes], url_path)
192
+ if check_whitelist_routes(px_config[:whitelist_routes], url_path)
147
193
  @logger.debug("PerimeterX[pxVerify]: whitelist route: #{url_path}")
148
194
  return nil
149
195
  end
@@ -176,6 +222,7 @@ module PxModule
176
222
  @px_http_client = PxHttpClient.new(@px_config)
177
223
 
178
224
  @px_activity_client = PerimeterxActivitiesClient.new(@px_config, @px_http_client)
225
+ @first_party = FirstPartyManager.new(@px_config, @px_http_client, @logger)
179
226
 
180
227
  @px_cookie_validator = PerimeterxCookieValidator.new(@px_config)
181
228
  @px_s2s_validator = PerimeterxS2SValidator.new(@px_config, @px_http_client)
@@ -30,7 +30,8 @@ module PxModule
30
30
  :ip_headers => [],
31
31
  :ip_header_function => nil,
32
32
  :bypass_monitor_header => nil,
33
- :risk_cookie_max_iterations => 5000
33
+ :risk_cookie_max_iterations => 5000,
34
+ :first_party_enabled => true
34
35
  }
35
36
 
36
37
  CONFIG_SCHEMA = {
@@ -53,14 +54,16 @@ module PxModule
53
54
  :whitelist_routes => {types: [Array], allowed_element_types: [String, Regexp], required: false},
54
55
  :ip_headers => {types: [Array], allowed_element_types: [String], required: false},
55
56
  :ip_header_function => {types: [Proc], required: false},
56
- :bypass_monitor_header => {types: [FalseClass, TrueClass], required: false},
57
+ :bypass_monitor_header => {types: [String], required: false},
57
58
  :risk_cookie_max_iterations => {types: [Integer], required: false},
58
59
  :custom_verification_handler => {types: [Proc], required: false},
59
60
  :additional_activity_handler => {types: [Proc], required: false},
60
61
  :custom_logo => {types: [String], required: false},
61
62
  :css_ref => {types: [String], required: false},
62
63
  :js_ref => {types: [String], required: false},
63
- :custom_uri => {types: [Proc], required: false}
64
+ :custom_uri => {types: [Proc], required: false},
65
+ :first_party_enabled => {types: [FalseClass, TrueClass], required: false}
66
+
64
67
  }
65
68
 
66
69
  def self.set_basic_config(basic_config)
@@ -0,0 +1,124 @@
1
+ require 'perimeterx/internal/perimeter_x_context'
2
+ module PxModule
3
+ class FirstPartyManager
4
+ def initialize(px_config, px_http_client, logger)
5
+ @px_config = px_config
6
+ @app_id = px_config[:app_id]
7
+ @px_http_client = px_http_client
8
+ @logger = logger
9
+ @from = [
10
+ "/#{@app_id[2..-1]}/init.js",
11
+ "/#{@app_id[2..-1]}/captcha",
12
+ "/#{@app_id[2..-1]}/xhr"
13
+ ]
14
+ end
15
+
16
+ def send_first_party_request(req)
17
+ uri = URI.parse(req.original_url)
18
+ url_path = uri.path
19
+
20
+ headers = extract_headers(req)
21
+ headers["x-px-first-party"] = "1"
22
+ headers["x-px-enforcer-true-ip"] = PerimeterXContext.extract_ip(req, @px_config)
23
+
24
+ if url_path.start_with?(@from[0])
25
+ return get_client(req, uri, headers)
26
+ elsif url_path.start_with?(@from[1])
27
+ return get_captcha(req, uri, headers)
28
+ elsif url_path.start_with?(@from[2])
29
+ return send_xhr(req, uri, headers)
30
+ else
31
+ return nil
32
+ end
33
+ end
34
+
35
+ def get_client(req, uri, headers)
36
+ @logger.debug("FirstPartyManager[get_client]")
37
+
38
+ # define host
39
+ headers["host"] = PxModule::CLIENT_HOST
40
+
41
+ # define request url
42
+ url = "#{uri.scheme}://#{PxModule::CLIENT_HOST}/#{@app_id}/main.min.js"
43
+
44
+ # send request
45
+ return @px_http_client.get(url, headers)
46
+ end
47
+
48
+ def get_captcha(req, uri, headers)
49
+ @logger.debug("FirstPartyManager[get_captcha]")
50
+
51
+ # define host
52
+ headers["host"] = PxModule::CAPTCHA_HOST
53
+
54
+ # define request url
55
+ path_and_query = uri.request_uri
56
+ uri_suffix = path_and_query.sub "/#{@app_id[2..-1]}/captcha", ""
57
+ url = "#{uri.scheme}://#{PxModule::CAPTCHA_HOST}#{uri_suffix}"
58
+
59
+ # send request
60
+ return @px_http_client.get(url, headers)
61
+ end
62
+
63
+ def send_xhr(req, uri, headers)
64
+ @logger.debug("FirstPartyManager[send_xhr]")
65
+
66
+ # handle vid cookies
67
+ if !req.cookies.nil?
68
+ if req.cookies.key?("_pxvid")
69
+ vid = PerimeterXContext.force_utf8(req.cookies["_pxvid"])
70
+ if headers.key?('cookie')
71
+ headers['cookie'] += "; pxvid=#{vid}";
72
+ else
73
+ headers['cookie'] = "pxvid=#{vid}";
74
+ end
75
+ end
76
+ end
77
+
78
+ # define host
79
+ headers["host"] = "collector-#{@app_id.downcase}.perimeterx.net"
80
+
81
+ # define request url
82
+ path_and_query = uri.request_uri
83
+ path_suffix = path_and_query.sub "/#{@app_id[2..-1]}/xhr", ""
84
+ url = "#{uri.scheme}://collector-#{@app_id.downcase}.perimeterx.net#{path_suffix}"
85
+
86
+ # send request
87
+ return @px_http_client.post_xhr(url, req.body.string, headers)
88
+ end
89
+
90
+ def extract_headers(req)
91
+ headers = Hash.new
92
+ req.headers.each do |k, v|
93
+ if (k.start_with? 'HTTP_') && (!@px_config[:sensitive_headers].include? k)
94
+ header = k.to_s.gsub('HTTP_', '')
95
+ header = header.gsub('_', '-').downcase
96
+ headers[header] = PerimeterXContext.force_utf8(v)
97
+ end
98
+ end
99
+ return headers
100
+ end
101
+
102
+ # -1 - not first party request
103
+ # 0 - /init.js
104
+ # 1 - /captcha
105
+ # 2 - /xhr
106
+ def get_first_party_request_type(req)
107
+ url_path = URI.parse(req.original_url).path
108
+ @from.each_with_index do |val,index|
109
+ if url_path.start_with?(val)
110
+ return index
111
+ end
112
+ end
113
+ return -1
114
+ end
115
+
116
+ def is_first_party_request(req)
117
+ return get_first_party_request_type(req) != -1
118
+ end
119
+
120
+ def get_response_content_type(req)
121
+ return get_first_party_request_type(req) == 2 ? :json : :js
122
+ end
123
+ end
124
+ end
@@ -6,6 +6,28 @@ module PxModule
6
6
 
7
7
  attr_accessor :context
8
8
  attr_accessor :px_config
9
+
10
+ # class methods
11
+
12
+ def self.extract_ip(req, px_config)
13
+ # Get IP from header/custom function
14
+ if px_config[:ip_headers].length() > 0
15
+ px_config[:ip_headers].each do |ip_header|
16
+ if req.headers[ip_header]
17
+ return PerimeterXContext.force_utf8(req.headers[ip_header])
18
+ end
19
+ end
20
+ elsif px_config[:ip_header_function] != nil
21
+ return px_config[:ip_header_function].call(req)
22
+ end
23
+ return req.ip
24
+ end
25
+
26
+ def self.force_utf8(str)
27
+ return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
28
+ end
29
+
30
+ # instance methods
9
31
 
10
32
  def initialize(px_config, req)
11
33
  @logger = px_config[:logger]
@@ -16,33 +38,22 @@ module PxModule
16
38
  @context[:headers] = Hash.new
17
39
  @context[:cookie_origin] = 'cookie'
18
40
  @context[:made_s2s_risk_api_call] = false
41
+ @context[:first_party_enabled] = px_config[:first_party_enabled]
42
+
19
43
  cookies = req.cookies
20
44
 
21
- # Get IP from header/custom function
22
- if px_config[:ip_headers].length() > 0
23
- px_config[:ip_headers].each do |ip_header|
24
- if req.headers[ip_header]
25
- @context[:ip] = force_utf8(req.headers[ip_header])
26
- end
27
- end
28
- elsif px_config[:ip_header_function] != nil
29
- @context[:ip] = px_config[:ip_header_function].call(req)
30
- end
31
-
32
- if @context[:ip] == nil
33
- @context[:ip] = req.ip
34
- end
45
+ @context[:ip] = PerimeterXContext.extract_ip(req, px_config)
35
46
 
36
47
  # Get token from header
37
48
  if req.headers[PxModule::TOKEN_HEADER]
38
49
  @context[:cookie_origin] = 'header'
39
- token = force_utf8(req.headers[PxModule::TOKEN_HEADER])
50
+ token = PerimeterXContext.force_utf8(req.headers[PxModule::TOKEN_HEADER])
40
51
  if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
41
52
  @context[:px_cookie][:v3] = token[2..-1]
42
53
  elsif token.match(PxModule::MOBILE_ERROR_REGEX)
43
54
  @context[:mobile_error] = token
44
55
  if req.headers[PxModule::ORIGINAL_TOKEN_HEADER]
45
- token = force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
56
+ token = PerimeterXContext.force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
46
57
  if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
47
58
  @context[:px_cookie][:v3] = token[2..-1]
48
59
  end
@@ -53,13 +64,13 @@ module PxModule
53
64
  cookies.each do |k, v|
54
65
  case k.to_s
55
66
  when '_px3'
56
- @context[:px_cookie][:v3] = force_utf8(v)
67
+ @context[:px_cookie][:v3] = PerimeterXContext.force_utf8(v)
57
68
  when '_px'
58
- @context[:px_cookie][:v1] = force_utf8(v)
69
+ @context[:px_cookie][:v1] = PerimeterXContext.force_utf8(v)
59
70
  when '_pxvid'
60
71
  if v.is_a?(String) && v.match(PxModule::VID_REGEX)
61
72
  @context[:vid_source] = "vid_cookie"
62
- @context[:vid] = force_utf8(v)
73
+ @context[:vid] = PerimeterXContext.force_utf8(v)
63
74
  end
64
75
  end
65
76
  end #end case
@@ -69,10 +80,10 @@ module PxModule
69
80
  if (k.start_with? 'HTTP_')
70
81
  header = k.to_s.gsub('HTTP_', '')
71
82
  header = header.gsub('_', '-').downcase
72
- @context[:headers][header.to_sym] = force_utf8(v)
83
+ @context[:headers][header.to_sym] = PerimeterXContext.force_utf8(v)
73
84
  end
74
85
  end #end headers foreach
75
-
86
+
76
87
  @context[:hostname]= req.server_name
77
88
  @context[:user_agent] = req.user_agent ? req.user_agent : ''
78
89
  @context[:uri] = px_config[:custom_uri] ? px_config[:custom_uri].call(req) : req.fullpath
@@ -97,10 +108,6 @@ module PxModule
97
108
  false
98
109
  end
99
110
 
100
- def force_utf8(str)
101
- return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
102
- end
103
-
104
111
  def set_block_action_type(action)
105
112
  @context[:block_action] = case action
106
113
  when 'c'
@@ -36,18 +36,19 @@ module PxModule
36
36
  PROP_APP_ID = :appId
37
37
  PROP_VID = :vid
38
38
  PROP_UUID = :uuid
39
- PROP_LOGO_VISIBILITY = :logoVisibility
40
39
  PROP_CUSTOM_LOGO = :customLogo
41
40
  PROP_CSS_REF = :cssRef
42
41
  PROP_JS_REF = :jsRef
43
42
  PROP_BLOCK_SCRIPT = :blockScript
43
+ PROP_ALT_BLOCK_SCRIPT = :altBlockScript
44
44
  PROP_JS_CLIENT_SRC = :jsClientSrc
45
45
  PROP_HOST_URL = :hostUrl
46
46
  PROP_FIRST_PARTY_ENABLED = :firstPartyEnabled
47
47
 
48
48
  # Hosts
49
- CLIENT_HOST = 'client.px-cloud.net'
50
- CAPTCHA_HOST = 'captcha.px-cloud.net'
49
+ CLIENT_HOST = 'client.perimeterx.net'
50
+ CAPTCHA_HOST = 'captcha.px-cdn.net'
51
+ ALT_CAPTCHA_HOST = 'captcha.px-cloud.net'
51
52
 
52
53
  VISIBLE = 'visible'
53
54
  HIDDEN = 'hidden'
@@ -1,6 +1,7 @@
1
1
  require 'perimeterx/utils/px_logger'
2
2
  require 'typhoeus'
3
3
  require 'concurrent'
4
+ require 'net/http'
4
5
 
5
6
  module PxModule
6
7
  class PxHttpClient
@@ -45,5 +46,61 @@ module PxModule
45
46
  return response
46
47
  end
47
48
 
49
+
50
+ def post_xhr(url, body, headers)
51
+ s = Time.now
52
+ begin
53
+ @logger.debug("PxHttpClient[post]: sending xhr post request to #{url} with headers {#{headers.to_json()}}")
54
+
55
+ #set url
56
+ uri = URI(url)
57
+ req = Net::HTTP::Post.new(uri)
58
+
59
+ # set body
60
+ req.body=body
61
+
62
+ # set headers
63
+ headers.each do |key, value|
64
+ req[key] = value
65
+ end
66
+
67
+ # send request
68
+ response = Net::HTTP.start(uri.hostname, uri.port) {|http|
69
+ http.request(req)
70
+ }
71
+
72
+ ensure
73
+ e = Time.now
74
+ @logger.debug("PxHttpClient[get]: runtime: #{(e-s) * 1000.0}")
75
+ end
76
+ return response
77
+ end
78
+
79
+
80
+ def get(url, headers)
81
+ s = Time.now
82
+ begin
83
+ @logger.debug("PxHttpClient[get]: sending get request to #{url} with headers {#{headers.to_json()}}")
84
+
85
+ #set url
86
+ uri = URI(url)
87
+ req = Net::HTTP::Get.new(uri)
88
+
89
+ # set headers
90
+ headers.each do |key, value|
91
+ req[key] = value
92
+ end
93
+
94
+ # send request
95
+ response = Net::HTTP.start(uri.hostname, uri.port) {|http|
96
+ http.request(req)
97
+ }
98
+
99
+ ensure
100
+ e = Time.now
101
+ @logger.debug("PxHttpClient[get]: runtime: #{(e-s) * 1000.0}")
102
+ end
103
+ return response
104
+ end
48
105
  end
49
106
  end
@@ -6,7 +6,7 @@ module PxModule
6
6
  def self.get_template(px_ctx, px_config, px_template_object)
7
7
  logger = px_config[:logger]
8
8
  if (px_config[:challenge_enabled] && px_ctx.context[:block_action] == 'challenge')
9
- logger.debug('PxTemplateFactory[get_template]: px challange triggered')
9
+ logger.debug('PxTemplateFactory[get_template]: px challenge triggered')
10
10
  return px_ctx.context[:block_action_data].html_safe
11
11
  end
12
12
 
@@ -23,17 +23,16 @@ module PxModule
23
23
  Mustache.template_file = "#{File.dirname(__FILE__) }/templates/#{template_type}#{PxModule::TEMPLATE_EXT}"
24
24
 
25
25
  view[PxModule::PROP_APP_ID] = px_config[:app_id]
26
- view[PxModule::PROP_REF_ID] = px_ctx.context[:uuid]
27
26
  view[PxModule::PROP_VID] = px_ctx.context[:vid]
28
27
  view[PxModule::PROP_UUID] = px_ctx.context[:uuid]
29
28
  view[PxModule::PROP_CUSTOM_LOGO] = px_config[:custom_logo]
30
29
  view[PxModule::PROP_CSS_REF] = px_config[:css_ref]
31
30
  view[PxModule::PROP_JS_REF] = px_config[:js_ref]
32
- view[PxModule::PROP_HOST_URL] = "https://collector-#{px_config[:app_id]}.perimeterx.net"
33
- view[PxModule::PROP_LOGO_VISIBILITY] = px_config[:custom_logo] ? PxModule::VISIBLE : PxModule::HIDDEN
31
+ view[PxModule::PROP_HOST_URL] = px_template_object[:host_url]
34
32
  view[PxModule::PROP_BLOCK_SCRIPT] = px_template_object[:block_script]
33
+ view[PxModule::PROP_ALT_BLOCK_SCRIPT] = px_template_object[:alt_block_script]
35
34
  view[PxModule::PROP_JS_CLIENT_SRC] = px_template_object[:js_client_src]
36
- view[PxModule::PROP_FIRST_PARTY_ENABLED] = false
35
+ view[PxModule::PROP_FIRST_PARTY_ENABLED] = px_ctx.context[:first_party_enabled]
37
36
 
38
37
  return view.render.html_safe
39
38
  end
@@ -3,173 +3,42 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <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>
89
8
  {{#cssRef}}
90
9
  <link rel="stylesheet" type="text/css" href="{{{cssRef}}}"/>
91
10
  {{/cssRef}}
92
11
  </head>
93
-
94
12
  <body>
95
- <section class="container">
96
- <div class="customer-logo-wrapper">
97
- <div class="customer-logo">
98
- <img src="{{customLogo}}" alt="Logo"/>
99
- </div>
100
- </div>
101
- <div class="page-title-wrapper">
102
- <div class="page-title">
103
- <h1>Please verify you are a human</h1>
104
- </div>
105
- </div>
106
- <div class="content-wrapper">
107
- <div class="content">
108
-
109
- <div id="px-captcha">
110
- </div>
111
- <p>
112
- Access to this page has been denied because we believe you are using automation tools to browse the
113
- website.
114
- </p>
115
- <p>
116
- This may happen as a result of the following:
117
- </p>
118
- <ul>
119
- <li>
120
- Javascript is disabled or blocked by an extension (ad blockers for example)
121
- </li>
122
- <li>
123
- Your browser does not support cookies
124
- </li>
125
- </ul>
126
- <p>
127
- Please make sure that Javascript and cookies are enabled on your browser and that you are not blocking
128
- them from loading.
129
- </p>
130
- <p>
131
- Reference ID: #{{refId}}
132
- </p>
133
- </div>
134
- </div>
135
- <div class="page-footer-wrapper">
136
- <div class="page-footer">
137
- <p>
138
- Powered by
139
- <a href="https://www.perimeterx.com/whywasiblocked">PerimeterX</a>
140
- , Inc.
141
- </p>
142
- </div>
143
- </div>
144
- </section>
145
- <!-- Px -->
146
- <script>
147
- window._pxAppId = '{{appId}}';
148
- window._pxJsClientSrc = '{{{jsClientSrc}}}';
149
- window._pxFirstPartyEnabled = {{firstPartyEnabled}};
150
- window._pxVid = '{{vid}}';
151
- window._pxUuid = '{{uuid}}';
152
- window._pxHostUrl = '{{{hostUrl}}}';
153
- </script>
154
- <script>
155
- var s = document.createElement('script');
156
- s.src = '{{{blockScript}}}';
157
- var p = document.getElementsByTagName('head')[0];
158
- p.insertBefore(s, null);
159
- if ({{firstPartyEnabled}}) {
160
- s.onerror = function () {
161
- s = document.createElement('script');
162
- var suffixIndex = '{{{blockScript}}}'.indexOf('captcha.js');
163
- var temperedBlockScript = '{{{blockScript}}}'.substring(suffixIndex);
164
- s.src = '//captcha.px-cdn.net/{{appId}}/' + temperedBlockScript;
165
- p.parentNode.insertBefore(s, p);
13
+ <script>
14
+ window._pxVid = '{{vid}}';
15
+ window._pxUuid = '{{uuid}}';
16
+ window._pxAppId = '{{appId}}';
17
+ window._pxCustomLogo = '{{customLogo}}';
18
+ window._pxHostUrl = '{{hostUrl}}';
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);
166
29
  };
167
- }
168
- </script>
169
-
170
- <!-- Custom Script -->
171
- {{#jsRef}}
172
- <script src="{{{jsRef}}}"></script>
173
- {{/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}@media (max-width:480px){body{background-color:#fff}}.px-captcha-error-container{position:fixed;height:328px;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}div.px-captcha-error-button{text-align:center;line-height:50px;width:253px;margin:auto;border-radius:25px;border:solid 1px #f0f1f2;font-size:20px;color:#f0f1f2}div.px-captcha-error-wrapper{margin:23px 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 10px -2px 0}@media (min-width:620px){.px-captcha-error-container{width:528px;top:50%;left:50%;margin-top:-164px;margin-left:-264px;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:-164px;margin-left:-42.5%;border-radius:3px;box-shadow:0 2px 9px -1px rgba(0,0,0,.13)}}@media (max-width:480px){.px-captcha-error-container{width:528px;top:50%;left:50%;margin-top:-164px;margin-left:-264px}}';
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 network connection or disable your ad-blocker.</div></div>';
37
+ document.body.appendChild(div);
38
+ };
39
+ </script>
40
+ {{#jsRef}}
41
+ <script src="{{{jsRef}}}"></script>
42
+ {{/jsRef}}
174
43
  </body>
175
- </html>
44
+ </html>
@@ -1,3 +1,3 @@
1
1
  module PxModule
2
- VERSION = '2.1.0'
2
+ VERSION = '2.3.0'
3
3
  end
data/px_metadata.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "version": "2.3.0",
3
+ "supported_features": [
4
+ "additional_activity_handler",
5
+ "advanced_blocking_response",
6
+ "batched_activities",
7
+ "block_activity",
8
+ "block_page_captcha",
9
+ "block_page_rate_limit",
10
+ "bypass_monitor_header",
11
+ "client_ip_extraction",
12
+ "cookie_v3",
13
+ "css_ref",
14
+ "custom_logo",
15
+ "logger",
16
+ "filter_by_route",
17
+ "first_party",
18
+ "js_ref",
19
+ "mobile_support",
20
+ "module_enable",
21
+ "module_mode",
22
+ "page_requested_activity",
23
+ "vid_extraction",
24
+ "risk_api",
25
+ "sensitive_headers",
26
+ "sensitive_routes"
27
+ ]
28
+ }
data/readme.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [PerimeterX](http://www.perimeterx.com) Ruby SDK
6
6
  =============================================================
7
7
 
8
- > Latest stable version: [v2.1.0](https://rubygems.org/gems/perimeter_x)
8
+ > Latest stable version: [v2.3.0](https://rubygems.org/gems/perimeter_x)
9
9
 
10
10
  Table of Contents
11
11
  -----------------
@@ -19,8 +19,6 @@ Table of Contents
19
19
  * [Blocking Score](#blocking-score)
20
20
  * [Custom Verification Action](#custom-verification-action)
21
21
  * [Custom Block Page](#custom-block-page)
22
- * [Enable/Disable Captcha](#captcha-support)
23
- * [Select Captcha Provider](#captcha-provider)
24
22
  * [Extracting Real IP Address](#real-ip)
25
23
  * [Custom URI](#custom-uri)
26
24
  * [Filter Sensitive Headers](#sensitive-headers)
@@ -31,7 +29,9 @@ Table of Contents
31
29
  * [Debug Mode](#debug-mode)
32
30
  * [Whitelist Routes](#whitelist-routes)
33
31
  * [Update Configuration on Runtime](#update-config)
32
+ * [First Party](#first-party)
34
33
 
34
+ **[Additional Information](#additional-information)**
35
35
  **[Contributing](#contributing)**
36
36
 
37
37
  <a name="Usage"></a>
@@ -224,26 +224,6 @@ Default mode: PxModule::ACTIVE_MODE
224
224
  params[:module_mode] = PxModule::MONITOR_MODE
225
225
  ```
226
226
 
227
- <a name="captcha-support"></a>**Enable/Disable CAPTCHA on the block page**
228
- Default mode: enabled
229
-
230
- By enabling CAPTCHA support, a CAPTCHA will be served as part of the block page, giving real users the ability to identify as a human. By solving the CAPTCHA, the user's score is then cleaned up and the user is allowed to continue normal use.
231
-
232
- ```ruby
233
- params[:captcha_enabled] = false
234
- ```
235
-
236
- <a name="captcha-provider"></a>**Select CAPTCHA Provider**
237
-
238
- The CAPTCHA part of the block page can use one of the following:
239
- * [reCAPTCHA](https://www.google.com/recaptcha)
240
-
241
- Default: 'reCaptcha'
242
-
243
- ```ruby
244
- captchaProvider = "reCaptcha"
245
- ```
246
-
247
227
  <a name="custom-uri"></a>**Custom URI**
248
228
 
249
229
  Default: 'REQUEST_URI'
@@ -325,11 +305,12 @@ However, it is possible to override configuration options on each request.
325
305
  To do so, send the configuration options as an argument when calling to `px_verify_request` as described in the following example.
326
306
  Notice that in case of an invalid argument, the module will raise an error. Therefore, when using this feature, make sure to wrap the call to `px_verify_request` with begin and rescue. It is highly recommended to log the error message to follow such errors.
327
307
 
308
+ Usage example:
309
+
328
310
  ```ruby
329
311
  class HomeController < ApplicationController
330
312
  include PxModule
331
313
 
332
-
333
314
  before_action do call_perimeterx_verify_request end
334
315
 
335
316
  def call_perimeterx_verify_request
@@ -347,7 +328,35 @@ class HomeController < ApplicationController
347
328
  end
348
329
  ```
349
330
 
350
- <a name="contributing"></a># Contributing #
331
+ <a name="first-party"></a>**First Party**
332
+
333
+ To enable first party on your enforcer, add the following routes to your `config/routes.rb` file:
334
+
335
+ ```ruby
336
+ get '/:appid_postfix/init.js', to: 'home#index', constraints: { appid_postfix: /XXXXXXXX/ }
337
+ get '/:appid_postfix/captcha/:all', to: 'home#index', constraints: { appid_postfix: /XXXXXXXX/, all:/.*/ }
338
+ post '/:appid_postfix/xhr/:all', to: 'home#index', constraints: { appid_postfix: /XXXXXXXX/, all:/.*/ }
339
+ ```
340
+
341
+ Notice that all occurences of `XXXXXXXX` should be replaced with your px_app_id without the "PX" prefix. For example, if your px_app_id is `PX2H4seK9L`, replace `XXXXXXXX` with `2H4seK9L`.
342
+
343
+ In case you are using more than one px_app_id, provide all of them with a `|` sign between them. For example: 2H4seK9L|9bMs6K94|Lc5kPMNx
344
+
345
+
346
+ First Party configuration:
347
+
348
+ Default: true
349
+
350
+ ```ruby
351
+ params[:first_party_enabled] = false
352
+ ```
353
+
354
+ <a name="additional_information"></a> Additional Information
355
+ ------------------------------
356
+ ### URI Delimiters
357
+ PerimeterX processes URI paths with general- and sub-delimiters according to RFC 3986. General delimiters (e.g., `?`, `#`) are used to separate parts of the URI. Sub-delimiters (e.g., `$`, `&`) are not used to split the URI as they are considered valid characters in the URI path.
358
+
359
+ <a name="contributing"></a> Contributing
351
360
  ------------------------------
352
361
  The following steps are welcome when contributing to our project.
353
362
  ###Fork/Clone
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perimeter_x
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nitzan Goldfeder
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2022-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -173,6 +173,7 @@ files:
173
173
  - lib/perimeterx/internal/clients/perimeter_x_risk_client.rb
174
174
  - lib/perimeterx/internal/exceptions/px_config_exception.rb
175
175
  - lib/perimeterx/internal/exceptions/px_cookie_decryption_exception.rb
176
+ - lib/perimeterx/internal/first_party/px_first_party.rb
176
177
  - lib/perimeterx/internal/payload/perimeter_x_cookie_v1.rb
177
178
  - lib/perimeterx/internal/payload/perimeter_x_cookie_v3.rb
178
179
  - lib/perimeterx/internal/payload/perimeter_x_payload.rb
@@ -190,6 +191,7 @@ files:
190
191
  - lib/perimeterx/utils/templates/ratelimit.mustache
191
192
  - lib/perimeterx/version.rb
192
193
  - perimeter_x.gemspec
194
+ - px_metadata.json
193
195
  - readme.md
194
196
  homepage: https://www.perimeterx.com
195
197
  licenses:
@@ -214,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
216
  - !ruby/object:Gem::Version
215
217
  version: '0'
216
218
  requirements: []
217
- rubygems_version: 3.0.3
219
+ rubygems_version: 3.0.3.1
218
220
  signing_key:
219
221
  specification_version: 4
220
222
  summary: PerimeterX ruby implmentation