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.

When ‘config.rolling_deploy_adapter` is the built-in Http adapter, the Pro engine auto-mounts this controller at `config.rolling_deploy_mount_path` (default: `/react_on_rails_pro/rolling_deploy`). Set the mount path to nil or blank to opt out of the auto-mount. Use `draw_routes` only when you need a manual mount, such as a secondary path or app-specific routing wrapper:

# config/routes.rb
ReactOnRailsPro::RollingDeploy::BundlesController.draw_routes(
  self,
  path: "/internal/rolling-deploy"
)

The engine auto-mount uses an internal route-helper prefix so existing manual mounts that use the default prefix keep booting during upgrades. Multiple manual mounts still need distinct ‘as_prefix:` values 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 =
ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN
ROUTE_HASH_PATTERN =

Rails route requirements reject anchor characters, while the route matcher applies segment constraints to the full segment. Derived from SAFE_HASH_PATTERN by stripping the A/z anchors; the controller still performs the anchored defense-in-depth validation before any filesystem lookup.

Regexp.new(SAFE_HASH_PATTERN.source.delete_prefix("\\A").delete_suffix("\\z"))
BUNDLE_ENTRY_NAME =

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. 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 manual route mounts. The Pro engine uses these same route definitions for the default auto-mount when the built-in Http adapter is configured, with an internal ‘as_prefix:` to avoid collisions with existing manual mounts.

‘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.



80
81
82
83
84
85
86
87
88
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 80

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: ROUTE_HASH_PATTERN },
             as: :"#{as_prefix}_bundle")
end

Instance Method Details

#manifestObject



108
109
110
111
112
113
114
115
116
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 108

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



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb', line 118

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