ronin-web-browser 0.1.0.rc1

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.
@@ -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