Class: ReactOnRailsPro::RollingDeploy::BundlesController
- Inherits:
-
ActionController::Base
- Object
- ActionController::Base
- ReactOnRailsPro::RollingDeploy::BundlesController
- Defined in:
- app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb
Overview
Server side of the built-in HTTP rolling-deploy adapter. Exposes the current deployment’s bundle hashes (‘GET /manifest`) and serves a gzipped tarball per hash (`GET /bundles/:hash`). The ReactOnRailsPro::RollingDeployAdapters::Http adapter on the next deploy’s build CI consumes both endpoints.
Mount this in your application’s routes with ‘draw_routes` so the Http adapter on the next deploy can reach it. (Engine auto-mount keyed on `config.rolling_deploy_adapter` is planned for a follow-up but is not wired yet, so an explicit mount is currently required.) You can also use a custom mount path or layer your own auth middleware:
# config/routes.rb
ReactOnRailsPro::RollingDeploy::BundlesController.draw_routes(
self,
path: "/internal/rolling-deploy"
)
Callers that mount the controller more than once (for example, a future engine auto-mount plus a user-controlled secondary path) must pass a distinct ‘as_prefix:` per call so Rails’ named-route registry doesn’t raise ‘ArgumentError: Invalid route name, already in use`.
Security:
* Bearer-token auth via `Authorization: Bearer <token>`, constant-time
compare (ActiveSupport::SecurityUtils.secure_compare). 401 returned
uniformly for missing / malformed / wrong token so callers can't
distinguish failure modes.
* `:hash` URL param is matched against an allowlist of the current
deployment's actual bundle hashes — anything else returns 404. The
hash never touches the filesystem layer.
* Responses include `Cache-Control: no-store` so a misconfigured
intermediary doesn't cache the bundle behind the auth check.
* Uses `protect_from_forgery with: :exception` (the Rails default)
rather than `:null_session`. CodeQL flags `:null_session` as a
weakened CSRF strategy, and an `ActionController::API` controller
with no `protect_from_forgery` at all as missing protection — both
are false positives here (this is a GET-only bearer-token API, so
CSRF never actually fires regardless of strategy), but `:exception`
on `ActionController::Base` is the form CodeQL accepts. The check
is a no-op at runtime because Rails only invokes
`verify_authenticity_token` on non-GET requests.
Constant Summary collapse
- DEFAULT_ROUTE_PREFIX =
"react_on_rails_pro_rolling_deploy"- SAFE_HASH_PATTERN =
Defense-in-depth: even if the route constraint somehow let a path-traversal value through, the controller still rejects it before any disk lookup because the hash must be in the (regex-validated) current-hash set.
ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN
- BUNDLE_ENTRY_NAME =
Tarball entry name reserved for the server bundle. Companion assets whose basename collides with this are skipped to keep the receiver from extracting the wrong bytes into the bundle slot.
Wire-format constant: must stay in sync with ‘ReactOnRailsPro::RollingDeployAdapters::Http::BUNDLE_ENTRY_NAME`. If one side bumps the entry name (e.g. a protocol version change) the other must follow or the client extraction will fail to find the bundle file.
"bundle.js"- PROTOCOL_VERSION =
1
Class Method Summary collapse
-
.draw_routes(mapper, path:, as_prefix: DEFAULT_ROUTE_PREFIX) ⇒ Object
Helper for mounting the controller in your application’s routes.
Instance Method Summary collapse
Class Method Details
.draw_routes(mapper, path:, as_prefix: DEFAULT_ROUTE_PREFIX) ⇒ Object
Helper for mounting the controller in your application’s routes. A planned engine auto-mount will reuse these same route definitions.
‘as_prefix:` controls the generated named-route helpers (`<prefix>_manifest`, `<prefix>_bundle`). Callers that mount the controller more than once (e.g. auto-mount plus a secondary user mount) must pass distinct prefixes so the Rails route registry doesn’t raise on duplicate names.
69 70 71 72 73 74 75 76 77 |
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 69 def draw_routes(mapper, path:, as_prefix: DEFAULT_ROUTE_PREFIX) mapper.get("#{path}/manifest", to: "react_on_rails_pro/rolling_deploy/bundles#manifest", as: :"#{as_prefix}_manifest") mapper.get("#{path}/bundles/:hash", to: "react_on_rails_pro/rolling_deploy/bundles#show", constraints: { hash: SAFE_HASH_PATTERN }, as: :"#{as_prefix}_bundle") end |
Instance Method Details
#manifest ⇒ Object
99 100 101 102 103 104 105 106 107 |
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 99 def manifest sources = safe_current_bundle_sources render json: { hashes: sources.map { |_, hash| hash }, rsc_enabled: ReactOnRailsPro.configuration.enable_rsc_support, generated_at: Time.now.utc.iso8601, protocol_version: PROTOCOL_VERSION } end |
#show ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 109 def show hash = params[:hash].to_s # Defense in depth — route constraint should already enforce this, # but we also reject any value that slipped past it before any # filesystem operation looks at it. return head(:not_found) unless SAFE_HASH_PATTERN.match?(hash) sources = safe_current_bundle_sources match = sources.find { |_, h| h == hash } return head(:not_found) unless match bundle_path, _matched_hash = match serve_bundle_tarball(bundle_path) end |