rack-steady_etag 0.1.1 → 0.2.2

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: 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