ronin-web-browser 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,430 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-web-browser - An automated Chrome API.
4
+ #
5
+ # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-web-browser is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-web-browser is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-web-browser. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/web/browser/cookie'
22
+ require 'ronin/web/browser/cookie_file'
23
+ require 'ronin/support/network/http'
24
+
25
+ require 'ferrum'
26
+ require 'uri'
27
+
28
+ module Ronin
29
+ module Web
30
+ module Browser
31
+ #
32
+ # Represents an instance of a Chrome headless or visible browser.
33
+ #
34
+ class Agent < Ferrum::Browser
35
+
36
+ # The configured proxy information.
37
+ #
38
+ # @return [Hash{Symbol => Object}, nil]
39
+ attr_reader :proxy
40
+
41
+ #
42
+ # Initializes the browser agent.
43
+ #
44
+ # @param [Boolean] visible
45
+ # Controls whether the browser will start in visible or headless mode.
46
+ #
47
+ # @param [Boolean] headless
48
+ # Controls whether the browser will start in headless or visible mode.
49
+ #
50
+ # @param [String, URI::HTTP, Addressible::URI, Hash, nil] proxy
51
+ # The proxy to send all browser requests through.
52
+ #
53
+ # @param [Hash{Symbol => Object}] kwargs
54
+ # Additional keyword arguments for `Ferrum::Browser#initialize`.
55
+ #
56
+ def initialize(visible: false,
57
+ headless: !visible,
58
+ proxy: Ronin::Support::Network::HTTP.proxy,
59
+ **kwargs)
60
+ proxy = case proxy
61
+ when Hash, nil then proxy
62
+ when URI::HTTP, Addressable::URI
63
+ {
64
+ host: proxy.host,
65
+ port: proxy.port,
66
+ user: proxy.user,
67
+ password: proxy.password
68
+ }
69
+ when String
70
+ uri = URI(proxy)
71
+
72
+ {
73
+ host: uri.host,
74
+ port: uri.port,
75
+ user: uri.user,
76
+ password: uri.password
77
+ }
78
+ else
79
+ raise(ArgumentError,"invalid proxy value (#{proxy.inspect}), must be either a Hash, URI::HTTP, String, or nil")
80
+ end
81
+
82
+ @headless = headless
83
+ @proxy = proxy
84
+
85
+ super(headless: headless, proxy: proxy, **kwargs)
86
+ end
87
+
88
+ #
89
+ # Opens a new browser.
90
+ #
91
+ # @param [Hash{Symbol => Object}] kwargs
92
+ # Additional keyword arguments for {#initialize}.
93
+ #
94
+ # @yield [browser]
95
+ # If a block is given, it will be passed the new browser object.
96
+ # Once the block returns, `quit` will be called on the browser object.
97
+ #
98
+ # @yieldparam [Agent] browser
99
+ # The newly created browser object.
100
+ #
101
+ # @return [Agent]
102
+ # The opened browser object.
103
+ #
104
+ def self.open(**kwargs)
105
+ browser = new(**kwargs)
106
+
107
+ if block_given?
108
+ yield browser
109
+ browser.quit
110
+ end
111
+
112
+ return browser
113
+ end
114
+
115
+ #
116
+ # Determines whether the browser was opened in headless mode.
117
+ #
118
+ # @return [Boolean]
119
+ #
120
+ def headless?
121
+ @headless
122
+ end
123
+
124
+ #
125
+ # Determines whether the browser was opened in visible mode.
126
+ #
127
+ # @return [Boolean]
128
+ #
129
+ def visible?
130
+ !@headless
131
+ end
132
+
133
+ #
134
+ # Determines whether the proxy was initialized with a proxy.
135
+ #
136
+ def proxy?
137
+ !@proxy.nil?
138
+ end
139
+
140
+ #
141
+ # Enables or disables bypassing CSP.
142
+ #
143
+ # @param [Boolean] mode
144
+ # Controls whether to enable or disable CSP bypassing.
145
+ #
146
+ def bypass_csp=(mode)
147
+ if mode then bypass_csp(enabled: true)
148
+ else bypass_csp(enabled: false)
149
+ end
150
+ end
151
+
152
+ #
153
+ # Registers a callback for the given event type.
154
+ #
155
+ # @param [:request, :response, :dialog, String] event
156
+ # The event to register a callback for.
157
+ # For an exhaustive list of event String names, see the
158
+ # [Chrome DevTools Protocol documentation](https://chromedevtools.github.io/devtools-protocol/1-3/)
159
+ #
160
+ # @yield [request]
161
+ # If the event type is `:request` the given block will be passed the
162
+ # request object.
163
+ #
164
+ # @yield [exchange]
165
+ # If the event type is `:response` the given block will be passed the
166
+ # network exchange object containing both the request and the response
167
+ # objects.
168
+ #
169
+ # @yield [params, index, total]
170
+ # Other event types will be passed a params Hash, index, and total.
171
+ #
172
+ # @yieldparam [Ferrum::Network::InterceptedRequest] request
173
+ # A network request object.
174
+ #
175
+ # @yieldparam [Ferrum::Network::Exchange] exchange
176
+ # A network exchange object containing both the request and respoonse
177
+ # objects.
178
+ #
179
+ # @yieldparam [Hash{String => Object}] params
180
+ # A params Hash containing the return value(s).
181
+ #
182
+ # @yieldparam [Integer] index
183
+ #
184
+ # @yieldparam [Integer] total
185
+ #
186
+ def on(event,&block)
187
+ case event
188
+ when :response
189
+ super('Network.responseReceived') do |params,index,total|
190
+ exchange = network.select(params['requestId']).last
191
+
192
+ if exchange
193
+ block.call(exchange,index,total)
194
+ end
195
+ end
196
+ when :close
197
+ super('Inspector.detached',&block)
198
+ else
199
+ super(event,&block)
200
+ end
201
+ end
202
+
203
+ #
204
+ # Passes every request to the given block.
205
+ #
206
+ # @yield [request]
207
+ # The given block will be passed each request before it's sent.
208
+ #
209
+ # @yieldparam [Ferrum::Network::InterceptRequest] request
210
+ # A network request object.
211
+ #
212
+ def every_request
213
+ network.intercept
214
+
215
+ on(:request) do |request|
216
+ yield request
217
+ request.continue
218
+ end
219
+ end
220
+
221
+ #
222
+ # Passes every response to the given block.
223
+ #
224
+ # @yield [response]
225
+ # If the given block accepts a single argument, it will be passed
226
+ # each response object.
227
+ #
228
+ # @yield [response, request]
229
+ # If the given block accepts two arguments, it will be passed the
230
+ # response and the request objects.
231
+ #
232
+ # @yieldparam [Ferrum::Network::Response] response
233
+ # A respone object returned for a request.
234
+ #
235
+ # @yieldparam [Ferrum::Network::Request] request
236
+ # The request object for the response.
237
+ #
238
+ def every_response(&block)
239
+ on(:response) do |exchange,index,total|
240
+ if block.arity == 2
241
+ yield exchange.response, exchange.request
242
+ else
243
+ yield exchange.response
244
+ end
245
+ end
246
+ end
247
+
248
+ #
249
+ # Passes every requested URL to the given block.
250
+ #
251
+ # @yield [url]
252
+ # The given block will be passed every URL.
253
+ #
254
+ # @yieldparam [String] url
255
+ # A URL requested by the browser.
256
+ #
257
+ def every_url
258
+ every_request do |request|
259
+ yield request.url
260
+ end
261
+ end
262
+
263
+ #
264
+ # Passes every requested URL that matches the given pattern to the given
265
+ # block.
266
+ #
267
+ # @param [String, Regexp] pattern
268
+ # The pattern to filter the URLs by.
269
+ #
270
+ # @yield [url]
271
+ # The given block will be passed every URL that matches the pattern.
272
+ #
273
+ # @yieldparam [String] url
274
+ # A matching URL requested by the browser.
275
+ #
276
+ def every_url_like(pattern)
277
+ every_url do |url|
278
+ if pattern.match(url)
279
+ yield url
280
+ end
281
+ end
282
+ end
283
+
284
+ #
285
+ # The page's current URI.
286
+ #
287
+ # @return [URI::HTTP]
288
+ #
289
+ def page_uri
290
+ URI.parse(url)
291
+ end
292
+
293
+ #
294
+ # Queries the XPath or CSS-path query and returns the matching nodes.
295
+ #
296
+ # @return [Array<Ferrum::Node>]
297
+ # The matching node.
298
+ #
299
+ def search(query)
300
+ if query.start_with?('/')
301
+ xpath(query)
302
+ else
303
+ css(query)
304
+ end
305
+ end
306
+
307
+ #
308
+ # Queries the XPath or CSS-path query and returns the first match.
309
+ #
310
+ # @return [Ferrum::Node, nil]
311
+ # The first matching node.
312
+ #
313
+ def at(query)
314
+ if query.start_with?('/')
315
+ at_xpath(query)
316
+ else
317
+ at_css(query)
318
+ end
319
+ end
320
+
321
+ #
322
+ # Queries all `<a href="...">` links in the current page.
323
+ #
324
+ # @return [Array<String>]
325
+ #
326
+ def links
327
+ xpath('//a/@href').map(&:value)
328
+ end
329
+
330
+ #
331
+ # All link URLs in the current page.
332
+ #
333
+ # @return [Array<URI::HTTP, URI::HTTPS>]
334
+ #
335
+ def urls
336
+ page_uri = self.page_uri
337
+
338
+ links.map { |link| page_uri.merge(link) }
339
+ end
340
+
341
+ alias eval_js evaluate
342
+ alias load_js add_script_tag
343
+ alias inject_js evaluate_on_new_document
344
+ alias load_css add_style_tag
345
+
346
+ #
347
+ # Enumerates over all session cookies.
348
+ #
349
+ # @yield [cookie]
350
+ # The given block will be passed each session cookie.
351
+ #
352
+ # @yieldparam [Ferrum::Cookies::Cookie] cookie
353
+ # A cookie that ends with `sess` or `session`.
354
+ #
355
+ # @return [Enumerator]
356
+ # If no block is given, then an Enumerator object will be returned.
357
+ #
358
+ def each_session_cookie
359
+ return enum_for(__method__) unless block_given?
360
+
361
+ cookies.each do |cookie|
362
+ yield cookie if cookie.session?
363
+ end
364
+ end
365
+
366
+ #
367
+ # Fetches all session cookies.
368
+ #
369
+ # @return [Array<Ferrum::Cookie>]
370
+ # The matching session cookies.
371
+ #
372
+ def session_cookies
373
+ each_session_cookie.to_a
374
+ end
375
+
376
+ #
377
+ # Sets a cookie.
378
+ #
379
+ # @param [String] name
380
+ # The cookie name.
381
+ #
382
+ # @param [String] value
383
+ # The cookie value.
384
+ #
385
+ # @param [Hash{Symbol => Object}] options
386
+ # Additional cookie attributes.
387
+ #
388
+ def set_cookie(name,value,**options)
389
+ cookies.set(name: name, value: value, **options)
390
+ end
391
+
392
+ #
393
+ # Loads the cookies from the cookie file.
394
+ #
395
+ # @param [String] path
396
+ # The path to the cookie file.
397
+ #
398
+ def load_cookies(path)
399
+ CookieFile.new(path).each do |cookie|
400
+ cookies.set(cookie)
401
+ end
402
+ end
403
+
404
+ #
405
+ # Saves the cookies to a cookie file.
406
+ #
407
+ # @param [String] path
408
+ # The path to the output cookie file.
409
+ #
410
+ def save_cookies(path)
411
+ CookieFile.save(path,cookies)
412
+ end
413
+
414
+ #
415
+ # Waits indefinitely until the browser window is closed.
416
+ #
417
+ def wait_until_closed
418
+ window_closed = false
419
+
420
+ on('Inspector.detached') do
421
+ window_closed = true
422
+ end
423
+
424
+ sleep(1) until window_closed
425
+ end
426
+
427
+ end
428
+ end
429
+ end
430
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-web-browser - An automated Chrome API.
4
+ #
5
+ # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-web-browser is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-web-browser is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-web-browser. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ferrum/cookies/cookie'
22
+
23
+ module Ronin
24
+ module Web
25
+ module Browser
26
+ #
27
+ # Represents a browser cookie.
28
+ #
29
+ class Cookie < Ferrum::Cookies::Cookie
30
+
31
+ #
32
+ # Parses a browser cookie from a raw String.
33
+ #
34
+ # @param [String] string
35
+ # The raw cookie String to parse.
36
+ #
37
+ # @return [Cookie]
38
+ # The parsed cookie.
39
+ #
40
+ # @raise [ArgumentError]
41
+ # The string was empty or contains an unknown field.
42
+ #
43
+ def self.parse(string)
44
+ fields = string.split(/;\s+/)
45
+
46
+ if fields.empty?
47
+ raise(ArgumentError,"cookie must not be empty: #{string.inspect}")
48
+ end
49
+
50
+ name, value = fields.shift.split('=',2)
51
+ attributes = {
52
+ 'name' => name,
53
+ 'value' => value
54
+ }
55
+
56
+ fields.each do |field|
57
+ if field.include?('=')
58
+ key, value = field.split('=',2)
59
+
60
+ case key
61
+ when 'Expires', 'Max-Age'
62
+ attributes['expires'] = Time.parse(value).to_i
63
+ when 'Path' then attributes['path'] = value
64
+ when 'Domain' then attributes['domain'] = value
65
+ when 'SameSite'then attributes['sameSite'] = value
66
+ else
67
+ raise(ArgumentError,"unrecognized Cookie field: #{field.inspect}")
68
+ end
69
+ else
70
+ case field
71
+ when 'HttpOnly' then attributes['httpOnly'] = true
72
+ when 'Secure' then attributes['secure'] = true
73
+ else
74
+ raise(ArgumentError,"unrecognized Cookie flag: #{field.inspect}")
75
+ end
76
+ end
77
+ end
78
+
79
+ return new(attributes)
80
+ end
81
+
82
+ #
83
+ # The priority of the cookie.
84
+ #
85
+ # @return [String]
86
+ #
87
+ def priority
88
+ @attributes['priority']
89
+ end
90
+
91
+ #
92
+ # @return [Boolean]
93
+ #
94
+ def sameparty?
95
+ @attributes['sameParty']
96
+ end
97
+
98
+ alias same_party? sameparty?
99
+
100
+ #
101
+ # @return [String]
102
+ #
103
+ def source_scheme
104
+ @attributes['sourceScheme']
105
+ end
106
+
107
+ #
108
+ # @return [Integer]
109
+ #
110
+ def source_port
111
+ @attributes['sourcePort']
112
+ end
113
+
114
+ #
115
+ # Converts the cookie back into a raw cookie String.
116
+ #
117
+ # @return [String]
118
+ # The raw cookie string.
119
+ #
120
+ def to_s
121
+ string = "#{@attributes['name']}=#{@attributes['value']}"
122
+
123
+ @attributes.each do |key,value|
124
+ case key
125
+ when 'name', 'value' # no-op
126
+ when 'domain' then string << "; Domain=#{value}"
127
+ when 'path' then string << "; Path=#{value}"
128
+ when 'expires' then string << "; Expires=#{Time.at(value).httpdate}"
129
+ when 'httpOnly' then string << "; httpOnly" if value
130
+ when 'secure' then string << "; Secure" if value
131
+ end
132
+ end
133
+
134
+ return string
135
+ end
136
+
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-web-browser - An automated Chrome API.
4
+ #
5
+ # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-web-browser is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-web-browser is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-web-browser. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/web/browser/cookie'
22
+
23
+ module Ronin
24
+ module Web
25
+ module Browser
26
+ #
27
+ # Represents a file of cookies.
28
+ #
29
+ class CookieFile
30
+
31
+ include Enumerable
32
+
33
+ # The path to the file.
34
+ #
35
+ # @return [String]
36
+ attr_reader :path
37
+
38
+ #
39
+ # Initializes a cookie file.
40
+ #
41
+ # @param [String] path
42
+ # The path to the cookie file.
43
+ #
44
+ def initialize(path)
45
+ @path = File.expand_path(path)
46
+ end
47
+
48
+ #
49
+ # Writes the cookies to the cookie file.
50
+ #
51
+ # @param [String] path
52
+ # The path to the cookie file to write to.
53
+ #
54
+ # @param [Array<Cookie>, Enumerator<Cookie>] cookies
55
+ # The cookies to write.
56
+ #
57
+ def self.save(path,cookies)
58
+ File.open(path,'w') do |file|
59
+ cookies.each do |cookie|
60
+ file.puts(cookie)
61
+ end
62
+ end
63
+ end
64
+
65
+ #
66
+ # Parses each cookie in the cookie file.
67
+ #
68
+ # @yield [cookie]
69
+ # The given block will be passed each cookie parsed from each line of
70
+ # the file.
71
+ #
72
+ # @yieldparam [Cookie] cookie
73
+ # A cookie parsed from the file.
74
+ #
75
+ # @return [Enumerator]
76
+ # If no block is given, then an Enumerator will be returned.
77
+ #
78
+ def each
79
+ return enum_for(__method__) unless block_given?
80
+
81
+ File.open(@path) do |file|
82
+ file.each_line(chomp: true) do |line|
83
+ yield Cookie.parse(line)
84
+ end
85
+ end
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-web-browser - An automated Chrome API.
4
+ #
5
+ # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-web-browser is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-web-browser is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-web-browser. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ module Ronin
22
+ module Web
23
+ module Browser
24
+ # ronin-web-browser version
25
+ VERSION = '0.1.0.rc1'
26
+ end
27
+ end
28
+ end