proscenium 0.25.1-arm64-darwin → 0.25.2-arm64-darwin

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: 8bc3aef9facc5a3b191a3852a5ce2f60bf8e1d4f0d49e38f1c11f9858be20a9c
4
- data.tar.gz: 649ad1680c27ae342f07bddfcdd5f1d6295f646f3871f0a1a1eed5f5c8bc422e
3
+ metadata.gz: '03228ac8a2e5c1a00f2630b863476b247dcf8c99f02a804a3a20ff39b15629df'
4
+ data.tar.gz: 68eb0c62e40a34a30823a85b64cff6a8fdee905517df5b166ddcf7f9d8459c5c
5
5
  SHA512:
6
- metadata.gz: 9fc0530ae677d835595137e2e7e8c9abeabc75cf9c83f7d77d522b5de3c634b40d4ab527c0228f15a83094c6bf1d63bbc595a3285877a4a8110c86a7f1e3de77
7
- data.tar.gz: bc9425a4a5767894ead8126a5d7a890577e19d66af500564c467c08b2d463493ffbf1b63daa50062acbc489206dfe95b3d891eeca591338fd15608d808078dd6
6
+ metadata.gz: 0eec830d9fd1828ea56e3afc1c0261487677fa183ecb22df9bf0a75526472c8ea7c9c32bb3b20d248d1e81f635448955c9509a9b622bfb414b47a3389706a728
7
+ data.tar.gz: e63f4a68f5d1be4d443ae369d470eada7df6f3f276c040bf871cac9350dbabdcbcfbf1cd6a9d196e7a831813daccb2cb319ac2692925b73c73214b9f4bd33cdf
Binary file
@@ -14,22 +14,34 @@ module Proscenium
14
14
  end
15
15
 
16
16
  def renderable!
17
+ # The single funnel every subclass reaches, so the guard sits here rather than in
18
+ # `renderable?` - a subclass that overrides that (RubyGems does) cannot skip it.
19
+ return unless normalised_path
20
+
17
21
  renderable? ? self : nil
18
22
  end
19
23
 
20
24
  private
21
25
 
26
+ # The request path, decoded and with `.` and `..` segments resolved. Everything below is
27
+ # derived from this one value; see `Middleware.normalise_path` for why that matters.
28
+ def normalised_path
29
+ return @normalised_path if defined?(@normalised_path)
30
+
31
+ @normalised_path = Middleware.normalise_path(@request.path)
32
+ end
33
+
22
34
  def real_path
23
- @real_path ||= @request.path
35
+ @real_path ||= normalised_path
24
36
  end
25
37
 
26
38
  # @return [String] the path to the file without the leading slash which will be built.
27
39
  def path_to_build
28
- @path_to_build ||= @request.path[1..]
40
+ @path_to_build ||= normalised_path[1..]
29
41
  end
30
42
 
31
43
  def sourcemap?
32
- @request.path.ends_with?('.map')
44
+ normalised_path.ends_with?('.map')
33
45
  end
34
46
 
35
47
  def renderable?
@@ -50,6 +62,9 @@ module Proscenium
50
62
  Rails.root
51
63
  end
52
64
 
65
+ # Secondary to `Middleware.normalise_path`, which every path here has already been through
66
+ # - this is idempotent on that output, and earns its keep by stripping the leading slash
67
+ # that the `join` above needs, and by handling the `.map`-stripped variant.
53
68
  def clean_path(file)
54
69
  path = Rack::Utils.unescape_path file.chomp('/').delete_prefix('/')
55
70
  Rack::Utils.clean_path_info path if Rack::Utils.valid_path? path
@@ -10,17 +10,31 @@ module Proscenium
10
10
  def call(env)
11
11
  request = ActionDispatch::Request.new(env)
12
12
 
13
- return @app.call(env) unless request.path.match?(CHUNKS_PATH)
13
+ # Decode and clean the path BEFORE deciding anything about it, then hand that same path
14
+ # to the file handler. `ActionDispatch::FileHandler` normalises `PATH_INFO` itself before
15
+ # it resolves a file, so a guard reading the raw path is not looking at the path that
16
+ # gets served: `/_asset_chunks/x-$FAKE$/%2e%2e/real-$ABC123$.js` passed both checks and
17
+ # then served `real-$ABC123$.js` tagged `FAKE`, and `x-$FAKE$/../../lib/foo.js` left the
18
+ # chunk directory altogether, since the handler's root is the whole output directory.
19
+ # Both under `immutable, max-age=100.years`, so a wrong ETag is not something a client
20
+ # can clear.
21
+ #
22
+ # The path also has to yield a content hash, not just match the prefix - `CHUNKS_PATH`
23
+ # checks only the latter, and `/_asset_chunks/nohash.js` used to index the nil match and
24
+ # return a 500 from here.
25
+ return @app.call(env) unless (path = Middleware.normalise_path(request.path)) &&
26
+ path.match?(CHUNKS_PATH) &&
27
+ (etag = path[/-\$([a-z0-9]+)\$/i, 1])
14
28
 
15
29
  ActionDispatch::FileHandler.new(
16
30
  Proscenium.config.output_path.to_s,
17
31
  headers: {
18
32
  'X-Proscenium-Middleware' => 'chunks',
19
- 'SourceMap' => "#{request.path}.map",
33
+ 'SourceMap' => "#{path}.map",
20
34
  'Cache-Control' => "public, max-age=#{100.years}, immutable",
21
- 'ETag' => request.path.match(/-\$([a-z0-9]+)\$/i)[1]
35
+ 'ETag' => etag
22
36
  }
23
- ).attempt(request.env) || @app.call(env)
37
+ ).attempt(env.merge('PATH_INFO' => path)) || @app.call(env)
24
38
  end
25
39
  end
26
40
  end
@@ -7,6 +7,14 @@ module Proscenium
7
7
  @real_path ||= Pathname.new(gem_request_path.delete_prefix("#{gem_name}/")).to_s
8
8
  end
9
9
 
10
+ # An unknown gem is "not mine", the same answer a missing file under an app path gets from
11
+ # `Base#file_readable?`. Without this, `pathname_for!` raised out of the readability probe
12
+ # and a request for a gem that is not in the Gemfile 500'd, while the sibling app-path
13
+ # branch quietly passed the same shape of miss down the stack.
14
+ def renderable?
15
+ BundledGems.pathname_for(gem_name) && super
16
+ end
17
+
10
18
  def root_for_readable
11
19
  BundledGems.pathname_for!(gem_name)
12
20
  end
@@ -15,8 +23,14 @@ module Proscenium
15
23
  @gem_name ||= gem_request_path.split('/').first
16
24
  end
17
25
 
26
+ # Taken from the normalised path, so the gem name and the suffix describe the file that
27
+ # will actually be built. Read from the request verbatim, `@rubygems/gem1/../index.js`
28
+ # named gem1 and a suffix the readability probe then cleaned to `index.js` - approving
29
+ # `<gem1>/index.js` while the builder resolved `../index.js` and served the file BESIDE
30
+ # the gem root. Normalised, it names a gem called `index.js`, which is not bundled, so
31
+ # the request passes through.
18
32
  def gem_request_path
19
- @gem_request_path ||= @request.path.delete_prefix('/node_modules/@rubygems/')
33
+ @gem_request_path ||= normalised_path.delete_prefix('/node_modules/@rubygems/')
20
34
  end
21
35
  end
22
36
  end
@@ -13,15 +13,18 @@ module Proscenium
13
13
 
14
14
  return @app.call(env) unless pathname.fnmatch?(VENDOR_PATH_GLOB, File::FNM_EXTGLOB)
15
15
 
16
- request.path_info = request.path.delete_prefix('/vendor')
17
-
16
+ # The prefix strip is an argument to the file lookup, not a rewrite of the request. It used
17
+ # to assign `request.path_info`, which writes `env['PATH_INFO']` - the same hash handed to
18
+ # `@app.call` on a miss - so `/vendor/lib/foo.js` with nothing under `vendor/` continued
19
+ # down the stack as `/lib/foo.js`, matched `APP_PATH_GLOB`, and was built and served from
20
+ # the app root under the URL the client had actually asked for.
18
21
  ActionDispatch::FileHandler.new(
19
22
  Rails.root.join('vendor').to_s,
20
23
  headers: {
21
24
  'X-Proscenium-Middleware' => 'vendor',
22
25
  'Cache-Control' => "public, max-age=#{100.years}, immutable"
23
26
  }
24
- ).attempt(request.env) || @app.call(env)
27
+ ).attempt(env.merge('PATH_INFO' => request.path.delete_prefix('/vendor'))) || @app.call(env)
25
28
  end
26
29
  end
27
30
  end
@@ -13,6 +13,22 @@ module Proscenium
13
13
  autoload :Chunks
14
14
  autoload :SilenceRequest
15
15
 
16
+ # The given request path, percent-decoded and with `.` and `..` segments resolved. Nil for a
17
+ # path Rack rejects outright, such as one containing a null byte.
18
+ #
19
+ # Every decision and every derived path in this subsystem is taken from this one form. The
20
+ # alternative was three derivations from the raw path that disagreed: `find_type` matched the
21
+ # allowed-directory glob against the request verbatim, `Base#file_readable?` normalised before
22
+ # probing the disk, and `Base#path_to_build` kept the request verbatim again - so a request
23
+ # could be routed by a directory it does not resolve to, approved against one file, and built
24
+ # as a third.
25
+ def self.normalise_path(path)
26
+ unescaped = Rack::Utils.unescape_path(path.chomp('/').delete_prefix('/'))
27
+ return unless Rack::Utils.valid_path?(unescaped)
28
+
29
+ "/#{Rack::Utils.clean_path_info(unescaped)}"
30
+ end
31
+
16
32
  def initialize(app)
17
33
  @app = app
18
34
  end
@@ -34,7 +50,12 @@ module Proscenium
34
50
  end
35
51
 
36
52
  def find_type(request)
37
- pathname = Pathname.new(request.path)
53
+ # Matched against the normalised path, not the request, or the allowed-directory list is
54
+ # bypassable: `/lib/../public/x.js` matches `APP_PATH_GLOB` as written and resolves to
55
+ # `public/`, which is not an allowed directory - and it was built and served from there.
56
+ return unless (path = Middleware.normalise_path(request.path))
57
+
58
+ pathname = Pathname.new(path)
38
59
 
39
60
  if pathname.fnmatch?(GEMS_PATH_GLOB, File::FNM_EXTGLOB)
40
61
  RubyGems
@@ -58,14 +58,18 @@ module Proscenium
58
58
  template.respond_to?(:type) && template.type == :html &&
59
59
  view.controller.respond_to?(:sideload_assets_options)
60
60
  options = view.controller.sideload_assets_options
61
- sideload_template_assets layout, options if layout
62
- sideload_template_assets template, options
61
+ sideload_template_assets layout, view.controller, options if layout
62
+ sideload_template_assets template, view.controller, options
63
63
  end
64
64
 
65
65
  result
66
66
  end
67
67
 
68
- def sideload_template_assets(tpl, options)
68
+ # `controller` is passed in rather than read off `self`: unlike TemplateRenderer, an
69
+ # ActionView::PartialRenderer has no `controller` - so a Proc option reaching a partial
70
+ # raised NameError until it was threaded through here, as its TemplateRenderer twin
71
+ # already did.
72
+ def sideload_template_assets(tpl, controller, options)
69
73
  return unless (tpl_path = Pathname.new(tpl.identifier)).file?
70
74
 
71
75
  options = {} if options.nil?
@@ -282,7 +282,7 @@ module Proscenium
282
282
  # because two of the places it ends up do not anchor it themselves. `File.join(Rails.root,
283
283
  # spec)` keeps a `..` verbatim, and `build_entry` hands the path to esbuild, which resolves
284
284
  # it relative to the app root and will happily climb out; the `serve` path is the only one
285
- # that gets `Rack::Utils.clean_path_info` for free, via Middleware::Base. A traversal 404s
285
+ # that gets normalised for free, by `Middleware.normalise_path`. A traversal 404s
286
286
  # there and then lands in `build_entry`, so without this guard `/lib/../../../secrets.js`
287
287
  # reads and returns any file esbuild can load.
288
288
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium
4
- VERSION = '0.25.1'
4
+ VERSION = '0.25.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proscenium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.1
4
+ version: 0.25.2
5
5
  platform: arm64-darwin
6
6
  authors:
7
7
  - Joel Moss