rack-steady_etag 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 733d26f0e71001a540dcd32beb688dd0b78e72ded8d6695bcb489db227a31d35
4
- data.tar.gz: 429c9a93ffad20173a5e8f603d427f54daeb964a748c955a4685d6dd8a7d2f70
3
+ metadata.gz: d3047cb500ace97518e12a5667bad5602d27b493f24d6a8a6c8236372fe7ca36
4
+ data.tar.gz: d9c3afe27e063227577e1921f6f4ce4e170f7f5627be0126096f1ed846ad7d1e
5
5
  SHA512:
6
- metadata.gz: 041f32e35729ad2a7cb72d447814dea132d2a463e9c13f5d4af113ffa4f5fc568791c7c6a67cc724046628560737ec9a9401c8c5c34dde9319af251aa3f7cc7c
7
- data.tar.gz: 20ecac0974ebf4dd59d55360ca872af812b5c2cf1fd4c20f81c9a0f6cb90cd89cb7980e8a8a667536bf4b79e5673c4b5220f7de3419e2e7ec8b3e448a9cee70b
6
+ metadata.gz: b86c5ec23d0d27170406530421acea4a54be83caf4c0513de01004136e92d2842aa771927f9ab8074bd918b83f3f10efc651b3e815dc8c5c2036769767c28077
7
+ data.tar.gz: b28cddf4577f20d1c0c75cdcf7c58d9ce5765f2148ab6fde4b9c0533725d09aa4c596b1624cab70c4e36a5b909e67153fd14ff95a272e9ef8fa0ee5ac81800c8
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ All notable changes to this project will be documented in this file.
2
+
3
+ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
4
+
5
+
6
+ ## Unreleased
7
+
8
+ ## 0.2.1 - 2022-05-12
9
+
10
+ - Only strip patterns for HTML and XHTML responses.
11
+
12
+ ## 0.2.0 - 2022-05-12
13
+
14
+ - Be more compatible with Rack 2.2.2:
15
+ - Always set a `Cache-Control` header, even for responses that we don't try to digest.
16
+ - Strip patterns for responses with `Cache-Control: public`
17
+ - Requires Rack 2.x (we want to break with Rack 3)
18
+
19
+ ## 0.1.1 - 2022-05-16
20
+
21
+ - Activate rubygems MFA
22
+
23
+ ## 0.1.0 - 2021-12-01
24
+
25
+ - initial release
data/Gemfile.lock CHANGED
@@ -1,25 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-steady_etag (0.1.0)
4
+ rack-steady_etag (0.2.1)
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
17
  concurrent-ruby (1.1.9)
19
18
  diff-lcs (1.4.4)
20
19
  i18n (1.8.11)
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  `Rack::SteadyTag` is a Rack middleware that generates the same default [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) for responses that only differ in CSRF tokens or CSP nonces.
4
4
 
5
- By default Rails uses [`Rack::ETag`](https://rdoc.info/github/rack/rack/Rack/ETag) to generate `ETag` headers by hashing the response body. In theory this would [enable caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) for multiple requests to the same resource. However, since most Rails application layouts insert randomly rotating CSRF tokens and CSP nonces into the HTML, two requests for the same content and user will never produce the same response bytes. This means the default ETags from Rails will [never hit a cache](https://github.com/rails/rails/issues/29889).
5
+ By default Rails uses [`Rack::ETag`](https://rdoc.info/github/rack/rack/Rack/ETag) to generate `ETag` headers by hashing the response body. In theory this would [enable caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) for multiple requests to the same resource. However, since most Rails application layouts insert randomly rotating CSRF tokens and CSP nonces into the HTML, two requests for the same content and user will never produce the same response bytes. This means `Rack::ETag` will never send the same ETag twice, causing responses to [never hit a cache](https://github.com/rails/rails/issues/29889).
6
6
 
7
7
  `Rack::SteadyETag` is a drop-in replacement for `Rack::ETag`. It excludes random content (like CSRF tokens) from the generated ETag, causing two requests for the same content to usually carry the same ETag.
8
8
 
@@ -20,17 +20,27 @@ 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
+ 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).
43
+
34
44
  ## Covered edge cases
35
45
 
36
46
  - Different `ETags` are generated when the same content is accessed with different Rack sessions.
@@ -39,7 +49,7 @@ Transformations are only applied for the `ETag` hash. The response body will not
39
49
  - No `ETag` is generated when the response already has an `Last-Modified` header.
40
50
 
41
51
 
42
- ## Installation
52
+ ## Installation in Rails
43
53
 
44
54
  Add this line to your application's Gemfile:
45
55
 
@@ -56,9 +66,11 @@ bundle install
56
66
  In your `config/application.rb`:
57
67
 
58
68
  ```ruby
59
- config.middleware.swap Rack::ETag, Rack::SteadyETag
69
+ config.middleware.swap Rack::ETag, Rack::SteadyETag, 'no-cache'
60
70
  ```
61
71
 
72
+ 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.
73
+
62
74
 
63
75
  ## Development
64
76
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class SteadyEtag
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.1"
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.2
10
+ # https://github.com/rack/rack/blob/v2.2.2/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.2.
66
91
  !body.respond_to?(:to_path)
67
92
  end
68
93
 
@@ -70,51 +95,45 @@ 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
107
  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
-
108
+ digest ||= initialize_digest(session)
109
+ part = strip_patterns(part) if strippable_response
99
110
  digest << part
100
111
  end
101
112
  end
102
113
 
103
114
  if digest
104
115
  digest = digest.hexdigest.byteslice(0,32)
105
- else
106
- set_cache_control_without_digest(headers)
107
116
  end
108
117
 
109
118
  [digest, parts]
110
119
  end
111
120
 
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)
121
+ def initialize_digest(session)
122
+ digest = Digest::SHA256.new
123
+
124
+ if session && (session_id = session['session_id'])
125
+ digest << session_id.to_s
126
+ end
127
+
128
+ digest
129
+ end
130
+
131
+ def strip_patterns(html)
132
+ STRIP_PATTERNS.each do |pattern|
133
+ if pattern.respond_to?(:call)
134
+ html = pattern.call(html)
116
135
  else
117
- html = html.gsub(ignore_pattern, '')
136
+ html = html.gsub(pattern, '')
118
137
  end
119
138
  end
120
139
  html
@@ -16,6 +16,9 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["bug_tracker_uri"] = spec.homepage + "/issues"
20
+ spec.metadata["changelog_uri"] = spec.homepage + "/blob/master/CHANGELOG.md"
21
+ spec.metadata["rubygems_mfa_required"] = 'true'
19
22
 
20
23
  # Specify which files should be added to the gem when it is released.
21
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -28,10 +31,10 @@ Gem::Specification.new do |spec|
28
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
32
  spec.require_paths = ["lib"]
30
33
 
31
- # Uncomment to register a new dependency of your gem
32
- spec.add_dependency "rack"
33
- 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'
34
38
 
35
- # For more information and examples about making a new gem, checkout our
36
- # guide at: https://bundler.io/guides/creating_gem.html
39
+ spec.add_dependency 'activesupport', '>= 3.2'
37
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.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-01 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
@@ -48,6 +48,7 @@ extra_rdoc_files: []
48
48
  files:
49
49
  - ".rspec"
50
50
  - ".ruby-version"
51
+ - CHANGELOG.md
51
52
  - Gemfile
52
53
  - Gemfile.lock
53
54
  - LICENSE.txt
@@ -64,6 +65,9 @@ licenses:
64
65
  metadata:
65
66
  homepage_uri: https://github.com/makandra/rack-steady_etag
66
67
  source_code_uri: https://github.com/makandra/rack-steady_etag
68
+ bug_tracker_uri: https://github.com/makandra/rack-steady_etag/issues
69
+ changelog_uri: https://github.com/makandra/rack-steady_etag/blob/master/CHANGELOG.md
70
+ rubygems_mfa_required: 'true'
67
71
  post_install_message:
68
72
  rdoc_options: []
69
73
  require_paths: