Class: Shugoi::Rack::Middleware
- Inherits:
-
Object
- Object
- Shugoi::Rack::Middleware
- Defined in:
- lib/shugoi/rack/middleware.rb
Overview
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app, options = {}) ⇒ Middleware
constructor
A new instance of Middleware.
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 28 29 30 31 32 33 34 |
# File 'lib/shugoi/rack/middleware.rb', line 15 def initialize(app, = {}) @app = app @config = Config.new() @disk_path_option = [:disk_path] @api_client = ApiClient.new(@config.base_url, debug: @config.debug) # Config (whitelist + flags + skipPaths) et render : via internalUrl pour éviter # le deadlock (appels serveur→serveur qui repasseraient par le middleware public). @api_internal = ApiClient.new(@config.internal_url, debug: @config.debug) @guard_cache = GuardCache.new(@api_client) @config_cache = ConfigCache.new(@api_internal) @token_signer = TokenSigner.new(@config.signing_secret) # multi_process → stockage disque du HTML (nécessaire en cluster PM2, parité # enableDiskStore(true) du module Node). @html_store = HtmlStore.new(disk_path: 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, @pow) @core = Core.new(@config, @pow, @api_client, @config_cache) @csp = Csp.build(site_key: @config.site_key, api_origin: Csp.origin_of(@config.base_url), extra_directives: @config.extra_directives, split_render: @config.split_render) end |
Instance Method Details
#call(env) ⇒ Object
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/shugoi/rack/middleware.rb', line 36 def call(env) path = env["PATH_INFO"].to_s query = parse_query(env["QUERY_STRING"].to_s) method = env["REQUEST_METHOD"].to_s.upcase # Render endpoint — GET/HEAD uniquement (round 13, parité module Node). if path.end_with?("/__shugoi/render") return method_not_allowed unless %w[GET HEAD].include?(method) return handle_render(env, query) end # Challenge page — GET/HEAD uniquement. if path == "/__sg_challenge" && !%w[GET HEAD].include?(method) return method_not_allowed end # SkipPaths (SSR direct) : on laisse l'app servir la page sans challenge ni skeleton. # Parité middleware.ts : le check est AVANT core.evaluate (un skipPath hors allowlist # ne doit PAS passer par le challenge PoW ni la protection des assets). 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 ctx = build_ctx(env, query) decision = @core.evaluate(ctx) if decision headers = {} headers["content-security-policy"] = @csp if @config.csp_enabled h = headers.merge(decision.headers || {}) h["content-type"] = decision.content_type return [decision.status, h, [decision.body]] 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 } # CSP fusionnée avec celle éventuellement posée par l'app (parité middleware.ts). if @config.csp_enabled existing = resp_headers["content-security-policy"] resp_headers["content-security-policy"] = Csp.merge(existing, @csp) end # PoW validé → pose le cookie __sg_ok (navigations suivantes sans challenge). if (proof = query["sg_proof"]) && !decision if ( = @pow.(proof, ctx[:ip].to_s, ctx[:ua].to_s)) resp_headers["set-cookie"] = end end is_bot = @core.is_trusted_bot?(ctx[:ua].to_s, ctx[:ip].to_s) # L'allowlist skip le split-render aussi (parité Node : `!core.isAllowlisted(path)`). return [status, resp_headers, body] unless @config.auto_inject && @config.split_render && !is_bot && !@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.}") if @config.debug end end [status, resp_headers, body] end |