html2rss 0.29.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -8
- data/html2rss.gemspec +1 -3
- data/lib/html2rss/article/deduplicator.rb +2 -4
- data/lib/html2rss/article.rb +16 -39
- data/lib/html2rss/auto_source/README.md +1 -1
- data/lib/html2rss/auto_source/cleanup.rb +15 -6
- data/lib/html2rss/auto_source/scraper/html.rb +5 -0
- data/lib/html2rss/auto_source/scraper/json_state.rb +5 -0
- data/lib/html2rss/auto_source/scraper/meta_oembed.rb +8 -1
- data/lib/html2rss/auto_source/scraper/microdata.rb +8 -0
- data/lib/html2rss/auto_source/scraper/microformats2.rb +8 -0
- data/lib/html2rss/auto_source/scraper/native_feed.rb +3 -0
- data/lib/html2rss/auto_source/scraper/schema/thing.rb +4 -5
- data/lib/html2rss/auto_source/scraper/schema.rb +8 -0
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +5 -0
- data/lib/html2rss/auto_source/scraper/sitemap.rb +3 -0
- data/lib/html2rss/auto_source/scraper/wordpress_api.rb +3 -0
- data/lib/html2rss/auto_source/scraper.rb +4 -14
- data/lib/html2rss/auto_source/segment.rb +7 -4
- data/lib/html2rss/auto_source/segmenter/cluster.rb +22 -23
- data/lib/html2rss/auto_source/segmenter/list.rb +54 -31
- data/lib/html2rss/auto_source/segmenter.rb +5 -5
- data/lib/html2rss/auto_source.rb +2 -2
- data/lib/html2rss/batch.rb +6 -4
- data/lib/html2rss/capture/README.md +7 -5
- data/lib/html2rss/capture.rb +68 -36
- data/lib/html2rss/channel.rb +3 -3
- data/lib/html2rss/cli/probe_view.rb +5 -3
- data/lib/html2rss/cli/render.rb +6 -5
- data/lib/html2rss/cli/validate.rb +28 -27
- data/lib/html2rss/cli.rb +1 -1
- data/lib/html2rss/config/issue_mapper.rb +108 -0
- data/lib/html2rss/config/request_headers.rb +6 -4
- data/lib/html2rss/config/schema.rb +16 -1
- data/lib/html2rss/config/selectors_validator.rb +92 -70
- data/lib/html2rss/config/validation_issue.rb +60 -0
- data/lib/html2rss/config/validation_report.rb +55 -0
- data/lib/html2rss/config/validator.rb +10 -6
- data/lib/html2rss/config.rb +35 -101
- data/lib/html2rss/defaults.rb +42 -30
- data/lib/html2rss/doctor/botasaurus.rb +19 -9
- data/lib/html2rss/doctor.rb +8 -0
- data/lib/html2rss/error.rb +1 -1
- data/lib/html2rss/feed_builder/rss.rb +9 -14
- data/lib/html2rss/feed_pipeline/README.md +8 -8
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +1 -1
- data/lib/html2rss/feed_pipeline/runtime_policy.rb +14 -10
- data/lib/html2rss/feed_pipeline/strategy_plan.rb +7 -3
- data/lib/html2rss/feed_resolution/candidate_generator.rb +1 -4
- data/lib/html2rss/feed_resolution/diag.rb +3 -1
- data/lib/html2rss/feed_resolution/options.rb +3 -1
- data/lib/html2rss/feed_resolution/policy.rb +2 -2
- data/lib/html2rss/html/article_extractor/date_extractor.rb +1 -1
- data/lib/html2rss/html/article_extractor.rb +2 -4
- data/lib/html2rss/html/feed_link.rb +3 -1
- data/lib/html2rss/html/navigator/text_extractor.rb +1 -1
- data/lib/html2rss/html/navigator.rb +10 -10
- data/lib/html2rss/html/rendering/description_builder.rb +1 -1
- data/lib/html2rss/html/sst_article_extractor.rb +8 -5
- data/lib/html2rss/link_destination/destination_facts.rb +4 -2
- data/lib/html2rss/link_destination/noise_policy.rb +10 -1
- data/lib/html2rss/link_destination/path_classifier.rb +45 -38
- data/lib/html2rss/mcp/README.md +5 -3
- data/lib/html2rss/mcp/config_argument.rb +3 -1
- data/lib/html2rss/mcp/contract.rb +15 -25
- data/lib/html2rss/mcp/outcome/playbook.rb +7 -7
- data/lib/html2rss/mcp/outcome.rb +36 -43
- data/lib/html2rss/mcp/server/tools.rb +112 -132
- data/lib/html2rss/mcp/server.rb +27 -19
- data/lib/html2rss/page_recon/README.md +5 -5
- data/lib/html2rss/page_recon/diagnostics.rb +71 -81
- data/lib/html2rss/page_recon.rb +9 -5
- data/lib/html2rss/recon.rb +20 -31
- data/lib/html2rss/request_service/botasaurus_contract.rb +11 -12
- data/lib/html2rss/request_service/botasaurus_strategy.rb +32 -28
- data/lib/html2rss/request_service/compressed_body.rb +17 -9
- data/lib/html2rss/request_service/httpx_strategy.rb +239 -0
- data/lib/html2rss/request_service/network_guard.rb +7 -5
- data/lib/html2rss/request_service/response.rb +1 -2
- data/lib/html2rss/request_service/response_guard.rb +0 -16
- data/lib/html2rss/request_service/strategy.rb +22 -11
- data/lib/html2rss/request_service.rb +46 -85
- data/lib/html2rss/scoring/container_assessor.rb +3 -1
- data/lib/html2rss/scoring/engine.rb +8 -8
- data/lib/html2rss/scoring/observation.rb +5 -3
- data/lib/html2rss/scoring/ranked_segment.rb +3 -1
- data/lib/html2rss/scrape_target.rb +3 -1
- data/lib/html2rss/selector_candidates.rb +234 -0
- data/lib/html2rss/selectors/README.md +64 -0
- data/lib/html2rss/selectors/extractors/attribute.rb +13 -15
- data/lib/html2rss/selectors/extractors/href.rb +15 -16
- data/lib/html2rss/selectors/extractors/html.rb +8 -9
- data/lib/html2rss/selectors/extractors/static.rb +12 -14
- data/lib/html2rss/selectors/extractors/text.rb +8 -9
- data/lib/html2rss/selectors/extractors.rb +33 -20
- data/lib/html2rss/selectors/item_env.rb +31 -0
- data/lib/html2rss/selectors/{object_to_xml_converter.rb → json_xml.rb} +11 -12
- data/lib/html2rss/selectors/option_spec.rb +140 -0
- data/lib/html2rss/selectors/post_processors/base.rb +79 -31
- data/lib/html2rss/selectors/post_processors/gsub.rb +103 -39
- data/lib/html2rss/selectors/post_processors/html_transformers/transform_urls_to_absolute_ones.rb +4 -4
- data/lib/html2rss/selectors/post_processors/markdown_to_html.rb +10 -11
- data/lib/html2rss/selectors/post_processors/parse_time.rb +50 -26
- data/lib/html2rss/selectors/post_processors/parse_uri.rb +11 -7
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +46 -32
- data/lib/html2rss/selectors/post_processors/substring.rb +14 -25
- data/lib/html2rss/selectors/post_processors/template.rb +23 -19
- data/lib/html2rss/selectors/post_processors.rb +6 -5
- data/lib/html2rss/selectors/{schema_doc.rb → schema_export.rb} +12 -31
- data/lib/html2rss/selectors/step_env.rb +24 -0
- data/lib/html2rss/selectors.rb +231 -142
- data/lib/html2rss/sst/attrs.rb +5 -3
- data/lib/html2rss/sst/document.rb +3 -1
- data/lib/html2rss/sst/index.rb +3 -1
- data/lib/html2rss/sst/node.rb +29 -10
- data/lib/html2rss/sst/normalizer.rb +12 -16
- data/lib/html2rss/sst/tags.rb +7 -7
- data/lib/html2rss/status.rb +13 -8
- data/lib/html2rss/surface_category.rb +14 -32
- data/lib/html2rss/syndication/parser.rb +2 -2
- data/lib/html2rss/test/enhance_audit.rb +1 -3
- data/lib/html2rss/test/policy.rb +106 -0
- data/lib/html2rss/test.rb +126 -172
- data/lib/html2rss/url.rb +25 -4
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +27 -19
- data/lib/tasks/config_schema.rake +2 -2
- data/schema/html2rss-config.schema.json +11 -8
- metadata +19 -44
- data/lib/html2rss/request_service/faraday_strategy.rb +0 -233
- data/lib/html2rss/selectors/item_scope.rb +0 -30
|
@@ -434,7 +434,8 @@
|
|
|
434
434
|
"properties": {
|
|
435
435
|
"selector": {
|
|
436
436
|
"type": "string",
|
|
437
|
-
"minLength": 1
|
|
437
|
+
"minLength": 1,
|
|
438
|
+
"default": "a[href]"
|
|
438
439
|
},
|
|
439
440
|
"order": {
|
|
440
441
|
"enum": [
|
|
@@ -507,9 +508,7 @@
|
|
|
507
508
|
]
|
|
508
509
|
}
|
|
509
510
|
},
|
|
510
|
-
"required": [
|
|
511
|
-
"selector"
|
|
512
|
-
],
|
|
511
|
+
"required": [],
|
|
513
512
|
"description": "Defines the items selector and list-card enhance settings."
|
|
514
513
|
},
|
|
515
514
|
"enclosure": {
|
|
@@ -848,7 +847,7 @@
|
|
|
848
847
|
"gsub": {
|
|
849
848
|
"type": "object",
|
|
850
849
|
"title": "gsub",
|
|
851
|
-
"description": "Replace matches of `pattern` in the extracted string with `replacement` (Ruby String#gsub; pattern may be a regexp-like string).",
|
|
850
|
+
"description": "Replace matches of `pattern` in the extracted string with `replacement` (Ruby String#gsub; pattern may be a regexp-like string). Patterns over 256 characters as written, or with nested quantifiers, are rejected.",
|
|
852
851
|
"examples": [
|
|
853
852
|
{
|
|
854
853
|
"name": "gsub",
|
|
@@ -862,10 +861,14 @@
|
|
|
862
861
|
"const": "gsub"
|
|
863
862
|
},
|
|
864
863
|
"pattern": {
|
|
865
|
-
"type": "string"
|
|
864
|
+
"type": "string",
|
|
865
|
+
"maxLength": 256
|
|
866
866
|
},
|
|
867
867
|
"replacement": {
|
|
868
|
-
"type":
|
|
868
|
+
"type": [
|
|
869
|
+
"string",
|
|
870
|
+
"object"
|
|
871
|
+
]
|
|
869
872
|
}
|
|
870
873
|
},
|
|
871
874
|
"required": [
|
|
@@ -1024,7 +1027,7 @@
|
|
|
1024
1027
|
"type": "string",
|
|
1025
1028
|
"const": "href",
|
|
1026
1029
|
"title": "href",
|
|
1027
|
-
"description": "Return the absolute URL from the selected element's `href` attribute (relative hrefs are resolved against the
|
|
1030
|
+
"description": "Return the absolute URL from the selected element's `href` attribute (relative hrefs are resolved against the page base URL).",
|
|
1028
1031
|
"examples": [
|
|
1029
1032
|
"href"
|
|
1030
1033
|
]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: html2rss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -52,53 +52,19 @@ dependencies:
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: 2.0.1
|
|
61
|
-
- - "<"
|
|
62
|
-
- !ruby/object:Gem::Version
|
|
63
|
-
version: '3.0'
|
|
64
|
-
type: :runtime
|
|
65
|
-
prerelease: false
|
|
66
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
-
requirements:
|
|
68
|
-
- - ">"
|
|
69
|
-
- !ruby/object:Gem::Version
|
|
70
|
-
version: 2.0.1
|
|
71
|
-
- - "<"
|
|
72
|
-
- !ruby/object:Gem::Version
|
|
73
|
-
version: '3.0'
|
|
74
|
-
- !ruby/object:Gem::Dependency
|
|
75
|
-
name: faraday-follow_redirects
|
|
76
|
-
requirement: !ruby/object:Gem::Requirement
|
|
77
|
-
requirements:
|
|
78
|
-
- - ">="
|
|
79
|
-
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0'
|
|
81
|
-
type: :runtime
|
|
82
|
-
prerelease: false
|
|
83
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
-
requirements:
|
|
85
|
-
- - ">="
|
|
86
|
-
- !ruby/object:Gem::Version
|
|
87
|
-
version: '0'
|
|
88
|
-
- !ruby/object:Gem::Dependency
|
|
89
|
-
name: faraday-gzip
|
|
55
|
+
name: httpx
|
|
90
56
|
requirement: !ruby/object:Gem::Requirement
|
|
91
57
|
requirements:
|
|
92
58
|
- - "~>"
|
|
93
59
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '
|
|
60
|
+
version: '1.8'
|
|
95
61
|
type: :runtime
|
|
96
62
|
prerelease: false
|
|
97
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
64
|
requirements:
|
|
99
65
|
- - "~>"
|
|
100
66
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: '
|
|
67
|
+
version: '1.8'
|
|
102
68
|
- !ruby/object:Gem::Dependency
|
|
103
69
|
name: kramdown
|
|
104
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -348,13 +314,17 @@ files:
|
|
|
348
314
|
- lib/html2rss/config.rb
|
|
349
315
|
- lib/html2rss/config/auto_source_contract.rb
|
|
350
316
|
- lib/html2rss/config/dynamic_params.rb
|
|
317
|
+
- lib/html2rss/config/issue_mapper.rb
|
|
351
318
|
- lib/html2rss/config/multiple_feeds_config.rb
|
|
352
319
|
- lib/html2rss/config/request_controls.rb
|
|
353
320
|
- lib/html2rss/config/request_headers.rb
|
|
354
321
|
- lib/html2rss/config/schema.rb
|
|
355
322
|
- lib/html2rss/config/selectors_validator.rb
|
|
323
|
+
- lib/html2rss/config/validation_issue.rb
|
|
324
|
+
- lib/html2rss/config/validation_report.rb
|
|
356
325
|
- lib/html2rss/config/validator.rb
|
|
357
326
|
- lib/html2rss/defaults.rb
|
|
327
|
+
- lib/html2rss/doctor.rb
|
|
358
328
|
- lib/html2rss/doctor/botasaurus.rb
|
|
359
329
|
- lib/html2rss/error.rb
|
|
360
330
|
- lib/html2rss/feed_builder.rb
|
|
@@ -431,7 +401,7 @@ files:
|
|
|
431
401
|
- lib/html2rss/request_service/budget.rb
|
|
432
402
|
- lib/html2rss/request_service/compressed_body.rb
|
|
433
403
|
- lib/html2rss/request_service/context.rb
|
|
434
|
-
- lib/html2rss/request_service/
|
|
404
|
+
- lib/html2rss/request_service/httpx_strategy.rb
|
|
435
405
|
- lib/html2rss/request_service/local_file_strategy.rb
|
|
436
406
|
- lib/html2rss/request_service/network_guard.rb
|
|
437
407
|
- lib/html2rss/request_service/policy.rb
|
|
@@ -455,15 +425,18 @@ files:
|
|
|
455
425
|
- lib/html2rss/scoring/ranked_segment.rb
|
|
456
426
|
- lib/html2rss/scoring/score.rb
|
|
457
427
|
- lib/html2rss/scrape_target.rb
|
|
428
|
+
- lib/html2rss/selector_candidates.rb
|
|
458
429
|
- lib/html2rss/selectors.rb
|
|
430
|
+
- lib/html2rss/selectors/README.md
|
|
459
431
|
- lib/html2rss/selectors/extractors.rb
|
|
460
432
|
- lib/html2rss/selectors/extractors/attribute.rb
|
|
461
433
|
- lib/html2rss/selectors/extractors/href.rb
|
|
462
434
|
- lib/html2rss/selectors/extractors/html.rb
|
|
463
435
|
- lib/html2rss/selectors/extractors/static.rb
|
|
464
436
|
- lib/html2rss/selectors/extractors/text.rb
|
|
465
|
-
- lib/html2rss/selectors/
|
|
466
|
-
- lib/html2rss/selectors/
|
|
437
|
+
- lib/html2rss/selectors/item_env.rb
|
|
438
|
+
- lib/html2rss/selectors/json_xml.rb
|
|
439
|
+
- lib/html2rss/selectors/option_spec.rb
|
|
467
440
|
- lib/html2rss/selectors/post_processors.rb
|
|
468
441
|
- lib/html2rss/selectors/post_processors/base.rb
|
|
469
442
|
- lib/html2rss/selectors/post_processors/gsub.rb
|
|
@@ -475,7 +448,8 @@ files:
|
|
|
475
448
|
- lib/html2rss/selectors/post_processors/sanitize_html.rb
|
|
476
449
|
- lib/html2rss/selectors/post_processors/substring.rb
|
|
477
450
|
- lib/html2rss/selectors/post_processors/template.rb
|
|
478
|
-
- lib/html2rss/selectors/
|
|
451
|
+
- lib/html2rss/selectors/schema_export.rb
|
|
452
|
+
- lib/html2rss/selectors/step_env.rb
|
|
479
453
|
- lib/html2rss/sst.rb
|
|
480
454
|
- lib/html2rss/sst/attrs.rb
|
|
481
455
|
- lib/html2rss/sst/document.rb
|
|
@@ -493,6 +467,7 @@ files:
|
|
|
493
467
|
- lib/html2rss/syndication/parser.rb
|
|
494
468
|
- lib/html2rss/test.rb
|
|
495
469
|
- lib/html2rss/test/enhance_audit.rb
|
|
470
|
+
- lib/html2rss/test/policy.rb
|
|
496
471
|
- lib/html2rss/url.rb
|
|
497
472
|
- lib/html2rss/version.rb
|
|
498
473
|
- lib/tasks/config_schema.rake
|
|
@@ -502,7 +477,7 @@ licenses:
|
|
|
502
477
|
- MIT
|
|
503
478
|
metadata:
|
|
504
479
|
allowed_push_host: https://rubygems.org
|
|
505
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.
|
|
480
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.31.0
|
|
506
481
|
rubygems_mfa_required: 'true'
|
|
507
482
|
rdoc_options: []
|
|
508
483
|
require_paths:
|
|
@@ -518,7 +493,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
518
493
|
- !ruby/object:Gem::Version
|
|
519
494
|
version: '0'
|
|
520
495
|
requirements: []
|
|
521
|
-
rubygems_version: 4.0.
|
|
496
|
+
rubygems_version: 4.0.20
|
|
522
497
|
specification_version: 4
|
|
523
498
|
summary: Generates RSS feeds from websites by scraping a URL and using CSS selectors
|
|
524
499
|
to extract item.
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'brotli'
|
|
4
|
-
require 'faraday'
|
|
5
|
-
require 'faraday/follow_redirects'
|
|
6
|
-
require 'faraday/gzip'
|
|
7
|
-
|
|
8
|
-
module Html2rss
|
|
9
|
-
class RequestService
|
|
10
|
-
##
|
|
11
|
-
# Strategy to use Faraday for the request.
|
|
12
|
-
# @see https://rubygems.org/gems/faraday
|
|
13
|
-
# rubocop:disable-next Metrics/ClassLength -- terminal redirect retry colocated with Faraday transport
|
|
14
|
-
class FaradayStrategy < Strategy
|
|
15
|
-
##
|
|
16
|
-
# Restores buffered streamed bytes so response middleware can process them.
|
|
17
|
-
class StreamingBodyMiddleware < Faraday::Middleware
|
|
18
|
-
# Request-context key used to store streamed chunks before middleware completion.
|
|
19
|
-
STREAM_BUFFER_KEY = :html2rss_stream_buffer
|
|
20
|
-
|
|
21
|
-
# @param env [Faraday::Env] completed response environment
|
|
22
|
-
# @return [void]
|
|
23
|
-
def on_complete(env)
|
|
24
|
-
buffer = env.request.context&.delete(STREAM_BUFFER_KEY)
|
|
25
|
-
return if buffer.nil? || buffer.empty?
|
|
26
|
-
|
|
27
|
-
env.body = buffer
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
##
|
|
32
|
-
# @return [ResponseGuard]
|
|
33
|
-
attr_reader :response_guard
|
|
34
|
-
|
|
35
|
-
# Executes the request with runtime policy enforcement, returning the normalized response.
|
|
36
|
-
#
|
|
37
|
-
# @return [Response] normalized response
|
|
38
|
-
def perform_execute
|
|
39
|
-
deadline = request_deadline
|
|
40
|
-
@response_guard = ResponseGuard.new(policy: ctx.policy)
|
|
41
|
-
reset_redirect_tracking!
|
|
42
|
-
raw_response = request_with_terminal_redirect_retry(response_guard, deadline:)
|
|
43
|
-
raw_response = retry_without_streaming(response_guard, deadline:) if retry_without_streaming?(raw_response)
|
|
44
|
-
build_response(raw_response)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def request_deadline
|
|
50
|
-
monotonic_now + ctx.budget.effective_timeout_seconds(fallback: ctx.policy.total_timeout_seconds)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def reset_redirect_tracking!
|
|
54
|
-
@last_redirect_to = nil
|
|
55
|
-
@terminal_redirect_retried = false
|
|
56
|
-
@request_url_override = nil
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def request_with_terminal_redirect_retry(response_guard, deadline:)
|
|
60
|
-
faraday_request(response_guard, deadline:, streaming_buffer: true)
|
|
61
|
-
rescue Faraday::FollowRedirects::RedirectLimitReached => error
|
|
62
|
-
raise RedirectLimitReached, error.message unless terminal_redirect_retryable?
|
|
63
|
-
|
|
64
|
-
begin
|
|
65
|
-
retry_from_terminal_redirect!(response_guard, deadline:)
|
|
66
|
-
rescue Faraday::FollowRedirects::RedirectLimitReached => retry_error
|
|
67
|
-
raise RedirectLimitReached, retry_error.message
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def terminal_redirect_retryable?
|
|
72
|
-
return false if @terminal_redirect_retried || @last_redirect_to.nil?
|
|
73
|
-
|
|
74
|
-
@last_redirect_to.to_s != request_url.to_s
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def retry_from_terminal_redirect!(response_guard, deadline:)
|
|
78
|
-
terminal_url = @last_redirect_to
|
|
79
|
-
@terminal_redirect_retried = true
|
|
80
|
-
Log.debug("#{self.class}: redirect limit reached; retrying once from #{terminal_url}")
|
|
81
|
-
begin_terminal_url_request!(terminal_url)
|
|
82
|
-
faraday_request(response_guard, deadline:, streaming_buffer: true, consume_budget: false)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def begin_terminal_url_request!(terminal_url)
|
|
86
|
-
ctx.policy.validate_request!(url: terminal_url, origin_url: ctx.origin_url, relation: ctx.relation)
|
|
87
|
-
@request_url_override = terminal_url
|
|
88
|
-
@client = nil
|
|
89
|
-
@last_redirect_to = nil
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def request_url
|
|
93
|
-
@request_url_override || ctx.url
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def build_response(response)
|
|
97
|
-
Response.new(body: CompressedBody.decode(response.body, headers: response.headers),
|
|
98
|
-
headers: response.headers, url: response_url(response),
|
|
99
|
-
status: response.status)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def faraday_request(response_guard, deadline:, streaming_buffer:, consume_budget: true)
|
|
103
|
-
preflight!(consume_budget:)
|
|
104
|
-
|
|
105
|
-
client.get do |req|
|
|
106
|
-
apply_timeouts(req, deadline:)
|
|
107
|
-
next unless streaming_buffer
|
|
108
|
-
|
|
109
|
-
buffer = prepare_stream_buffer(req)
|
|
110
|
-
req.options.on_data = on_data_callback(response_guard, buffer)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def retry_without_streaming(response_guard, deadline:)
|
|
115
|
-
faraday_request(response_guard, deadline:, streaming_buffer: false, consume_budget: false)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
##
|
|
119
|
-
# Validates the remote socket peer IP during Net::HTTP start.
|
|
120
|
-
module PeerIpValidator
|
|
121
|
-
module_function
|
|
122
|
-
|
|
123
|
-
# @param http [Net::HTTP] connection to configure
|
|
124
|
-
# @param policy [Policy] request policy
|
|
125
|
-
# @return [void]
|
|
126
|
-
def install!(http, policy:)
|
|
127
|
-
orig_start = http.method(:start)
|
|
128
|
-
http.define_singleton_method(:start) do |&block|
|
|
129
|
-
orig_start.call do |opened_http|
|
|
130
|
-
PeerIpValidator.validate!(opened_http, policy:)
|
|
131
|
-
block.call(opened_http)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# @param opened_http [Net::HTTP] active connection
|
|
137
|
-
# @param policy [Policy] request policy
|
|
138
|
-
# @return [void]
|
|
139
|
-
def validate!(opened_http, policy:)
|
|
140
|
-
scheme = opened_http.use_ssl? ? 'https' : 'http'
|
|
141
|
-
url = Html2rss::Url.from_absolute("#{scheme}://#{opened_http.address}:#{opened_http.port}")
|
|
142
|
-
policy.validate_remote_ip!(ip: peer_ip_for(opened_http), url:)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# @param opened_http [Net::HTTP]
|
|
146
|
-
# @return [String, nil]
|
|
147
|
-
def peer_ip_for(opened_http)
|
|
148
|
-
sock = opened_http.instance_variable_get(:@socket)
|
|
149
|
-
io = sock.respond_to?(:io) ? sock.io : sock
|
|
150
|
-
peeraddr = io.respond_to?(:peeraddr) ? io.peeraddr : nil
|
|
151
|
-
peeraddr&.[](3) || peeraddr&.[](2)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
156
|
-
def client
|
|
157
|
-
@client ||= Faraday.new(url: request_url.to_s, headers: ctx.headers) do |faraday|
|
|
158
|
-
faraday.use Faraday::FollowRedirects::Middleware, limit: ctx.policy.max_redirects, callback: redirect_callback
|
|
159
|
-
faraday.request :gzip
|
|
160
|
-
faraday.use StreamingBodyMiddleware
|
|
161
|
-
faraday.adapter Faraday.default_adapter do |http|
|
|
162
|
-
PeerIpValidator.install!(http, policy: ctx.policy)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def apply_timeouts(request, deadline:)
|
|
168
|
-
remaining_timeout = remaining_timeout_seconds(deadline)
|
|
169
|
-
request.options.timeout = remaining_timeout
|
|
170
|
-
request.options.open_timeout = [ctx.policy.connect_timeout_seconds, remaining_timeout].min
|
|
171
|
-
request.options.read_timeout = [ctx.policy.read_timeout_seconds, remaining_timeout].min
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def prepare_stream_buffer(request)
|
|
175
|
-
request.options.context ||= {}
|
|
176
|
-
request.options.context[StreamingBodyMiddleware::STREAM_BUFFER_KEY] = +''
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def on_data_callback(response_guard, buffer)
|
|
180
|
-
proc do |chunk, total_bytes, env|
|
|
181
|
-
response_guard.inspect_chunk!(total_bytes:, headers: env&.response_headers)
|
|
182
|
-
buffer&.<< chunk
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def remaining_timeout_seconds(deadline)
|
|
187
|
-
remaining = deadline - monotonic_now
|
|
188
|
-
raise RequestTimedOut, 'Request timed out' if remaining <= 0
|
|
189
|
-
|
|
190
|
-
remaining
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def retry_without_streaming?(response)
|
|
194
|
-
return false if response.body.to_s.empty? == false
|
|
195
|
-
return false unless response_success?(response)
|
|
196
|
-
|
|
197
|
-
final_url = response.env&.url
|
|
198
|
-
return false unless final_url
|
|
199
|
-
|
|
200
|
-
final_url.to_s != ctx.url.to_s
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def response_success?(response)
|
|
204
|
-
return true if response.status.nil?
|
|
205
|
-
|
|
206
|
-
response.status >= 200 && response.status < 300
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def response_url(response)
|
|
210
|
-
return ctx.url unless (url = response.env&.url)
|
|
211
|
-
|
|
212
|
-
Html2rss::Url.from_absolute(url.to_s)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def redirect_callback
|
|
216
|
-
lambda do |old_env, new_env|
|
|
217
|
-
from_url = normalize_url(old_env[:url])
|
|
218
|
-
to_url = normalize_url(new_env[:url])
|
|
219
|
-
@last_redirect_to = to_url
|
|
220
|
-
ctx.policy.validate_redirect!(from_url:, to_url:, origin_url: ctx.origin_url, relation: ctx.relation)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def normalize_url(url)
|
|
225
|
-
Html2rss::Url.from_absolute(url.to_s)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def monotonic_now
|
|
229
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
class Selectors
|
|
5
|
-
##
|
|
6
|
-
# Per-item extraction scope: owns the article node and page +base_url+ for one
|
|
7
|
-
# extraction pass. Channel hashes are injected (and cached) by {Selectors}.
|
|
8
|
-
#
|
|
9
|
-
# Distinct from {Context}, which is the post-processor invocation bag (+options+).
|
|
10
|
-
# Post-processor +config+ is derived from {#channel} — not a parallel bag.
|
|
11
|
-
ItemScope = Data.define(:item, :base_url, :scraper, :channel) do
|
|
12
|
-
##
|
|
13
|
-
# Selects an attribute using this scope's item and base_url.
|
|
14
|
-
#
|
|
15
|
-
# @param name [Symbol, String]
|
|
16
|
-
# @return [Object, Array<Object>]
|
|
17
|
-
def select(name) = scraper.select_in_scope(name, self)
|
|
18
|
-
|
|
19
|
-
##
|
|
20
|
-
# Builds a post-processor {Context} carrying this scope for nested selects.
|
|
21
|
-
#
|
|
22
|
-
# @param options [Hash] post-processor options from the selector config
|
|
23
|
-
# @option options [String] :name post-processor name
|
|
24
|
-
# @return [Context]
|
|
25
|
-
def context_for(options:)
|
|
26
|
-
Context.new(options:, item:, config: { channel: }, scraper:, item_scope: self)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|