kataba 1.0.3 → 1.1.1
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/lib/kataba/fetcher.rb +109 -0
- data/lib/kataba.rb +16 -15
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 701caff7f874cbe1f9a02c4d30e58576e3c9970aba61cef220ac96460b729f9c
|
|
4
|
+
data.tar.gz: 803f46524665ca8f9d0e1ee82dfa154dee798363a2326195feeb54ef412e26b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b323b6a3742abfba45ebf1dd98345c73f5e7b703229b9a7f852069a616e2b7cac26db91e9d23f71b5495878108bad8bb71d0e765ea5a4c7f8ce7b8415349ecb
|
|
7
|
+
data.tar.gz: e653e7a40a363a9d4b174d84ebc5e3907edc0a05c8c9a13231688d86ed033e6c0c38ff26f84ac8d58c6b97fb4eb22fa70897a2b29aaf774164e20a1a31ac4741
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
module Kataba
|
|
5
|
+
# Fetches a schema body, recovering from LoC-shaped delivery quirks
|
|
6
|
+
# that a verbatim URI.open would surface as cache-poisoning errors:
|
|
7
|
+
#
|
|
8
|
+
# - 5xx (Cloudflare bot-management 503/529, origin overload):
|
|
9
|
+
# retry once on the alternate scheme.
|
|
10
|
+
# - same-origin HTTPS->HTTP 3xx: follow. open-uri refuses all
|
|
11
|
+
# scheme downgrades; we relax to same-origin because the
|
|
12
|
+
# consumer already trusted this host by putting it in their
|
|
13
|
+
# schemaLocation. Cross-origin downgrades stay refused — that's
|
|
14
|
+
# the actual DNS-redirect attack vector.
|
|
15
|
+
# - /mods/xml.xsd: rewrite to /standards/mods/xml.xsd before the
|
|
16
|
+
# first request. The /mods/xml.xsd path is what every mods-3-N.xsd
|
|
17
|
+
# embeds in its xs:import, but LoC only serves the file from
|
|
18
|
+
# /standards/mods/xml.xsd today — the embedded path bounces
|
|
19
|
+
# HTTPS->HTTP and 503s. Applied here so transitive xs:import
|
|
20
|
+
# resolution benefits, not just top-level fetch_schema calls.
|
|
21
|
+
#
|
|
22
|
+
# mirror_list remains the consumer's backstop for URI-identity
|
|
23
|
+
# changes (path renames, host moves) that no delivery heuristic
|
|
24
|
+
# can rescue.
|
|
25
|
+
class Fetcher
|
|
26
|
+
MAX_REDIRECTS = 5
|
|
27
|
+
|
|
28
|
+
# Map from a path that's embedded in published schemas but no longer
|
|
29
|
+
# serves the file, to the path that does. xml.xsd: every mods-3-N.xsd
|
|
30
|
+
# imports /mods/xml.xsd, but only /standards/mods/xml.xsd serves it.
|
|
31
|
+
PATH_REWRITES = {
|
|
32
|
+
'/mods/xml.xsd' => '/standards/mods/xml.xsd',
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
class FetchError < StandardError; end
|
|
36
|
+
|
|
37
|
+
def initialize(uri)
|
|
38
|
+
@original_uri = uri
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fetch
|
|
42
|
+
attempt(normalize(@original_uri), alt_scheme_retry: true)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def normalize(uri)
|
|
48
|
+
PATH_REWRITES.each do |from, to|
|
|
49
|
+
rewritten = uri.sub(%r{(\Ahttps?://[^/]+)#{Regexp.escape(from)}\z}i, "\\1#{to}")
|
|
50
|
+
return rewritten unless rewritten == uri
|
|
51
|
+
end
|
|
52
|
+
uri
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def attempt(uri, alt_scheme_retry:)
|
|
56
|
+
response = request_with_redirects(uri, redirect_depth: 0)
|
|
57
|
+
|
|
58
|
+
case response
|
|
59
|
+
when Net::HTTPSuccess
|
|
60
|
+
response.body
|
|
61
|
+
when Net::HTTPServerError
|
|
62
|
+
if alt_scheme_retry && (alt = swap_scheme(uri))
|
|
63
|
+
attempt(alt, alt_scheme_retry: false)
|
|
64
|
+
else
|
|
65
|
+
raise FetchError, "#{response.code} #{response.message} fetching #{uri}"
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
raise FetchError, "#{response.code} #{response.message} fetching #{uri}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def request_with_redirects(uri, redirect_depth:)
|
|
73
|
+
raise FetchError, "too many redirects fetching #{@original_uri}" if redirect_depth > MAX_REDIRECTS
|
|
74
|
+
|
|
75
|
+
parsed = URI.parse(uri)
|
|
76
|
+
http = Net::HTTP.new(parsed.host, parsed.port)
|
|
77
|
+
http.use_ssl = (parsed.scheme == 'https')
|
|
78
|
+
response = http.get(parsed.request_uri)
|
|
79
|
+
|
|
80
|
+
if response.is_a?(Net::HTTPRedirection)
|
|
81
|
+
target = resolve_redirect(parsed, response['location'])
|
|
82
|
+
request_with_redirects(target, redirect_depth: redirect_depth + 1)
|
|
83
|
+
else
|
|
84
|
+
response
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_redirect(from, location)
|
|
89
|
+
raise FetchError, "redirect with no Location header from #{from}" if location.nil? || location.empty?
|
|
90
|
+
|
|
91
|
+
target = URI.parse(location)
|
|
92
|
+
target = from + target if target.relative?
|
|
93
|
+
|
|
94
|
+
if from.scheme == 'https' && target.scheme == 'http' && from.host != target.host
|
|
95
|
+
raise FetchError, "cross-origin HTTPS->HTTP redirect refused: #{from} -> #{target}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
target.to_s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def swap_scheme(uri)
|
|
102
|
+
if uri.start_with?('https://')
|
|
103
|
+
uri.sub(/\Ahttps:/, 'http:')
|
|
104
|
+
elsif uri.start_with?('http://')
|
|
105
|
+
uri.sub(/\Ahttp:/, 'https:')
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/kataba.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
require 'nokogiri'
|
|
2
2
|
require 'tmpdir'
|
|
3
3
|
require 'digest/md5'
|
|
4
|
-
require 'open-uri'
|
|
5
4
|
require 'fileutils'
|
|
6
5
|
require 'yaml'
|
|
6
|
+
require 'kataba/fetcher'
|
|
7
7
|
|
|
8
8
|
module Kataba
|
|
9
9
|
|
|
@@ -110,25 +110,26 @@ module Kataba
|
|
|
110
110
|
file_path = "#{dir_name}/#{uri_md5}.xsd"
|
|
111
111
|
tmp_path = "#{file_path}.part"
|
|
112
112
|
|
|
113
|
+
# Resolve mirror first, if configured, then fetch. Fetching BEFORE
|
|
114
|
+
# opening tmp_path means a failed network fetch can't leave an
|
|
115
|
+
# orphaned 0-byte .part on disk — the file simply isn't created.
|
|
116
|
+
fetch_uri = xsd_uri
|
|
117
|
+
if !self.configuration.mirror_list.to_s.empty?
|
|
118
|
+
# YAML.load_file returns nil for a comments-only or empty file.
|
|
119
|
+
# Treat that as "no mirror configured" rather than NoMethodError.
|
|
120
|
+
mirror_list = YAML.load_file(self.configuration.mirror_list) || {}
|
|
121
|
+
mirror = mirror_list[xsd_uri]
|
|
122
|
+
fetch_uri = mirror unless mirror.to_s.empty?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
body = Kataba::Fetcher.new(fetch_uri).fetch
|
|
126
|
+
|
|
113
127
|
# Write to a .part file first; only rename to the final cache path
|
|
114
128
|
# after we've confirmed the bytes parse as XML. Without this, a
|
|
115
129
|
# malformed response (HTML error page, truncated TCP stream, captive
|
|
116
130
|
# portal stub) would land at the canonical cache path and poison
|
|
117
131
|
# every subsequent fetch.
|
|
118
|
-
File.open(tmp_path, "wb+")
|
|
119
|
-
if !self.configuration.mirror_list.to_s.empty?
|
|
120
|
-
mirror_list = YAML.load_file(self.configuration.mirror_list)
|
|
121
|
-
mirror = mirror_list[xsd_uri]
|
|
122
|
-
if mirror.to_s.empty?
|
|
123
|
-
# No mirror for that uri
|
|
124
|
-
file.write(URI.open(xsd_uri).read)
|
|
125
|
-
else
|
|
126
|
-
file.write(URI.open(mirror).read)
|
|
127
|
-
end
|
|
128
|
-
else
|
|
129
|
-
file.write(URI.open(xsd_uri).read)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
+
File.open(tmp_path, "wb+") { |file| file.write(body) }
|
|
132
133
|
|
|
133
134
|
begin
|
|
134
135
|
Nokogiri::XML(File.read(tmp_path)) { |c| c.strict }
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kataba
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -74,6 +74,7 @@ extensions: []
|
|
|
74
74
|
extra_rdoc_files: []
|
|
75
75
|
files:
|
|
76
76
|
- lib/kataba.rb
|
|
77
|
+
- lib/kataba/fetcher.rb
|
|
77
78
|
homepage: http://rubygems.org/gems/kataba
|
|
78
79
|
licenses:
|
|
79
80
|
- MIT
|