Class: Shugoi::Rack::Middleware

Inherits:
Object
  • Object
show all
Defined in:
lib/shugoi/rack/middleware.rb

Overview

Middleware Rack principal — parité avec createShugoiMiddleware (module Node).

use Shugoi::Rack::Middleware, site_key: "sg_sk_live_…", secret: ""

Ou via la gem :

use Shugoi, site_key: ""

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Middleware

Returns a new instance of Middleware.



15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/shugoi/rack/middleware.rb', line 15

def initialize(app, options = {})
  @app = app
  @config = Config.new(options)
  @api_client = ApiClient.new(@config.base_url, debug: @config.debug)
  @guard_cache = GuardCache.new(@api_client)
  @config_cache = ConfigCache.new(@api_client)
  @token_signer = TokenSigner.new(@config.signing_secret)
  @html_store = HtmlStore.new(disk_path: options[:disk_path])
  @pow = Pow.new(@config.signing_secret, @config.pow_difficulty, @config.pow_ttl_ms)
  @skeleton = SkeletonGenerator.new(@config, @guard_cache, @config_cache, @token_signer)
  @render = RenderHandler.new(@config, @token_signer, @html_store, @config_cache)
  @core = Core.new(@config, @pow, @api_client)
end

Instance Method Details

#call(env) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/shugoi/rack/middleware.rb', line 29

def call(env)
  path = env["PATH_INFO"].to_s
  query = parse_query(env["QUERY_STRING"].to_s)

  # Render endpoint.
  if path.end_with?("/__shugoi/render")
    return handle_render(env, query)
  end

  # CSP.
  headers = {}
  headers["content-security-policy"] = csp_header if @config.csp_enabled

  ctx = build_ctx(env, query)
  decision = @core.evaluate(ctx)

  if decision
    h = headers.merge(decision.headers || {})
    h["content-type"] = decision.content_type
    return [decision.status, h, [decision.body]]
  end

  # SkipPaths (SSR direct) : on laisse l'app servir la page sans challenge ni skeleton.
  if @config.auto_inject && @config.site_key
    cfg = @config_cache.fetch(@config.site_key)
    if cfg[:skip_paths].include?(path)
      return @app.call(env)
    end
  end

  status, resp_headers, body = @app.call(env)

  # Rack 3 exige des noms de headers en minuscules → on normalise.
  resp_headers = resp_headers.each_with_object({}) { |(k, v), acc| acc[k.to_s.downcase] = v }

  # L'allowlist skip le split-render aussi (parité Node : `!core.isAllowlisted(path)`).
  return [status, resp_headers, body] unless @config.auto_inject && @config.split_render && !@config.is_allowlisted?(path)

  html = body.respond_to?(:each) ? body.each.to_a.join : body.to_s
  ct = resp_headers["content-type"].to_s
  if html.include?("<html") && (ct.include?("text/html") || ct.empty?)
    begin
      skeleton = inject_guards(html, ctx)
      body = [skeleton]
      # CRITIQUE : le bootcode unicode (code points > 917504) s'encode en UTF-8 sur
      # 4 octets commençant par 0xF3. Sans `charset=utf-8`, le navigateur interprète
      # le body en Latin-1 → les octets 0xF3 deviennent 'ó' → le décodage
      # `codePointAt(0)-917504` produit un code point négatif → RangeError.
      # Forcer UTF-8 est indispensable pour que le skeleton se décode correctement.
      resp_headers["content-type"] = "text/html; charset=utf-8"
    rescue StandardError => e
      warn("[shugoi] inject error: #{e.message}") if @config.debug
    end
  end

  [status, resp_headers, body]
end