ronin-support-web 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,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