kataba 1.0.1 → 1.1.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.
Files changed (4) hide show
  1. checksums.yaml +5 -5
  2. data/lib/kataba/fetcher.rb +88 -0
  3. data/lib/kataba.rb +50 -30
  4. metadata +55 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 91cf7e773f0cead00236519792f3b0b6bf888fb8
4
- data.tar.gz: 2033b27334cefc6ad872c7084759e60da09edc19
2
+ SHA256:
3
+ metadata.gz: 1849ebd6214469739d5a16981525a9c5ab23d0ca7ddb91ad028064a43e63dad0
4
+ data.tar.gz: 46da3306e49890c4801362c7c5af2c8e7cbf68cf2d3a8b9604e4d69f9d3f7887
5
5
  SHA512:
6
- metadata.gz: 0c2cf4238e8b166b3646427aeeb40ca80107e8a5b08f80eea2e02a8906a2fdeb94121d3465333ad4bff29ac766a7ce90187f84af84c9c5876c5aa5b727449eff
7
- data.tar.gz: 38a54143ba8b1ac21f7c5da6b1556bfc810f2a9d2ceaf8bf7ff391b09cddb42f2f7f677e26a7a532e8074eace7538170d52b6d9202330286a3e5355571aabac3
6
+ metadata.gz: 7d26265f3e4b30fbc11c82f12bec40860b56f3910d8a7aefccc9150bf6cd24d61a543da9fb33eb11111a8410386e1c632df6c02f56e2f91a8681515e4f2e5548
7
+ data.tar.gz: e35b99af877e7b660838d9b58943806c35084861c8295ea93e4a456d66e20943fb987261ee5adf795a0c858c26fa8a7c4cd41212d9012d26d5ba14cab80d51bc
@@ -0,0 +1,88 @@
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
+ #
16
+ # mirror_list remains the consumer's backstop for URI-identity
17
+ # changes (path renames, host moves) that no delivery heuristic
18
+ # can rescue.
19
+ class Fetcher
20
+ MAX_REDIRECTS = 5
21
+
22
+ class FetchError < StandardError; end
23
+
24
+ def initialize(uri)
25
+ @original_uri = uri
26
+ end
27
+
28
+ def fetch
29
+ attempt(@original_uri, alt_scheme_retry: true)
30
+ end
31
+
32
+ private
33
+
34
+ def attempt(uri, alt_scheme_retry:)
35
+ response = request_with_redirects(uri, redirect_depth: 0)
36
+
37
+ case response
38
+ when Net::HTTPSuccess
39
+ response.body
40
+ when Net::HTTPServerError
41
+ if alt_scheme_retry && (alt = swap_scheme(uri))
42
+ attempt(alt, alt_scheme_retry: false)
43
+ else
44
+ raise FetchError, "#{response.code} #{response.message} fetching #{uri}"
45
+ end
46
+ else
47
+ raise FetchError, "#{response.code} #{response.message} fetching #{uri}"
48
+ end
49
+ end
50
+
51
+ def request_with_redirects(uri, redirect_depth:)
52
+ raise FetchError, "too many redirects fetching #{@original_uri}" if redirect_depth > MAX_REDIRECTS
53
+
54
+ parsed = URI.parse(uri)
55
+ http = Net::HTTP.new(parsed.host, parsed.port)
56
+ http.use_ssl = (parsed.scheme == 'https')
57
+ response = http.get(parsed.request_uri)
58
+
59
+ if response.is_a?(Net::HTTPRedirection)
60
+ target = resolve_redirect(parsed, response['location'])
61
+ request_with_redirects(target, redirect_depth: redirect_depth + 1)
62
+ else
63
+ response
64
+ end
65
+ end
66
+
67
+ def resolve_redirect(from, location)
68
+ raise FetchError, "redirect with no Location header from #{from}" if location.nil? || location.empty?
69
+
70
+ target = URI.parse(location)
71
+ target = from + target if target.relative?
72
+
73
+ if from.scheme == 'https' && target.scheme == 'http' && from.host != target.host
74
+ raise FetchError, "cross-origin HTTPS->HTTP redirect refused: #{from} -> #{target}"
75
+ end
76
+
77
+ target.to_s
78
+ end
79
+
80
+ def swap_scheme(uri)
81
+ if uri.start_with?('https://')
82
+ uri.sub(/\Ahttps:/, 'http:')
83
+ elsif uri.start_with?('http://')
84
+ uri.sub(/\Ahttp:/, 'https:')
85
+ end
86
+ end
87
+ end
88
+ end
data/lib/kataba.rb CHANGED
@@ -1,15 +1,12 @@
1
1
  require 'nokogiri'
2
2
  require 'tmpdir'
3
3
  require 'digest/md5'
4
- require 'open-uri'
4
+ require 'fileutils'
5
+ require 'yaml'
6
+ require 'kataba/fetcher'
5
7
 
6
8
  module Kataba
7
9
 
8
- class << self
9
- # Simple attribute for configuration
10
- attr_accessor :configuration
11
- end
12
-
13
10
  # Allows for configuration by block
14
11
  #
15
12
  # Example:
@@ -69,17 +66,27 @@ module Kataba
69
66
  dir_path = "#{self.configuration.offline_storage}"
70
67
  xsd_path = "#{dir_path}/#{uri_md5}.xsd"
71
68
 
72
- # Does the offline version exist already?
73
- if !(File.exists?(xsd_path))
74
- # If not, go download
75
- xsd_array = []
76
- xsd_array << xsd_uri
77
- download_xsd(xsd_array)
78
- end
69
+ attempts = 0
70
+ begin
71
+ # Does the offline version exist already?
72
+ if !(File.exist?(xsd_path))
73
+ # If not, go download
74
+ xsd_array = []
75
+ xsd_array << xsd_uri
76
+ download_xsd(xsd_array)
77
+ end
79
78
 
80
- # Validate and return Nokogiri schema
81
- Dir.chdir(dir_path) do
82
- return Nokogiri::XML::Schema(IO.read(xsd_path))
79
+ # Validate and return Nokogiri schema
80
+ Dir.chdir(dir_path) do
81
+ return Nokogiri::XML::Schema(IO.read(xsd_path))
82
+ end
83
+ rescue Nokogiri::XML::SyntaxError
84
+ # Poisoned cache (e.g. a pre-fix install that stored a bad fetch).
85
+ # Evict the offending file and refetch once.
86
+ File.delete(xsd_path) if File.exist?(xsd_path)
87
+ attempts += 1
88
+ retry if attempts < 2
89
+ raise
83
90
  end
84
91
  end
85
92
 
@@ -101,23 +108,36 @@ module Kataba
101
108
  end
102
109
 
103
110
  file_path = "#{dir_name}/#{uri_md5}.xsd"
111
+ tmp_path = "#{file_path}.part"
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
+ mirror_list = YAML.load_file(self.configuration.mirror_list)
119
+ mirror = mirror_list[xsd_uri]
120
+ fetch_uri = mirror unless mirror.to_s.empty?
121
+ end
104
122
 
105
- file_paths << file_path
123
+ body = Kataba::Fetcher.new(fetch_uri).fetch
124
+
125
+ # Write to a .part file first; only rename to the final cache path
126
+ # after we've confirmed the bytes parse as XML. Without this, a
127
+ # malformed response (HTML error page, truncated TCP stream, captive
128
+ # portal stub) would land at the canonical cache path and poison
129
+ # every subsequent fetch.
130
+ File.open(tmp_path, "wb+") { |file| file.write(body) }
106
131
 
107
- open(file_path, "wb+") do |file|
108
- if !self.configuration.mirror_list.to_s.empty?
109
- mirror_list = YAML.load_file(self.configuration.mirror_list)
110
- mirror = mirror_list[xsd_uri]
111
- if mirror.to_s.empty?
112
- # No mirror for that uri
113
- file.write(open(xsd_uri).read)
114
- else
115
- file.write(open(mirror).read)
116
- end
117
- else
118
- file.write(open(xsd_uri).read)
119
- end
132
+ begin
133
+ Nokogiri::XML(File.read(tmp_path)) { |c| c.strict }
134
+ rescue Nokogiri::XML::SyntaxError
135
+ File.delete(tmp_path)
136
+ raise
120
137
  end
138
+
139
+ File.rename(tmp_path, file_path)
140
+ file_paths << file_path
121
141
  end
122
142
 
123
143
  # Search inside for other schemaLocations
metadata CHANGED
@@ -1,29 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kataba
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-02 00:00:00.000000000 Z
11
+ date: 2026-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
- version: '1.6'
19
+ version: '1.19'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
- version: '1.6'
26
+ version: '1.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
27
69
  description: Kataba allows for mirroring and offline storage of XSD files, to enhance
28
70
  Nokogiri
29
71
  email: d.cliff@northeastern.edu
@@ -32,28 +74,28 @@ extensions: []
32
74
  extra_rdoc_files: []
33
75
  files:
34
76
  - lib/kataba.rb
77
+ - lib/kataba/fetcher.rb
35
78
  homepage: http://rubygems.org/gems/kataba
36
79
  licenses:
37
80
  - MIT
38
81
  metadata: {}
39
- post_install_message:
82
+ post_install_message:
40
83
  rdoc_options: []
41
84
  require_paths:
42
85
  - lib
43
86
  required_ruby_version: !ruby/object:Gem::Requirement
44
87
  requirements:
45
- - - ~>
88
+ - - '>='
46
89
  - !ruby/object:Gem::Version
47
- version: '2.0'
90
+ version: '3.2'
48
91
  required_rubygems_version: !ruby/object:Gem::Requirement
49
92
  requirements:
50
93
  - - '>='
51
94
  - !ruby/object:Gem::Version
52
95
  version: '0'
53
96
  requirements: []
54
- rubyforge_project:
55
- rubygems_version: 2.2.2
56
- signing_key:
97
+ rubygems_version: 3.0.9
98
+ signing_key:
57
99
  specification_version: 4
58
100
  summary: XML Schema Definition (XSD) mirroring and offline validation for Nokogiri
59
101
  test_files: []