perimeter_x 1.3.0 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +1 -1
- data/Dockerfile +12 -7
- data/Gemfile.lock +32 -31
- data/changelog.md +55 -1
- data/lib/perimeter_x.rb +186 -82
- data/lib/perimeterx/configuration.rb +74 -22
- data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +29 -6
- data/lib/perimeterx/internal/exceptions/px_config_exception.rb +6 -0
- data/lib/perimeterx/internal/first_party/px_first_party.rb +124 -0
- data/lib/perimeterx/internal/payload/perimeter_x_payload.rb +3 -0
- data/lib/perimeterx/internal/perimeter_x_context.rb +50 -19
- data/lib/perimeterx/internal/validators/hash_schema_validator.rb +26 -0
- data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +17 -12
- data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +30 -7
- data/lib/perimeterx/utils/px_constants.rb +17 -5
- data/lib/perimeterx/utils/px_http_client.rb +59 -2
- data/lib/perimeterx/utils/px_template_factory.rb +13 -9
- data/lib/perimeterx/utils/templates/{captcha.mobile.mustache → block_template.mustache} +48 -69
- data/lib/perimeterx/utils/templates/ratelimit.mustache +9 -0
- data/lib/perimeterx/version.rb +1 -1
- data/perimeter_x.gemspec +3 -3
- data/readme.md +106 -40
- metadata +18 -19
- data/lib/perimeterx/internal/validators/perimeter_x_captcha_validator.rb +0 -65
- data/lib/perimeterx/utils/templates/block.mobile.mustache +0 -133
- data/lib/perimeterx/utils/templates/block.mustache +0 -146
- data/lib/perimeterx/utils/templates/captcha.mustache +0 -185
data/lib/perimeterx/version.rb
CHANGED
data/perimeter_x.gemspec
CHANGED
@@ -22,8 +22,8 @@ Gem::Specification.new do |gem|
|
|
22
22
|
gem.bindir = "exe"
|
23
23
|
gem.executables = gem.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
24
|
gem.require_paths = ["lib"]
|
25
|
-
gem.add_development_dependency "bundler", "
|
26
|
-
gem.add_development_dependency "rake", "
|
25
|
+
gem.add_development_dependency "bundler", ">= 2.1"
|
26
|
+
gem.add_development_dependency "rake", ">= 12.3"
|
27
27
|
|
28
28
|
gem.extra_rdoc_files = ["readme.md", "changelog.md"]
|
29
29
|
gem.rdoc_options = ["--line-numbers", "--inline-source", "--title", "PerimeterX"]
|
@@ -33,7 +33,7 @@ Gem::Specification.new do |gem|
|
|
33
33
|
gem.add_dependency('concurrent-ruby', '~> 1.0', '>= 1.0.5')
|
34
34
|
gem.add_dependency('typhoeus', '~> 1.1', '>= 1.1.2')
|
35
35
|
gem.add_dependency('mustache', '~> 1.0', '>= 1.0.3')
|
36
|
-
gem.add_dependency('activesupport', '>=
|
36
|
+
gem.add_dependency('activesupport', '>= 5.2.4.3')
|
37
37
|
|
38
38
|
gem.add_development_dependency 'rspec', '~> 3.0'
|
39
39
|
gem.add_development_dependency 'mocha', '~> 1.2', '>= 1.2.1'
|
data/readme.md
CHANGED
@@ -1,20 +1,24 @@
|
|
1
|
-
![
|
1
|
+
[![Build Status](https://travis-ci.org/PerimeterX/perimeterx-ruby-sdk.svg?branch=master)](https://travis-ci.org/PerimeterX/perimeterx-ruby-sdk)
|
2
|
+
|
3
|
+
![image](https://storage.googleapis.com/perimeterx-logos/primary_logo_red_cropped.png)
|
2
4
|
#
|
3
5
|
[PerimeterX](http://www.perimeterx.com) Ruby SDK
|
4
6
|
=============================================================
|
5
7
|
|
8
|
+
> Latest stable version: [v2.2.1](https://rubygems.org/gems/perimeter_x)
|
9
|
+
|
6
10
|
Table of Contents
|
7
11
|
-----------------
|
8
|
-
|
12
|
+
**[Usage](#usage)**
|
9
13
|
* [Dependencies](#dependencies)
|
10
14
|
* [Installation](#installation)
|
11
15
|
* [Basic Usage Example](#basic-usage)
|
12
|
-
|
16
|
+
|
17
|
+
**[Configuration](#configuration)**
|
13
18
|
* [Configuring Required Parameters](#requireied-params)
|
14
19
|
* [Blocking Score](#blocking-score)
|
20
|
+
* [Custom Verification Action](#custom-verification-action)
|
15
21
|
* [Custom Block Page](#custom-block-page)
|
16
|
-
* [Custom Block Action](#custom-block-action)
|
17
|
-
* [Enable/Disable Captcha](#captcha-support)
|
18
22
|
* [Extracting Real IP Address](#real-ip)
|
19
23
|
* [Custom URI](#custom-uri)
|
20
24
|
* [Filter Sensitive Headers](#sensitive-headers)
|
@@ -23,7 +27,11 @@ Table of Contents
|
|
23
27
|
* [Additional Page Activity Handler](#additional-page-activity-handler)
|
24
28
|
* [Monitor Only](#logging)
|
25
29
|
* [Debug Mode](#debug-mode)
|
26
|
-
|
30
|
+
* [Whitelist Routes](#whitelist-routes)
|
31
|
+
* [Update Configuration on Runtime](#update-config)
|
32
|
+
* [First Party](#first-party)
|
33
|
+
|
34
|
+
**[Contributing](#contributing)**
|
27
35
|
|
28
36
|
<a name="Usage"></a>
|
29
37
|
<a name="dependencies"></a> Dependencies
|
@@ -80,7 +88,7 @@ All parameters are obtainable via the PerimeterX Portal. (Applications and Polic
|
|
80
88
|
|
81
89
|
<a name="blocking-score"></a>**Changing the Minimum Score for Blocking**
|
82
90
|
|
83
|
-
>Note: Default blocking value:
|
91
|
+
>Note: Default blocking value: 100
|
84
92
|
|
85
93
|
```ruby
|
86
94
|
params = {
|
@@ -90,69 +98,71 @@ params = {
|
|
90
98
|
}
|
91
99
|
```
|
92
100
|
|
101
|
+
<a name="custom-verification-action"></a>**Custom Verification Handler**
|
93
102
|
|
103
|
+
> Note: This handler replaces the now deprecated `custom_block_handler`.
|
94
104
|
|
95
|
-
|
105
|
+
A custom verification handler is being executed inside `px_verify_request` instead of the the default behavior and allows a user to use a custom action based on the risk score returned by PerimeterX.
|
96
106
|
|
97
|
-
|
107
|
+
When implemented, this method receives a hash variable as input which represents data from the PerimeterX context of the request (px_ctx).
|
98
108
|
|
99
|
-
|
109
|
+
- `px_ctx.context[:score]` - contains the risk score
|
110
|
+
- `px_ctx.context[:uuid]` - contains the request UUID
|
111
|
+
- `px_ctx.context[:verified]` - contains indication whether the request passed verification or was blocked (inspect `px_ctx.context[:block_reason]` for block reason)
|
100
112
|
|
101
|
-
|
102
|
-
- `px_ctx[:uuid] ` contains the request UUID
|
103
|
-
|
104
|
-
>> Note: to determine whether to return a captcha/block page (HTML) or block JSON payload a reference key on the context will be available: ```px_ctx.context[:format]```
|
113
|
+
> Note: to determine whether to return a captcha/block page (HTML) or block JSON payload a reference key on the context will be available: ```px_ctx.context[:format]```
|
105
114
|
|
106
115
|
To replace the default verification behavior, add the configuration a lambda member as shown in the example below.
|
107
116
|
|
108
|
-
The method must return boolen value.
|
109
|
-
|
110
117
|
```ruby
|
111
118
|
params = {
|
112
119
|
:app_id => <APP_ID>,
|
113
120
|
:auth_token => <AUTH_TOKEN>,
|
114
|
-
:
|
121
|
+
:custom_verification_handler => -> (px_ctx) {
|
115
122
|
if px_ctx.context[:score] >= 60
|
116
|
-
|
123
|
+
# take your action and render an html page or JSON with applicable status code.
|
124
|
+
render json: { :score => px_ctx.context[:score] }
|
117
125
|
end
|
118
|
-
return true
|
119
126
|
}
|
120
127
|
}
|
121
128
|
```
|
122
129
|
|
130
|
+
> Note: Unlike previous versions, the method no longer needs to return a boolean value.
|
131
|
+
|
123
132
|
**Example**
|
124
|
-
|
133
|
+
#### Serving a Custom HTML Page ####
|
125
134
|
```ruby
|
126
135
|
|
127
|
-
params
|
128
|
-
|
136
|
+
params = {
|
137
|
+
:app_id => <APP_ID>,
|
138
|
+
:auth_token => <AUTH_TOKEN>,
|
139
|
+
...
|
140
|
+
:custom_verification_handler => -> (px_ctx) {
|
129
141
|
block_score = px_ctx.context[:score];
|
130
|
-
|
142
|
+
client_uuid = px_ctx.context[:uuid];
|
131
143
|
full_url = px_ctx.context[:full_url];
|
132
144
|
|
133
145
|
html = "<html>
|
134
146
|
<body>
|
135
147
|
<div>Access to #{full_url} has been blocked.</div>
|
136
|
-
<div>Block reference - #{
|
148
|
+
<div>Block reference - #{client_uuid} </div>
|
137
149
|
<div>Block score - #{block_score} </div>
|
138
150
|
</body>
|
139
151
|
</html>".html_safe
|
140
152
|
response.headers["Content-Type"] = "text/html"
|
141
153
|
response.status = 403
|
142
154
|
render :html => html
|
143
|
-
|
144
|
-
}
|
145
|
-
|
146
|
-
PxModule.configure(params)
|
155
|
+
}
|
156
|
+
}
|
147
157
|
```
|
148
158
|
|
149
|
-
<a name="real-ip"></a>**
|
159
|
+
<a name="real-ip"></a>**Custom User IP**
|
150
160
|
|
151
161
|
> Note: IP extraction, according to your network setup, is very important. It is common to have a load balancer/proxy on top of your applications, in which case the PerimeterX module will send the system's internal IP as the user's. In order to properly perform processing and detection on server-to-server calls, PerimeterX module needs the real user's IP.
|
152
162
|
|
153
163
|
By default the clients IP is taken from the ``REMOTE_ADDR`` header, in case the user decides to use different header or custom function that extract the header the following key should be added to the configuration
|
154
164
|
|
155
|
-
***
|
165
|
+
***Custom header***
|
156
166
|
```ruby
|
157
167
|
configuration = {
|
158
168
|
"app_id" => <APP_ID>,
|
@@ -160,7 +170,7 @@ configuration = {
|
|
160
170
|
"custom_user_ip" => <HTTP_HEADER_NAME>,
|
161
171
|
```
|
162
172
|
|
163
|
-
***
|
173
|
+
***Custom Function***
|
164
174
|
> Note: the function receive as a first parameter the controller request and must return the ip at the end as string
|
165
175
|
|
166
176
|
```ruby
|
@@ -213,15 +223,6 @@ Default mode: PxModule::ACTIVE_MODE
|
|
213
223
|
params[:module_mode] = PxModule::MONITOR_MODE
|
214
224
|
```
|
215
225
|
|
216
|
-
<a name="captcha-support"></a>**Enable/Disable CAPTCHA on the block page**
|
217
|
-
Default mode: enabled
|
218
|
-
|
219
|
-
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.
|
220
|
-
|
221
|
-
```ruby
|
222
|
-
params[:captcha_enabled] = false
|
223
|
-
```
|
224
|
-
|
225
226
|
<a name="custom-uri"></a>**Custom URI**
|
226
227
|
|
227
228
|
Default: 'REQUEST_URI'
|
@@ -285,6 +286,71 @@ Enables debug logging mode to STDOUT
|
|
285
286
|
params[:debug] = true
|
286
287
|
```
|
287
288
|
|
289
|
+
<a name="whitelist-routes"></a>**Whitelist Routes**
|
290
|
+
Default: []
|
291
|
+
|
292
|
+
An array of route prefixes and/or regular expressions that are always whitelisted and not validated by PerimeterX.
|
293
|
+
A string value of a path will be treated as a prefix.
|
294
|
+
A regexp value of a path will be treated as is.
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
params[:whitelist_routes] = ["/example", /\A\/example\z/]
|
298
|
+
```
|
299
|
+
|
300
|
+
<a name="update-config"></a>**Update Configuration on Runtime**
|
301
|
+
|
302
|
+
As mentioned before, PerimeterX Module should be configured in `<rails_app>/config/initializers/perimeterx.rb`.
|
303
|
+
However, it is possible to override configuration options on each request.
|
304
|
+
To do so, send the configuration options as an argument when calling to `px_verify_request` as described in the following example.
|
305
|
+
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.
|
306
|
+
|
307
|
+
Usage example:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
class HomeController < ApplicationController
|
311
|
+
include PxModule
|
312
|
+
|
313
|
+
before_action do call_perimeterx_verify_request end
|
314
|
+
|
315
|
+
def call_perimeterx_verify_request
|
316
|
+
params = {
|
317
|
+
:blocking_score => 70,
|
318
|
+
:module_mode => 2
|
319
|
+
}
|
320
|
+
begin
|
321
|
+
px_verify_request(params)
|
322
|
+
rescue StandardError => e
|
323
|
+
# $stdout.write(e.message)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
<a name="first-party"></a>**First Party**
|
331
|
+
|
332
|
+
To enable first party on your enforcer, add the following routes to your `config/routes.rb` file:
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
get '/:appid_postfix/init.js', to: 'home#index', constraints: { appid_postfix: /XXXXXXXX/ }
|
336
|
+
get '/:appid_postfix/captcha/:all', to: 'home#index', constraints: { appid_postfix: /XXXXXXXX/, all:/.*/ }
|
337
|
+
post '/:appid_postfix/xhr/:all', to: 'home#index', constraints: { appid_postfix: /XXXXXXXX/, all:/.*/ }
|
338
|
+
```
|
339
|
+
|
340
|
+
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`.
|
341
|
+
|
342
|
+
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
|
343
|
+
|
344
|
+
|
345
|
+
First Party configuration:
|
346
|
+
|
347
|
+
Default: true
|
348
|
+
|
349
|
+
```ruby
|
350
|
+
params[:first_party_enabled] = false
|
351
|
+
```
|
352
|
+
|
353
|
+
|
288
354
|
<a name="contributing"></a># Contributing #
|
289
355
|
------------------------------
|
290
356
|
The following steps are welcome when contributing to our project.
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perimeter_x
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nitzan Goldfeder
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1
|
19
|
+
version: '2.1'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1
|
26
|
+
version: '2.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '12.3'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '12.3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: concurrent-ruby
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,14 +104,14 @@ dependencies:
|
|
104
104
|
requirements:
|
105
105
|
- - ">="
|
106
106
|
- !ruby/object:Gem::Version
|
107
|
-
version:
|
107
|
+
version: 5.2.4.3
|
108
108
|
type: :runtime
|
109
109
|
prerelease: false
|
110
110
|
version_requirements: !ruby/object:Gem::Requirement
|
111
111
|
requirements:
|
112
112
|
- - ">="
|
113
113
|
- !ruby/object:Gem::Version
|
114
|
-
version:
|
114
|
+
version: 5.2.4.3
|
115
115
|
- !ruby/object:Gem::Dependency
|
116
116
|
name: rspec
|
117
117
|
requirement: !ruby/object:Gem::Requirement
|
@@ -171,24 +171,24 @@ files:
|
|
171
171
|
- lib/perimeterx/configuration.rb
|
172
172
|
- lib/perimeterx/internal/clients/perimeter_x_activity_client.rb
|
173
173
|
- lib/perimeterx/internal/clients/perimeter_x_risk_client.rb
|
174
|
+
- lib/perimeterx/internal/exceptions/px_config_exception.rb
|
174
175
|
- lib/perimeterx/internal/exceptions/px_cookie_decryption_exception.rb
|
176
|
+
- lib/perimeterx/internal/first_party/px_first_party.rb
|
175
177
|
- lib/perimeterx/internal/payload/perimeter_x_cookie_v1.rb
|
176
178
|
- lib/perimeterx/internal/payload/perimeter_x_cookie_v3.rb
|
177
179
|
- lib/perimeterx/internal/payload/perimeter_x_payload.rb
|
178
180
|
- lib/perimeterx/internal/payload/perimeter_x_token_v1.rb
|
179
181
|
- lib/perimeterx/internal/payload/perimeter_x_token_v3.rb
|
180
182
|
- lib/perimeterx/internal/perimeter_x_context.rb
|
181
|
-
- lib/perimeterx/internal/validators/
|
183
|
+
- lib/perimeterx/internal/validators/hash_schema_validator.rb
|
182
184
|
- lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb
|
183
185
|
- lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb
|
184
186
|
- lib/perimeterx/utils/px_constants.rb
|
185
187
|
- lib/perimeterx/utils/px_http_client.rb
|
186
188
|
- lib/perimeterx/utils/px_logger.rb
|
187
189
|
- lib/perimeterx/utils/px_template_factory.rb
|
188
|
-
- lib/perimeterx/utils/templates/
|
189
|
-
- lib/perimeterx/utils/templates/
|
190
|
-
- lib/perimeterx/utils/templates/captcha.mobile.mustache
|
191
|
-
- lib/perimeterx/utils/templates/captcha.mustache
|
190
|
+
- lib/perimeterx/utils/templates/block_template.mustache
|
191
|
+
- lib/perimeterx/utils/templates/ratelimit.mustache
|
192
192
|
- lib/perimeterx/version.rb
|
193
193
|
- perimeter_x.gemspec
|
194
194
|
- readme.md
|
@@ -215,8 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
215
215
|
- !ruby/object:Gem::Version
|
216
216
|
version: '0'
|
217
217
|
requirements: []
|
218
|
-
|
219
|
-
rubygems_version: 2.6.11
|
218
|
+
rubygems_version: 3.0.3
|
220
219
|
signing_key:
|
221
220
|
specification_version: 4
|
222
221
|
summary: PerimeterX ruby implmentation
|
@@ -1,65 +0,0 @@
|
|
1
|
-
require 'perimeterx/internal/clients/perimeter_x_risk_client'
|
2
|
-
|
3
|
-
module PxModule
|
4
|
-
class PerimeterxCaptchaValidator < PerimeterxRiskClient
|
5
|
-
|
6
|
-
def initialize(px_config, http_client)
|
7
|
-
super(px_config, http_client)
|
8
|
-
end
|
9
|
-
|
10
|
-
def send_captcha_request(vid, uuid, captcha, px_ctx)
|
11
|
-
|
12
|
-
request_body = {
|
13
|
-
:request => {
|
14
|
-
:ip => px_ctx.context[:ip],
|
15
|
-
:headers => format_headers(px_ctx),
|
16
|
-
:uri => px_ctx.context[:uri]
|
17
|
-
},
|
18
|
-
:pxCaptcha => captcha,
|
19
|
-
:vid => vid,
|
20
|
-
:uuid => uuid,
|
21
|
-
:hostname => px_ctx.context[:hostname]
|
22
|
-
}
|
23
|
-
|
24
|
-
# Prepare request
|
25
|
-
headers = {
|
26
|
-
"Authorization" => "Bearer #{@px_config[:auth_token]}" ,
|
27
|
-
"Content-Type" => "application/json"
|
28
|
-
};
|
29
|
-
|
30
|
-
return @http_client.post(PxModule::API_V1_CAPTCHA, request_body, headers, @px_config[:api_timeout], @px_config[:api_timeout_connection])
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
def verify(px_ctx)
|
35
|
-
captcha_validated = false
|
36
|
-
begin
|
37
|
-
if(!px_ctx.context.key?(:px_captcha))
|
38
|
-
return captcha_validated, px_ctx
|
39
|
-
end
|
40
|
-
captcha, vid, uuid = px_ctx.context[:px_captcha].split(':', 3)
|
41
|
-
if captcha.nil? || vid.nil? || uuid.nil?
|
42
|
-
return captcha_validated, px_ctx
|
43
|
-
end
|
44
|
-
|
45
|
-
px_ctx.context[:vid] = vid
|
46
|
-
px_ctx.context[:uuid] = uuid
|
47
|
-
response = send_captcha_request(vid, uuid, captcha, px_ctx)
|
48
|
-
|
49
|
-
if (response.status_code == 200)
|
50
|
-
response_body = eval(response.body)
|
51
|
-
if ( response_body[:code] == 0 )
|
52
|
-
captcha_validated = true
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
return captcha_validated, px_ctx
|
57
|
-
|
58
|
-
rescue Exception => e
|
59
|
-
@logger.error("PerimeterxCaptchaValidator[verify]: failed, returning false")
|
60
|
-
return captcha_validated, px_ctx
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
65
|
-
end
|