ferrum 0.3 → 0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +92 -62
- data/lib/ferrum/browser.rb +2 -8
- data/lib/ferrum/network.rb +141 -0
- data/lib/ferrum/network/error.rb +18 -16
- data/lib/ferrum/network/exchange.rb +43 -0
- data/lib/ferrum/network/intercepted_request.rb +57 -48
- data/lib/ferrum/network/request.rb +39 -26
- data/lib/ferrum/network/response.rb +46 -39
- data/lib/ferrum/page.rb +25 -104
- data/lib/ferrum/page/dom.rb +17 -0
- data/lib/ferrum/page/frame.rb +20 -8
- data/lib/ferrum/page/screenshot.rb +1 -5
- data/lib/ferrum/version.rb +1 -1
- metadata +4 -3
- data/lib/ferrum/page/net.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f524fb695add05ab04355a4f694710f0cda6ce317048f234cd6d597a6db5b54
|
4
|
+
data.tar.gz: 4f755ea3ce87efa0a92b2689188ec8ce78979b716e8053e9687fe4f9e97baf2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f52f33ea6d6292ab9f829f9e4870ba7be5fe0f9c045f4b4fe1e540b5363440027ae8890987e92e95612b91e1d82d28a19ff3d204cabeac0bebde96943d399baa
|
7
|
+
data.tar.gz: 3156ae9cc12403dd2b497119371b895d7f37865318ffa2d424676df91346403d85386615abe2db020dffe77b055de72b5e44ccf69137d9bd1fa01a9ae75ffa0f
|
data/README.md
CHANGED
@@ -159,15 +159,6 @@ browser.goto("https://github.com/")
|
|
159
159
|
browser.refresh
|
160
160
|
```
|
161
161
|
|
162
|
-
#### status : `Integer`
|
163
|
-
|
164
|
-
Contains the status code of the response (e.g., 200 for a success).
|
165
|
-
|
166
|
-
```ruby
|
167
|
-
browser.goto("https://github.com/")
|
168
|
-
browser.status # => 200
|
169
|
-
```
|
170
|
-
|
171
162
|
|
172
163
|
## Finders
|
173
164
|
|
@@ -261,8 +252,8 @@ browser.body # => '<html itemscope="" itemtype="http://schema.org/WebPage" lang=
|
|
261
252
|
Saves screenshot on a disk or returns it as base64.
|
262
253
|
|
263
254
|
* options `Hash`
|
264
|
-
* :path `String` to save a screenshot on the disk.
|
265
|
-
|
255
|
+
* :path `String` to save a screenshot on the disk. `:encoding` will be set to
|
256
|
+
`:binary` automatically
|
266
257
|
* :encoding `Symbol` `:base64` | `:binary` you can set it to return image as
|
267
258
|
Base64
|
268
259
|
* :format `String` "jpeg" | "png"
|
@@ -286,8 +277,8 @@ browser.screenshot(full: true, quality: 60) # "iVBORw0KGgoAAAANSUhEUgAABAAAAAMAC
|
|
286
277
|
Saves PDF on a disk or returns it as base64.
|
287
278
|
|
288
279
|
* options `Hash`
|
289
|
-
* :path `String` to save a
|
290
|
-
|
280
|
+
* :path `String` to save a pdf on the disk. `:encoding` will be set to
|
281
|
+
`:binary` automatically
|
291
282
|
* :encoding `Symbol` `:base64` | `:binary` you can set it to return pdf as
|
292
283
|
Base64
|
293
284
|
* :landscape `Boolean` paper orientation. Defaults to false.
|
@@ -305,17 +296,98 @@ browser.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => 14983
|
|
305
296
|
|
306
297
|
## Network
|
307
298
|
|
308
|
-
|
299
|
+
browser.network
|
300
|
+
|
301
|
+
#### traffic `Array<Network::Exchange>`
|
302
|
+
|
303
|
+
Returns all information about network traffic as `Network::Exchange` instance
|
304
|
+
which in general is a wrapper around `request`, `response` and `error`.
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
browser.goto("https://github.com/")
|
308
|
+
browser.network.traffic # => [#<Ferrum::Network::Exchange, ...]
|
309
|
+
```
|
310
|
+
|
311
|
+
#### request : `Network::Request`
|
312
|
+
|
313
|
+
Page request of the main frame.
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
browser.goto("https://github.com/")
|
317
|
+
browser.network.request # => #<Ferrum::Network::Request...
|
318
|
+
```
|
319
|
+
|
320
|
+
#### response : `Network::Response`
|
321
|
+
|
322
|
+
Page response of the main frame.
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
browser.goto("https://github.com/")
|
326
|
+
browser.network.response # => #<Ferrum::Network::Response...
|
327
|
+
```
|
309
328
|
|
310
|
-
|
329
|
+
#### status : `Integer`
|
330
|
+
|
331
|
+
Contains the status code of the main page response (e.g., 200 for a
|
332
|
+
success). This is just a shortcut for `response.status`.
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
browser.goto("https://github.com/")
|
336
|
+
browser.network.status # => 200
|
337
|
+
```
|
338
|
+
|
339
|
+
#### clear(type)
|
340
|
+
|
341
|
+
Clear browser's cache or collected traffic.
|
342
|
+
|
343
|
+
* type `Symbol` it is either `:traffic` or `:cache`
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
traffic = browser.network.traffic # => []
|
347
|
+
browser.goto("https://github.com/")
|
348
|
+
traffic.size # => 51
|
349
|
+
browser.network.clear(:traffic)
|
350
|
+
traffic.size # => 0
|
351
|
+
```
|
352
|
+
|
353
|
+
#### intercept(\*\*options)
|
354
|
+
|
355
|
+
Set request interception for given options. This method is only sets request
|
356
|
+
interception, you should use `on` callback to catch requests and abort or
|
357
|
+
continue them.
|
358
|
+
|
359
|
+
* options `Hash`
|
360
|
+
* :pattern `String` \* by default
|
361
|
+
* :resource_type `Symbol` one of the [resource types](https://chromedevtools.github.io/devtools-protocol/tot/Network#type-ResourceType)
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
browser = Ferrum::Browser.new
|
365
|
+
browser.network.intercept
|
366
|
+
browser.on(:request) do |request|
|
367
|
+
if request.match?(/bla-bla/)
|
368
|
+
request.abort
|
369
|
+
else
|
370
|
+
request.continue
|
371
|
+
end
|
372
|
+
end
|
373
|
+
browser.goto("https://google.com")
|
374
|
+
```
|
311
375
|
|
312
|
-
####
|
376
|
+
#### authorize(\*\*options)
|
313
377
|
|
314
|
-
|
378
|
+
If site uses authorization you can provide credentials using this method.
|
315
379
|
|
316
|
-
|
380
|
+
* options `Hash`
|
381
|
+
* :type `Symbol` `:server` | `:proxy` site or proxy authorization
|
382
|
+
* :user `String`
|
383
|
+
* :password `String`
|
317
384
|
|
318
|
-
|
385
|
+
```ruby
|
386
|
+
browser.network.authorize(user: "login", password: "pass")
|
387
|
+
browser.goto("http://example.com/authenticated")
|
388
|
+
puts browser.network.status # => 200
|
389
|
+
puts browser.body # => Welcome, authenticated client
|
390
|
+
```
|
319
391
|
|
320
392
|
|
321
393
|
### Mouse
|
@@ -540,23 +612,12 @@ Play around inside given frame
|
|
540
612
|
browser.goto("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
541
613
|
frame = browser.at_xpath("//iframe")
|
542
614
|
browser.within_frame(frame) do
|
615
|
+
puts browser.frame_title # => HTML Demo: <iframe>
|
543
616
|
puts browser.frame_url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
|
544
617
|
end
|
545
618
|
```
|
546
619
|
|
547
620
|
|
548
|
-
## Authorization
|
549
|
-
|
550
|
-
#### authorize(\*\*options)
|
551
|
-
|
552
|
-
If site uses authorization you can provide credentials using this method.
|
553
|
-
|
554
|
-
* options `Hash`
|
555
|
-
* :type `Symbol` `:server` | `:proxy` site or proxy authorization
|
556
|
-
* :user `String`
|
557
|
-
* :password `String`
|
558
|
-
|
559
|
-
|
560
621
|
## Dialog
|
561
622
|
|
562
623
|
#### accept(text)
|
@@ -580,34 +641,3 @@ browser.on(:dialog) do |dialog|
|
|
580
641
|
end
|
581
642
|
browser.goto("https://google.com")
|
582
643
|
```
|
583
|
-
|
584
|
-
|
585
|
-
## Interception
|
586
|
-
|
587
|
-
#### intercept_request(\*\*options)
|
588
|
-
|
589
|
-
Set request interception for given options. This method is only sets request
|
590
|
-
interception, you should use `on` callback to catch it.
|
591
|
-
|
592
|
-
* options `Hash`
|
593
|
-
* :pattern `String` \* by default
|
594
|
-
* :resource_type `Symbol` one of the [resource types](https://chromedevtools.github.io/devtools-protocol/tot/Network#type-ResourceType)
|
595
|
-
|
596
|
-
#### on(event)
|
597
|
-
|
598
|
-
Set callback for given event.
|
599
|
-
|
600
|
-
* event `Symbol`
|
601
|
-
|
602
|
-
```ruby
|
603
|
-
browser = Ferrum::Browser.new
|
604
|
-
browser.intercept_request
|
605
|
-
browser.on(:request_intercepted) do |request|
|
606
|
-
if request.match?(/bla-bla/)
|
607
|
-
request.abort
|
608
|
-
else
|
609
|
-
request.continue
|
610
|
-
end
|
611
|
-
end
|
612
|
-
browser.goto("https://google.com")
|
613
|
-
```
|
data/lib/ferrum/browser.rb
CHANGED
@@ -16,15 +16,13 @@ module Ferrum
|
|
16
16
|
extend Forwardable
|
17
17
|
delegate %i[window_handle window_handles switch_to_window
|
18
18
|
open_new_window close_window within_window page] => :targets
|
19
|
-
delegate %i[goto back forward refresh
|
19
|
+
delegate %i[goto back forward refresh
|
20
20
|
at_css at_xpath css xpath current_url title body
|
21
|
-
headers cookies
|
22
|
-
intercept_request continue_request abort_request
|
21
|
+
headers cookies network
|
23
22
|
mouse keyboard
|
24
23
|
screenshot pdf
|
25
24
|
evaluate evaluate_on evaluate_async execute
|
26
25
|
frame_url frame_title within_frame
|
27
|
-
authorize
|
28
26
|
on] => :page
|
29
27
|
|
30
28
|
attr_reader :client, :process, :logger, :js_errors, :slowmo, :base_url,
|
@@ -82,10 +80,6 @@ module Ferrum
|
|
82
80
|
raise
|
83
81
|
end
|
84
82
|
|
85
|
-
def clear_memory_cache
|
86
|
-
page.command("Network.clearBrowserCache")
|
87
|
-
end
|
88
|
-
|
89
83
|
def reset
|
90
84
|
@window_size = @original_window_size
|
91
85
|
targets.reset
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ferrum/network/exchange"
|
4
|
+
require "ferrum/network/intercepted_request"
|
5
|
+
|
6
|
+
module Ferrum
|
7
|
+
class Network
|
8
|
+
CLEAR_TYPE = %i[traffic cache].freeze
|
9
|
+
AUTHORIZE_TYPE = %i[server proxy].freeze
|
10
|
+
RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
|
11
|
+
XHR Fetch EventSource WebSocket Manifest
|
12
|
+
SignedExchange Ping CSPViolationReport Other].freeze
|
13
|
+
|
14
|
+
attr_reader :traffic
|
15
|
+
|
16
|
+
def initialize(page)
|
17
|
+
@page = page
|
18
|
+
@traffic = []
|
19
|
+
@exchange = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def request
|
23
|
+
@exchange&.request
|
24
|
+
end
|
25
|
+
|
26
|
+
def response
|
27
|
+
@exchange&.response
|
28
|
+
end
|
29
|
+
|
30
|
+
def status
|
31
|
+
response&.status
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear(type)
|
35
|
+
unless CLEAR_TYPE.include?(type)
|
36
|
+
raise ArgumentError, ":type should be in #{CLEAR_TYPE}"
|
37
|
+
end
|
38
|
+
|
39
|
+
if type == :traffic
|
40
|
+
@traffic.clear
|
41
|
+
else
|
42
|
+
@page.command("Network.clearBrowserCache")
|
43
|
+
end
|
44
|
+
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def intercept(pattern: "*", resource_type: nil)
|
49
|
+
pattern = { urlPattern: pattern }
|
50
|
+
if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
|
51
|
+
pattern[:resourceType] = resource_type
|
52
|
+
end
|
53
|
+
|
54
|
+
@page.command("Network.setRequestInterception", patterns: [pattern])
|
55
|
+
end
|
56
|
+
|
57
|
+
def authorize(user:, password:, type: :server)
|
58
|
+
unless AUTHORIZE_TYPE.include?(type)
|
59
|
+
raise ArgumentError, ":type should be in #{AUTHORIZE_TYPE}"
|
60
|
+
end
|
61
|
+
|
62
|
+
@authorized_ids ||= {}
|
63
|
+
@authorized_ids[type] ||= []
|
64
|
+
|
65
|
+
intercept
|
66
|
+
|
67
|
+
@page.on(:request) do |request, index, total|
|
68
|
+
if request.auth_challenge?(type)
|
69
|
+
response = authorized_response(@authorized_ids[type],
|
70
|
+
request.interception_id,
|
71
|
+
user, password)
|
72
|
+
|
73
|
+
@authorized_ids[type] << request.interception_id
|
74
|
+
request.continue(authChallengeResponse: response)
|
75
|
+
elsif index + 1 < total
|
76
|
+
next # There are other callbacks that can handle this, skip
|
77
|
+
else
|
78
|
+
request.continue
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def subscribe
|
84
|
+
@page.on("Network.requestWillBeSent") do |params|
|
85
|
+
# On redirects Chrome doesn't change `requestId` and there's no
|
86
|
+
# `Network.responseReceived` event for such request. If there's already
|
87
|
+
# exchange object with this id then we got redirected and params has
|
88
|
+
# `redirectResponse` key which contains the response.
|
89
|
+
if exchange = first_by(params["requestId"])
|
90
|
+
exchange.build_response(params)
|
91
|
+
end
|
92
|
+
|
93
|
+
exchange = Network::Exchange.new(params)
|
94
|
+
@exchange = exchange if exchange.navigation_request?(@page.frame_id)
|
95
|
+
@traffic << exchange
|
96
|
+
end
|
97
|
+
|
98
|
+
@page.on("Network.responseReceived") do |params|
|
99
|
+
if exchange = last_by(params["requestId"])
|
100
|
+
exchange.build_response(params)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
@page.on("Network.loadingFinished") do |params|
|
105
|
+
exchange = last_by(params["requestId"])
|
106
|
+
if exchange && exchange.response
|
107
|
+
exchange.response.body_size = params["encodedDataLength"]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
@page.on("Log.entryAdded") do |params|
|
112
|
+
entry = params["entry"] || {}
|
113
|
+
if entry["source"] == "network" &&
|
114
|
+
entry["level"] == "error" &&
|
115
|
+
exchange = last_by(entry["networkRequestId"])
|
116
|
+
exchange.build_error(entry)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def authorized_response(ids, interception_id, username, password)
|
122
|
+
if ids.include?(interception_id)
|
123
|
+
{ response: "CancelAuth" }
|
124
|
+
elsif username && password
|
125
|
+
{ response: "ProvideCredentials",
|
126
|
+
username: username,
|
127
|
+
password: password }
|
128
|
+
else
|
129
|
+
{ response: "CancelAuth" }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def first_by(request_id)
|
134
|
+
@traffic.find { |e| e.request.id == request_id }
|
135
|
+
end
|
136
|
+
|
137
|
+
def last_by(request_id)
|
138
|
+
@traffic.select { |e| e.request.id == request_id }.last
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/ferrum/network/error.rb
CHANGED
@@ -1,25 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Ferrum
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Ferrum
|
4
|
+
class Network
|
5
|
+
class Error
|
6
|
+
def initialize(data)
|
7
|
+
@data = data
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def id
|
11
|
+
@data["networkRequestId"]
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
def url
|
15
|
+
@data["url"]
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
def description
|
19
|
+
@data["text"]
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
def time
|
23
|
+
@time ||= Time.strptime(@data["timestamp"].to_s, "%s")
|
24
|
+
end
|
23
25
|
end
|
24
26
|
end
|
25
27
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ferrum/network/error"
|
4
|
+
require "ferrum/network/request"
|
5
|
+
require "ferrum/network/response"
|
6
|
+
|
7
|
+
module Ferrum
|
8
|
+
class Network
|
9
|
+
class Exchange
|
10
|
+
attr_reader :request, :response, :error
|
11
|
+
|
12
|
+
def initialize(params)
|
13
|
+
@response = @error = nil
|
14
|
+
build_request(params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_request(params)
|
18
|
+
@request = Network::Request.new(params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_response(params)
|
22
|
+
@response = Network::Response.new(params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_error(params)
|
26
|
+
@error = Network::Error.new(params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def navigation_request?(frame_id)
|
30
|
+
request.type?(:document) &&
|
31
|
+
request.frame_id == frame_id
|
32
|
+
end
|
33
|
+
|
34
|
+
def blocked?
|
35
|
+
response.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_a
|
39
|
+
[request, response, error]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,53 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Ferrum
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
3
|
+
module Ferrum
|
4
|
+
class Network
|
5
|
+
class InterceptedRequest
|
6
|
+
attr_accessor :interception_id, :frame_id, :resource_type
|
7
|
+
|
8
|
+
def initialize(page, params)
|
9
|
+
@page, @params = page, params
|
10
|
+
@interception_id = params["interceptionId"]
|
11
|
+
@frame_id = params["frameId"]
|
12
|
+
@resource_type = params["resourceType"]
|
13
|
+
@request = params["request"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def navigation_request?
|
17
|
+
@params["isNavigationRequest"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def auth_challenge?(source)
|
21
|
+
@params.dig("authChallenge", "source")&.downcase&.to_s == source.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def match?(regexp)
|
25
|
+
!!url.match(regexp)
|
26
|
+
end
|
27
|
+
|
28
|
+
def continue(**options)
|
29
|
+
options = options.merge(interceptionId: interception_id)
|
30
|
+
@page.command("Network.continueInterceptedRequest", **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def abort
|
34
|
+
continue(errorReason: "Aborted")
|
35
|
+
end
|
36
|
+
|
37
|
+
def url
|
38
|
+
@request["url"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def method
|
42
|
+
@request["method"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def headers
|
46
|
+
@request["headers"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def initial_priority
|
50
|
+
@request["initialPriority"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def referrer_policy
|
54
|
+
@request["referrerPolicy"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def inspect
|
58
|
+
%(#<#{self.class} @interception_id=#{@interception_id.inspect} @frame_id=#{@frame_id.inspect} @resource_type=#{@resource_type.inspect} @request=#{@request.inspect}>)
|
59
|
+
end
|
51
60
|
end
|
52
61
|
end
|
53
62
|
end
|
@@ -2,32 +2,45 @@
|
|
2
2
|
|
3
3
|
require "time"
|
4
4
|
|
5
|
-
module Ferrum
|
6
|
-
class
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
5
|
+
module Ferrum
|
6
|
+
class Network
|
7
|
+
class Request
|
8
|
+
def initialize(params)
|
9
|
+
@params = params
|
10
|
+
@request = @params["request"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def id
|
14
|
+
@params["requestId"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
@params["type"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def type?(value)
|
22
|
+
type.downcase == value.to_s.downcase
|
23
|
+
end
|
24
|
+
|
25
|
+
def frame_id
|
26
|
+
@params["frameId"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def url
|
30
|
+
@request["url"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def method
|
34
|
+
@request["method"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def headers
|
38
|
+
@request["headers"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def time
|
42
|
+
@time ||= Time.strptime(@params["wallTime"].to_s, "%s")
|
43
|
+
end
|
31
44
|
end
|
32
45
|
end
|
33
46
|
end
|
@@ -1,44 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Ferrum
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
3
|
+
module Ferrum
|
4
|
+
class Network
|
5
|
+
class Response
|
6
|
+
attr_reader :body_size
|
7
|
+
|
8
|
+
def initialize(params)
|
9
|
+
@params = params
|
10
|
+
@response = params["response"] || @params["redirectResponse"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def id
|
14
|
+
@params["requestId"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def url
|
18
|
+
@response["url"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def status
|
22
|
+
@response["status"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def status_text
|
26
|
+
@response["statusText"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def headers
|
30
|
+
@response["headers"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def headers_size
|
34
|
+
@response["encodedDataLength"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def content_type
|
38
|
+
@content_type ||= @response.dig("headers", "contentType")&.sub(/;.*\z/, "")
|
39
|
+
end
|
40
|
+
|
41
|
+
# See https://crbug.com/883475
|
42
|
+
# Sometimes we never get the Network.responseReceived event.
|
43
|
+
# See https://crbug.com/764946
|
44
|
+
# `Network.loadingFinished` encodedDataLength contains both body and
|
45
|
+
# headers sizes received by wire.
|
46
|
+
def body_size=(size)
|
47
|
+
@body_size = size - headers_size
|
48
|
+
end
|
42
49
|
end
|
43
50
|
end
|
44
51
|
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -5,34 +5,13 @@ require "ferrum/keyboard"
|
|
5
5
|
require "ferrum/headers"
|
6
6
|
require "ferrum/cookies"
|
7
7
|
require "ferrum/dialog"
|
8
|
+
require "ferrum/network"
|
8
9
|
require "ferrum/page/dom"
|
9
10
|
require "ferrum/page/runtime"
|
10
11
|
require "ferrum/page/frame"
|
11
|
-
require "ferrum/page/net"
|
12
12
|
require "ferrum/page/screenshot"
|
13
13
|
require "ferrum/browser/client"
|
14
|
-
|
15
|
-
require "ferrum/network/request"
|
16
|
-
require "ferrum/network/response"
|
17
|
-
require "ferrum/network/intercepted_request"
|
18
|
-
|
19
|
-
# RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
|
20
|
-
# object, including JS wrappers for DOM nodes. There is a way to convert between
|
21
|
-
# node ids and remote object ids (DOM.requestNode and DOM.resolveNode).
|
22
|
-
#
|
23
|
-
# NodeId is used for inspection, when backend tracks the node and sends updates to
|
24
|
-
# the frontend. If you somehow got NodeId over protocol, backend should have
|
25
|
-
# pushed to the frontend all of it's ancestors up to the Document node via
|
26
|
-
# DOM.setChildNodes. After that, frontend is always kept up-to-date about anything
|
27
|
-
# happening to the node.
|
28
|
-
#
|
29
|
-
# BackendNodeId is just a unique identifier for a node. Obtaining it does not send
|
30
|
-
# any updates, for example, the node may be destroyed without any notification.
|
31
|
-
# This is a way to keep a reference to the Node, when you don't necessarily want
|
32
|
-
# to keep track of it. One example would be linking to the node from performance
|
33
|
-
# data (e.g. relayout root node). BackendNodeId may be either resolved to
|
34
|
-
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
|
35
|
-
# details (DOM.describeNode).
|
14
|
+
|
36
15
|
module Ferrum
|
37
16
|
class Page
|
38
17
|
NEW_WINDOW_WAIT = ENV.fetch("FERRUM_NEW_WINDOW_WAIT", 0.3).to_f
|
@@ -51,17 +30,15 @@ module Ferrum
|
|
51
30
|
end
|
52
31
|
end
|
53
32
|
|
54
|
-
include DOM, Runtime, Frame,
|
33
|
+
include DOM, Runtime, Frame, Screenshot
|
55
34
|
|
56
35
|
attr_accessor :referrer
|
57
|
-
attr_reader :target_id, :
|
58
|
-
:headers, :cookies, :
|
59
|
-
:mouse, :keyboard
|
60
|
-
:browser
|
36
|
+
attr_reader :target_id, :browser,
|
37
|
+
:headers, :cookies, :network,
|
38
|
+
:mouse, :keyboard
|
61
39
|
|
62
40
|
def initialize(target_id, browser, new_window = false)
|
63
41
|
@target_id, @browser = target_id, browser
|
64
|
-
@network_traffic = []
|
65
42
|
@event = Event.new.tap(&:set)
|
66
43
|
|
67
44
|
@frames = {}
|
@@ -80,6 +57,7 @@ module Ferrum
|
|
80
57
|
|
81
58
|
@mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
|
82
59
|
@headers, @cookies = Headers.new(self), Cookies.new(self)
|
60
|
+
@network = Network.new(self)
|
83
61
|
|
84
62
|
subscribe
|
85
63
|
prepare_page
|
@@ -131,21 +109,6 @@ module Ferrum
|
|
131
109
|
command("Page.reload", wait: timeout)
|
132
110
|
end
|
133
111
|
|
134
|
-
def network_traffic(type = nil)
|
135
|
-
case type.to_s
|
136
|
-
when "all"
|
137
|
-
@network_traffic
|
138
|
-
when "blocked"
|
139
|
-
@network_traffic.select { |r| r.response.nil? } # when request blocked
|
140
|
-
else
|
141
|
-
@network_traffic.select { |r| r.response } # when request isn't blocked
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def clear_network_traffic
|
146
|
-
@network_traffic = []
|
147
|
-
end
|
148
|
-
|
149
112
|
def back
|
150
113
|
history_navigate(delta: -1)
|
151
114
|
end
|
@@ -171,7 +134,7 @@ module Ferrum
|
|
171
134
|
dialog = Dialog.new(self, params)
|
172
135
|
block.call(dialog, index, total)
|
173
136
|
end
|
174
|
-
when :
|
137
|
+
when :request
|
175
138
|
@client.on("Network.requestIntercepted") do |params, index, total|
|
176
139
|
request = Network::InterceptedRequest.new(self, params)
|
177
140
|
block.call(request, index, total)
|
@@ -186,87 +149,45 @@ module Ferrum
|
|
186
149
|
def subscribe
|
187
150
|
super
|
188
151
|
|
152
|
+
network.subscribe
|
153
|
+
|
154
|
+
on("Network.loadingFailed") do |params|
|
155
|
+
id, canceled = params.values_at("requestId", "canceled")
|
156
|
+
# Set event as we aborted main request we are waiting for
|
157
|
+
if network.request&.id == id && canceled == true
|
158
|
+
@event.set
|
159
|
+
@document_id = get_document_id
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
189
163
|
if @browser.logger
|
190
|
-
|
164
|
+
on("Runtime.consoleAPICalled") do |params|
|
191
165
|
params["args"].each { |r| @browser.logger.puts(r["value"]) }
|
192
166
|
end
|
193
167
|
end
|
194
168
|
|
195
169
|
if @browser.js_errors
|
196
|
-
|
170
|
+
on("Runtime.exceptionThrown") do |params|
|
197
171
|
# FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
198
172
|
Thread.main.raise JavaScriptError.new(params.dig("exceptionDetails", "exception"))
|
199
173
|
end
|
200
174
|
end
|
201
175
|
|
202
|
-
|
176
|
+
on("Page.windowOpen") do
|
203
177
|
@browser.targets.refresh
|
204
178
|
end
|
205
179
|
|
206
|
-
|
180
|
+
on("Page.navigatedWithinDocument") do
|
207
181
|
@event.set if @waiting_frames.empty?
|
208
182
|
end
|
209
183
|
|
210
|
-
|
184
|
+
on("Page.domContentEventFired") do |params|
|
211
185
|
# `frameStoppedLoading` doesn't occur if status isn't success
|
212
|
-
if
|
186
|
+
if network.status != 200
|
213
187
|
@event.set
|
214
188
|
@document_id = get_document_id
|
215
189
|
end
|
216
190
|
end
|
217
|
-
|
218
|
-
@client.on("Network.requestWillBeSent") do |params|
|
219
|
-
if params["frameId"] == @frame_id
|
220
|
-
# Possible types:
|
221
|
-
# Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
|
222
|
-
# Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
|
223
|
-
# CSPViolationReport, Other
|
224
|
-
if params["type"] == "Document"
|
225
|
-
@event.reset
|
226
|
-
@request_id = params["requestId"]
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
id, time = params.values_at("requestId", "wallTime")
|
231
|
-
params = params["request"].merge("id" => id, "time" => time)
|
232
|
-
@network_traffic << Network::Request.new(params)
|
233
|
-
end
|
234
|
-
|
235
|
-
@client.on("Network.responseReceived") do |params|
|
236
|
-
if params["requestId"] == @request_id
|
237
|
-
@response_headers = params.dig("response", "headers")
|
238
|
-
@status = params.dig("response", "status")
|
239
|
-
end
|
240
|
-
|
241
|
-
if request = @network_traffic.find { |r| r.id == params["requestId"] }
|
242
|
-
params = params["response"].merge("id" => params["requestId"])
|
243
|
-
request.response = Network::Response.new(params)
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
@client.on("Network.loadingFinished") do |params|
|
248
|
-
if request = @network_traffic.find { |r| r.id == params["requestId"] }
|
249
|
-
# Sometimes we never get the Network.responseReceived event.
|
250
|
-
# See https://crbug.com/883475
|
251
|
-
#
|
252
|
-
# Network.loadingFinished's encodedDataLength contains both body and headers
|
253
|
-
# sizes received by wire. See https://crbug.com/764946
|
254
|
-
if response = request.response
|
255
|
-
response.body_size = params["encodedDataLength"] - response.headers_size
|
256
|
-
end
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
@client.on("Log.entryAdded") do |params|
|
261
|
-
source = params.dig("entry", "source")
|
262
|
-
level = params.dig("entry", "level")
|
263
|
-
if source == "network" && level == "error"
|
264
|
-
id = params.dig("entry", "networkRequestId")
|
265
|
-
if request = @network_traffic.find { |r| r.id == id }
|
266
|
-
request.error = Network::Error.new(params["entry"])
|
267
|
-
end
|
268
|
-
end
|
269
|
-
end
|
270
191
|
end
|
271
192
|
|
272
193
|
def prepare_page
|
data/lib/ferrum/page/dom.rb
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
|
4
|
+
# object, including JS wrappers for DOM nodes. There is a way to convert between
|
5
|
+
# node ids and remote object ids (DOM.requestNode and DOM.resolveNode).
|
6
|
+
#
|
7
|
+
# NodeId is used for inspection, when backend tracks the node and sends updates to
|
8
|
+
# the frontend. If you somehow got NodeId over protocol, backend should have
|
9
|
+
# pushed to the frontend all of it's ancestors up to the Document node via
|
10
|
+
# DOM.setChildNodes. After that, frontend is always kept up-to-date about anything
|
11
|
+
# happening to the node.
|
12
|
+
#
|
13
|
+
# BackendNodeId is just a unique identifier for a node. Obtaining it does not send
|
14
|
+
# any updates, for example, the node may be destroyed without any notification.
|
15
|
+
# This is a way to keep a reference to the Node, when you don't necessarily want
|
16
|
+
# to keep track of it. One example would be linking to the node from performance
|
17
|
+
# data (e.g. relayout root node). BackendNodeId may be either resolved to
|
18
|
+
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
|
19
|
+
# details (DOM.describeNode).
|
3
20
|
module Ferrum
|
4
21
|
class Page
|
5
22
|
module DOM
|
data/lib/ferrum/page/frame.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Page
|
5
5
|
module Frame
|
6
|
+
attr_reader :frame_id
|
7
|
+
|
6
8
|
def execution_context_id
|
7
9
|
context_id = current_execution_context_id
|
8
10
|
raise NoExecutionContextError unless context_id
|
@@ -42,28 +44,28 @@ module Ferrum
|
|
42
44
|
def subscribe
|
43
45
|
super if defined?(super)
|
44
46
|
|
45
|
-
|
47
|
+
on("Page.frameAttached") do |params|
|
46
48
|
@frames[params["frameId"]] = { "parent_id" => params["parentFrameId"] }
|
47
49
|
end
|
48
50
|
|
49
|
-
|
51
|
+
on("Page.frameStartedLoading") do |params|
|
50
52
|
@waiting_frames << params["frameId"]
|
51
53
|
@event.reset
|
52
54
|
end
|
53
55
|
|
54
|
-
|
56
|
+
on("Page.frameNavigated") do |params|
|
55
57
|
id = params["frame"]["id"]
|
56
58
|
if frame = @frames[id]
|
57
59
|
frame.merge!(params["frame"].select { |k, _| k == "name" || k == "url" })
|
58
60
|
end
|
59
61
|
end
|
60
62
|
|
61
|
-
|
63
|
+
on("Page.frameScheduledNavigation") do |params|
|
62
64
|
@waiting_frames << params["frameId"]
|
63
65
|
@event.reset
|
64
66
|
end
|
65
67
|
|
66
|
-
|
68
|
+
on("Page.frameStoppedLoading") do |params|
|
67
69
|
# `DOM.performSearch` doesn't work without getting #document node first.
|
68
70
|
# It returns node with nodeId 1 and nodeType 9 from which descend the
|
69
71
|
# tree and we save it in a variable because if we call that again root
|
@@ -79,7 +81,17 @@ module Ferrum
|
|
79
81
|
end
|
80
82
|
end
|
81
83
|
|
82
|
-
|
84
|
+
on("Network.requestWillBeSent") do |params|
|
85
|
+
if params["frameId"] == @frame_id
|
86
|
+
# Possible types:
|
87
|
+
# Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
|
88
|
+
# Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
|
89
|
+
# CSPViolationReport, Other
|
90
|
+
@event.reset if params["type"] == "Document"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
on("Runtime.executionContextCreated") do |params|
|
83
95
|
context_id = params.dig("context", "id")
|
84
96
|
@execution_context_id ||= context_id
|
85
97
|
|
@@ -93,7 +105,7 @@ module Ferrum
|
|
93
105
|
end
|
94
106
|
end
|
95
107
|
|
96
|
-
|
108
|
+
on("Runtime.executionContextDestroyed") do |params|
|
97
109
|
context_id = params["executionContextId"]
|
98
110
|
|
99
111
|
if @execution_context_id == context_id
|
@@ -104,7 +116,7 @@ module Ferrum
|
|
104
116
|
frame["execution_context_id"] = nil if frame
|
105
117
|
end
|
106
118
|
|
107
|
-
|
119
|
+
on("Runtime.executionContextsCleared") do
|
108
120
|
# If we didn't have time to set context id at the beginning we have
|
109
121
|
# to set lock and release it when we set something.
|
110
122
|
@execution_context_id = nil
|
@@ -23,17 +23,13 @@ module Ferrum
|
|
23
23
|
|
24
24
|
def save_file(path, data)
|
25
25
|
bin = Base64.decode64(data)
|
26
|
+
return bin unless path
|
26
27
|
File.open(path.to_s, "wb") { |f| f.write(bin) }
|
27
28
|
end
|
28
29
|
|
29
30
|
def common_options(encoding: :base64, path: nil, **_)
|
30
31
|
encoding = encoding.to_sym
|
31
32
|
encoding = :binary if path
|
32
|
-
|
33
|
-
if encoding == :binary && !path
|
34
|
-
raise "You have to provide `:path` for `:binary` encoding"
|
35
|
-
end
|
36
|
-
|
37
33
|
[path, encoding]
|
38
34
|
end
|
39
35
|
|
data/lib/ferrum/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ferrum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.4'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-09-
|
11
|
+
date: 2019-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-driver
|
@@ -205,7 +205,9 @@ files:
|
|
205
205
|
- lib/ferrum/keyboard.json
|
206
206
|
- lib/ferrum/keyboard.rb
|
207
207
|
- lib/ferrum/mouse.rb
|
208
|
+
- lib/ferrum/network.rb
|
208
209
|
- lib/ferrum/network/error.rb
|
210
|
+
- lib/ferrum/network/exchange.rb
|
209
211
|
- lib/ferrum/network/intercepted_request.rb
|
210
212
|
- lib/ferrum/network/request.rb
|
211
213
|
- lib/ferrum/network/response.rb
|
@@ -213,7 +215,6 @@ files:
|
|
213
215
|
- lib/ferrum/page.rb
|
214
216
|
- lib/ferrum/page/dom.rb
|
215
217
|
- lib/ferrum/page/frame.rb
|
216
|
-
- lib/ferrum/page/net.rb
|
217
218
|
- lib/ferrum/page/runtime.rb
|
218
219
|
- lib/ferrum/page/screenshot.rb
|
219
220
|
- lib/ferrum/targets.rb
|
data/lib/ferrum/page/net.rb
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Page
|
5
|
-
module Net
|
6
|
-
AUTHORIZE_TYPE = %i[server proxy]
|
7
|
-
RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
|
8
|
-
XHR Fetch EventSource WebSocket Manifest
|
9
|
-
SignedExchange Ping CSPViolationReport Other]
|
10
|
-
|
11
|
-
def authorize(user:, password:, type: :server)
|
12
|
-
unless AUTHORIZE_TYPE.include?(type)
|
13
|
-
raise ArgumentError, ":type should be in #{AUTHORIZE_TYPE}"
|
14
|
-
end
|
15
|
-
|
16
|
-
@authorized_ids ||= {}
|
17
|
-
@authorized_ids[type] ||= []
|
18
|
-
|
19
|
-
intercept_request
|
20
|
-
|
21
|
-
on(:request_intercepted) do |request, index, total|
|
22
|
-
if request.auth_challenge?(type)
|
23
|
-
response = authorized_response(@authorized_ids[type],
|
24
|
-
request.interception_id,
|
25
|
-
user, password)
|
26
|
-
|
27
|
-
@authorized_ids[type] << request.interception_id
|
28
|
-
request.continue(authChallengeResponse: response)
|
29
|
-
elsif index + 1 < total
|
30
|
-
next # There are other callbacks that can handle this, skip
|
31
|
-
else
|
32
|
-
request.continue
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def intercept_request(pattern: "*", resource_type: nil)
|
38
|
-
pattern = { urlPattern: pattern }
|
39
|
-
if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
|
40
|
-
pattern[:resourceType] = resource_type
|
41
|
-
end
|
42
|
-
|
43
|
-
command("Network.setRequestInterception", patterns: [pattern])
|
44
|
-
end
|
45
|
-
|
46
|
-
def continue_request(interception_id, options = nil)
|
47
|
-
options ||= {}
|
48
|
-
options = options.merge(interceptionId: interception_id)
|
49
|
-
command("Network.continueInterceptedRequest", **options)
|
50
|
-
end
|
51
|
-
|
52
|
-
def abort_request(interception_id)
|
53
|
-
continue_request(interception_id, errorReason: "Aborted")
|
54
|
-
end
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
def subscribe
|
59
|
-
super if defined?(super)
|
60
|
-
|
61
|
-
@client.on("Network.loadingFailed") do |params|
|
62
|
-
# Free mutex as we aborted main request we are waiting for
|
63
|
-
if params["requestId"] == @request_id && params["canceled"] == true
|
64
|
-
@event.set
|
65
|
-
@document_id = get_document_id
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def authorized_response(ids, interception_id, username, password)
|
71
|
-
if ids.include?(interception_id)
|
72
|
-
{ response: "CancelAuth" }
|
73
|
-
elsif username && password
|
74
|
-
{ response: "ProvideCredentials",
|
75
|
-
username: username,
|
76
|
-
password: password }
|
77
|
-
else
|
78
|
-
{ response: "CancelAuth" }
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|