react_on_rails_pro 16.7.0.rc.2 → 16.7.0.rc.3
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/.gitignore +0 -1
- data/.rubocop.yml +12 -0
- data/CLAUDE.md +8 -7
- data/Gemfile.development_dependencies +2 -1
- data/Gemfile.lock +31 -10
- data/README.md +2 -3
- data/app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb +298 -0
- data/lib/react_on_rails_pro/configuration.rb +116 -14
- data/lib/react_on_rails_pro/renderer_http_client.rb +415 -0
- data/lib/react_on_rails_pro/request.rb +92 -177
- data/lib/react_on_rails_pro/rolling_deploy/safe_hash_pattern.rb +13 -0
- data/lib/react_on_rails_pro/rolling_deploy/tarball.rb +235 -0
- data/lib/react_on_rails_pro/rolling_deploy_adapters/http.rb +264 -0
- data/lib/react_on_rails_pro/rolling_deploy_cache_stager.rb +14 -6
- data/lib/react_on_rails_pro/stream_request.rb +121 -48
- data/lib/react_on_rails_pro/version.rb +1 -1
- data/lib/react_on_rails_pro.rb +2 -3
- data/rakelib/rbs.rake +4 -5
- data/react_on_rails_pro.gemspec +6 -7
- data/scripts/load/README.md +110 -0
- data/scripts/load/lib/config.rb +164 -0
- data/scripts/load/lib/harness.rb +177 -0
- data/scripts/load/lib/memory_sampler.rb +115 -0
- data/scripts/load/lib/metrics.rb +61 -0
- data/scripts/load/lib/reporters/csv_reporter.rb +48 -0
- data/scripts/load/lib/reporters/json_reporter.rb +17 -0
- data/scripts/load/lib/reporters/terminal_reporter.rb +59 -0
- data/scripts/load/lib/request_result.rb +16 -0
- data/scripts/load/lib/runner.rb +345 -0
- data/scripts/load/lib/scenario_registry.rb +11 -0
- data/scripts/load/lib/scenarios/base.rb +168 -0
- data/scripts/load/lib/scenarios/standard_render.rb +45 -0
- data/scripts/load/lib/scenarios/streaming_render.rb +53 -0
- data/scripts/load/renderer_harness.rb +25 -0
- data/sig/react_on_rails_pro/renderer_http_client.rbs +76 -0
- metadata +42 -33
- data/.prettierignore +0 -14
- data/.prettierrc +0 -19
- data/eslint.config.mjs +0 -239
- data/lib/react_on_rails_pro/httpx_stream_bidi_patch.rb +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a4266ac7cb6a9cdbb62755a17d95282222b6909e64d41ff7b7a7bcf897c2f0a
|
|
4
|
+
data.tar.gz: 8372bde1ffd861b8f8026e6fa28f4a9f65130ec6c70a42cdfa09a33f6a449c5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40ebe444b6abb44184984519f44a61df910f69e9e0fa73841ae552de49ba799ed872e8fc3bd8d8b0af8ca27c5c88946fa8abc28278fe42ac0603ac3d5de8cfde
|
|
7
|
+
data.tar.gz: fd3568d5daff73971afddf96575237d2e07a738c972c1f0067e6ac8dc32ed4923c3dbdcf41fbe8dc06cb9d446c48b8544fb30e7ede7e8a57f9606fa2ec25dcc7
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -17,6 +17,18 @@ AllCops:
|
|
|
17
17
|
- 'spec/dummy/bin/**/*'
|
|
18
18
|
- 'bin/**/*'
|
|
19
19
|
|
|
20
|
+
Naming/VariableNumber:
|
|
21
|
+
AllowedIdentifiers:
|
|
22
|
+
- p99_9
|
|
23
|
+
|
|
24
|
+
RSpec/FilePath:
|
|
25
|
+
Exclude:
|
|
26
|
+
- 'spec/load/**/*_spec.rb'
|
|
27
|
+
|
|
28
|
+
RSpec/SpecFilePathFormat:
|
|
29
|
+
Exclude:
|
|
30
|
+
- 'spec/load/**/*_spec.rb'
|
|
31
|
+
|
|
20
32
|
RSpec/BeforeAfterAll:
|
|
21
33
|
Exclude:
|
|
22
34
|
- 'spec/react_on_rails/generators/dev_tests_generator_spec.rb'
|
data/CLAUDE.md
CHANGED
|
@@ -16,11 +16,14 @@ Pro-specific guidance. See root CLAUDE.md for general project rules.
|
|
|
16
16
|
|
|
17
17
|
### Linting
|
|
18
18
|
|
|
19
|
-
Pro
|
|
19
|
+
Pro uses the root ESLint and Prettier configs. Run JS/TS lint and formatting from the repo root.
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
21
|
+
- JS/TS lint: `pnpm run eslint --report-unused-disable-directives`
|
|
22
|
+
- Prettier check: `pnpm start format.listDifferent`
|
|
23
|
+
- Pro Ruby lint: `cd react_on_rails_pro && bundle exec rubocop --ignore-parent-exclusion`
|
|
24
|
+
- Pro RBS validation: `cd react_on_rails_pro && bundle exec rake rbs:validate`
|
|
25
|
+
- Pro TypeScript check: `pnpm run nps check-typescript`
|
|
26
|
+
- CI runs unified ESLint/Prettier in `lint-js-and-ruby.yml`; Pro Ruby/RBS/TypeScript checks run in the `pro-lint` job in `pro-test-package-and-gem.yml`
|
|
24
27
|
|
|
25
28
|
### Building
|
|
26
29
|
|
|
@@ -91,8 +94,7 @@ Order matters. If the base package isn't published first, the chain breaks.
|
|
|
91
94
|
GitHub Actions workflows for Pro (at repo root `.github/workflows/`):
|
|
92
95
|
|
|
93
96
|
- `pro-integration-tests.yml` — Pro dummy app integration tests
|
|
94
|
-
- `pro-
|
|
95
|
-
- `pro-test-package-and-gem.yml` — Pro gem + JS package tests
|
|
97
|
+
- `pro-test-package-and-gem.yml` — Pro gem + JS package tests, plus Pro Ruby/RBS/TypeScript linting
|
|
96
98
|
|
|
97
99
|
## Key Differences from Open-Source
|
|
98
100
|
|
|
@@ -102,5 +104,4 @@ GitHub Actions workflows for Pro (at repo root `.github/workflows/`):
|
|
|
102
104
|
| SSR | ExecJS or basic Node | Dedicated node renderer process |
|
|
103
105
|
| Server bundles | Public | Private (`ssr-generated/`, `enforce_private_server_bundles = true`) |
|
|
104
106
|
| Transpiler | SWC | Babel (needed for advanced transforms) |
|
|
105
|
-
| Lint/format config | Root configs | Own ESLint + Prettier configs |
|
|
106
107
|
| Test commands | `rake run_rspec:dummy` | Separate Pro commands (see above) |
|
|
@@ -55,9 +55,10 @@ group :development, :test do
|
|
|
55
55
|
gem "scss_lint", require: false
|
|
56
56
|
gem "fakefs", require: "fakefs/safe"
|
|
57
57
|
|
|
58
|
-
# Ruby 3.
|
|
58
|
+
# Ruby 3.4+ removed these from the default gem set; they must now be declared explicitly
|
|
59
59
|
# to avoid `cannot load such file` errors from gems that lazy-require them (e.g. jbuilder).
|
|
60
60
|
gem "benchmark", require: false
|
|
61
|
+
gem "csv", require: false
|
|
61
62
|
gem "logger", require: false
|
|
62
63
|
gem "ostruct", require: false
|
|
63
64
|
# base64 was promoted from a default gem to a bundled gem in Ruby 3.4, so Bundler-managed
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (16.7.0.rc.
|
|
12
|
+
react_on_rails (16.7.0.rc.3)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,16 +20,15 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (16.7.0.rc.
|
|
23
|
+
react_on_rails_pro (16.7.0.rc.3)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.29)
|
|
26
|
-
|
|
26
|
+
async-http (~> 0.95)
|
|
27
27
|
execjs (~> 2.9)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
jwt (>= 2.7)
|
|
28
|
+
io-endpoint (~> 0.17.0)
|
|
29
|
+
jwt (>= 2.5, < 4)
|
|
31
30
|
rainbow
|
|
32
|
-
react_on_rails (= 16.7.0.rc.
|
|
31
|
+
react_on_rails (= 16.7.0.rc.3)
|
|
33
32
|
|
|
34
33
|
GEM
|
|
35
34
|
remote: https://rubygems.org/
|
|
@@ -117,6 +116,19 @@ GEM
|
|
|
117
116
|
io-event (~> 1.11)
|
|
118
117
|
metrics (~> 0.12)
|
|
119
118
|
traces (~> 0.18)
|
|
119
|
+
async-http (0.95.1)
|
|
120
|
+
async (>= 2.10.2)
|
|
121
|
+
async-pool (~> 0.11)
|
|
122
|
+
io-endpoint (~> 0.14)
|
|
123
|
+
io-stream (~> 0.6)
|
|
124
|
+
metrics (~> 0.12)
|
|
125
|
+
protocol-http (~> 0.62)
|
|
126
|
+
protocol-http1 (~> 0.39)
|
|
127
|
+
protocol-http2 (~> 0.26)
|
|
128
|
+
protocol-url (~> 0.2)
|
|
129
|
+
traces (~> 0.10)
|
|
130
|
+
async-pool (0.11.2)
|
|
131
|
+
async (>= 2.0)
|
|
120
132
|
base64 (0.3.0)
|
|
121
133
|
benchmark (0.5.0)
|
|
122
134
|
bigdecimal (4.0.1)
|
|
@@ -153,6 +165,7 @@ GEM
|
|
|
153
165
|
bigdecimal
|
|
154
166
|
rexml
|
|
155
167
|
crass (1.0.6)
|
|
168
|
+
csv (3.3.5)
|
|
156
169
|
date (3.5.1)
|
|
157
170
|
diff-lcs (1.5.1)
|
|
158
171
|
docile (1.4.0)
|
|
@@ -181,13 +194,12 @@ GEM
|
|
|
181
194
|
graphiql-rails (1.10.0)
|
|
182
195
|
railties
|
|
183
196
|
hashdiff (1.1.0)
|
|
184
|
-
http-2 (1.1.1)
|
|
185
|
-
httpx (1.7.0)
|
|
186
|
-
http-2 (>= 1.0.0)
|
|
187
197
|
i18n (1.14.8)
|
|
188
198
|
concurrent-ruby (~> 1.0)
|
|
189
199
|
io-console (0.8.2)
|
|
200
|
+
io-endpoint (0.17.2)
|
|
190
201
|
io-event (1.14.2)
|
|
202
|
+
io-stream (0.13.0)
|
|
191
203
|
irb (1.17.0)
|
|
192
204
|
pp (>= 0.6.0)
|
|
193
205
|
prism (>= 1.3.0)
|
|
@@ -258,6 +270,14 @@ GEM
|
|
|
258
270
|
prettyprint
|
|
259
271
|
prettyprint (0.2.0)
|
|
260
272
|
prism (1.9.0)
|
|
273
|
+
protocol-hpack (1.5.1)
|
|
274
|
+
protocol-http (0.62.2)
|
|
275
|
+
protocol-http1 (0.39.0)
|
|
276
|
+
protocol-http (~> 0.62)
|
|
277
|
+
protocol-http2 (0.26.0)
|
|
278
|
+
protocol-hpack (~> 1.4)
|
|
279
|
+
protocol-http (~> 0.62)
|
|
280
|
+
protocol-url (0.4.0)
|
|
261
281
|
pry (0.14.2)
|
|
262
282
|
coderay (~> 1.1)
|
|
263
283
|
method_source (~> 1.0)
|
|
@@ -483,6 +503,7 @@ DEPENDENCIES
|
|
|
483
503
|
capybara (>= 3.38.0)
|
|
484
504
|
capybara-screenshot
|
|
485
505
|
commonmarker
|
|
506
|
+
csv
|
|
486
507
|
equivalent-xml
|
|
487
508
|
fakefs
|
|
488
509
|
faker
|
data/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[](./LICENSE)
|
|
4
4
|
[](https://github.com/shakacode/react_on_rails/actions/workflows/pro-integration-tests.yml)
|
|
5
|
-
[](https://github.com/shakacode/react_on_rails/actions/workflows/pro-lint.yml)
|
|
6
5
|
[](https://github.com/shakacode/react_on_rails/actions/workflows/pro-test-package-and-gem.yml)
|
|
7
6
|
|
|
8
7
|
**Performance enhancements and advanced features for [React on Rails](https://github.com/shakacode/react_on_rails).**
|
|
@@ -257,7 +256,7 @@ end
|
|
|
257
256
|
|
|
258
257
|
### Prerequisites
|
|
259
258
|
|
|
260
|
-
- **Ruby**: >= 3.
|
|
259
|
+
- **Ruby**: >= 3.3
|
|
261
260
|
- **Rails**: >= 6.0 (recommended: 7.0+)
|
|
262
261
|
- **React on Rails**: >= 11.0.7 (recommended: latest)
|
|
263
262
|
- **Node.js**: >= 18 (for Node Renderer)
|
|
@@ -267,7 +266,7 @@ end
|
|
|
267
266
|
|
|
268
267
|
| React on Rails Pro | React on Rails | Rails | Ruby | React |
|
|
269
268
|
| ------------------ | -------------- | ------ | ------ | ------- |
|
|
270
|
-
| 16.x | >= 16.0 | >= 7.0 | >= 3.
|
|
269
|
+
| 16.x | >= 16.0 | >= 7.0 | >= 3.3 | >= 18 |
|
|
271
270
|
| 3.x | >= 13.0 | >= 6.0 | >= 3.0 | >= 16.8 |
|
|
272
271
|
|
|
273
272
|
> **Note:** Pro version numbers were aligned with the core gem starting at 16.2.0. Pro 16.x is the direct successor to Pro 3.x/4.x.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/security_utils"
|
|
4
|
+
|
|
5
|
+
require "react_on_rails_pro/rolling_deploy/safe_hash_pattern"
|
|
6
|
+
require "react_on_rails_pro/rolling_deploy/tarball"
|
|
7
|
+
|
|
8
|
+
module ReactOnRailsPro
|
|
9
|
+
module RollingDeploy
|
|
10
|
+
# Server side of the built-in HTTP rolling-deploy adapter. Exposes the
|
|
11
|
+
# current deployment's bundle hashes (`GET /manifest`) and serves a
|
|
12
|
+
# gzipped tarball per hash (`GET /bundles/:hash`). The
|
|
13
|
+
# ReactOnRailsPro::RollingDeployAdapters::Http adapter on the next
|
|
14
|
+
# deploy's build CI consumes both endpoints.
|
|
15
|
+
#
|
|
16
|
+
# Auto-mounted by the engine when `config.rolling_deploy_adapter` is the
|
|
17
|
+
# Http adapter (or a subclass). Users who need a custom mount path or want
|
|
18
|
+
# to layer their own auth middleware can mount manually:
|
|
19
|
+
#
|
|
20
|
+
# # config/routes.rb
|
|
21
|
+
# ReactOnRailsPro::RollingDeploy::BundlesController.draw_routes(
|
|
22
|
+
# self,
|
|
23
|
+
# path: "/internal/rolling-deploy"
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# Callers that need to mount the controller more than once (for example,
|
|
27
|
+
# the engine auto-mount plus a user-controlled secondary path) must pass
|
|
28
|
+
# a distinct `as_prefix:` per call so Rails' named-route registry
|
|
29
|
+
# doesn't raise `ArgumentError: Invalid route name, already in use`.
|
|
30
|
+
#
|
|
31
|
+
# Security:
|
|
32
|
+
# * Bearer-token auth via `Authorization: Bearer <token>`, constant-time
|
|
33
|
+
# compare (ActiveSupport::SecurityUtils.secure_compare). 401 returned
|
|
34
|
+
# uniformly for missing / malformed / wrong token so callers can't
|
|
35
|
+
# distinguish failure modes.
|
|
36
|
+
# * `:hash` URL param is matched against an allowlist of the current
|
|
37
|
+
# deployment's actual bundle hashes — anything else returns 404. The
|
|
38
|
+
# hash never touches the filesystem layer.
|
|
39
|
+
# * Responses include `Cache-Control: no-store` so a misconfigured
|
|
40
|
+
# intermediary doesn't cache the bundle behind the auth check.
|
|
41
|
+
# * Uses `protect_from_forgery with: :exception` (the Rails default)
|
|
42
|
+
# rather than `:null_session`. CodeQL flags `:null_session` as a
|
|
43
|
+
# weakened CSRF strategy, and an `ActionController::API` controller
|
|
44
|
+
# with no `protect_from_forgery` at all as missing protection — both
|
|
45
|
+
# are false positives here (this is a GET-only bearer-token API, so
|
|
46
|
+
# CSRF never actually fires regardless of strategy), but `:exception`
|
|
47
|
+
# on `ActionController::Base` is the form CodeQL accepts. The check
|
|
48
|
+
# is a no-op at runtime because Rails only invokes
|
|
49
|
+
# `verify_authenticity_token` on non-GET requests.
|
|
50
|
+
class BundlesController < ActionController::Base
|
|
51
|
+
protect_from_forgery with: :exception
|
|
52
|
+
|
|
53
|
+
before_action :authenticate_rolling_deploy_request
|
|
54
|
+
before_action :set_no_store_headers
|
|
55
|
+
|
|
56
|
+
DEFAULT_ROUTE_PREFIX = "react_on_rails_pro_rolling_deploy"
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
# Helper for users who want to mount manually under a custom path. The
|
|
60
|
+
# auto-mount path uses these same route definitions via the engine
|
|
61
|
+
# initializer (see ReactOnRailsPro::Engine).
|
|
62
|
+
#
|
|
63
|
+
# `as_prefix:` controls the generated named-route helpers
|
|
64
|
+
# (`<prefix>_manifest`, `<prefix>_bundle`). Callers that mount the
|
|
65
|
+
# controller more than once (e.g. auto-mount plus a secondary user
|
|
66
|
+
# mount) must pass distinct prefixes so the Rails route registry
|
|
67
|
+
# doesn't raise on duplicate names.
|
|
68
|
+
def draw_routes(mapper, path:, as_prefix: DEFAULT_ROUTE_PREFIX)
|
|
69
|
+
mapper.get("#{path}/manifest",
|
|
70
|
+
to: "react_on_rails_pro/rolling_deploy/bundles#manifest",
|
|
71
|
+
as: :"#{as_prefix}_manifest")
|
|
72
|
+
mapper.get("#{path}/bundles/:hash",
|
|
73
|
+
to: "react_on_rails_pro/rolling_deploy/bundles#show",
|
|
74
|
+
constraints: { hash: SAFE_HASH_PATTERN },
|
|
75
|
+
as: :"#{as_prefix}_bundle")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Defense-in-depth: even if the route constraint somehow let a
|
|
80
|
+
# path-traversal value through, the controller still rejects it
|
|
81
|
+
# before any disk lookup because the hash must be in the
|
|
82
|
+
# (regex-validated) current-hash set.
|
|
83
|
+
SAFE_HASH_PATTERN = ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN
|
|
84
|
+
|
|
85
|
+
# Tarball entry name reserved for the server bundle. Companion assets
|
|
86
|
+
# whose basename collides with this are skipped to keep the receiver
|
|
87
|
+
# from extracting the wrong bytes into the bundle slot.
|
|
88
|
+
#
|
|
89
|
+
# Wire-format constant: must stay in sync with
|
|
90
|
+
# `ReactOnRailsPro::RollingDeployAdapters::Http::BUNDLE_ENTRY_NAME`. If
|
|
91
|
+
# one side bumps the entry name (e.g. a protocol version change) the
|
|
92
|
+
# other must follow or the client extraction will fail to find the
|
|
93
|
+
# bundle file.
|
|
94
|
+
BUNDLE_ENTRY_NAME = "bundle.js"
|
|
95
|
+
|
|
96
|
+
PROTOCOL_VERSION = 1
|
|
97
|
+
|
|
98
|
+
def manifest
|
|
99
|
+
sources = safe_current_bundle_sources
|
|
100
|
+
render json: {
|
|
101
|
+
hashes: sources.map { |_, hash| hash },
|
|
102
|
+
rsc_enabled: ReactOnRailsPro.configuration.enable_rsc_support,
|
|
103
|
+
generated_at: Time.now.utc.iso8601,
|
|
104
|
+
protocol_version: PROTOCOL_VERSION
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def show
|
|
109
|
+
hash = params[:hash].to_s
|
|
110
|
+
# Defense in depth — route constraint should already enforce this,
|
|
111
|
+
# but we also reject any value that slipped past it before any
|
|
112
|
+
# filesystem operation looks at it.
|
|
113
|
+
return head(:not_found) unless SAFE_HASH_PATTERN.match?(hash)
|
|
114
|
+
|
|
115
|
+
sources = safe_current_bundle_sources
|
|
116
|
+
match = sources.find { |_, h| h == hash }
|
|
117
|
+
return head(:not_found) unless match
|
|
118
|
+
|
|
119
|
+
bundle_path, _matched_hash = match
|
|
120
|
+
serve_bundle_tarball(bundle_path)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def authenticate_rolling_deploy_request
|
|
126
|
+
configured = ReactOnRailsPro.configuration.rolling_deploy_token.to_s
|
|
127
|
+
# If the controller is reached without a configured token, refuse
|
|
128
|
+
# unconditionally. This is defense-in-depth — the engine should not
|
|
129
|
+
# mount the controller in that state — but it makes the no-token
|
|
130
|
+
# mode a hard fail rather than an open endpoint.
|
|
131
|
+
return head(:unauthorized) if configured.empty?
|
|
132
|
+
|
|
133
|
+
provided = extract_bearer_token(request.headers["Authorization"])
|
|
134
|
+
return head(:unauthorized) if provided.empty?
|
|
135
|
+
|
|
136
|
+
# `secure_compare` raises ArgumentError when the two strings differ
|
|
137
|
+
# in length, so we gate on bytesize first. This does leak whether
|
|
138
|
+
# the provided token has the correct byte length, but the
|
|
139
|
+
# configuration validator enforces a minimum of 32 bytes and
|
|
140
|
+
# operators are advised to use `SecureRandom.hex(32)` (64 bytes),
|
|
141
|
+
# so the only information exposed is the token's exact byte length
|
|
142
|
+
# — not any of its bits. The body of the comparison is constant-time
|
|
143
|
+
# via `secure_compare` regardless of which byte differs.
|
|
144
|
+
match = provided.bytesize == configured.bytesize &&
|
|
145
|
+
ActiveSupport::SecurityUtils.secure_compare(provided, configured)
|
|
146
|
+
head(:unauthorized) unless match
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def extract_bearer_token(header)
|
|
150
|
+
return "" if header.blank?
|
|
151
|
+
return "" unless header.start_with?("Bearer ", "bearer ")
|
|
152
|
+
|
|
153
|
+
# Take the token bytes verbatim. We deliberately do not `.strip` here
|
|
154
|
+
# because the configured side is compared without stripping — if an
|
|
155
|
+
# operator misconfigures a token with trailing whitespace, an
|
|
156
|
+
# asymmetric strip would silently authenticate a shorter token.
|
|
157
|
+
header[7..].to_s
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def set_no_store_headers
|
|
161
|
+
response.headers["Cache-Control"] = "no-store"
|
|
162
|
+
response.headers["Pragma"] = "no-cache"
|
|
163
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Wraps bundle_sources to absorb the "bundle file not present yet" case
|
|
167
|
+
# so the manifest endpoint can still 200 with an empty hashes list during
|
|
168
|
+
# the brief window after Rails boots but before assets:precompile has
|
|
169
|
+
# produced the bundle on this dyno. Returns `[]` rather than raising so
|
|
170
|
+
# the build-CI side sees "this server has nothing to seed" instead of a
|
|
171
|
+
# 500 that would otherwise show up as a noisy deploy alert.
|
|
172
|
+
def safe_current_bundle_sources
|
|
173
|
+
unless ReactOnRailsPro.configuration.node_renderer?
|
|
174
|
+
Rails.logger.warn(
|
|
175
|
+
"[ReactOnRailsPro::RollingDeploy::BundlesController] " \
|
|
176
|
+
"node_renderer? is false — returning an empty manifest. " \
|
|
177
|
+
"Verify that the Pro configuration enables the node renderer; " \
|
|
178
|
+
"the HTTP rolling-deploy adapter only serves bundles when it does."
|
|
179
|
+
)
|
|
180
|
+
return []
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
|
|
184
|
+
ReactOnRailsPro::RendererCacheHelpers.bundle_sources(pool, "serving rolling-deploy tarball")
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
Rails.logger.warn(
|
|
187
|
+
"[ReactOnRailsPro::RollingDeploy::BundlesController] " \
|
|
188
|
+
"bundle source discovery failed: #{e.class}: #{e.message}. " \
|
|
189
|
+
"Returning empty manifest — verify bundles have been precompiled."
|
|
190
|
+
)
|
|
191
|
+
[]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def serve_bundle_tarball(bundle_path)
|
|
195
|
+
entries = tarball_entries(bundle_path)
|
|
196
|
+
|
|
197
|
+
ReactOnRailsPro::RollingDeploy::Tarball.compose_to_tempfile(entries) do |io|
|
|
198
|
+
# We've already buffered the tarball to a Tempfile inside
|
|
199
|
+
# compose_to_tempfile; send_data reads the contents once. For very
|
|
200
|
+
# large bundles a streaming send via ActionController::Live would
|
|
201
|
+
# save memory; that's deferred to a follow-up PR — the current
|
|
202
|
+
# default ceiling (200 MB) fits comfortably in memory on every
|
|
203
|
+
# Rails app instance we'd expect to deploy.
|
|
204
|
+
send_data io.read,
|
|
205
|
+
type: "application/gzip",
|
|
206
|
+
disposition: "inline",
|
|
207
|
+
filename: "#{params[:hash]}.tar.gz"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Pairs the bundle file (renamed to `bundle.js` on the wire) with the
|
|
212
|
+
# current build's companion assets. Each tarball carries the full
|
|
213
|
+
# companion set so the receiver can stage a complete cache entry without
|
|
214
|
+
# a second round-trip — matching the rolling_deploy_adapter contract
|
|
215
|
+
# that `fetch(hash)` returns bundle + assets together.
|
|
216
|
+
#
|
|
217
|
+
# Companions are skipped (with a warning) when they would shadow the
|
|
218
|
+
# bundle entry, collide with another companion's basename, or carry a
|
|
219
|
+
# name that the tarball helper would reject during compose. This
|
|
220
|
+
# matches the publish-side behavior in AssetsPrecompile where missing
|
|
221
|
+
# or unsafe assets degrade rather than fail the build.
|
|
222
|
+
def tarball_entries(bundle_path)
|
|
223
|
+
entries = { BUNDLE_ENTRY_NAME => bundle_path }
|
|
224
|
+
companion_assets.each do |asset_path|
|
|
225
|
+
name = File.basename(asset_path)
|
|
226
|
+
next if skip_companion?(name, asset_path, entries)
|
|
227
|
+
|
|
228
|
+
entries[name] = asset_path
|
|
229
|
+
end
|
|
230
|
+
entries
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def skip_companion?(name, asset_path, entries)
|
|
234
|
+
if name == BUNDLE_ENTRY_NAME
|
|
235
|
+
warn_companion_skipped(
|
|
236
|
+
"companion #{asset_path.inspect} basename collides with bundle entry #{BUNDLE_ENTRY_NAME.inspect}"
|
|
237
|
+
)
|
|
238
|
+
return true
|
|
239
|
+
end
|
|
240
|
+
unless ReactOnRailsPro::RollingDeploy::Tarball::ENTRY_NAME_PATTERN.match?(name)
|
|
241
|
+
warn_companion_skipped(
|
|
242
|
+
"companion #{asset_path.inspect} basename #{name.inspect} is not a safe tarball entry name"
|
|
243
|
+
)
|
|
244
|
+
return true
|
|
245
|
+
end
|
|
246
|
+
if entries.key?(name)
|
|
247
|
+
warn_companion_skipped(
|
|
248
|
+
"duplicate companion basename #{name.inspect}; " \
|
|
249
|
+
"keeping #{entries[name].inspect}, dropping #{asset_path.inspect}"
|
|
250
|
+
)
|
|
251
|
+
return true
|
|
252
|
+
end
|
|
253
|
+
false
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def warn_companion_skipped(message)
|
|
257
|
+
Rails.logger.warn("[ReactOnRailsPro::RollingDeploy::BundlesController] #{message}.")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def companion_assets
|
|
261
|
+
rails_root = File.expand_path(Rails.root.to_s)
|
|
262
|
+
rails_root_realpath = File.realpath(rails_root)
|
|
263
|
+
|
|
264
|
+
# `collect_assets` returns the live build's loadable-stats + RSC
|
|
265
|
+
# manifests; missing assets are silently dropped to match the
|
|
266
|
+
# publish-side behavior in AssetsPrecompile.
|
|
267
|
+
ReactOnRailsPro::RendererCacheHelpers.collect_assets
|
|
268
|
+
.map(&:to_s)
|
|
269
|
+
.reject { |p| ReactOnRailsPro::RendererCacheHelpers.http_url?(p) }
|
|
270
|
+
.filter_map do |path|
|
|
271
|
+
safe_companion_asset_path(path, rails_root, rails_root_realpath)
|
|
272
|
+
end
|
|
273
|
+
rescue StandardError => e
|
|
274
|
+
Rails.logger.warn(
|
|
275
|
+
"[ReactOnRailsPro::RollingDeploy::BundlesController] " \
|
|
276
|
+
"companion asset discovery failed: #{e.class}: #{e.message}. " \
|
|
277
|
+
"Serving bundle without companion assets — RSC clients may fall back to runtime 410-retry."
|
|
278
|
+
)
|
|
279
|
+
[]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def safe_companion_asset_path(path, rails_root, rails_root_realpath)
|
|
283
|
+
expanded = File.expand_path(path, rails_root)
|
|
284
|
+
return nil unless path_within_root?(expanded, rails_root)
|
|
285
|
+
return nil unless File.file?(expanded)
|
|
286
|
+
|
|
287
|
+
realpath = File.realpath(expanded)
|
|
288
|
+
return nil unless path_within_root?(realpath, rails_root_realpath)
|
|
289
|
+
|
|
290
|
+
expanded
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def path_within_root?(path, root)
|
|
294
|
+
path == root || path.start_with?("#{root}#{File::SEPARATOR}")
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|