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.
Auto-mounted by the engine when ‘config.rolling_deploy_adapter` is the Http adapter (or a subclass). Users who need a custom mount path or want to layer their own auth middleware can mount manually:
# config/routes.rb
ReactOnRailsPro::RollingDeploy::BundlesController.draw_routes(
self,
path: "/internal/rolling-deploy"
)
Callers that need to mount the controller more than once (for example, the 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 users who want to mount manually under a custom path.
Instance Method Summary collapse
Class Method Details
.draw_routes(mapper, path:, as_prefix: DEFAULT_ROUTE_PREFIX) ⇒ Object
Helper for users who want to mount manually under a custom path. The auto-mount path uses these same route definitions via the engine initializer (see ReactOnRailsPro::Engine).
‘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.
68 69 70 71 72 73 74 75 76 |
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 68 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
98 99 100 101 102 103 104 105 106 |
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 98 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
108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 108 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 |