rack-steady_etag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 733d26f0e71001a540dcd32beb688dd0b78e72ded8d6695bcb489db227a31d35
4
+ data.tar.gz: 429c9a93ffad20173a5e8f603d427f54daeb964a748c955a4685d6dd8a7d2f70
5
+ SHA512:
6
+ metadata.gz: 041f32e35729ad2a7cb72d447814dea132d2a463e9c13f5d4af113ffa4f5fc568791c7c6a67cc724046628560737ec9a9401c8c5c34dde9319af251aa3f7cc7c
7
+ data.tar.gz: 20ecac0974ebf4dd59d55360ca872af812b5c2cf1fd4c20f81c9a0f6cb90cd89cb7980e8a8a667536bf4b79e5673c4b5220f7de3419e2e7ec8b3e448a9cee70b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.2
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rack-steady-etag.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem 'byebug'
data/Gemfile.lock ADDED
@@ -0,0 +1,52 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rack-steady_etag (0.1.0)
5
+ activesupport (>= 3.2)
6
+ rack
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (6.1.4.1)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+ zeitwerk (~> 2.3)
17
+ byebug (11.1.3)
18
+ concurrent-ruby (1.1.9)
19
+ diff-lcs (1.4.4)
20
+ i18n (1.8.11)
21
+ concurrent-ruby (~> 1.0)
22
+ minitest (5.14.4)
23
+ rack (2.2.3)
24
+ rake (13.0.6)
25
+ rspec (3.10.0)
26
+ rspec-core (~> 3.10.0)
27
+ rspec-expectations (~> 3.10.0)
28
+ rspec-mocks (~> 3.10.0)
29
+ rspec-core (3.10.1)
30
+ rspec-support (~> 3.10.0)
31
+ rspec-expectations (3.10.1)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.10.0)
34
+ rspec-mocks (3.10.2)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.10.0)
37
+ rspec-support (3.10.3)
38
+ tzinfo (2.0.4)
39
+ concurrent-ruby (~> 1.0)
40
+ zeitwerk (2.5.1)
41
+
42
+ PLATFORMS
43
+ x86_64-linux
44
+
45
+ DEPENDENCIES
46
+ byebug
47
+ rack-steady_etag!
48
+ rake (~> 13.0)
49
+ rspec (~> 3.0)
50
+
51
+ BUNDLED WITH
52
+ 2.2.32
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Based on Rack, Copyright (C) 2007-2021 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
4
+ Modifications Copyright (C) 2021 Henning Koch
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to
8
+ deal in the Software without restriction, including without limitation the
9
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ sell copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Rack::SteadyETag
2
+
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
+
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).
6
+
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
+
9
+ ## What is ignored
10
+
11
+ `Rack::SteadyTag` ignores the following patterns from the `ETag` hash:
12
+
13
+ ```html
14
+ <meta name="csrf-token" value="random" ...>
15
+ <meta name="csp-nonce" value="random" ...>
16
+ <input name="authenticity_token" value="random" ...>
17
+ <script nonce="random" ...> <!-- only the [nonce] attribute -->
18
+ ```
19
+
20
+ You can add your own patterns:
21
+
22
+ ```ruby
23
+ Rack::SteadyETag::IGNORED_PATTERNS << /<meta name="XSRF-TOKEN" value="[^"]+">/
24
+ ```
25
+
26
+ You can also push lambda for arbitrary transformations:
27
+
28
+ ```ruby
29
+ Rack::SteadyETag::IGNORED_PATTERNS << -> { |text| text.gsub(/<meta name="XSRF-TOKEN" value="[^"]+">/, '') }
30
+ ```
31
+
32
+ Transformations are only applied for the `ETag` hash. The response body will not be changed.
33
+
34
+ ## Covered edge cases
35
+
36
+ - Different `ETags` are generated when the same content is accessed with different Rack sessions.
37
+ - `ETags` are only generated when the response is `Cache-Control: private` (this is a default in Rails).
38
+ - No `ETag` is generated when the response already has an `ETag` header.
39
+ - No `ETag` is generated when the response already has an `Last-Modified` header.
40
+
41
+
42
+ ## Installation
43
+
44
+ Add this line to your application's Gemfile:
45
+
46
+ ```ruby
47
+ gem 'rack-steady_etag'
48
+ ```
49
+
50
+ And then execute:
51
+
52
+ ```bash
53
+ bundle install
54
+ ```
55
+
56
+ In your `config/application.rb`:
57
+
58
+ ```ruby
59
+ config.middleware.swap Rack::ETag, Rack::SteadyETag
60
+ ```
61
+
62
+
63
+ ## Development
64
+
65
+ - After checking out the repo, run `bin/setup` to install dependencies.
66
+ - Run `bundle exec rspec` to run the tests.
67
+ - You can also run `bin/console` for an interactive prompt that will allow you to experiment.
68
+ - To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
69
+
70
+ ## Credits
71
+
72
+ This library is based on `Rack::ETag`, created by the [Rack Core Team](https://github.com/rack/rack#label-Thanks) and [Rack contributors](https://github.com/rack/rack/graphs/contributors).
73
+
74
+ Additional changes by [Henning Koch](https://twitter.com/triskweline) from [makandra](https://makandra.com).
75
+
76
+ ## Limitations
77
+
78
+ - No streaming support. [This will be broken until at least Rack 3](https://github.com/rack/rack/issues/1619). This is not a use case of mine.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "rack/steady/etag"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class SteadyEtag
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,124 @@
1
+ require 'byebug'
2
+ require 'digest/sha2'
3
+ require "active_support/all"
4
+ require_relative "steady_etag"
5
+ require_relative "steady_etag/version"
6
+
7
+ module Rack
8
+
9
+ # Based on Rack::Etag
10
+ # https://github.com/rack/rack/blob/master/lib/rack/etag.rb
11
+ #
12
+ # Automatically sets the ETag header on all String bodies.
13
+ #
14
+ # The ETag header is skipped if ETag or Last-Modified headers are sent or if
15
+ # a sendfile body (body.responds_to :to_path) is given (since such cases
16
+ # should be handled by apache/nginx).
17
+ class SteadyETag
18
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
19
+
20
+ IGNORE_PATTERNS = [
21
+ /<meta\b[^>]*\bname=(["'])csrf-token\1[^>]+>/i,
22
+ /<meta\b[^>]*\bname=(["'])csp-nonce\1[^>]+>/i,
23
+ /<input\b[^>]*\bname=(["'])authenticity_token\1[^>]+>/i,
24
+ lambda { |string| string.gsub(/(<script\b[^>]*)\bnonce=(["'])[^"']+\2+/i, '\1') }
25
+ ]
26
+
27
+ def initialize(app, no_digest_cache_control: nil, digest_cache_control: DEFAULT_CACHE_CONTROL, ignore_patterns: IGNORE_PATTERNS.dup)
28
+ @app = app
29
+ @digest_cache_control = digest_cache_control
30
+ @no_digest_cache_control = no_digest_cache_control
31
+ @ignore_patterns = ignore_patterns
32
+ end
33
+
34
+ def call(env)
35
+ status, headers, body = @app.call(env)
36
+ headers = Utils::HeaderHash[headers]
37
+ session = env[RACK_SESSION]
38
+
39
+ if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
40
+ original_body = body
41
+ digest, new_body = digest_body(body, headers, session)
42
+ body = Rack::BodyProxy.new(new_body) do
43
+ original_body.close if original_body.respond_to?(:close)
44
+ end
45
+ headers[ETAG] = %(W/"#{digest}") if digest
46
+ end
47
+
48
+ [status, headers, body]
49
+ end
50
+
51
+ private
52
+
53
+ def set_cache_control_with_digest(headers)
54
+ headers[CACHE_CONTROL] ||= @digest_cache_control if @digest_cache_control
55
+ end
56
+
57
+ def set_cache_control_without_digest(headers)
58
+ headers[CACHE_CONTROL] ||= @no_digest_cache_control if @no_digest_cache_control
59
+ end
60
+
61
+ def etag_status?(status)
62
+ status == 200 || status == 201
63
+ end
64
+
65
+ def etag_body?(body)
66
+ !body.respond_to?(:to_path)
67
+ end
68
+
69
+ def skip_caching?(headers)
70
+ headers.key?(ETAG) || headers.key?('Last-Modified')
71
+ end
72
+
73
+ def cache_control_private?(headers)
74
+ headers[CACHE_CONTROL] && headers[CACHE_CONTROL] =~ /\bprivate\b/
75
+ end
76
+
77
+ def digest_body(body, headers, session)
78
+ parts = []
79
+ digest = nil
80
+
81
+ body.each do |part|
82
+ parts << part
83
+
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
+
99
+ digest << part
100
+ end
101
+ end
102
+
103
+ if digest
104
+ digest = digest.hexdigest.byteslice(0,32)
105
+ else
106
+ set_cache_control_without_digest(headers)
107
+ end
108
+
109
+ [digest, parts]
110
+ end
111
+
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)
116
+ else
117
+ html = html.gsub(ignore_pattern, '')
118
+ end
119
+ end
120
+ html
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rack/steady_etag/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rack-steady_etag"
7
+ spec.version = Rack::SteadyEtag::VERSION
8
+ spec.authors = ["Henning Koch"]
9
+ spec.email = ["henning.koch@makandra.de"]
10
+
11
+ spec.summary = "Rack Middleware that produces the same ETag for responses that only differ in CSRF tokens or CSP nonce"
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/makandra/rack-steady_etag"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.5.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency "rack"
33
+ spec.add_dependency 'activesupport', '>= 3.2'
34
+
35
+ # For more information and examples about making a new gem, checkout our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-steady_etag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Henning Koch
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ description: Rack Middleware that produces the same ETag for responses that only differ
42
+ in CSRF tokens or CSP nonce
43
+ email:
44
+ - henning.koch@makandra.de
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - ".ruby-version"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE.txt
54
+ - README.md
55
+ - Rakefile
56
+ - bin/console
57
+ - bin/setup
58
+ - lib/rack/steady_etag.rb
59
+ - lib/rack/steady_etag/version.rb
60
+ - rack-steady_etag.gemspec
61
+ homepage: https://github.com/makandra/rack-steady_etag
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ homepage_uri: https://github.com/makandra/rack-steady_etag
66
+ source_code_uri: https://github.com/makandra/rack-steady_etag
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.5.0
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.2.6
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Rack Middleware that produces the same ETag for responses that only differ
86
+ in CSRF tokens or CSP nonce
87
+ test_files: []