fetch_util 0.5.2 → 0.6.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/CHANGELOG.md +49 -0
- data/README.md +3 -1
- data/Rakefile +0 -2
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +12 -17
- data/lib/fetch_util/browser/interaction_helpers/timing_helpers.rb +42 -15
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb +48 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb +80 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script.rb +53 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script.rb +118 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_stabilization.rb +54 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_state.rb +27 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_product_state.rb +55 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_request_state.rb +100 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_state.rb +59 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules.rb +26 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_state.rb +93 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resources.rb +73 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_threads.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +9 -10
- data/lib/fetch_util/browser/site_stabilization/facebook_stabilization.rb +56 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_product_state_script.rb +97 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_request_state_script.rb +75 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_state.rb +39 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script.rb +76 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script.rb +111 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_state.rb +42 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script.rb +49 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state.rb +88 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_state.rb +109 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resources.rb +30 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_stabilization.rb +60 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_product_state.rb +74 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state.rb +69 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_threads.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_visibility_script.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resource_stabilization.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resources.rb +105 -0
- data/lib/fetch_util/browser/site_stabilization/github_threads.rb +82 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state.rb +17 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script.rb +98 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb +43 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resources.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_repo.rb +3 -11
- data/lib/fetch_util/browser/site_stabilization/gitlab_stabilization.rb +33 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_threads.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +19 -38
- data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +3 -3
- data/lib/fetch_util/browser/site_stabilization.rb +89 -0
- data/lib/fetch_util/browser/stabilization/anubis.rb +4 -5
- data/lib/fetch_util/browser/stabilization/page_flow.rb +71 -27
- data/lib/fetch_util/browser/stabilization/spa_hydration.rb +7 -5
- data/lib/fetch_util/browser.rb +88 -17
- data/lib/fetch_util/cli.rb +69 -23
- data/lib/fetch_util/extractor.rb +8 -2
- data/lib/fetch_util/fetcher.rb +144 -43
- data/lib/fetch_util/http_redirect_client.rb +161 -0
- data/lib/fetch_util/parallel_fetcher.rb +125 -28
- data/lib/fetch_util/raw_docs_fallback.rb +158 -21
- data/lib/fetch_util/regulatory/cache_store.rb +47 -14
- data/lib/fetch_util/regulatory/fetch_records.rb +10 -7
- data/lib/fetch_util/regulatory/headers.rb +8 -1
- data/lib/fetch_util/regulatory/http_client.rb +2 -97
- data/lib/fetch_util/regulatory/orchestration.rb +10 -6
- data/lib/fetch_util/regulatory/page.rb +8 -11
- data/lib/fetch_util/regulatory/robots.rb +2 -10
- data/lib/fetch_util/regulatory/source_selection.rb +13 -3
- data/lib/fetch_util/regulatory/tdm_page.rb +9 -4
- data/lib/fetch_util/regulatory/tdm_policy.rb +16 -9
- data/lib/fetch_util/regulatory/tdm_rep.rb +4 -3
- data/lib/fetch_util/regulatory/tdm_support.rb +10 -6
- data/lib/fetch_util/regulatory.rb +5 -1
- data/lib/fetch_util/request_log.rb +24 -3
- data/lib/fetch_util/result.rb +37 -89
- data/lib/fetch_util/search_transport.rb +102 -35
- data/lib/fetch_util/searcher.rb +15 -13
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +4 -1
- metadata +66 -2
|
@@ -1,103 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "uri"
|
|
3
|
+
require_relative "../http_redirect_client"
|
|
5
4
|
|
|
6
5
|
module FetchUtil
|
|
7
|
-
class HttpRedirectClient
|
|
8
|
-
REDIRECT_LIMIT = 5
|
|
9
|
-
TRANSIENT_ERRORS = [EOFError, IOError, SocketError, SystemCallError, Timeout::Error].freeze
|
|
10
|
-
Response = Struct.new(:url, :status, :headers, :body, :redirects, keyword_init: true)
|
|
11
|
-
|
|
12
|
-
def initialize(timeout:, headers: {})
|
|
13
|
-
@timeout = timeout.to_f
|
|
14
|
-
@headers = headers.reject { |_key, value| value.to_s.empty? }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def get(url, limit: REDIRECT_LIMIT)
|
|
18
|
-
@connections = {}
|
|
19
|
-
fetch(parse_http_uri(url), limit, [])
|
|
20
|
-
ensure
|
|
21
|
-
close_connections
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
attr_reader :timeout, :headers
|
|
27
|
-
|
|
28
|
-
def fetch(uri, limit, redirects)
|
|
29
|
-
response = request(uri)
|
|
30
|
-
return build_response(uri, response, redirects: redirects) unless response.is_a?(Net::HTTPRedirection)
|
|
31
|
-
|
|
32
|
-
raise FetchUtil::Error, "too many redirects for #{uri}" if limit <= 0
|
|
33
|
-
|
|
34
|
-
location = response["location"].to_s.strip
|
|
35
|
-
return build_response(uri, response, redirects: redirects) if location.empty?
|
|
36
|
-
|
|
37
|
-
redirect_response = build_response(uri, response)
|
|
38
|
-
fetch(uri.merge(location), limit - 1, redirects + [redirect_response])
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def request(uri)
|
|
42
|
-
attempts = 0
|
|
43
|
-
begin
|
|
44
|
-
http = connection_for(uri)
|
|
45
|
-
request = Net::HTTP::Get.new(uri.request_uri.empty? ? "/" : uri.request_uri)
|
|
46
|
-
headers.each { |key, value| request[key] = value }
|
|
47
|
-
http.request(request)
|
|
48
|
-
rescue *TRANSIENT_ERRORS
|
|
49
|
-
close_connection(uri)
|
|
50
|
-
attempts += 1
|
|
51
|
-
retry if attempts <= 1
|
|
52
|
-
raise
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def connection_for(uri)
|
|
57
|
-
key = [uri.scheme, uri.host, uri.port]
|
|
58
|
-
@connections[key] ||= Net::HTTP.start(
|
|
59
|
-
uri.host,
|
|
60
|
-
uri.port,
|
|
61
|
-
use_ssl: uri.scheme == "https",
|
|
62
|
-
open_timeout: timeout,
|
|
63
|
-
read_timeout: timeout
|
|
64
|
-
)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def close_connection(uri)
|
|
68
|
-
key = [uri.scheme, uri.host, uri.port]
|
|
69
|
-
@connections.delete(key)&.finish
|
|
70
|
-
rescue IOError
|
|
71
|
-
nil
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def close_connections
|
|
75
|
-
@connections&.each_value do |http|
|
|
76
|
-
http.finish if http.started?
|
|
77
|
-
rescue IOError
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
@connections = nil
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def build_response(uri, response, redirects: [])
|
|
84
|
-
Response.new(
|
|
85
|
-
url: uri.to_s,
|
|
86
|
-
status: response.code.to_i,
|
|
87
|
-
headers: response.to_hash.transform_keys(&:downcase),
|
|
88
|
-
body: response.body.to_s,
|
|
89
|
-
redirects: redirects
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def parse_http_uri(url)
|
|
94
|
-
uri = URI.parse(url.to_s)
|
|
95
|
-
raise URI::InvalidURIError, "unsupported url: #{url}" unless uri.is_a?(URI::HTTP) && uri.host
|
|
96
|
-
|
|
97
|
-
uri
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
6
|
class Regulatory
|
|
102
7
|
class HttpClient
|
|
103
8
|
DEFAULT_ACCEPT = "text/html,application/json,text/plain,*/*"
|
|
@@ -110,7 +15,7 @@ module FetchUtil
|
|
|
110
15
|
def get(url, limit: FetchUtil::HttpRedirectClient::REDIRECT_LIMIT)
|
|
111
16
|
build_response(redirect_client.get(url, limit: limit))
|
|
112
17
|
rescue URI::InvalidURIError
|
|
113
|
-
raise
|
|
18
|
+
raise InputError, "unsupported url: #{url}"
|
|
114
19
|
end
|
|
115
20
|
|
|
116
21
|
private
|
|
@@ -5,8 +5,8 @@ module FetchUtil
|
|
|
5
5
|
module Orchestration
|
|
6
6
|
def initialize(client: nil, cache_path: DEFAULT_CACHE_PATH, sources: nil, timeout: 20, user_agent: nil)
|
|
7
7
|
@client = client || HttpClient.new(timeout: timeout, user_agent: user_agent || default_user_agent)
|
|
8
|
-
@cache_path = cache_path || DEFAULT_CACHE_PATH
|
|
9
|
-
@source_tokens = sources
|
|
8
|
+
@cache_path = (cache_path || DEFAULT_CACHE_PATH).dup.freeze
|
|
9
|
+
@source_tokens = normalized_source_selection(sources)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def call(url)
|
|
@@ -14,7 +14,7 @@ module FetchUtil
|
|
|
14
14
|
origin_query = origin_query?(requested_uri)
|
|
15
15
|
query_target = request_target(requested_uri)
|
|
16
16
|
effective_query_target = query_target
|
|
17
|
-
selected_sources =
|
|
17
|
+
selected_sources = source_selection
|
|
18
18
|
result = {}
|
|
19
19
|
policy_refs = []
|
|
20
20
|
|
|
@@ -33,7 +33,7 @@ module FetchUtil
|
|
|
33
33
|
|
|
34
34
|
if needs_robots_fetch?(selected_sources)
|
|
35
35
|
record = robots_record(requested_uri)
|
|
36
|
-
|
|
36
|
+
ROBOTS_RECORD_SOURCES.each do |source|
|
|
37
37
|
next unless selected_sources.include?(source)
|
|
38
38
|
|
|
39
39
|
add_source_payload(
|
|
@@ -47,7 +47,7 @@ module FetchUtil
|
|
|
47
47
|
if needs_page_fetch?(selected_sources)
|
|
48
48
|
record = page_record(requested_uri)
|
|
49
49
|
effective_query_target = page_query_target(record, fallback: query_target)
|
|
50
|
-
|
|
50
|
+
PAGE_RECORD_SOURCES.each do |source|
|
|
51
51
|
next unless selected_sources.include?(source)
|
|
52
52
|
|
|
53
53
|
add_source_payload(
|
|
@@ -63,7 +63,11 @@ module FetchUtil
|
|
|
63
63
|
add_source_payload(
|
|
64
64
|
result,
|
|
65
65
|
"tdmpolicy",
|
|
66
|
-
scoped_signals(
|
|
66
|
+
scoped_signals(
|
|
67
|
+
expanded_tdm_policy_signals(policy_refs, target_origin: requested_uri),
|
|
68
|
+
origin_query: origin_query,
|
|
69
|
+
query_target: effective_query_target
|
|
70
|
+
)
|
|
67
71
|
)
|
|
68
72
|
end
|
|
69
73
|
|
|
@@ -13,13 +13,17 @@ module FetchUtil
|
|
|
13
13
|
header_policies = []
|
|
14
14
|
|
|
15
15
|
response_chain(response).each do |step|
|
|
16
|
-
|
|
16
|
+
step_uri = parse_http_uri(step.url)
|
|
17
|
+
next unless origin_key(step_uri).casecmp?(origin_key(final_uri))
|
|
18
|
+
|
|
19
|
+
path = request_target(step_uri)
|
|
17
20
|
xrobotstag.concat(extract_x_robot_signals(step.headers, path: path))
|
|
18
21
|
contentusageheader.concat(extract_content_usage_header_signals(step.headers, path: path))
|
|
19
22
|
step_tdmheaders, step_policies = extract_tdm_value_signals(
|
|
20
23
|
reservation: first_header_value(step.headers, "tdm-reservation"),
|
|
21
24
|
policy_url: first_header_value(step.headers, "tdm-policy"),
|
|
22
|
-
path: path
|
|
25
|
+
path: path,
|
|
26
|
+
target_origin: step_uri
|
|
23
27
|
)
|
|
24
28
|
tdmheaders.concat(step_tdmheaders)
|
|
25
29
|
header_policies.concat(step_policies)
|
|
@@ -32,7 +36,7 @@ module FetchUtil
|
|
|
32
36
|
if html_content?(response.headers, response.body)
|
|
33
37
|
meta_tags = parse_meta_tags(response.body)
|
|
34
38
|
metarobots = sort_generic_signals(extract_meta_robot_signals(meta_tags, path: final_path))
|
|
35
|
-
tdmmeta, meta_policies = extract_tdm_meta_signals(meta_tags, path: final_path)
|
|
39
|
+
tdmmeta, meta_policies = extract_tdm_meta_signals(meta_tags, path: final_path, target_origin: final_uri)
|
|
36
40
|
human = sort_generic_signals(extract_human_signals(response.body, path: final_path))
|
|
37
41
|
end
|
|
38
42
|
|
|
@@ -54,14 +58,7 @@ module FetchUtil
|
|
|
54
58
|
def empty_page_record
|
|
55
59
|
{
|
|
56
60
|
"final_url" => nil,
|
|
57
|
-
"signals" => {
|
|
58
|
-
"xrobotstag" => [],
|
|
59
|
-
"metarobots" => [],
|
|
60
|
-
"tdmheaders" => [],
|
|
61
|
-
"tdmmeta" => [],
|
|
62
|
-
"contentusageheader" => [],
|
|
63
|
-
"human" => []
|
|
64
|
-
},
|
|
61
|
+
"signals" => PAGE_RECORD_SOURCES.to_h { |source| [source, []] },
|
|
65
62
|
"policies" => []
|
|
66
63
|
}
|
|
67
64
|
end
|
|
@@ -37,11 +37,7 @@ module FetchUtil
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def extract_robots_source_signals(body)
|
|
40
|
-
signals = {
|
|
41
|
-
"robotstxt" => [],
|
|
42
|
-
"contentsignal" => [],
|
|
43
|
-
"contentusagerobots" => []
|
|
44
|
-
}
|
|
40
|
+
signals = ROBOTS_RECORD_SOURCES.to_h { |source| [source, []] }
|
|
45
41
|
user_agents = []
|
|
46
42
|
in_rules = false
|
|
47
43
|
|
|
@@ -91,11 +87,7 @@ module FetchUtil
|
|
|
91
87
|
|
|
92
88
|
def empty_robots_record
|
|
93
89
|
{
|
|
94
|
-
"signals" => {
|
|
95
|
-
"robotstxt" => [],
|
|
96
|
-
"contentsignal" => [],
|
|
97
|
-
"contentusagerobots" => []
|
|
98
|
-
}
|
|
90
|
+
"signals" => ROBOTS_RECORD_SOURCES.to_h { |source| [source, []] }
|
|
99
91
|
}
|
|
100
92
|
end
|
|
101
93
|
|
|
@@ -32,14 +32,24 @@ module FetchUtil
|
|
|
32
32
|
selected
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def source_selection
|
|
36
|
+
@source_tokens ? @source_tokens.dup : resolve_sources(nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalized_source_selection(sources)
|
|
40
|
+
return if sources.nil?
|
|
41
|
+
|
|
42
|
+
resolve_sources(sources).map { |source| source.dup.freeze }.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
def validate_source!(source)
|
|
36
46
|
return if all_sources.include?(source)
|
|
37
47
|
|
|
38
|
-
raise
|
|
48
|
+
raise InputError, "unsupported regulatory source: #{source}"
|
|
39
49
|
end
|
|
40
50
|
|
|
41
51
|
def needs_page_fetch?(selected_sources)
|
|
42
|
-
(selected_sources &
|
|
52
|
+
(selected_sources & PAGE_RECORD_SOURCES).any? || selected_sources.include?("tdmpolicy")
|
|
43
53
|
end
|
|
44
54
|
|
|
45
55
|
def needs_tdmrep_fetch?(selected_sources)
|
|
@@ -47,7 +57,7 @@ module FetchUtil
|
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
def needs_robots_fetch?(selected_sources)
|
|
50
|
-
(selected_sources &
|
|
60
|
+
(selected_sources & ROBOTS_RECORD_SOURCES).any?
|
|
51
61
|
end
|
|
52
62
|
|
|
53
63
|
def add_source_payload(result, source, signals)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module FetchUtil
|
|
4
4
|
class Regulatory
|
|
5
5
|
module TdmPage
|
|
6
|
-
def extract_tdm_meta_signals(meta_tags, path:)
|
|
6
|
+
def extract_tdm_meta_signals(meta_tags, path:, target_origin:)
|
|
7
7
|
reservation = nil
|
|
8
8
|
policy_url = nil
|
|
9
9
|
|
|
@@ -13,10 +13,15 @@ module FetchUtil
|
|
|
13
13
|
policy_url ||= attributes["content"] if name == "tdm-policy"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
extract_tdm_value_signals(
|
|
16
|
+
extract_tdm_value_signals(
|
|
17
|
+
reservation: reservation,
|
|
18
|
+
policy_url: policy_url,
|
|
19
|
+
path: path,
|
|
20
|
+
target_origin: target_origin
|
|
21
|
+
)
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
def extract_tdm_value_signals(reservation:, policy_url:, path:)
|
|
24
|
+
def extract_tdm_value_signals(reservation:, policy_url:, path:, target_origin:)
|
|
20
25
|
value = reservation.to_s.strip
|
|
21
26
|
return [[], []] unless %w[0 1].include?(value)
|
|
22
27
|
|
|
@@ -31,7 +36,7 @@ module FetchUtil
|
|
|
31
36
|
conditions: conditions
|
|
32
37
|
)
|
|
33
38
|
]
|
|
34
|
-
policies = value == "1" ? [policy_ref(policy, path)] : []
|
|
39
|
+
policies = value == "1" ? [policy_ref(policy, path, target_origin: target_origin)] : []
|
|
35
40
|
[signals, dedupe_policy_refs(policies)]
|
|
36
41
|
end
|
|
37
42
|
end
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module FetchUtil
|
|
4
4
|
class Regulatory
|
|
5
5
|
module TdmPolicy
|
|
6
|
-
def expanded_tdm_policy_signals(policy_refs)
|
|
6
|
+
def expanded_tdm_policy_signals(policy_refs, target_origin:)
|
|
7
7
|
dedupe_policy_refs(policy_refs).flat_map do |policy_ref|
|
|
8
|
-
|
|
8
|
+
policy_origin = policy_ref["target_origin"]
|
|
9
|
+
policy_origin = policy_origin ? parse_http_uri(policy_origin) : target_origin
|
|
10
|
+
record = tdm_policy_record(policy_ref["url"], target_origin: policy_origin)
|
|
9
11
|
Array(record["signals"]).map do |template|
|
|
10
12
|
signal = deep_copy(template)
|
|
11
13
|
signal["path"] ||= policy_ref["path"]
|
|
@@ -17,25 +19,31 @@ module FetchUtil
|
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def tdm_policy_record(url)
|
|
21
|
-
cache_fetch("tdmpolicy:#{url}") do
|
|
22
|
-
response = safe_get(url)
|
|
22
|
+
def tdm_policy_record(url, target_origin:)
|
|
23
|
+
cache_fetch("tdmpolicy:#{origin_key(target_origin)}:#{url}") do
|
|
24
|
+
response, cacheable = safe_get(url)
|
|
23
25
|
signals = []
|
|
24
26
|
if response&.status&.between?(200, 299) && json_like_response?(response.headers, response.body)
|
|
25
|
-
signals = extract_tdm_policy_signals(response.body)
|
|
27
|
+
signals = extract_tdm_policy_signals(response.body, target_origin: target_origin)
|
|
26
28
|
end
|
|
27
|
-
{ "signals" => sort_specificity_signals(signals) }
|
|
29
|
+
[{ "signals" => sort_specificity_signals(signals) }, cacheable]
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def extract_tdm_policy_signals(body)
|
|
33
|
+
def extract_tdm_policy_signals(body, target_origin: nil)
|
|
32
34
|
payload = JSON.parse(body.to_s)
|
|
35
|
+
return [] unless payload.is_a?(Hash)
|
|
36
|
+
|
|
33
37
|
permissions = array_value(payload, "permission")
|
|
34
38
|
|
|
35
39
|
permissions.filter_map do |permission|
|
|
36
40
|
next unless permission.is_a?(Hash)
|
|
37
41
|
next unless tdm_policy_action?(permission["action"])
|
|
38
42
|
|
|
43
|
+
target = permission["target"]
|
|
44
|
+
target_path = policy_target_path(target, target_origin: target_origin)
|
|
45
|
+
next if !target.to_s.strip.empty? && target_path.nil?
|
|
46
|
+
|
|
39
47
|
conditions = {}
|
|
40
48
|
duties = array_value(permission, "duty").filter_map { |item| duty_name(item) }
|
|
41
49
|
conditions["duty"] = duties if duties.any?
|
|
@@ -43,7 +51,6 @@ module FetchUtil
|
|
|
43
51
|
conditions["purpose"] = purpose if purpose
|
|
44
52
|
|
|
45
53
|
signal = build_signal("allow", "text-and-data-mining", conditions: conditions)
|
|
46
|
-
target_path = policy_target_path(permission["target"])
|
|
47
54
|
signal["path"] = target_path if target_path
|
|
48
55
|
signal
|
|
49
56
|
end
|
|
@@ -9,7 +9,7 @@ module FetchUtil
|
|
|
9
9
|
tdmrep_uri(requested_uri),
|
|
10
10
|
fallback: { "signals" => [], "policies" => [] }
|
|
11
11
|
) do |body|
|
|
12
|
-
signals, policies = extract_tdmrep_signals(body)
|
|
12
|
+
signals, policies = extract_tdmrep_signals(body, target_origin: requested_uri)
|
|
13
13
|
{
|
|
14
14
|
"signals" => sort_specificity_signals(signals),
|
|
15
15
|
"policies" => policies
|
|
@@ -17,7 +17,7 @@ module FetchUtil
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def extract_tdmrep_signals(body)
|
|
20
|
+
def extract_tdmrep_signals(body, target_origin:)
|
|
21
21
|
payload = JSON.parse(body.to_s)
|
|
22
22
|
return [[], []] unless payload.is_a?(Array)
|
|
23
23
|
|
|
@@ -35,7 +35,8 @@ module FetchUtil
|
|
|
35
35
|
rule_signals, rule_policies = extract_tdm_value_signals(
|
|
36
36
|
reservation: reservation,
|
|
37
37
|
policy_url: policy_url,
|
|
38
|
-
path: normalize_output_path(location)
|
|
38
|
+
path: normalize_output_path(location),
|
|
39
|
+
target_origin: target_origin
|
|
39
40
|
)
|
|
40
41
|
signals.concat(rule_signals)
|
|
41
42
|
policies.concat(rule_policies)
|
|
@@ -38,12 +38,16 @@ module FetchUtil
|
|
|
38
38
|
nil
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def policy_target_path(value)
|
|
41
|
+
def policy_target_path(value, target_origin:)
|
|
42
42
|
target = value.to_s.strip
|
|
43
43
|
return nil if target.empty?
|
|
44
44
|
|
|
45
45
|
uri = URI.parse(target)
|
|
46
|
-
|
|
46
|
+
if uri.is_a?(URI::HTTP)
|
|
47
|
+
return nil unless target_origin && origin_key(uri).casecmp?(origin_key(target_origin))
|
|
48
|
+
|
|
49
|
+
return normalize_output_path(request_target(uri))
|
|
50
|
+
end
|
|
47
51
|
return normalize_output_path(target) if target.start_with?("/")
|
|
48
52
|
|
|
49
53
|
nil
|
|
@@ -63,12 +67,12 @@ module FetchUtil
|
|
|
63
67
|
[value]
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
def policy_ref(url, path)
|
|
70
|
+
def policy_ref(url, path, target_origin:)
|
|
67
71
|
candidate = url.to_s.strip
|
|
68
72
|
return nil if candidate.empty?
|
|
69
73
|
|
|
70
74
|
parse_http_uri(candidate)
|
|
71
|
-
{ "url" => candidate, "path" => path }
|
|
75
|
+
{ "url" => candidate, "path" => path, "target_origin" => origin_key(target_origin) }
|
|
72
76
|
rescue ArgumentError
|
|
73
77
|
nil
|
|
74
78
|
end
|
|
@@ -77,7 +81,7 @@ module FetchUtil
|
|
|
77
81
|
seen = {}
|
|
78
82
|
list = []
|
|
79
83
|
Array(policy_refs).compact.each do |policy_ref|
|
|
80
|
-
key = [policy_ref["url"], policy_ref["path"]]
|
|
84
|
+
key = [policy_ref["url"], policy_ref["path"], policy_ref["target_origin"]]
|
|
81
85
|
next if seen[key]
|
|
82
86
|
|
|
83
87
|
seen[key] = true
|
|
@@ -87,7 +91,7 @@ module FetchUtil
|
|
|
87
91
|
end
|
|
88
92
|
|
|
89
93
|
def odrl_token(value)
|
|
90
|
-
value.to_s.strip.downcase.split(
|
|
94
|
+
value.to_s.strip.downcase.split(%r{[/#:]}).reject(&:empty?).last.to_s
|
|
91
95
|
end
|
|
92
96
|
end
|
|
93
97
|
end
|
|
@@ -5,6 +5,7 @@ require "digest"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "json"
|
|
7
7
|
require "openssl"
|
|
8
|
+
require "securerandom"
|
|
8
9
|
require "time"
|
|
9
10
|
require "timeout"
|
|
10
11
|
require "uri"
|
|
@@ -12,8 +13,10 @@ require "uri"
|
|
|
12
13
|
module FetchUtil
|
|
13
14
|
class Regulatory
|
|
14
15
|
CACHE_TTL = 86_400
|
|
15
|
-
CACHE_VERSION =
|
|
16
|
+
CACHE_VERSION = 3
|
|
16
17
|
DEFAULT_CACHE_PATH = File.expand_path("~/.local/state/fetch_util/regulatory-cache")
|
|
18
|
+
ROBOTS_RECORD_SOURCES = %w[robotstxt contentsignal contentusagerobots].freeze
|
|
19
|
+
PAGE_RECORD_SOURCES = %w[xrobotstag metarobots tdmheaders tdmmeta contentusageheader human].freeze
|
|
17
20
|
MACHINE_SOURCES = %w[
|
|
18
21
|
robotstxt
|
|
19
22
|
contentsignal
|
|
@@ -32,6 +35,7 @@ module FetchUtil
|
|
|
32
35
|
"machine" => MACHINE_SOURCES,
|
|
33
36
|
"human" => HUMAN_SOURCES
|
|
34
37
|
}.freeze
|
|
38
|
+
private_constant :ROBOTS_RECORD_SOURCES, :PAGE_RECORD_SOURCES
|
|
35
39
|
|
|
36
40
|
Response = Struct.new(:url, :status, :headers, :body, :redirects, keyword_init: true)
|
|
37
41
|
autoload :HttpClient, "fetch_util/regulatory/http_client"
|
|
@@ -6,19 +6,40 @@ require "time"
|
|
|
6
6
|
module FetchUtil
|
|
7
7
|
class RequestLog
|
|
8
8
|
DEFAULT_PATH = File.expand_path("~/.local/state/fetch_util/requests.log")
|
|
9
|
+
DELIMITER_ESCAPES = { "\t" => "\\t", "\r" => "\\r", "\n" => "\\n" }.freeze
|
|
9
10
|
|
|
10
|
-
def initialize(path:
|
|
11
|
-
|
|
11
|
+
def initialize(path: nil)
|
|
12
|
+
configured_path = path || ENV.fetch("FETCH_UTIL_REQUEST_LOG", DEFAULT_PATH)
|
|
13
|
+
@path = (path.nil? ? configured_path.dup : normalized_path(path)).freeze
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
attr_reader :path
|
|
15
17
|
|
|
16
18
|
def append(entry, duration: nil)
|
|
17
19
|
FileUtils.mkdir_p(File.dirname(path))
|
|
18
|
-
|
|
20
|
+
escaped_entry = entry.to_s.gsub(/[\t\r\n]/, DELIMITER_ESCAPES)
|
|
21
|
+
line = "#{Time.now.utc.iso8601}\t#{escaped_entry}"
|
|
19
22
|
line = "#{line}\t#{format("%.2f", duration)}s" if duration
|
|
20
23
|
File.open(path, "a") { |file| file.puts(line) }
|
|
21
24
|
path
|
|
22
25
|
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalized_path(value)
|
|
30
|
+
raw_path = value.to_s
|
|
31
|
+
return raw_path.dup if raw_path.empty? || directory_path?(raw_path)
|
|
32
|
+
|
|
33
|
+
expanded_path = File.expand_path(raw_path)
|
|
34
|
+
basename = File.basename(raw_path)
|
|
35
|
+
return expanded_path if basename.start_with?(".") || !File.extname(basename).empty?
|
|
36
|
+
|
|
37
|
+
"#{expanded_path}.log"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def directory_path?(path)
|
|
41
|
+
separators = [File::SEPARATOR, File::ALT_SEPARATOR].compact
|
|
42
|
+
separators.any? { |separator| path.end_with?(separator) } || %w[. ..].include?(File.basename(path))
|
|
43
|
+
end
|
|
23
44
|
end
|
|
24
45
|
end
|