ronin-support-web 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1386 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-support-web - A web support library for ronin-rb.
4
+ #
5
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-support-web 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-support-web 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-support-web. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/support/network/http'
22
+
23
+ require 'addressable/uri'
24
+ require 'nokogiri'
25
+ require 'json'
26
+
27
+ module Ronin
28
+ module Support
29
+ module Web
30
+ #
31
+ # Web Agent represents a stripped-down web browser, which can request
32
+ # URLs, follow redirects, and parse responses.
33
+ #
34
+ # ## Features
35
+ #
36
+ # * Automatically follows redirects.
37
+ # * Provides low-level HTTP methods.
38
+ # * Provides high-level methods for requesting and parsing HTML, XML, or
39
+ # JSON.
40
+ # * Maintains a persistent connection pool.
41
+ #
42
+ # ## Anti-Features
43
+ #
44
+ # * Does not cache files or write to the disk.
45
+ # * Does not evaluate JavaScript.
46
+ #
47
+ class Agent
48
+
49
+ #
50
+ # Base-class for all {Agent} exceptions.
51
+ #
52
+ class Error < RuntimeError
53
+ end
54
+
55
+ #
56
+ # Indicates that too many redirects were encountered in succession.
57
+ #
58
+ class TooManyRedirects < Error
59
+ end
60
+
61
+ #
62
+ # Indicates that the response does not have a compatible or expected
63
+ # `Content-Type` header.
64
+ #
65
+ class ContentTypeError < Error
66
+ end
67
+
68
+ # The proxy to send requests through.
69
+ #
70
+ # @return [URI::HTTP, Addressable::URI, nil]
71
+ attr_reader :proxy
72
+
73
+ # The `User-Agent` header value.
74
+ #
75
+ # @return [String, nil]
76
+ attr_reader :user_agent
77
+
78
+ # Maximum number of redirects to follow.
79
+ #
80
+ # @return [Integer]
81
+ attr_reader :max_redirects
82
+
83
+ #
84
+ # Initializes the Web agent.
85
+ #
86
+ # @param [String, URI::HTTP, Addressable::URI, nil] proxy
87
+ # The optional proxy to send requests through.
88
+ #
89
+ # @param [String, :random, :chrome, :chrome_linux, :chrome_macos, :chrome_windows, :chrome_iphone, :chrome_ipad, :chrome_android, :firefox, :firefox_linux, :firefox_macos, :firefox_windows, :firefox_iphone, :firefox_ipad, :firefox_android, :safari, :safari_macos, :safari_iphone, :safari_ipad, :edge, :linux, :macos, :windows, :iphone, :ipad, :android, nil] user_agent
90
+ # The default `User-Agent` string to add to each request.
91
+ #
92
+ # @param [Boolean, Hash{Symbol => Object}, nil] ssl
93
+ # Additional SSL/TLS configuration.
94
+ #
95
+ # @option ssl [String, nil] :ca_bundle
96
+ # The path to the CA bundle directory or file.
97
+ #
98
+ # @option ssl [OpenSSL::X509::Store, nil] :cert_store
99
+ # The certificate store to use for the SSL/TLS connection.
100
+ #
101
+ # @option ssl [Array<(name, version, bits, alg_bits)>, nil] :ciphers
102
+ # The accepted ciphers to use for the SSL/TLS connection.
103
+ #
104
+ # @option ssl [Integer, nil] :timeout
105
+ # The connection timeout limit.
106
+ #
107
+ # @option ssl [1, 1.1, 1.2, Symbol, nil] :version
108
+ # The desired SSL/TLS version.
109
+ #
110
+ # @option ssl [1, 1.1, 1.2, Symbol, nil] :min_version
111
+ # The minimum SSL/TLS version.
112
+ #
113
+ # @option ssl [1, 1.1, 1.2, Symbol, nil] :max_version
114
+ # The maximum SSL/TLS version.
115
+ #
116
+ # @option ssl [Proc, nil] :verify_callback
117
+ # The callback to use when verifying the server's certificate.
118
+ #
119
+ # @option ssl [Integer, nil] :verify_depth
120
+ # The verification depth limit.
121
+ #
122
+ # @option ssl [:none, :peer, :fail_if_no_peer_cert, true, false, Integer, nil] :verify
123
+ # The verification mode.
124
+ #
125
+ # @option ssl [Boolean, nil] :verify_hostname
126
+ # Indicates whether to verify the server's hostname.
127
+ #
128
+ def initialize(follow_redirects: true,
129
+ max_redirects: 20,
130
+ # HTTP options
131
+ proxy: Support::Network::HTTP.proxy,
132
+ ssl: nil,
133
+ user_agent: Support::Network::HTTP.user_agent)
134
+ @follow_redirects = follow_redirects
135
+ @max_redirects = max_redirects
136
+
137
+ # HTTP options
138
+ @proxy = proxy
139
+ @ssl = ssl
140
+ @user_agent = user_agent
141
+
142
+ @sessions = {}
143
+ end
144
+
145
+ #
146
+ # Indicates whether redirects will automatically be followed.
147
+ #
148
+ # @return [Boolean]
149
+ #
150
+ def follow_redirects?
151
+ @follow_redirects
152
+ end
153
+
154
+ #
155
+ # @!macro request_kwargs
156
+ # @option kwargs [String, nil] :query
157
+ # The query-string to append to the request path.
158
+ #
159
+ # @option kwargs [Hash, nil] :query_params
160
+ # The query-params to append to the request path.
161
+ #
162
+ # @option kwargs [String, nil] :user
163
+ # The user to authenticate as.
164
+ #
165
+ # @option kwargs [String, nil] :password
166
+ # The password to authenticate with.
167
+ #
168
+ # @option kwargs [Hash{Symbol,String => String}, nil] :headers
169
+ # Additional HTTP headers to use for the request.
170
+ #
171
+ # @option kwargs [String, :text, :xml, :html, :json, nil] :content_type
172
+ # The `Content-Type` header value for the request.
173
+ # If a Symbol is given it will be resolved to a common MIME type:
174
+ # * `:text` - `text/plain`
175
+ # * `:xml` - `text/xml`
176
+ # * `:html` - `text/html`
177
+ # * `:json` - `application/json`
178
+ #
179
+ # @option kwargs [String, :text, :xml, :html, :json, nil] :accept
180
+ # The `Accept` header value for the request.
181
+ # If a Symbol is given it will be resolved to a common MIME type:
182
+ # * `:text` - `text/plain`
183
+ # * `:xml` - `text/xml`
184
+ # * `:html` - `text/html`
185
+ # * `:json` - `application/json`
186
+ #
187
+ # @option kwargs [String, Hash{String => String}, Ronin::Support::Network::HTTP::Cookie, nil] :cookie
188
+ # Additional `Cookie` header.
189
+ # * If a `Hash` is given, it will be converted to a `String` using
190
+ # [Ronin::Support::Network::HTTP::Cookie](https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP/Cookie.html).
191
+ # * If the cookie value is empty, the `Cookie` header will not be
192
+ # set.
193
+ #
194
+ # @option kwargs [String, nil] :body
195
+ # The body of the request.
196
+ #
197
+ # @option kwargs [Hash, String, nil] :form_data
198
+ # The form data that may be sent in the body of the request.
199
+ #
200
+ # @option kwargs [#to_json, nil] :json
201
+ # The JSON data that will be sent in the body of the request.
202
+ # Will also default the `Content-Type` header to
203
+ # `application/json`, unless already set.
204
+ #
205
+
206
+ #
207
+ # Performs and arbitrary HTTP request.
208
+ #
209
+ # @param [Symbol, String] method
210
+ # The HTTP method to use for the request.
211
+ #
212
+ # @param [URI::HTTP, Addressable::URI, String] url
213
+ # The URL to create the HTTP request for.
214
+ #
215
+ # @!macro request_kwargs
216
+ #
217
+ # @yield [response]
218
+ # If a block is given it will be passed the received HTTP response.
219
+ #
220
+ # @yieldparam [Net::HTTPResponse] response
221
+ # The received HTTP response object.
222
+ #
223
+ # @return [Net::HTTPResponse]
224
+ # The HTTP response object.
225
+ #
226
+ # @raise [ArgumentError]
227
+ # The `:method` option did not match a known `Net::HTTP` request
228
+ # class.
229
+ #
230
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#request-instance_method
231
+ #
232
+ # @api public
233
+ #
234
+ def http_request(method,url,**kwargs,&block)
235
+ uri = normalize_url(url)
236
+
237
+ session_for(uri).request(
238
+ method, uri.request_uri, user: uri.user,
239
+ password: uri.password,
240
+ **kwargs, &block
241
+ )
242
+ end
243
+
244
+ #
245
+ # Sends an arbitrary HTTP request and returns the response status.
246
+ #
247
+ # @param [Symbol, String] method
248
+ # The HTTP method to use for the request.
249
+ #
250
+ # @param [URI::HTTP, Addressable::URI, String] url
251
+ # The URL to create the HTTP request for.
252
+ #
253
+ # @!macro request_kwargs
254
+ #
255
+ # @return [Integer]
256
+ # The status code of the response.
257
+ #
258
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#response_status-instance_method
259
+ #
260
+ # @api public
261
+ #
262
+ def http_response_status(method=:head,url,**kwargs)
263
+ uri = normalize_url(url)
264
+
265
+ session_for(uri).response_status(
266
+ method, uri.request_uri, user: uri.user,
267
+ password: uri.password,
268
+ **kwargs
269
+ )
270
+ end
271
+
272
+ #
273
+ # Sends a HTTP request and determines if the response status was 200.
274
+ #
275
+ # @param [Symbol, String] method
276
+ # The HTTP method to use for the request.
277
+ #
278
+ # @param [URI::HTTP, Addressable::URI, String] url
279
+ # The URL to create the HTTP request for.
280
+ #
281
+ # @!macro request_kwargs
282
+ #
283
+ # @return [Boolean]
284
+ # Indicates that the response status was 200.
285
+ #
286
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#ok%3F-instance_method
287
+ #
288
+ # @api public
289
+ #
290
+ def http_ok?(method=:head,url,**kwargs)
291
+ uri = normalize_url(url)
292
+
293
+ session_for(uri).ok?(
294
+ method, uri.request_uri, user: uri.user,
295
+ password: uri.password,
296
+ **kwargs
297
+ )
298
+ end
299
+
300
+ #
301
+ # Sends an arbitrary HTTP request and returns the response headers.
302
+ #
303
+ # @param [Symbol, String] method
304
+ # The HTTP method to use for the request.
305
+ #
306
+ # @param [URI::HTTP, Addressable::URI, String] url
307
+ # The URL to create the HTTP request for.
308
+ #
309
+ # @!macro request_kwargs
310
+ #
311
+ # @return [Hash{String => String}]
312
+ # The response headers.
313
+ #
314
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#response_headers-instance_method
315
+ #
316
+ # @api public
317
+ #
318
+ def http_response_headers(method=:head,url,**kwargs)
319
+ uri = normalize_url(url)
320
+
321
+ session_for(uri).response_headers(
322
+ method, uri.request_uri, user: uri.user,
323
+ password: uri.password,
324
+ **kwargs
325
+ )
326
+ end
327
+
328
+ #
329
+ # Sends an HTTP request and returns the `Server` header.
330
+ #
331
+ # @param [URI::HTTP, Addressable::URI, String] url
332
+ # The URL to create the HTTP request for.
333
+ #
334
+ # @!macro request_kwargs
335
+ #
336
+ # @return [String, nil]
337
+ # The `Server` header.
338
+ #
339
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#server_header-instance_method
340
+ #
341
+ # @api public
342
+ #
343
+ def http_server_header(url,**kwargs)
344
+ uri = normalize_url(url)
345
+
346
+ session_for(uri).server_header(
347
+ user: uri.user,
348
+ password: uri.password,
349
+ path: uri.request_uri,
350
+ **kwargs
351
+ )
352
+ end
353
+
354
+ #
355
+ # Sends an HTTP request and returns the `X-Powered-By` header.
356
+ #
357
+ # @param [URI::HTTP, Addressable::URI, String] url
358
+ # The URL to create the HTTP request for.
359
+ #
360
+ # @!macro request_kwargs
361
+ #
362
+ # @return [String, nil]
363
+ # The `X-Powered-By` header.
364
+ #
365
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#powered_by_header-instance_method
366
+ #
367
+ # @api public
368
+ #
369
+ def http_powered_by_header(url,**kwargs)
370
+ uri = normalize_url(url)
371
+
372
+ session_for(uri).powered_by_header(
373
+ user: uri.user,
374
+ password: uri.password,
375
+ path: uri.request_uri,
376
+ **kwargs
377
+ )
378
+ end
379
+
380
+ #
381
+ # Sends an arbitrary HTTP request and returns the response body.
382
+ #
383
+ # @param [Symbol, String] method
384
+ # The HTTP method to use for the request.
385
+ #
386
+ # @param [URI::HTTP, Addressable::URI, String] url
387
+ # The URL to create the HTTP request for.
388
+ #
389
+ # @!macro request_kwargs
390
+ #
391
+ # @return [String]
392
+ # The response body.
393
+ #
394
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#response_body-instance_method
395
+ #
396
+ # @api public
397
+ #
398
+ def http_response_body(method=:get,url,**kwargs)
399
+ uri = normalize_url(url)
400
+
401
+ session_for(uri).response_body(
402
+ method, uri.request_uri, user: uri.user,
403
+ password: uri.password,
404
+ **kwargs
405
+ )
406
+ end
407
+
408
+ #
409
+ # Performs a `COPY` request for the given URI.
410
+ #
411
+ # @param [URI::HTTP, Addressable::URI, String] url
412
+ # The URL to create the HTTP request for.
413
+ #
414
+ # @!macro request_kwargs
415
+ #
416
+ # @yield [response]
417
+ # If a block is given it will be passed the received HTTP response.
418
+ #
419
+ # @yieldparam [Net::HTTPResponse] response
420
+ # The received HTTP response object.
421
+ #
422
+ # @return [Net::HTTPResponse]
423
+ # The HTTP response object.
424
+ #
425
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#copy-instance_method
426
+ #
427
+ # @api public
428
+ #
429
+ def http_copy(url,**kwargs,&block)
430
+ uri = normalize_url(url)
431
+
432
+ session_for(uri).copy(
433
+ uri.request_uri, user: uri.user,
434
+ password: uri.password,
435
+ **kwargs, &block
436
+ )
437
+ end
438
+
439
+ #
440
+ # Performs a `DELETE` request for the given URI.
441
+ #
442
+ # @param [URI::HTTP, Addressable::URI, String] url
443
+ # The URL to create the HTTP request for.
444
+ #
445
+ # @!macro request_kwargs
446
+ #
447
+ # @yield [response]
448
+ # If a block is given it will be passed the received HTTP response.
449
+ #
450
+ # @yieldparam [Net::HTTPResponse] response
451
+ # The received HTTP response object.
452
+ #
453
+ # @return [Net::HTTPResponse]
454
+ # The HTTP response object.
455
+ #
456
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#delete-instance_method
457
+ #
458
+ # @api public
459
+ #
460
+ def http_delete(url,**kwargs,&block)
461
+ uri = normalize_url(url)
462
+
463
+ session_for(uri).delete(
464
+ uri.request_uri, user: uri.user,
465
+ password: uri.password,
466
+ **kwargs, &block
467
+ )
468
+ end
469
+
470
+ #
471
+ # Performs a `GET` request for the given URI.
472
+ #
473
+ # @param [URI::HTTP, Addressable::URI, String] url
474
+ # The URL to create the HTTP request for.
475
+ #
476
+ # @!macro request_kwargs
477
+ #
478
+ # @yield [response]
479
+ # If a block is given it will be passed the received HTTP response.
480
+ #
481
+ # @yieldparam [Net::HTTPResponse] response
482
+ # The received HTTP response object.
483
+ #
484
+ # @return [Net::HTTPResponse]
485
+ # The HTTP response object.
486
+ #
487
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get-instance_method
488
+ #
489
+ # @api public
490
+ #
491
+ def http_get(url,**kwargs,&block)
492
+ uri = normalize_url(url)
493
+
494
+ session_for(uri).get(
495
+ uri.request_uri, user: uri.user,
496
+ password: uri.password,
497
+ **kwargs, &block
498
+ )
499
+ end
500
+
501
+ #
502
+ # Performs a `GET` request for the given URI and returns the response
503
+ # headers.
504
+ #
505
+ # @param [URI::HTTP, Addressable::URI, String] url
506
+ # The URL to create the HTTP request for.
507
+ #
508
+ # @!macro request_kwargs
509
+ #
510
+ # @return [Hash{String => String}]
511
+ # The response headers.
512
+ #
513
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get_headers-instance_method
514
+ #
515
+ # @api public
516
+ #
517
+ def http_get_headers(url,**kwargs)
518
+ uri = normalize_url(url)
519
+
520
+ session_for(uri).get_headers(
521
+ uri.request_uri, user: uri.user,
522
+ password: uri.password,
523
+ **kwargs
524
+ )
525
+ end
526
+
527
+ #
528
+ # Sends an HTTP request and returns the parsed `Set-Cookie`
529
+ # header(s).
530
+ #
531
+ # @param [URI::HTTP, Addressable::URI, String] url
532
+ # The URL to create the HTTP request for.
533
+ #
534
+ # @!macro request_kwargs
535
+ #
536
+ # @return [Array<SetCookie>, nil]
537
+ # The parsed `SetCookie` header(s).
538
+ #
539
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get_cookies-instance_method
540
+ #
541
+ # @api public
542
+ #
543
+ def http_get_cookies(url,**kwargs)
544
+ uri = normalize_url(url)
545
+
546
+ session_for(uri).get_cookies(
547
+ uri.request_uri, user: uri.user,
548
+ password: uri.password,
549
+ **kwargs
550
+ )
551
+ end
552
+
553
+ #
554
+ # Performs a `GET` request for the given URI and returns the response
555
+ # body.
556
+ #
557
+ # @param [URI::HTTP, Addressable::URI, String] url
558
+ # The URL to create the HTTP request for.
559
+ #
560
+ # @!macro request_kwargs
561
+ #
562
+ # @return [String]
563
+ # The response body.
564
+ #
565
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#get_body-instance_method
566
+ #
567
+ # @api public
568
+ #
569
+ def http_get_body(url,**kwargs)
570
+ uri = normalize_url(url)
571
+
572
+ session_for(uri).get_body(
573
+ uri.request_uri, user: uri.user,
574
+ password: uri.password,
575
+ **kwargs
576
+ )
577
+ end
578
+
579
+ #
580
+ # Performs a `HEAD` request for the given URI.
581
+ #
582
+ # @param [URI::HTTP, Addressable::URI, String] url
583
+ # The URL to create the HTTP request for.
584
+ #
585
+ # @!macro request_kwargs
586
+ #
587
+ # @yield [response]
588
+ # If a block is given it will be passed the received HTTP response.
589
+ #
590
+ # @yieldparam [Net::HTTPResponse] response
591
+ # The received HTTP response object.
592
+ #
593
+ # @return [Net::HTTPResponse]
594
+ # The HTTP response object.
595
+ #
596
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#head-instance_method
597
+ #
598
+ # @api public
599
+ #
600
+ def http_head(url,**kwargs,&block)
601
+ uri = normalize_url(url)
602
+
603
+ session_for(uri).head(
604
+ uri.request_uri, user: uri.user,
605
+ password: uri.password,
606
+ **kwargs, &block
607
+ )
608
+ end
609
+
610
+ #
611
+ # Performs a `LOCK` request for the given URI.
612
+ #
613
+ # @param [URI::HTTP, Addressable::URI, String] url
614
+ # The URL to create the HTTP request for.
615
+ #
616
+ # @!macro request_kwargs
617
+ #
618
+ # @yield [response]
619
+ # If a block is given it will be passed the received HTTP response.
620
+ #
621
+ # @yieldparam [Net::HTTPResponse] response
622
+ # The received HTTP response object.
623
+ #
624
+ # @return [Net::HTTPResponse]
625
+ # The HTTP response object.
626
+ #
627
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#lock-instance_method
628
+ #
629
+ # @api public
630
+ #
631
+ def http_lock(url,**kwargs,&block)
632
+ uri = normalize_url(url)
633
+
634
+ session_for(uri).lock(
635
+ uri.request_uri, user: uri.user,
636
+ password: uri.password,
637
+ **kwargs, &block
638
+ )
639
+ end
640
+
641
+ #
642
+ # Performs a `MKCOL` request for the given URI.
643
+ #
644
+ # @param [URI::HTTP, Addressable::URI, String] url
645
+ # The URL to create the HTTP request for.
646
+ #
647
+ # @!macro request_kwargs
648
+ #
649
+ # @yield [response]
650
+ # If a block is given it will be passed the received HTTP response.
651
+ #
652
+ # @yieldparam [Net::HTTPResponse] response
653
+ # The received HTTP response object.
654
+ #
655
+ # @return [Net::HTTPResponse]
656
+ # The HTTP response object.
657
+ #
658
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#mkcol-instance_method
659
+ #
660
+ # @api public
661
+ #
662
+ def http_mkcol(url,**kwargs,&block)
663
+ uri = normalize_url(url)
664
+
665
+ session_for(uri).mkcol(
666
+ uri.request_uri, user: uri.user,
667
+ password: uri.password,
668
+ **kwargs, &block
669
+ )
670
+ end
671
+
672
+ #
673
+ # Performs a `MOVE` request for the given URI.
674
+ #
675
+ # @param [URI::HTTP, Addressable::URI, String] url
676
+ # The URL to create the HTTP request for.
677
+ #
678
+ # @!macro request_kwargs
679
+ #
680
+ # @yield [response]
681
+ # If a block is given it will be passed the received HTTP response.
682
+ #
683
+ # @yieldparam [Net::HTTPResponse] response
684
+ # The received HTTP response object.
685
+ #
686
+ # @return [Net::HTTPResponse]
687
+ # The HTTP response object.
688
+ #
689
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#move-instance_method
690
+ #
691
+ # @api public
692
+ #
693
+ def http_move(url,**kwargs,&block)
694
+ uri = normalize_url(url)
695
+
696
+ session_for(uri).move(
697
+ uri.request_uri, user: uri.user,
698
+ password: uri.password,
699
+ **kwargs, &block
700
+ )
701
+ end
702
+
703
+ #
704
+ # Performs a `OPTIONS` request for the given URI.
705
+ #
706
+ # @param [URI::HTTP, Addressable::URI, String] url
707
+ # The URL to create the HTTP request for.
708
+ #
709
+ # @!macro request_kwargs
710
+ #
711
+ # @yield [response]
712
+ # If a block is given it will be passed the received HTTP response.
713
+ #
714
+ # @yieldparam [Net::HTTPResponse] response
715
+ # The received HTTP response object.
716
+ #
717
+ # @return [Net::HTTPResponse]
718
+ # The HTTP response object.
719
+ #
720
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#options-instance_method
721
+ #
722
+ # @api public
723
+ #
724
+ def http_options(url,**kwargs,&block)
725
+ uri = normalize_url(url)
726
+
727
+ session_for(uri).options(
728
+ uri.request_uri, user: uri.user,
729
+ password: uri.password,
730
+ **kwargs, &block
731
+ )
732
+ end
733
+
734
+ #
735
+ # Performs a `OPTIONS` HTTP request for the given URI and parses the
736
+ # `Allow` response header.
737
+ #
738
+ # @param [URI::HTTP, Addressable::URI, String] url
739
+ # The URL to create the HTTP request for.
740
+ #
741
+ # @!macro request_kwargs
742
+ #
743
+ # @return [Array<Symbol>]
744
+ # The allowed HTTP request methods for the given URL.
745
+ #
746
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#allowed_methods-instance_method
747
+ #
748
+ # @api public
749
+ #
750
+ def http_allowed_methods(url,**kwargs)
751
+ uri = normalize_url(url)
752
+
753
+ session_for(uri).allowed_methods(
754
+ uri.request_uri, user: uri.user,
755
+ password: uri.password,
756
+ **kwargs
757
+ )
758
+ end
759
+
760
+ #
761
+ # Performs a `PATCH` request for the given URI.
762
+ #
763
+ # @param [URI::HTTP, Addressable::URI, String] url
764
+ # The URL to create the HTTP request for.
765
+ #
766
+ # @!macro request_kwargs
767
+ #
768
+ # @yield [response]
769
+ # If a block is given it will be passed the received HTTP response.
770
+ #
771
+ # @yieldparam [Net::HTTPResponse] response
772
+ # The received HTTP response object.
773
+ #
774
+ # @return [Net::HTTPResponse]
775
+ # The HTTP response object.
776
+ #
777
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#patch-instance_method
778
+ #
779
+ # @api public
780
+ #
781
+ def http_patch(url,**kwargs,&block)
782
+ uri = normalize_url(url)
783
+
784
+ session_for(uri).patch(
785
+ uri.request_uri, user: uri.user,
786
+ password: uri.password,
787
+ **kwargs, &block
788
+ )
789
+ end
790
+
791
+ #
792
+ # Performs a `POST` request for the given URI.
793
+ #
794
+ # @param [URI::HTTP, Addressable::URI, String] url
795
+ # The URL to create the HTTP request for.
796
+ #
797
+ # @!macro request_kwargs
798
+ #
799
+ # @yield [response]
800
+ # If a block is given it will be passed the received HTTP response.
801
+ #
802
+ # @yieldparam [Net::HTTPResponse] response
803
+ # The received HTTP response object.
804
+ #
805
+ # @return [Net::HTTPResponse]
806
+ # The HTTP response object.
807
+ #
808
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#post-instance_method
809
+ #
810
+ # @api public
811
+ #
812
+ def http_post(url,**kwargs,&block)
813
+ uri = normalize_url(url)
814
+
815
+ session_for(uri).post(
816
+ uri.request_uri, user: uri.user,
817
+ password: uri.password,
818
+ **kwargs, &block
819
+ )
820
+ end
821
+
822
+ #
823
+ # Performs a `POST` request on the given URI and returns the response
824
+ # headers.
825
+ #
826
+ # @param [URI::HTTP, Addressable::URI, String] url
827
+ # The URL to create the HTTP request for.
828
+ #
829
+ # @!macro request_kwargs
830
+ #
831
+ # @return [Hash{String => String}]
832
+ # The response headers.
833
+ #
834
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#post_headers-instance_method
835
+ #
836
+ # @api public
837
+ #
838
+ def http_post_headers(url,**kwargs)
839
+ uri = normalize_url(url)
840
+
841
+ session_for(uri).post_headers(
842
+ uri.request_uri, user: uri.user,
843
+ password: uri.password,
844
+ **kwargs
845
+ )
846
+ end
847
+
848
+ #
849
+ # Performs a `POST` request for the given URI and returns the
850
+ # response body.
851
+ #
852
+ # @param [URI::HTTP, Addressable::URI, String] url
853
+ # The URL to create the HTTP request for.
854
+ #
855
+ # @!macro request_kwargs
856
+ #
857
+ # @return [String]
858
+ # The response body.
859
+ #
860
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#post_body-instance_method
861
+ #
862
+ # @api public
863
+ #
864
+ def http_post_body(url,**kwargs)
865
+ uri = normalize_url(url)
866
+
867
+ session_for(uri).post_body(
868
+ uri.request_uri, user: uri.user,
869
+ password: uri.password,
870
+ **kwargs
871
+ )
872
+ end
873
+
874
+ #
875
+ # Performs a `PROPFIND` request for the given URI.
876
+ #
877
+ # @param [URI::HTTP, Addressable::URI, String] url
878
+ # The URL to create the HTTP request for.
879
+ #
880
+ # @!macro request_kwargs
881
+ #
882
+ # @yield [response]
883
+ # If a block is given it will be passed the received HTTP response.
884
+ #
885
+ # @yieldparam [Net::HTTPResponse] response
886
+ # The received HTTP response object.
887
+ #
888
+ # @return [Net::HTTPResponse]
889
+ # The HTTP response object.
890
+ #
891
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#propfind-instance_method
892
+ #
893
+ # @api public
894
+ #
895
+ def http_propfind(url,**kwargs,&block)
896
+ uri = normalize_url(url)
897
+
898
+ session_for(uri).propfind(
899
+ uri.request_uri, user: uri.user,
900
+ password: uri.password,
901
+ **kwargs, &block
902
+ )
903
+ end
904
+
905
+ alias http_prop_find http_propfind
906
+
907
+ #
908
+ # Performs a `PROPPATCH` request for the given URI.
909
+ #
910
+ # @param [URI::HTTP, Addressable::URI, String] url
911
+ # The URL to create the HTTP request for.
912
+ #
913
+ # @!macro request_kwargs
914
+ #
915
+ # @yield [response]
916
+ # If a block is given it will be passed the received HTTP response.
917
+ #
918
+ # @yieldparam [Net::HTTPResponse] response
919
+ # The received HTTP response object.
920
+ #
921
+ # @return [Net::HTTPResponse]
922
+ # The HTTP response object.
923
+ #
924
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#proppatch-instance_method
925
+ #
926
+ # @api public
927
+ #
928
+ def http_proppatch(url,**kwargs,&block)
929
+ uri = normalize_url(url)
930
+
931
+ session_for(uri).proppatch(
932
+ uri.request_uri, user: uri.user,
933
+ password: uri.password,
934
+ **kwargs, &block
935
+ )
936
+ end
937
+
938
+ alias http_prop_patch http_proppatch
939
+
940
+ #
941
+ # Performs a `PUT` request for the given URI.
942
+ #
943
+ # @param [URI::HTTP, Addressable::URI, String] url
944
+ # The URL to create the HTTP request for.
945
+ #
946
+ # @!macro request_kwargs
947
+ #
948
+ # @yield [response]
949
+ # If a block is given it will be passed the received HTTP response.
950
+ #
951
+ # @yieldparam [Net::HTTPResponse] response
952
+ # The received HTTP response object.
953
+ #
954
+ # @return [Net::HTTPResponse]
955
+ # The HTTP response object.
956
+ #
957
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#put-instance_method
958
+ #
959
+ # @api public
960
+ #
961
+ def http_put(url,**kwargs,&block)
962
+ uri = normalize_url(url)
963
+
964
+ session_for(uri).put(
965
+ uri.request_uri, user: uri.user,
966
+ password: uri.password,
967
+ **kwargs, &block
968
+ )
969
+ end
970
+
971
+ #
972
+ # Performs a `TRACE` request for the given URI.
973
+ #
974
+ # @param [URI::HTTP, Addressable::URI, String] url
975
+ # The URL to create the HTTP request for.
976
+ #
977
+ # @!macro request_kwargs
978
+ #
979
+ # @yield [response]
980
+ # If a block is given it will be passed the received HTTP response.
981
+ #
982
+ # @yieldparam [Net::HTTPResponse] response
983
+ # The received HTTP response object.
984
+ #
985
+ # @return [Net::HTTPResponse]
986
+ # The HTTP response object.
987
+ #
988
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#trace-instance_method
989
+ #
990
+ # @api public
991
+ #
992
+ def http_trace(url,**kwargs,&block)
993
+ uri = normalize_url(url)
994
+
995
+ session_for(uri).trace(
996
+ uri.request_uri, user: uri.user,
997
+ password: uri.password,
998
+ **kwargs, &block
999
+ )
1000
+ end
1001
+
1002
+ #
1003
+ # Performs a `UNLOCK` request for the given URI.
1004
+ #
1005
+ # @param [URI::HTTP, Addressable::URI, String] url
1006
+ # The URL to create the HTTP request for.
1007
+ #
1008
+ # @!macro request_kwargs
1009
+ #
1010
+ # @yield [response]
1011
+ # If a block is given it will be passed the received HTTP response.
1012
+ #
1013
+ # @yieldparam [Net::HTTPResponse] response
1014
+ # The received HTTP response object.
1015
+ #
1016
+ # @return [Net::HTTPResponse]
1017
+ # The HTTP response object.
1018
+ #
1019
+ # @see https://ronin-rb.dev/docs/ronin-support/Ronin/Support/Network/HTTP.html#unlock-instance_method
1020
+ #
1021
+ # @api public
1022
+ #
1023
+ def http_unlock(url,**kwargs,&block)
1024
+ uri = normalize_url(url)
1025
+
1026
+ session_for(uri).unlock(
1027
+ uri.request_uri, user: uri.user,
1028
+ password: uri.password,
1029
+ **kwargs, &block
1030
+ )
1031
+ end
1032
+
1033
+ #
1034
+ # Gets a URL and returns the response.
1035
+ #
1036
+ # @param [URI::HTTP, Addressable::URI, String] url
1037
+ # The URL to create the HTTP GET request for.
1038
+ #
1039
+ # @!macro request_kwargs
1040
+ #
1041
+ # @yield [response]
1042
+ # If a block is given it will be passed the received HTTP response.
1043
+ #
1044
+ # @yieldparam [Net::HTTPResponse] response
1045
+ # The received HTTP response object.
1046
+ #
1047
+ # @return [Net::HTTPResponse]
1048
+ # The HTTP response object.
1049
+ #
1050
+ # @raise [TooManyRedirects]
1051
+ # Maximum number of redirects reached.
1052
+ #
1053
+ # @note This method will follow redirects by default.
1054
+ #
1055
+ # @example
1056
+ # response = agent.get('https://example.com/')
1057
+ # # => #<Net::HTTPResponse:...>
1058
+ #
1059
+ def get(url, follow_redirects: @follow_redirects,
1060
+ max_redirects: @max_redirects,
1061
+ **kwargs)
1062
+ response = http_get(url,**kwargs)
1063
+
1064
+ if follow_redirects && response.kind_of?(Net::HTTPRedirection)
1065
+ redirect_count = 0
1066
+
1067
+ while response.kind_of?(Net::HTTPRedirection)
1068
+ if redirect_count >= max_redirects
1069
+ raise(TooManyRedirects,"maximum number of redirects reached: #{url.inspect}")
1070
+ end
1071
+
1072
+ location = response['Location']
1073
+ response = http_get(location)
1074
+
1075
+ redirect_count += 1
1076
+ end
1077
+ end
1078
+
1079
+ yield response if block_given?
1080
+ return response
1081
+ end
1082
+
1083
+ #
1084
+ # Gets the URL and returns the parsed HTML.
1085
+ #
1086
+ # @param [URI::HTTP, Addressable::URI, String] url
1087
+ # The URL to create the HTTP GET request for.
1088
+ #
1089
+ # @!macro request_kwargs
1090
+ #
1091
+ # @return [Nokogiri::HTML::Document]
1092
+ # The parsed HTML response.
1093
+ #
1094
+ # @raise [ContentTypeError]
1095
+ # Did not receive a response with a `Content-Type` of `text/html`.
1096
+ #
1097
+ # @raise [TooManyRedirects]
1098
+ # Maximum number of redirects reached.
1099
+ #
1100
+ # @note This method will follow redirects by default.
1101
+ #
1102
+ # @example
1103
+ # doc = agent.get_html('https://example.com/page.html')
1104
+ # # => #<Nokogiri::HTML::Document:...>
1105
+ #
1106
+ def get_html(url,**kwargs)
1107
+ response = get(url,**kwargs)
1108
+
1109
+ unless response.content_type.include?('text/html')
1110
+ raise(ContentTypeError,"response 'Content-Type' was not 'text/html': #{response.content_type.inspect}")
1111
+ end
1112
+
1113
+ return Nokogiri::HTML(response.body)
1114
+ end
1115
+
1116
+ #
1117
+ # Gets the URL and returns the parsed XML.
1118
+ #
1119
+ # @param [URI::HTTP, Addressable::URI, String] url
1120
+ # The URL to create the HTTP GET request for.
1121
+ #
1122
+ # @!macro request_kwargs
1123
+ #
1124
+ # @return [Nokogiri::XML::Document]
1125
+ # The parsed XML response.
1126
+ #
1127
+ # @raise [ContentTypeError]
1128
+ # Did not receive a response with a `Content-Type` of `text/xml`.
1129
+ #
1130
+ # @raise [TooManyRedirects]
1131
+ # Maximum number of redirects reached.
1132
+ #
1133
+ # @note This method will follow redirects by default.
1134
+ #
1135
+ # @example
1136
+ # doc = agent.get_xml('https://example.com/data.xml')
1137
+ # # => #<Nokogiri::XML::Document:...>
1138
+ #
1139
+ def get_xml(url,**kwargs)
1140
+ response = get(url,**kwargs)
1141
+
1142
+ unless response.content_type.include?('text/xml')
1143
+ raise(ContentTypeError,"response 'Content-Type' was not 'text/xml': #{response.content_type.inspect}")
1144
+ end
1145
+
1146
+ return Nokogiri::XML(response.body)
1147
+ end
1148
+
1149
+ #
1150
+ # Gets the URL and returns the parsed JSON.
1151
+ #
1152
+ # @param [URI::HTTP, Addressable::URI, String] url
1153
+ # The URL to create the HTTP GET request for.
1154
+ #
1155
+ # @!macro request_kwargs
1156
+ #
1157
+ # @return [Hash{String => Object}, Array]
1158
+ # The parsed JSON.
1159
+ #
1160
+ # @raise [ContentTypeError]
1161
+ # Did not receive a response with a `Content-Type` of
1162
+ # `application/json`.
1163
+ #
1164
+ # @raise [TooManyRedirects]
1165
+ # Maximum number of redirects reached.
1166
+ #
1167
+ # @note This method will follow redirects by default.
1168
+ #
1169
+ # @example
1170
+ # json = agent.get_json('https://example.com/data.json')
1171
+ # # => {...}
1172
+ #
1173
+ def get_json(url,**kwargs)
1174
+ response = get(url,**kwargs)
1175
+
1176
+ unless response.content_type.include?('application/json')
1177
+ raise(ContentTypeError,"response 'Content-Type' was not 'application/json': #{response.content_type.inspect}")
1178
+ end
1179
+
1180
+ return ::JSON.parse(response.body)
1181
+ end
1182
+
1183
+ #
1184
+ # Performs an HTTP POST to the URL.
1185
+ #
1186
+ # @param [URI::HTTP, Addressable::URI, String] url
1187
+ # The URL to create the HTTP GET request for.
1188
+ #
1189
+ # @!macro request_kwargs
1190
+ #
1191
+ # @yield [response]
1192
+ # If a block is given it will be passed the received HTTP response.
1193
+ #
1194
+ # @yieldparam [Net::HTTPResponse] response
1195
+ # The received HTTP response object.
1196
+ #
1197
+ # @return [Net::HTTPResponse]
1198
+ # The HTTP response object.
1199
+ #
1200
+ # @raise [TooManyRedirects]
1201
+ # Maximum number of redirects reached.
1202
+ #
1203
+ # @note
1204
+ # If the response is an HTTP redirect, then {#get} will be called to
1205
+ # follow any redirects.
1206
+ #
1207
+ # @example
1208
+ # response = agent.post('https://example.com/form', form_data: {'foo' => 'bar'})
1209
+ # # => #<Net::HTTPResponse:...>
1210
+ #
1211
+ def post(url, follow_redirects: @follow_redirects,
1212
+ max_redirects: @max_redirects,
1213
+ **kwargs)
1214
+ response = http_post(url,**kwargs)
1215
+
1216
+ if follow_redirects && response.kind_of?(Net::HTTPRedirection)
1217
+ location = response['Location']
1218
+
1219
+ response = begin
1220
+ get(location, follow_redirects: follow_redirects,
1221
+ max_redirects: max_redirects - 1)
1222
+ rescue TooManyRedirects
1223
+ raise(TooManyRedirects,"maximum number of redirects reached: #{url.inspect}")
1224
+ end
1225
+ end
1226
+
1227
+ yield response if block_given?
1228
+ return response
1229
+ end
1230
+
1231
+ #
1232
+ # Performs an HTTP POST to the URL and parses the HTML response.
1233
+ #
1234
+ # @param [URI::HTTP, Addressable::URI, String] url
1235
+ # The URL to create the HTTP POST request for.
1236
+ #
1237
+ # @!macro request_kwargs
1238
+ #
1239
+ # @return [Nokogiri::HTML::Document]
1240
+ # The parsed HTML response.
1241
+ #
1242
+ # @raise [TooManyRedirects]
1243
+ # Maximum number of redirects reached.
1244
+ #
1245
+ # @raise [ContentTypeError]
1246
+ # Did not receive a response with a `Content-Type` of
1247
+ # `text/html`.
1248
+ #
1249
+ # @note
1250
+ # If the response is an HTTP redirect, then {#get} will be called to
1251
+ # follow any redirects.
1252
+ #
1253
+ # @example Send a POST request and parses the HTML response:
1254
+ # doc = agent.post_html 'https://example.com/form', form_data: {foo: 'bar'})
1255
+ # # => #<Nokogiri::HTML::Document:...>
1256
+ #
1257
+ def post_html(url,**kwargs)
1258
+ response = post(url,**kwargs)
1259
+
1260
+ unless response.content_type.include?('text/html')
1261
+ raise(ContentTypeError,"response 'Content-Type' was not 'text/html': #{response.content_type.inspect}")
1262
+ end
1263
+
1264
+ return Nokogiri::HTML(response.body)
1265
+ end
1266
+
1267
+ #
1268
+ # Performs an HTTP POST to the URL and parses the XML response.
1269
+ #
1270
+ # @param [URI::HTTP, Addressable::URI, String] url
1271
+ # The URL to create the HTTP POST request for.
1272
+ #
1273
+ # @!macro request_kwargs
1274
+ #
1275
+ # @return [Nokogiri::XML::Document]
1276
+ # The parsed XML response.
1277
+ #
1278
+ # @raise [TooManyRedirects]
1279
+ # Maximum number of redirects reached.
1280
+ #
1281
+ # @raise [ContentTypeError]
1282
+ # Did not receive a response with a `Content-Type` of
1283
+ # `text/xml`.
1284
+ #
1285
+ # @note
1286
+ # If the response is an HTTP redirect, then {#get} will be called to
1287
+ # follow any redirects.
1288
+ #
1289
+ # @example Send a POST request to the form and parses the XML response:
1290
+ # doc = agent.post_xml 'https://example.com/form', form_data: {foo: 'bar'}
1291
+ # # => #<Nokogiri::XML::Document:...>
1292
+ #
1293
+ def post_xml(url,**kwargs)
1294
+ response = post(url,**kwargs)
1295
+
1296
+ unless response.content_type.include?('text/xml')
1297
+ raise(ContentTypeError,"response 'Content-Type' was not 'application/json': #{response.content_type.inspect}")
1298
+ end
1299
+
1300
+ return Nokogiri::XML(response.body)
1301
+ end
1302
+
1303
+ #
1304
+ # Performs an HTTP POST to the URL and parses the JSON response.
1305
+ #
1306
+ # @param [URI::HTTP, Addressable::URI, String] url
1307
+ # The URL to create the HTTP POST request for.
1308
+ #
1309
+ # @!macro request_kwargs
1310
+ #
1311
+ # @return [Hash{String => Object}, Array]
1312
+ # The parses JSON response.
1313
+ #
1314
+ # @raise [TooManyRedirects]
1315
+ # Maximum number of redirects reached.
1316
+ #
1317
+ # @raise [ContentTypeError]
1318
+ # Did not receive a response with a `Content-Type` of
1319
+ # `application/json`.
1320
+ #
1321
+ # @note
1322
+ # If the response is an HTTP redirect, then {#get} will be called to
1323
+ # follow any redirects.
1324
+ #
1325
+ # @example Send a POST request to the form and parse the JSON response:
1326
+ # json = agent.post_json 'https://example.com/form', form_data: {foo: 'bar'}
1327
+ # # => {...}
1328
+ #
1329
+ # @example Send a POST request containing JSON and parse the JSON response:
1330
+ # json = agent.post_json 'https://example.com/api/end-point', json: {foo: 'bar'}
1331
+ # # => {...}
1332
+ #
1333
+ def post_json(url,**kwargs)
1334
+ response = post(url,**kwargs)
1335
+
1336
+ unless response.content_type.include?('application/json')
1337
+ raise(ContentTypeError,"response 'Content-Type' was not 'application/json': #{response.content_type.inspect}")
1338
+ end
1339
+
1340
+ return ::JSON.parse(response.body)
1341
+ end
1342
+
1343
+ private
1344
+
1345
+ #
1346
+ # Normalizes a URL.
1347
+ #
1348
+ # @param [URI::HTTP, Addressable::URI, String, Object] url
1349
+ # The URL or URI to normalize.
1350
+ #
1351
+ # @return [URI::HTTP, Addressable::URI]
1352
+ # The parsed URL.
1353
+ #
1354
+ def normalize_url(url)
1355
+ case url
1356
+ when URI::HTTP, Addressable::URI then url
1357
+ when String then Addressable::URI.parse(url)
1358
+ else
1359
+ raise(ArgumentError,"url must be a URI::HTTP, Addressable::URI, or a String: #{url.inspect}")
1360
+ end
1361
+ end
1362
+
1363
+ #
1364
+ # Fetches an existing HTTP session or creates a new one for the given
1365
+ # URI.
1366
+ #
1367
+ # @param [URI::HTTP] uri
1368
+ # The URL to retrieve or create an HTTP session for.
1369
+ #
1370
+ # @return [Ronin::Support::Network::HTTP]
1371
+ # The HTTP session.
1372
+ #
1373
+ def session_for(uri)
1374
+ key = [uri.scheme, uri.host, uri.port]
1375
+
1376
+ @sessions[key] ||= Support::Network::HTTP.connect_uri(
1377
+ uri, proxy: @proxy,
1378
+ ssl: (@ssl if uri.scheme == 'https'),
1379
+ user_agent: @user_agent
1380
+ )
1381
+ end
1382
+
1383
+ end
1384
+ end
1385
+ end
1386
+ end