Skip to content

Commit

Permalink
Add nonces to support streaming with CSPs (#182)
Browse files Browse the repository at this point in the history
Co-authored-by: Will Cosgrove <[email protected]>
  • Loading branch information
joeldrapper and willcosgrove authored Mar 26, 2024
1 parent 9757231 commit 67f2ff7
Showing 1 changed file with 67 additions and 5 deletions.
72 changes: 67 additions & 5 deletions lib/phlex/rails/streaming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,47 @@

# @api private
module Phlex::Rails::Streaming
include ActionController::Live

private

def stream(view)
def stream(view, last_modified: Time.now.httpdate, filename: nil)
set_stream_headers(last_modified:)

case view
when Phlex::HTML
stream_html(view)
when Phlex::CSV
stream_csv(view, filename:)
end
end

def set_stream_headers(last_modified:)
headers.delete("Content-Length")

headers["X-Accel-Buffering"] = "no"
headers["Cache-Control"] = "no-transform"
headers["Last-Modified"] = last_modified
end

def stream_csv(view, filename:)
headers["Content-Type"] = "text/csv; charset=utf-8"
headers["Content-Disposition"] = "attachment; filename=\"#{filename || view.filename}\""

self.response_body = Enumerator.new do |buffer|
view.call(buffer, view_context: view_context)
end
end

def stream_html(view)
headers["Content-Type"] = "text/html; charset=utf-8"
headers["Last-Modified"] = Time.now.httpdate

response.status = 200
# Ensure we have a session id.
# See https://github.com/rails/rails/issues/51424
if session.id.nil?
session[:phlex_init_session] = 1
session.delete(:phlex_init_session)
end

self.response_body = Enumerator.new do |buffer|
view.call(buffer, view_context: view_context)
Expand All @@ -34,8 +64,42 @@ def stream(view)
document.querySelectorAll("script").forEach((script) => {
const newScript = document.createElement("script");
newScript.text = script.text;
newScript.nonce = "#{view_context.content_security_policy_nonce}";
script.replaceWith(newScript);
});
// Re-evaluate all style tags
document.querySelectorAll("style").forEach((style) => {
const newStyle = document.createElement("style");
newStyle.textContent = style.textContent;
newStyle.nonce = "#{view_context.content_security_policy_nonce}";
style.replaceWith(newStyle);
});
// Map onclick events to event listeners
document.querySelectorAll("[onclick]").forEach((element) => {
const action = element.getAttribute("onclick");
const newScript = document.createElement("script")
element.dataset.onclick = action;
element.removeAttribute("onclick");
newScript.text = `
(function() {
const action = () => { ${action} };
document.addEventListener("click", (event) => {
const element = event.target;
if (element.dataset.onclick === "${action}") {
action.bind(element)();
};
});
})();
`;
newScript.nonce = "#{view_context.content_security_policy_nonce}";
document.body.appendChild(newScript);
});
JAVASCRIPT
else
js = <<~JAVASCRIPT
Expand All @@ -45,8 +109,6 @@ def stream(view)

buffer << %(-->"'>)
buffer << view_context.javascript_tag(js, nonce: true)

p e
end
end
end

0 comments on commit 67f2ff7

Please sign in to comment.