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.

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

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

#manifestObject



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

#showObject



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