Skip to content

Commit

Permalink
Implement importmap-rails with minimal js changes (#4396)
Browse files Browse the repository at this point in the history
Add importmap:verify to check vendored javascript
Run importmap:verify as part of lint

Update content security policy based on recommendations from google csp validator
in order to get importmap script tag to load.

Use a random base64 if a nonce isn't available from the session
Use script-src: sha256-* instead of nonce for importmap script tag
This allows the importmap to be cached without creating a long lived nonce in the cache

Fix issue with blank cookie causing rack-test crash
  • Loading branch information
martinemde authored Feb 1, 2024
1 parent 3453412 commit 29d69a5
Show file tree
Hide file tree
Showing 37 changed files with 508 additions and 924 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ jobs:
bundler-cache: true
- name: Brakeman
run: bundle exec brakeman
importmap:
name: Importmap Verify
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ruby/setup-ruby@bd03e04863f52d169e18a2b190e8fa6b84938215 # v1.170.0
with:
bundler-cache: true
- name: Importmap Verify
run: bundle exec rake importmap:verify
kubeconform:
name: Kubeconform
runs-on: ubuntu-22.04
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ COPY --link config/database.yml.sample /app/config/database.yml
RUN <<BASH
set -ex
RAILS_GROUPS=assets SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile
rm -r /app/tmp/cache/assets/
rm -fr /app/tmp/cache/assets/
BASH

RUN <<BASH
Expand Down
6 changes: 4 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ gem "gravtastic", "~> 3.2"
gem "high_voltage", "~> 3.1"
gem "honeybadger", "~> 5.4"
gem "http_accept_language", "~> 2.1"
gem "jquery-rails", "~> 4.5"
gem "kaminari", "~> 1.2"
gem "launchdarkly-server-sdk", "~> 8.1"
gem "mail", "~> 2.8"
Expand All @@ -43,7 +42,6 @@ gem "searchkick", "~> 5.3"
gem "faraday_middleware-aws-sigv4", "~> 1.0"
gem "xml-simple", "~> 1.1"
gem "compact_index", "~> 0.15.0"
gem "sprockets-rails", "~> 3.4"
gem "rack-attack", "~> 6.6"
gem "rqrcode", "~> 2.1"
gem "rotp", "~> 6.2"
Expand Down Expand Up @@ -73,6 +71,10 @@ gem "pp", "0.5.0"
gem "csv", "~> 3.2" # zeitwerk-2.6.12
gem "observer", "~> 0.1.2" # launchdarkly-server-sdk-8.0.0

# Assets
gem "sprockets-rails", "~> 3.4"
gem "importmap-rails", "~> 2.0"

group :assets, :development do
gem "tailwindcss-rails", "~> 2.3"
end
Expand Down
12 changes: 6 additions & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ GEM
multi_xml (>= 0.5.2)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
importmap-rails (2.0.1)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
inline_svg (1.9.0)
activesupport (>= 3.0)
nokogiri (>= 1.6)
Expand All @@ -300,10 +304,6 @@ GEM
jmespath (1.6.2)
job-iteration (1.4.1)
activejob (>= 5.2)
jquery-rails (4.6.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.7.1)
json-jwt (1.16.5)
activesupport (>= 4.2)
Expand Down Expand Up @@ -784,7 +784,7 @@ DEPENDENCIES
high_voltage (~> 3.1)
honeybadger (~> 5.4)
http_accept_language (~> 2.1)
jquery-rails (~> 4.5)
importmap-rails (~> 2.0)
kaminari (~> 1.2)
launchdarkly-server-sdk (~> 8.1)
launchy (~> 2.5)
Expand Down Expand Up @@ -964,12 +964,12 @@ CHECKSUMS
http_accept_language (2.1.1) sha256=0043f0d55a148cf45b604dbdd197cb36437133e990016c68c892d49dbea31634
httparty (0.21.0) sha256=00ef7bf9a71f30a3bff88edeb5b16a34bea883ab67c246b3f0db2d6794fe1214
i18n (1.14.1) sha256=9d03698903547c060928e70a9bc8b6b87fda674453cda918fc7ab80235ae4a61
importmap-rails (2.0.1) sha256=e739a6e70c09f797688c6983fa79567ec1edc9becc30d55b3f7cc897b1825586
inline_svg (1.9.0) sha256=f44c5e3d2e401fd619ad3047b7c8cee384517d855edb1d1fb1a248d3cae535d6
io-console (0.7.2) sha256=f0dccff252f877a4f60d04a4dc6b442b185ebffb4b320ab69212a92b48a7a221
irb (1.11.1) sha256=0700d626c92f7d47d12e73932ddb0c11b73c1f00608f5eae78dbc44968690842
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
job-iteration (1.4.1) sha256=7243c40e4decc3d49529867e9c504afaea332976c967ffdebed9ff863c6424af
jquery-rails (4.6.0) sha256=3c4e6bf47274340b44d836b8aa1b5472c6d451e2739af5ec094421f39025a7e2
json (2.7.1) sha256=187ea312fb58420ff0c40f40af1862651d4295c8675267c6a1c353f1a0ac3265
json-jwt (1.16.5) sha256=c899d6d9c6892e1ed8e423ac153837d4ca4f7069777342f0e3a3398482f309fb
jwt (2.7.1) sha256=07357cd2f180739b2f8184eda969e252d850ac996ed0a23f616e8ff0a90ae19b
Expand Down
3 changes: 2 additions & 1 deletion app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//= link application.css
//= link application.js
//= link_tree ../../../vendor/assets/images
//= link_tree ../builds
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
24 changes: 0 additions & 24 deletions app/assets/javascripts/application.js

This file was deleted.

17 changes: 17 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ class ApplicationController < ActionController::Base

add_flash_types :notice_html

###
# Content security policy override for script-src
# This is necessary because we use a SHA256 for the importmap script tag
# because caching behavior of the mostly static pages could mean longer lived nonces
# being served from cache instead of unique nonces for each request.
# This ensures that importmap passes CSP and can be cached safely.
content_security_policy do |policy|
policy.script_src(
:self,
"'sha256-#{Digest::SHA256.base64digest(Rails.application.importmap.to_json(resolver: ApplicationController.helpers))}'",
"https://secure.gaug.es",
"https://www.fastly-insights.com",
"https://unpkg.com/@hotwired/stimulus/dist/stimulus.umd.js",
"https://unpkg.com/stimulus-rails-nested-form/dist/stimulus-rails-nested-form.umd.js"
)
end

def set_locale
I18n.locale = user_locale

Expand Down
7 changes: 7 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ def atom_feed_link(title, url)
title: title)
end

# Copied from importmap-rails but with the nonce removed. We rely on the sha256 hash instead.
# Relying on the hash improves the caching behavior by not sending the cached nonce to the client.
def javascript_inline_importmap_tag(importmap_json = Rails.application.importmap.to_json(resolver: self))
tag.script importmap_json.html_safe,
type: "importmap", "data-turbo-track": "reload"
end

def short_info(rubygem)
info = gem_info(rubygem).strip.truncate(90)
escape_once(sanitize(info))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
$(function() {
import $ from "jquery";

$(function () {
var enableGemScopeCheckboxes = $("#push_rubygem, #yank_rubygem, #add_owner, #remove_owner");
var hiddenRubygemId = "hidden_api_key_rubygem_id";
toggleGemSelector();
Expand Down
16 changes: 16 additions & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import Rails from "@rails/ujs";
Rails.start();

import "./api_key_form";
import "./autocomplete";
import "./clipboard_buttons";
import "./mobile-nav";
import "./multifactor_auths";
import "./oidc_api_key_role_form";
import "./pages";
import "./popup-nav";
import "./search";
import "./transitive_dependencies";
import "./webauthn";
import "github-buttons";
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import $ from "jquery";

$(function() {
if ($('#home_query').length){
autocomplete($('#home_query'));
Expand All @@ -13,13 +15,17 @@ $(function() {
search.bind('input', function(e) {
var term = $.trim($(search).val());
if (term.length >= 2) {
search.removeClass('autocomplete-done');
search.addClass('autocomplete-loading');
$.ajax({
url: '/api/v1/search/autocomplete',
type: 'GET',
data: ('query=' + term),
processData: false,
dataType: 'json'
}).done(function(data) {
search.removeClass('autocomplete-loading');
search.addClass('autocomplete-done');
addToSuggestList(search, data);
});
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import $ from "jquery";
import ClipboardJS from "clipboard";

$(function() {
var clipboard = new ClipboardJS('.gem__code__icon');
var copyTooltip = $('.gem__code__tooltip--copy');
Expand Down
16 changes: 16 additions & 0 deletions app/javascript/handle-click.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function handleClick(
event,
nav,
removeNavExpandedClass,
addNavExpandedClass
) {
var isMobileNavExpanded = nav.popUp.hasClass(nav.expandedClass);

event.preventDefault();

if (isMobileNavExpanded) {
removeNavExpandedClass();
} else {
addNavExpandedClass();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import $ from "jquery";
import { handleClick } from "./handle-click";

$(function() {
// cache jQuery lookups into variables
// so we don't have to traverse the DOM every time
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import $ from "jquery";
import ClipboardJS from "clipboard";

function popUp (e) {
e.preventDefault();
e.returnValue = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import $ from "jquery";

$(function () {
function wire() {
var removeNestedButtons = $("button.form__remove_nested_button");
Expand Down
14 changes: 9 additions & 5 deletions app/assets/javascripts/pages.js → app/javascript/pages.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import $ from "jquery";

//data page
$(document).ready(function() {
$(function() {
var getDumpData = function(target, type) {
return $.get('https://s3-us-west-2.amazonaws.com/rubygems-dumps/?prefix=production/public_' + type).done(function(data) {
var files, xml;
Expand Down Expand Up @@ -56,16 +58,18 @@ $(document).ready(function() {
getDumpData('ul.rubygems-dump-listing-postgresql', 'postgresql');
getDumpData('ul.rubygems-dump-listing-redis', 'redis');
}

});

//stats page
$('.stats__graph__gem__meter').each(function() {
bar_width = $(this).data("bar_width");
$(this).animate({ width: bar_width + '%' }, 700).removeClass('t-item--hidden').css("display", "block");
$(function() {
$('.stats__graph__gem__meter').each(function() {
$(this).animate({ width: $(this).data("bar-width") + '%' }, 700).removeClass('t-item--hidden').css("display", "block");
});
});

//gem page
$(document).ready(function() {
$(function() {
$('.gem__users__mfa-text.mfa-warn').on('click', function() {
$('.gem__users__mfa-text.mfa-warn').toggleClass('t-item--hidden');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import $ from "jquery";
import { handleClick } from "./handle-click";

$(function() {
var arrowIcon = $('.header__popup-link');
var popupNav = $('.header__popup__nav-links');
Expand All @@ -17,4 +20,3 @@ $(function() {
handleClick(e, nav, removeNavExpandedClass, addNavExpandedClass);
});
});

2 changes: 2 additions & 0 deletions app/assets/javascripts/search.js → app/javascript/search.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import $ from "jquery";

if($("#advanced-search").length){
var $main = $('#home_query');
var $name = $('input#name');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import $ from "jquery";

$(document).on('click', '.deps_expanded-link', function () {
var current = $(this);
var gem_id = $(this).attr('data-gem_id');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import $ from "jquery";
import { bufferToBase64url, base64urlToBuffer } from "webauthn-json"

(function() {
const handleEvent = function(event) {
event.preventDefault();
Expand Down Expand Up @@ -48,7 +51,7 @@
};

const parseCreationOptionsFromJSON = function(json) {
return {
return {
...json,
challenge: base64urlToBuffer(json.challenge),
user: { ...json.user, id: base64urlToBuffer(json.user.id) },
Expand Down
3 changes: 1 addition & 2 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<%= render "layouts/feeds" %>
<%= csrf_meta_tag %>
<%= yield :head %>
<%= javascript_importmap_tags %>
</head>

<body class="<%= 'body--index' if request.path_info == '/' %>">
Expand Down Expand Up @@ -216,8 +217,6 @@
<% end %>
</div>
</footer>

<%= javascript_include_tag "application" %>
<%= yield :javascript %>
<script type="text/javascript" defer src="https://www.fastly-insights.com/insights.js?k=3e63c3cd-fc37-4b19-80b9-65ce64af060a"></script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion app/views/stats/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<div class="stats__graph__gem">
<h3 class="stats__graph__gem__name"><%= link_to gem.name, rubygem_path(gem.slug) %></h3>
<div class="stats__graph__gem__meter-wrap">
<div class="stats__graph__gem__meter t-item--hidden" data-bar_width="<%= stats_graph_meter(gem, @most_downloaded_count) %>">
<div class="stats__graph__gem__meter t-item--hidden" data-bar-width="<%= stats_graph_meter(gem, @most_downloaded_count) %>">
<span class="stats__graph__gem__count"><%= number_to_delimited gem.downloads %></span>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions bin/importmap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby

require_relative "../config/application"
require "importmap/commands"
12 changes: 12 additions & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Pin npm packages by running ./bin/importmap

pin "application", preload: true

# vendored and adapted from https://github.com/mdo/github-buttons/blob/master/src/js.js
pin "github-buttons"
# vendored from github in the before times
pin "webauthn-json"

pin "jquery" # @3.7.1
pin "@rails/ujs", to: "@rails--ujs.js" # @7.1.3
pin "clipboard" # @2.0.11
Loading

0 comments on commit 29d69a5

Please sign in to comment.