proscenium 0.25.1 → 0.25.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 +4 -4
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/middleware/base.rb +18 -3
- data/lib/proscenium/middleware/chunks.rb +18 -4
- data/lib/proscenium/middleware/ruby_gems.rb +15 -1
- data/lib/proscenium/middleware/vendor.rb +6 -3
- data/lib/proscenium/middleware.rb +22 -1
- data/lib/proscenium/monkey.rb +7 -3
- data/lib/proscenium/runtime/server.rb +1 -1
- data/lib/proscenium/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a41356f4c86df48cf657fe9c7a030aad7fbd5b4e6521d58afd73d835097648c7
|
|
4
|
+
data.tar.gz: 0b33accaba8356175250da5c57c5f12c939c2f9646b8bf020dab984bb2ab8a3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fbe6585929d0fc321c2452b2743df39bd5bea72bb6248d1d973d5ca65326f3b7eb66bf0939d59481948852ab01b6d1377520392d8535b4d6467e5cd1cff0626
|
|
7
|
+
data.tar.gz: 31af1466da5b9a85527d062e73941b76a9e52d013e88319b45410958eecd1328f170f05ecb52e4039f6cd38c6937ec1653d7e5c451d574434058805cddb758b0
|
|
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 ||=
|
|
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 ||=
|
|
40
|
+
@path_to_build ||= normalised_path[1..]
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
def sourcemap?
|
|
32
|
-
|
|
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
|
-
|
|
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' => "#{
|
|
33
|
+
'SourceMap' => "#{path}.map",
|
|
20
34
|
'Cache-Control' => "public, max-age=#{100.years}, immutable",
|
|
21
|
-
'ETag' =>
|
|
35
|
+
'ETag' => etag
|
|
22
36
|
}
|
|
23
|
-
).attempt(
|
|
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 ||=
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
data/lib/proscenium/monkey.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
#
|
data/lib/proscenium/version.rb
CHANGED