Skip to content

Commit 18a1f2a

Browse files
committed
Make etag opt in
1 parent 918437e commit 18a1f2a

File tree

7 files changed

+162
-92
lines changed

7 files changed

+162
-92
lines changed

lib/puppet/defaults.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def self.default_file_checksum_types
2626

2727
def self.valid_file_checksum_types
2828
Puppet::Util::Platform.fips_enabled? ?
29-
%w[sha256 sha256lite sha384 sha512 sha224 sha1 sha1lite mtime ctime] :
30-
%w[sha256 sha256lite sha384 sha512 sha224 sha1 sha1lite md5 md5lite mtime ctime]
29+
%w[sha256 sha256lite sha384 sha512 sha224 sha1 sha1lite mtime ctime etag] :
30+
%w[sha256 sha256lite sha384 sha512 sha224 sha1 sha1lite md5 md5lite mtime ctime etag]
3131
end
3232

3333
def self.default_cadir

lib/puppet/file_serving/http_metadata.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ def initialize(http_response, path = '/dev/null')
4040
etag_value = etag.delete('"').strip
4141
case etag_value
4242
when /\A[0-9a-f]{64}\z/i
43-
@checksums[:sha256] ||= "{sha256}#{etag_value.downcase}"
43+
@checksums[:etag] = "{sha256}#{etag_value.downcase}"
4444
when /\A[0-9a-f]{40}\z/i
45-
@checksums[:sha1] ||= "{sha1}#{etag_value.downcase}"
45+
@checksums[:etag] = "{sha1}#{etag_value.downcase}"
4646
when /\A[0-9a-f]{32}\z/i
47-
@checksums[:md5] ||= "{md5}#{etag_value.downcase}"
47+
@checksums[:etag] = "{md5}#{etag_value.downcase}"
4848
end
4949
end
5050

@@ -64,6 +64,18 @@ def collect
6464
# Prefer the checksum_type from the indirector request options
6565
# but fall back to the alternative otherwise
6666
[@checksum_type, :sha256, :sha1, :md5, :mtime].each do |type|
67+
if type == :etag
68+
if @checksums[:etag]
69+
@checksum = @checksums[:etag]
70+
resolved = sumtype(@checksum).to_sym
71+
next if resolved == :md5 && Puppet::Util::Platform.fips_enabled?
72+
73+
@checksum_type = resolved
74+
break
75+
end
76+
next
77+
end
78+
6779
next if type == :md5 && Puppet::Util::Platform.fips_enabled?
6880

6981
@checksum_type = type

lib/puppet/type/file/checksum.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
The default checksum type is sha256."
1414

1515
# The values are defined in Puppet::Util::Checksums.known_checksum_types
16-
newvalues(:sha256, :sha256lite, :md5, :md5lite, :sha1, :sha1lite, :sha512, :sha384, :sha224, :mtime, :ctime, :none)
16+
newvalues(:sha256, :sha256lite, :md5, :md5lite, :sha1, :sha1lite, :sha512, :sha384, :sha224, :mtime, :ctime, :none, :etag)
1717

1818
defaultto do
1919
Puppet[:digest_algorithm].to_sym
@@ -47,8 +47,18 @@ def sum_stream(&block)
4747
private
4848

4949
# Return the appropriate digest algorithm with fallbacks in case puppet defaults have not
50-
# been initialized.
50+
# been initialized. When the checksum type is :etag, resolve to the actual
51+
# hash algorithm that the HTTP server's ETag represents.
5152
def digest_algorithm
52-
value || Puppet[:digest_algorithm].to_sym
53+
type = value || Puppet[:digest_algorithm].to_sym
54+
return type unless type == :etag
55+
56+
source = resource.parameter(:source)
57+
resolved = source&.metadata&.checksum_type
58+
if resolved && ![:etag, :mtime, :ctime, :none].include?(resolved)
59+
return resolved
60+
end
61+
62+
:md5
5363
end
5464
end

lib/puppet/util/checksums.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ module Puppet::Util::Checksums
1717
:sha512,
1818
:sha384,
1919
:sha224,
20-
:mtime, :ctime, :none
20+
:mtime, :ctime, :none,
21+
:etag
2122
].freeze
2223

2324
# It's not a good idea to use some of these in some contexts: for example, I
@@ -339,6 +340,25 @@ def none_stream
339340
""
340341
end
341342

343+
# ETag-based checksum delegates to md5 for local file computation.
344+
# The actual algorithm is determined at runtime by HttpMetadata#collect
345+
# based on the ETag header length.
346+
def etag(content)
347+
md5(content)
348+
end
349+
350+
def etag?(string)
351+
string =~ /^\h{32,64}$/
352+
end
353+
354+
def etag_file(filename, lite = false)
355+
md5_file(filename, lite)
356+
end
357+
358+
def etag_stream(lite = false, &block)
359+
md5_stream(lite, &block)
360+
end
361+
342362
class DigestLite
343363
def initialize(digest, lite = false)
344364
@digest = digest

spec/unit/file_serving/http_metadata_spec.rb

Lines changed: 97 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -115,112 +115,131 @@
115115
end
116116

117117
context "with an ETag header" do
118-
context "containing an MD5 hash" do
118+
context "without checksum => etag" do
119119
let(:md5) { "f5ffec8d8d16b43d5e9ac6ad4330c445" }
120120

121-
it "uses the ETag as an md5 checksum" do
121+
it "does not auto-activate ETag and falls back to mtime" do
122122
http_response.add_field('ETag', %("#{md5}"))
123123
metadata = described_class.new(http_response)
124124
metadata.collect
125-
expect( metadata.checksum_type ).to eq :md5
126-
expect( metadata.checksum ).to eq "{md5}#{md5}"
127-
end
128-
129-
it "normalizes uppercase hex to lowercase" do
130-
http_response.add_field('ETag', %("#{md5.upcase}"))
131-
metadata = described_class.new(http_response)
132-
metadata.collect
133-
expect( metadata.checksum ).to eq "{md5}#{md5}"
125+
expect( metadata.checksum_type ).to eq :mtime
134126
end
135127
end
136128

137-
context "containing a SHA1 hash" do
138-
let(:sha1) { "01e4d15746f4274b84d740a93e04b9fd2882e3ea" }
139-
140-
it "uses the ETag as a sha1 checksum" do
141-
http_response.add_field('ETag', %("#{sha1}"))
142-
metadata = described_class.new(http_response)
143-
metadata.collect
144-
expect( metadata.checksum_type ).to eq :sha1
145-
expect( metadata.checksum ).to eq "{sha1}#{sha1}"
129+
context "with checksum_type => etag" do
130+
context "containing an MD5 hash" do
131+
let(:md5) { "f5ffec8d8d16b43d5e9ac6ad4330c445" }
132+
133+
it "resolves to md5" do
134+
http_response.add_field('ETag', %("#{md5}"))
135+
metadata = described_class.new(http_response)
136+
metadata.checksum_type = :etag
137+
metadata.collect
138+
expect( metadata.checksum_type ).to eq :md5
139+
expect( metadata.checksum ).to eq "{md5}#{md5}"
140+
end
141+
142+
it "normalizes uppercase hex to lowercase" do
143+
http_response.add_field('ETag', %("#{md5.upcase}"))
144+
metadata = described_class.new(http_response)
145+
metadata.checksum_type = :etag
146+
metadata.collect
147+
expect( metadata.checksum ).to eq "{md5}#{md5}"
148+
end
146149
end
147-
end
148150

149-
context "containing a SHA256 hash" do
150-
let(:sha256) { "a3eda98259c30e1e75039c2123670c18105e1c46efb672e42ca0e4cbe77b002a" }
151+
context "containing a SHA1 hash" do
152+
let(:sha1) { "01e4d15746f4274b84d740a93e04b9fd2882e3ea" }
153+
154+
it "resolves to sha1" do
155+
http_response.add_field('ETag', %("#{sha1}"))
156+
metadata = described_class.new(http_response)
157+
metadata.checksum_type = :etag
158+
metadata.collect
159+
expect( metadata.checksum_type ).to eq :sha1
160+
expect( metadata.checksum ).to eq "{sha1}#{sha1}"
161+
end
162+
end
151163

152-
it "uses the ETag as a sha256 checksum" do
153-
http_response.add_field('ETag', %("#{sha256}"))
154-
metadata = described_class.new(http_response)
155-
metadata.collect
156-
expect( metadata.checksum_type ).to eq :sha256
157-
expect( metadata.checksum ).to eq "{sha256}#{sha256}"
164+
context "containing a SHA256 hash" do
165+
let(:sha256) { "a3eda98259c30e1e75039c2123670c18105e1c46efb672e42ca0e4cbe77b002a" }
166+
167+
it "resolves to sha256" do
168+
http_response.add_field('ETag', %("#{sha256}"))
169+
metadata = described_class.new(http_response)
170+
metadata.checksum_type = :etag
171+
metadata.collect
172+
expect( metadata.checksum_type ).to eq :sha256
173+
expect( metadata.checksum ).to eq "{sha256}#{sha256}"
174+
end
158175
end
159-
end
160176

161-
context "that is a weak ETag" do
162-
it "ignores the ETag and falls back to mtime" do
163-
http_response.add_field('ETag', 'W/"f5ffec8d8d16b43d5e9ac6ad4330c445"')
164-
metadata = described_class.new(http_response)
165-
metadata.collect
166-
expect( metadata.checksum_type ).to eq :mtime
177+
context "that is a weak ETag" do
178+
it "ignores the ETag and falls back to mtime" do
179+
http_response.add_field('ETag', 'W/"f5ffec8d8d16b43d5e9ac6ad4330c445"')
180+
metadata = described_class.new(http_response)
181+
metadata.checksum_type = :etag
182+
metadata.collect
183+
expect( metadata.checksum_type ).to eq :mtime
184+
end
167185
end
168-
end
169186

170-
context "that is not a recognizable hash" do
171-
it "ignores the ETag and falls back to mtime" do
172-
http_response.add_field('ETag', '"5e8c5-27a-3e8b8840"')
173-
metadata = described_class.new(http_response)
174-
metadata.collect
175-
expect( metadata.checksum_type ).to eq :mtime
187+
context "that is not a recognizable hash" do
188+
it "ignores the ETag and falls back to mtime" do
189+
http_response.add_field('ETag', '"5e8c5-27a-3e8b8840"')
190+
metadata = described_class.new(http_response)
191+
metadata.checksum_type = :etag
192+
metadata.collect
193+
expect( metadata.checksum_type ).to eq :mtime
194+
end
176195
end
177-
end
178196

179-
context "when explicit checksum headers are also present" do
180-
let(:explicit_md5) { "c58989e9740a748de4f5054286faf99b" }
181-
let(:etag_md5) { "f5ffec8d8d16b43d5e9ac6ad4330c445" }
197+
context "when explicit checksum headers are also present" do
198+
let(:explicit_md5) { "c58989e9740a748de4f5054286faf99b" }
199+
let(:etag_md5) { "f5ffec8d8d16b43d5e9ac6ad4330c445" }
200+
201+
it "prefers the ETag over X-Checksum-Md5" do
202+
http_response.add_field('X-Checksum-Md5', explicit_md5)
203+
http_response.add_field('ETag', %("#{etag_md5}"))
204+
metadata = described_class.new(http_response)
205+
metadata.checksum_type = :etag
206+
metadata.collect
207+
expect( metadata.checksum_type ).to eq :md5
208+
expect( metadata.checksum ).to eq "{md5}#{etag_md5}"
209+
end
210+
end
182211

183-
it "prefers X-Checksum-Md5 over ETag" do
184-
http_response.add_field('X-Checksum-Md5', explicit_md5)
185-
http_response.add_field('ETag', %("#{etag_md5}"))
186-
metadata = described_class.new(http_response)
187-
metadata.collect
188-
expect( metadata.checksum_type ).to eq :md5
189-
expect( metadata.checksum ).to eq "{md5}#{explicit_md5}"
212+
context "with ETag and Last-Modified" do
213+
let(:md5) { "f5ffec8d8d16b43d5e9ac6ad4330c445" }
214+
let(:time) { Time.now.utc }
215+
216+
it "prefers ETag-derived md5 over mtime" do
217+
http_response.add_field('ETag', %("#{md5}"))
218+
http_response.add_field('last-modified', time.strftime("%a, %d %b %Y %T GMT"))
219+
metadata = described_class.new(http_response)
220+
metadata.checksum_type = :etag
221+
metadata.collect
222+
expect( metadata.checksum_type ).to eq :md5
223+
expect( metadata.checksum ).to eq "{md5}#{md5}"
224+
end
190225
end
191226

192-
it "prefers Content-MD5 over ETag" do
193-
base64 = [etag_md5].pack("H*").then { |bin| [bin].pack("m0") }
194-
http_response.add_field('Content-MD5', base64)
195-
http_response.add_field('ETag', '"0000000000000000000000000000dead"')
227+
it "skips ETag-derived md5 on FIPS platforms and falls back" do
228+
allow(Puppet::Util::Platform).to receive(:fips_enabled?).and_return(true)
229+
http_response.add_field('ETag', '"f5ffec8d8d16b43d5e9ac6ad4330c445"')
196230
metadata = described_class.new(http_response)
231+
metadata.checksum_type = :etag
197232
metadata.collect
198-
expect( metadata.checksum ).to start_with "{md5}"
199-
expect( metadata.checksum ).not_to include "dead"
233+
expect( metadata.checksum_type ).to eq :mtime
200234
end
201-
end
202235

203-
context "with ETag and Last-Modified" do
204-
let(:md5) { "f5ffec8d8d16b43d5e9ac6ad4330c445" }
205-
let(:time) { Time.now.utc }
206-
207-
it "prefers ETag-derived md5 over mtime" do
208-
http_response.add_field('ETag', %("#{md5}"))
209-
http_response.add_field('last-modified', time.strftime("%a, %d %b %Y %T GMT"))
236+
it "falls back to other checksums when no ETag is present" do
210237
metadata = described_class.new(http_response)
238+
metadata.checksum_type = :etag
211239
metadata.collect
212-
expect( metadata.checksum_type ).to eq :md5
213-
expect( metadata.checksum ).to eq "{md5}#{md5}"
240+
expect( metadata.checksum_type ).to eq :mtime
214241
end
215242
end
216-
217-
it "skips ETag-derived md5 on FIPS platforms" do
218-
allow(Puppet::Util::Platform).to receive(:fips_enabled?).and_return(true)
219-
http_response.add_field('ETag', '"f5ffec8d8d16b43d5e9ac6ad4330c445"')
220-
metadata = described_class.new(http_response)
221-
metadata.collect
222-
expect( metadata.checksum_type ).to eq :mtime
223-
end
224243
end
225244
end
226245
end

spec/unit/indirector/file_metadata/http_spec.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,26 @@
8080
expect(result.checksum).to eq("{mtime}2020-01-01 08:00:00 UTC")
8181
end
8282

83-
it "uses ETag as md5 when it contains a hex digest" do
83+
it "does not auto-activate ETag without checksum_type => etag" do
8484
etag_md5 = "f5ffec8d8d16b43d5e9ac6ad4330c445"
8585
stub_request(:head, key)
8686
.to_return(status: 200, headers: DEFAULT_HEADERS.merge("ETag" => %("#{etag_md5}")))
8787

8888
result = model.indirection.find(key)
89+
expect(result.checksum_type).to eq(:mtime)
90+
end
91+
92+
it "uses ETag as md5 when checksum_type is etag" do
93+
etag_md5 = "f5ffec8d8d16b43d5e9ac6ad4330c445"
94+
stub_request(:head, key)
95+
.to_return(status: 200, headers: DEFAULT_HEADERS.merge("ETag" => %("#{etag_md5}")))
96+
97+
result = model.indirection.find(key, checksum_type: :etag)
8998
expect(result.checksum_type).to eq(:md5)
9099
expect(result.checksum).to eq("{md5}#{etag_md5}")
91100
end
92101

93-
it "prefers explicit X-Checksum-Sha256 over ETag" do
102+
it "prefers explicit X-Checksum-Sha256 over ETag when not using etag checksum" do
94103
sha256 = "a3eda98259c30e1e75039c2123670c18105e1c46efb672e42ca0e4cbe77b002a"
95104
etag_md5 = "f5ffec8d8d16b43d5e9ac6ad4330c445"
96105
stub_request(:head, key)
@@ -104,13 +113,13 @@
104113
expect(result.checksum).to eq("{sha256}#{sha256}")
105114
end
106115

107-
it "ignores weak ETags" do
116+
it "ignores weak ETags even with checksum_type => etag" do
108117
stub_request(:head, key)
109118
.to_return(status: 200, headers: DEFAULT_HEADERS.merge(
110119
"ETag" => 'W/"f5ffec8d8d16b43d5e9ac6ad4330c445"'
111120
))
112121

113-
result = model.indirection.find(key)
122+
result = model.indirection.find(key, checksum_type: :etag)
114123
expect(result.checksum_type).to eq(:mtime)
115124
end
116125

spec/unit/util/checksums_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
end
1111

1212
content_sums = [:md5, :md5lite, :sha1, :sha1lite, :sha256, :sha256lite, :sha512, :sha384, :sha224]
13-
file_only = [:ctime, :mtime, :none]
13+
file_only = [:ctime, :mtime, :none, :etag]
1414

1515
content_sums.each do |sumtype|
1616
it "should be able to calculate #{sumtype} sums from strings" do

0 commit comments

Comments
 (0)