Class: ReactOnRailsPro::RollingDeploy::BundlesController

Inherits:
ActionController::Base
  • Object
show all
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

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

#manifestObject



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

#showObject



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