rack-steady_etag 0.1.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbaf5489b11a9fe181a1f92469ebaebafaa41bb11d911f8fb0f0ee0589fb7f15
4
- data.tar.gz: ff4685ce1f6215217bfadeb9cd3d02fc750b84b9aa74b1d2deb6186c8ed7da89
3
+ metadata.gz: 8caa8d352b72433a6659f50b6c9216cf03b265b19016134fa259719d24c46b4e
4
+ data.tar.gz: 74f01fc86755933b87361615bd6cb62568e44bfa404565261c0c180dcd5b399c
5
5
  SHA512:
6
- metadata.gz: 4004ed7d3b297299c3bfb343c15053548957afe60835804e087a71cf6b94558ef3dfbdb41918d54034b8fd5a464142fe137ef9a535f5f28a3ddcc9e3330c95c2
7
- data.tar.gz: ec21028482dc4c0da1a4534d0c322ab012a21b988df7ffda07764f7cb66063c0606b4656707149307ac94ea8c301c27e20fce4e7d71ac88093f75a1c36f7d3de
6
+ metadata.gz: 3cbd8de00297924f758d0c32151d17991fe8e235d943969544cdad632f50cca43eba12ef133c1a7690d91def4d8f997ab48535417457f62c8cb9d36a07a7e9d9
7
+ data.tar.gz: c2df595d03a3e3f46b2be21f4da14f76460556a0b8e6628959d7156d1a5285c79eb9c1a45415ad343904ed388c4645320ddc94a2422184727798d4b432eecd2e
data/CHANGELOG.md CHANGED
@@ -5,13 +5,22 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
5
5
 
6
6
  ## Unreleased
7
7
 
8
- ### Breaking changes
8
+ ## 0.2.2 - 2022-05-12
9
9
 
10
- ### Compatible changes
10
+ - Don't raise an error when processing binary content.
11
11
 
12
- ## 0.1.1 - 2022-05-16
12
+ ## 0.2.1 - 2022-05-12
13
+
14
+ - Only strip patterns for HTML and XHTML responses.
15
+
16
+ ## 0.2.0 - 2022-05-12
13
17
 
14
- ### Compatible changes
18
+ - Be more compatible with Rack 2.2.3:
19
+ - Always set a `Cache-Control` header, even for responses that we don't try to digest.
20
+ - Strip patterns for responses with `Cache-Control: public`
21
+ - Requires Rack 2.x (we want to break with Rack 3)
22
+
23
+ ## 0.1.1 - 2022-05-16
15
24
 
16
25
  - Activate rubygems MFA
17
26
 
data/Gemfile.lock CHANGED
@@ -1,25 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-steady_etag (0.1.1)
4
+ rack-steady_etag (0.2.2)
5
5
  activesupport (>= 3.2)
6
- rack
6
+ rack (~> 2.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (6.1.4.1)
11
+ activesupport (7.0.1)
12
12
  concurrent-ruby (~> 1.0, >= 1.0.2)
13
13
  i18n (>= 1.6, < 2)
14
14
  minitest (>= 5.1)
15
15
  tzinfo (~> 2.0)
16
- zeitwerk (~> 2.3)
17
16
  byebug (11.1.3)
18
- concurrent-ruby (1.1.9)
17
+ concurrent-ruby (1.1.10)
19
18
  diff-lcs (1.4.4)
20
- i18n (1.8.11)
19
+ i18n (1.10.0)
21
20
  concurrent-ruby (~> 1.0)
22
- minitest (5.14.4)
21
+ minitest (5.15.0)
23
22
  rack (2.2.3)
24
23
  rake (13.0.6)
25
24
  rspec (3.10.0)
@@ -37,7 +36,6 @@ GEM
37
36
  rspec-support (3.10.3)
38
37
  tzinfo (2.0.4)
39
38
  concurrent-ruby (~> 1.0)
40
- zeitwerk (2.5.1)
41
39
 
42
40
  PLATFORMS
43
41
  x86_64-linux
data/README.md CHANGED
@@ -20,17 +20,29 @@ By default Rails uses [`Rack::ETag`](https://rdoc.info/github/rack/rack/Rack/ETa
20
20
  You can add your own patterns:
21
21
 
22
22
  ```ruby
23
- Rack::SteadyETag::IGNORED_PATTERNS << /<meta name="XSRF-TOKEN" value="[^"]+">/
23
+ Rack::SteadyETag::STRIP_PATTERNS << /<meta name="XSRF-TOKEN" value="[^"]+">/
24
24
  ```
25
25
 
26
26
  You can also push lambda for arbitrary transformations:
27
27
 
28
28
  ```ruby
29
- Rack::SteadyETag::IGNORED_PATTERNS << -> { |text| text.gsub(/<meta name="XSRF-TOKEN" value="[^"]+">/, '') }
29
+ Rack::SteadyETag::STRIP_PATTERNS << -> { |text| text.gsub(/<meta name="XSRF-TOKEN" value="[^"]+">/, '') }
30
30
  ```
31
31
 
32
32
  Transformations are only applied for the `ETag` hash. The response body will not be changed.
33
33
 
34
+ ## What responses are processed
35
+
36
+ This middleware will process responses that match all of the following:
37
+
38
+ - Responses with a HTTP status of 200 or 201.
39
+ - Responses with a `Content-Type` of `text/html` or `application/xhtml+xml`.
40
+ - Responses with a body.
41
+
42
+ Responses should also have an UTF-8 encoding (not checked by the middleware).
43
+
44
+ This middleware can also add a default `Cache-Control` header for responses it *didn't* process. This is passed as an argument during middleware initialization (see *Installation* below).
45
+
34
46
  ## Covered edge cases
35
47
 
36
48
  - Different `ETags` are generated when the same content is accessed with different Rack sessions.
@@ -39,7 +51,7 @@ Transformations are only applied for the `ETag` hash. The response body will not
39
51
  - No `ETag` is generated when the response already has an `Last-Modified` header.
40
52
 
41
53
 
42
- ## Installation
54
+ ## Installation in Rails
43
55
 
44
56
  Add this line to your application's Gemfile:
45
57
 
@@ -56,9 +68,11 @@ bundle install
56
68
  In your `config/application.rb`:
57
69
 
58
70
  ```ruby
59
- config.middleware.swap Rack::ETag, Rack::SteadyETag
71
+ config.middleware.swap Rack::ETag, Rack::SteadyETag, 'no-cache'
60
72
  ```
61
73
 
74
+ The `'no-cache'` argument is the default `Cache-Control` for responses that cannot be digested. While it may feel surprising that the middleware changes the `Cache-Control` header in such a case, the [Rails default middleware stack](https://github.com/rails/rails/blob/d96609505511a76c618dc3adfa3ca4679317d008/railties/lib/rails/application/default_middleware_stack.rb#L81) configures the same behavior.
75
+
62
76
 
63
77
  ## Development
64
78
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class SteadyEtag
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
@@ -6,8 +6,8 @@ require_relative "steady_etag/version"
6
6
 
7
7
  module Rack
8
8
 
9
- # Based on Rack::Etag
10
- # https://github.com/rack/rack/blob/master/lib/rack/etag.rb
9
+ # Based on Rack::Etag from rack 2.2.3
10
+ # https://github.com/rack/rack/blob/v2.2.3/lib/rack/etag.rb
11
11
  #
12
12
  # Automatically sets the ETag header on all String bodies.
13
13
  #
@@ -15,20 +15,30 @@ module Rack
15
15
  # a sendfile body (body.responds_to :to_path) is given (since such cases
16
16
  # should be handled by apache/nginx).
17
17
  class SteadyETag
18
+
19
+ # Yes, Rack::ETag sets a default Cache-Control for responses that it can digest.
18
20
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
19
21
 
20
- IGNORE_PATTERNS = [
22
+ STRIP_PATTERNS = [
21
23
  /<meta\b[^>]*\bname=(["'])csrf-token\1[^>]+>/i,
22
24
  /<meta\b[^>]*\bname=(["'])csp-nonce\1[^>]+>/i,
23
25
  /<input\b[^>]*\bname=(["'])authenticity_token\1[^>]+>/i,
24
26
  lambda { |string| string.gsub(/(<script\b[^>]*)\bnonce=(["'])[^"']+\2+/i, '\1') }
25
27
  ]
26
28
 
27
- def initialize(app, no_digest_cache_control: nil, digest_cache_control: DEFAULT_CACHE_CONTROL, ignore_patterns: IGNORE_PATTERNS.dup)
29
+ STRIP_CONTENT_TYPES = %w[
30
+ text/html
31
+ application/xhtml+xml
32
+ ]
33
+
34
+ def initialize(app, no_digest_cache_control = nil, digest_cache_control = DEFAULT_CACHE_CONTROL)
28
35
  @app = app
36
+
29
37
  @digest_cache_control = digest_cache_control
38
+
39
+ # Rails sets a default `Cache-Control: no-cache` for responses that we cannot digest.
40
+ # See https://github.com/rails/rails/blob/d96609505511a76c618dc3adfa3ca4679317d008/railties/lib/rails/application/default_middleware_stack.rb#L81
30
41
  @no_digest_cache_control = no_digest_cache_control
31
- @ignore_patterns = ignore_patterns
32
42
  end
33
43
 
34
44
  def call(env)
@@ -45,6 +55,18 @@ module Rack
45
55
  headers[ETAG] = %(W/"#{digest}") if digest
46
56
  end
47
57
 
58
+ # It would make more sense to only set a Cache-Control for responses that we process.
59
+ # However, the original Rack::ETag sets Cache-Control: @no_digest_cache_control
60
+ # for all responses, even responses that we don't otherwise modify.
61
+ # Hence if we move this code into the `if` above we would remove Rails' default
62
+ # Cache-Control headers for non-digestable responses, which would be a considerable
63
+ # change in behavior.
64
+ if digest
65
+ set_cache_control_with_digest(headers)
66
+ else
67
+ set_cache_control_without_digest(headers)
68
+ end
69
+
48
70
  [status, headers, body]
49
71
  end
50
72
 
@@ -63,6 +85,9 @@ module Rack
63
85
  end
64
86
 
65
87
  def etag_body?(body)
88
+ # Rack main branch checks for `:to_ary` here to exclude streaming responses,
89
+ # but that had other issues for me in testing. Maybe recheck when there is a
90
+ # new Rack release after 2.2.3.
66
91
  !body.respond_to?(:to_path)
67
92
  end
68
93
 
@@ -70,51 +95,48 @@ module Rack
70
95
  headers.key?(ETAG) || headers.key?('Last-Modified')
71
96
  end
72
97
 
73
- def cache_control_private?(headers)
74
- headers[CACHE_CONTROL] && headers[CACHE_CONTROL] =~ /\bprivate\b/
75
- end
76
-
77
98
  def digest_body(body, headers, session)
78
99
  parts = []
79
100
  digest = nil
80
101
 
102
+ strippable_response = STRIP_CONTENT_TYPES.include?(headers['Content-Type'])
103
+
81
104
  body.each do |part|
82
105
  parts << part
83
106
 
84
- if part.present?
85
- set_cache_control_with_digest(headers)
86
-
87
- if cache_control_private?(headers)
88
- part = strip_ignore_patterns(part)
89
- end
90
-
91
- unless digest
92
- digest = Digest::SHA256.new
93
-
94
- if session && (session_id = session['session_id'])
95
- digest << session_id.to_s
96
- end
97
- end
98
-
107
+ # Note that `part` can be a string with binary data here.
108
+ # It's important to check emptiness with #empty? instead of #blank?, since #blank?
109
+ # internally calls String#match? and that explodes if the string is not valid UTF-8.
110
+ unless part.empty?
111
+ digest ||= initialize_digest(session)
112
+ part = strip_patterns(part) if strippable_response
99
113
  digest << part
100
114
  end
101
115
  end
102
116
 
103
117
  if digest
104
118
  digest = digest.hexdigest.byteslice(0,32)
105
- else
106
- set_cache_control_without_digest(headers)
107
119
  end
108
120
 
109
121
  [digest, parts]
110
122
  end
111
123
 
112
- def strip_ignore_patterns(html)
113
- @ignore_patterns.each do |ignore_pattern|
114
- if ignore_pattern.respond_to?(:call)
115
- html = ignore_pattern.call(html)
124
+ def initialize_digest(session)
125
+ digest = Digest::SHA256.new
126
+
127
+ if session && (session_id = session['session_id'])
128
+ digest << session_id.to_s
129
+ end
130
+
131
+ digest
132
+ end
133
+
134
+ def strip_patterns(html)
135
+ STRIP_PATTERNS.each do |pattern|
136
+ if pattern.respond_to?(:call)
137
+ html = pattern.call(html)
116
138
  else
117
- html = html.gsub(ignore_pattern, '')
139
+ html = html.gsub(pattern, '')
118
140
  end
119
141
  end
120
142
  html
@@ -31,10 +31,10 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- # Uncomment to register a new dependency of your gem
35
- spec.add_dependency "rack"
36
- spec.add_dependency 'activesupport', '>= 3.2'
34
+ # I expect Rack 3 to have a new ETag middleware to no longer buffer
35
+ # streaming responses: https://github.com/rack/rack/issues/1619
36
+ # Once Rack 3 is out we should release a new version of this gem.
37
+ spec.add_dependency "rack", '~>2.0'
37
38
 
38
- # For more information and examples about making a new gem, checkout our
39
- # guide at: https://bundler.io/guides/creating_gem.html
39
+ spec.add_dependency 'activesupport', '>= 3.2'
40
40
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-steady_etag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-16 00:00:00.000000000 Z
11
+ date: 2022-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2.0'
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: '0'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -83,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  requirements: []
86
- rubygems_version: 3.1.4
86
+ rubygems_version: 3.2.6
87
87
  signing_key:
88
88
  specification_version: 4
89
89
  summary: Rack Middleware that produces the same ETag for responses that only differ