Skip to content

Commit

Permalink
Merge pull request #52 from PerimeterX/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
chennakarpx authored Sep 15, 2020
2 parents 7dc3f27 + 1fb6370 commit 9e23de7
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 42 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.2.0] - 2020-09-15
### Added
- First Party

## [2.1.0] - 2020-09-01
### Added
- Added option to set a different px configuration on each request
Expand Down
62 changes: 52 additions & 10 deletions lib/perimeter_x.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@
require 'perimeterx/internal/validators/perimeter_x_s2s_validator'
require 'perimeterx/internal/validators/perimeter_x_cookie_validator'
require 'perimeterx/internal/exceptions/px_config_exception'
require 'perimeterx/internal/first_party/px_first_party'

module PxModule
# Module expose API
def px_verify_request(request_config={})
begin
px_instance = PerimeterX.new(request_config)
px_ctx = px_instance.verify(request.env)
req = ActionDispatch::Request.new(request.env)

# handle first party requests
if px_instance.first_party.is_first_party_request(req)
render_first_party_response(req, px_instance)
return true
end

# verify request
px_ctx = px_instance.verify(req)
px_config = px_instance.px_config

msg_title = 'PxModule[px_verify_request]'
Expand Down Expand Up @@ -46,12 +56,21 @@ def px_verify_request(request_config={})
end

is_mobile = px_ctx.context[:cookie_origin] == 'header' ? '1' : '0'
action = px_ctx.context[:block_action][0,1]
action = px_ctx.context[:block_action][0,1]

px_template_object = {
if px_config[:first_party_enabled]
px_template_object = {
js_client_src: "/#{px_config[:app_id][2..-1]}/init.js",
block_script: "/#{px_config[:app_id][2..-1]}/captcha/#{px_config[:app_id]}/captcha.js?a=#{action}&u=#{px_ctx.context[:uuid]}&v=#{px_ctx.context[:vid]}&m=#{is_mobile}",
host_url: "/#{px_config[:app_id][2..-1]}/xhr"
}
else
px_template_object = {
js_client_src: "//#{PxModule::CLIENT_HOST}/#{px_config[:app_id]}/main.min.js",
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}",
js_client_src: "//#{PxModule::CLIENT_HOST}/#{px_config[:app_id]}/main.min.js"
}
host_url: "https://collector-#{px_config[:app_id]}.perimeterx.net"
}
end

html = PxTemplateFactory.get_template(px_ctx, px_config, px_template_object)

Expand All @@ -68,7 +87,7 @@ def px_verify_request(request_config={})
hash_json = {
:appId => px_config[:app_id],
:jsClientSrc => px_template_object[:js_client_src],
:firstPartyEnabled => false,
:firstPartyEnabled => px_ctx.context[:first_party_enabled],
:uuid => px_ctx.context[:uuid],
:vid => px_ctx.context[:vid],
:hostUrl => "https://collector-#{px_config[:app_id]}.perimeterx.net",
Expand Down Expand Up @@ -110,6 +129,29 @@ def px_verify_request(request_config={})
end
end

def render_first_party_response(req, px_instance)
fp = px_instance.first_party
px_config = px_instance.px_config

if px_config[:first_party_enabled]
# first party enabled - proxy response
fp_response = fp.send_first_party_request(req)
response.status = fp_response.code
fp_response.to_hash.each do |header_name, header_value_arr|
if header_name!="content-length"
response.headers[header_name] = header_value_arr[0]
end
end
res_type = fp.get_response_content_type(req)
render res_type => fp_response.body
else
# first party disabled - return empty response
response.status = 200
res_type = fp.get_response_content_type(req)
render res_type => ""
end
end

def self.configure(basic_config)
PerimeterX.set_basic_config(basic_config)
end
Expand All @@ -119,6 +161,7 @@ def self.configure(basic_config)
class PerimeterX

attr_reader :px_config
attr_reader :first_party
attr_accessor :px_http_client
attr_accessor :px_activity_client

Expand All @@ -128,7 +171,7 @@ def self.set_basic_config(basic_config)
end

#Instance Methods
def verify(env)
def verify(req)
begin

# check module_enabled
Expand All @@ -137,13 +180,11 @@ def verify(env)
@logger.warn('Module is disabled')
return nil
end

req = ActionDispatch::Request.new(env)

# filter whitelist routes
url_path = URI.parse(req.original_url).path
if url_path && !url_path.empty?
if check_whitelist_routes(px_config[:whitelist_routes], url_path)
if check_whitelist_routes(px_config[:whitelist_routes], url_path)
@logger.debug("PerimeterX[pxVerify]: whitelist route: #{url_path}")
return nil
end
Expand Down Expand Up @@ -176,6 +217,7 @@ def initialize(request_config)
@px_http_client = PxHttpClient.new(@px_config)

@px_activity_client = PerimeterxActivitiesClient.new(@px_config, @px_http_client)
@first_party = FirstPartyManager.new(@px_config, @px_http_client, @logger)

@px_cookie_validator = PerimeterxCookieValidator.new(@px_config)
@px_s2s_validator = PerimeterxS2SValidator.new(@px_config, @px_http_client)
Expand Down
7 changes: 5 additions & 2 deletions lib/perimeterx/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class Configuration
:ip_headers => [],
:ip_header_function => nil,
:bypass_monitor_header => nil,
:risk_cookie_max_iterations => 5000
:risk_cookie_max_iterations => 5000,
:first_party_enabled => true
}

CONFIG_SCHEMA = {
Expand Down Expand Up @@ -60,7 +61,9 @@ class Configuration
:custom_logo => {types: [String], required: false},
:css_ref => {types: [String], required: false},
:js_ref => {types: [String], required: false},
:custom_uri => {types: [Proc], required: false}
:custom_uri => {types: [Proc], required: false},
:first_party_enabled => {types: [FalseClass, TrueClass], required: false}

}

def self.set_basic_config(basic_config)
Expand Down
124 changes: 124 additions & 0 deletions lib/perimeterx/internal/first_party/px_first_party.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
require 'perimeterx/internal/perimeter_x_context'
module PxModule
class FirstPartyManager
def initialize(px_config, px_http_client, logger)
@px_config = px_config
@app_id = px_config[:app_id]
@px_http_client = px_http_client
@logger = logger
@from = [
"/#{@app_id[2..-1]}/init.js",
"/#{@app_id[2..-1]}/captcha",
"/#{@app_id[2..-1]}/xhr"
]
end

def send_first_party_request(req)
uri = URI.parse(req.original_url)
url_path = uri.path

headers = extract_headers(req)
headers["x-px-first-party"] = "1"
headers["x-px-enforcer-true-ip"] = PerimeterXContext.extract_ip(req, @px_config)

if url_path.start_with?(@from[0])
return get_client(req, uri, headers)
elsif url_path.start_with?(@from[1])
return get_captcha(req, uri, headers)
elsif url_path.start_with?(@from[2])
return send_xhr(req, uri, headers)
else
return nil
end
end

def get_client(req, uri, headers)
@logger.debug("FirstPartyManager[get_client]")

# define host
headers["host"] = PxModule::CLIENT_HOST

# define request url
url = "#{uri.scheme}://#{PxModule::CLIENT_HOST}/#{@app_id}/main.min.js"

# send request
return @px_http_client.get(url, headers)
end

def get_captcha(req, uri, headers)
@logger.debug("FirstPartyManager[get_captcha]")

# define host
headers["host"] = PxModule::CAPTCHA_HOST

# define request url
path_and_query = uri.request_uri
uri_suffix = path_and_query.sub "/#{@app_id[2..-1]}/captcha", ""
url = "#{uri.scheme}://#{PxModule::CAPTCHA_HOST}#{uri_suffix}"

# send request
return @px_http_client.get(url, headers)
end

def send_xhr(req, uri, headers)
@logger.debug("FirstPartyManager[send_xhr]")

# handle vid cookies
if !req.cookies.nil?
if req.cookies.key?("_pxvid")
vid = PerimeterXContext.force_utf8(req.cookies["_pxvid"])
if headers.key?('cookie')
headers['cookie'] += "; pxvid=#{vid}";
else
headers['cookie'] = "pxvid=#{vid}";
end
end
end

# define host
headers["host"] = "collector-#{@app_id.downcase}.perimeterx.net"

# define request url
path_and_query = uri.request_uri
path_suffix = path_and_query.sub "/#{@app_id[2..-1]}/xhr", ""
url = "#{uri.scheme}://collector-#{@app_id.downcase}.perimeterx.net#{path_suffix}"

# send request
return @px_http_client.post_xhr(url, req.body.string, headers)
end

def extract_headers(req)
headers = Hash.new
req.headers.each do |k, v|
if (k.start_with? 'HTTP_') && (!@px_config[:sensitive_headers].include? k)
header = k.to_s.gsub('HTTP_', '')
header = header.gsub('_', '-').downcase
headers[header] = PerimeterXContext.force_utf8(v)
end
end
return headers
end

# -1 - not first party request
# 0 - /init.js
# 1 - /captcha
# 2 - /xhr
def get_first_party_request_type(req)
url_path = URI.parse(req.original_url).path
@from.each_with_index do |val,index|
if url_path.start_with?(val)
return index
end
end
return -1
end

def is_first_party_request(req)
return get_first_party_request_type(req) != -1
end

def get_response_content_type(req)
return get_first_party_request_type(req) == 2 ? :json : :js
end
end
end
57 changes: 32 additions & 25 deletions lib/perimeterx/internal/perimeter_x_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ class PerimeterXContext

attr_accessor :context
attr_accessor :px_config

# class methods

def self.extract_ip(req, px_config)
# Get IP from header/custom function
if px_config[:ip_headers].length() > 0
px_config[:ip_headers].each do |ip_header|
if req.headers[ip_header]
return PerimeterXContext.force_utf8(req.headers[ip_header])
end
end
elsif px_config[:ip_header_function] != nil
return px_config[:ip_header_function].call(req)
end
return req.ip
end

def self.force_utf8(str)
return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
end

# instance methods

def initialize(px_config, req)
@logger = px_config[:logger]
Expand All @@ -16,33 +38,22 @@ def initialize(px_config, req)
@context[:headers] = Hash.new
@context[:cookie_origin] = 'cookie'
@context[:made_s2s_risk_api_call] = false
@context[:first_party_enabled] = px_config[:first_party_enabled]

cookies = req.cookies

# Get IP from header/custom function
if px_config[:ip_headers].length() > 0
px_config[:ip_headers].each do |ip_header|
if req.headers[ip_header]
@context[:ip] = force_utf8(req.headers[ip_header])
end
end
elsif px_config[:ip_header_function] != nil
@context[:ip] = px_config[:ip_header_function].call(req)
end

if @context[:ip] == nil
@context[:ip] = req.ip
end
@context[:ip] = PerimeterXContext.extract_ip(req, px_config)

# Get token from header
if req.headers[PxModule::TOKEN_HEADER]
@context[:cookie_origin] = 'header'
token = force_utf8(req.headers[PxModule::TOKEN_HEADER])
token = PerimeterXContext.force_utf8(req.headers[PxModule::TOKEN_HEADER])
if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
@context[:px_cookie][:v3] = token[2..-1]
elsif token.match(PxModule::MOBILE_ERROR_REGEX)
@context[:mobile_error] = token
if req.headers[PxModule::ORIGINAL_TOKEN_HEADER]
token = force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
token = PerimeterXContext.force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
@context[:px_cookie][:v3] = token[2..-1]
end
Expand All @@ -53,13 +64,13 @@ def initialize(px_config, req)
cookies.each do |k, v|
case k.to_s
when '_px3'
@context[:px_cookie][:v3] = force_utf8(v)
@context[:px_cookie][:v3] = PerimeterXContext.force_utf8(v)
when '_px'
@context[:px_cookie][:v1] = force_utf8(v)
@context[:px_cookie][:v1] = PerimeterXContext.force_utf8(v)
when '_pxvid'
if v.is_a?(String) && v.match(PxModule::VID_REGEX)
@context[:vid_source] = "vid_cookie"
@context[:vid] = force_utf8(v)
@context[:vid] = PerimeterXContext.force_utf8(v)
end
end
end #end case
Expand All @@ -69,10 +80,10 @@ def initialize(px_config, req)
if (k.start_with? 'HTTP_')
header = k.to_s.gsub('HTTP_', '')
header = header.gsub('_', '-').downcase
@context[:headers][header.to_sym] = force_utf8(v)
@context[:headers][header.to_sym] = PerimeterXContext.force_utf8(v)
end
end #end headers foreach

@context[:hostname]= req.server_name
@context[:user_agent] = req.user_agent ? req.user_agent : ''
@context[:uri] = px_config[:custom_uri] ? px_config[:custom_uri].call(req) : req.fullpath
Expand All @@ -97,10 +108,6 @@ def check_sensitive_route(sensitive_routes, uri)
false
end

def force_utf8(str)
return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
end

def set_block_action_type(action)
@context[:block_action] = case action
when 'c'
Expand Down
Loading

0 comments on commit 9e23de7

Please sign in to comment.