From e3de45359f9bc7a71d9b8045a7e369fd0cf8f433 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 28 Oct 2020 23:36:19 +0530 Subject: [PATCH] DEV: enable cors to all cdn get requests from workbox. (#10685) Now all external requests from the service worker will be in CORS mode without credentials. --- app/assets/javascripts/service-worker.js.erb | 93 +++++++- app/controllers/application_controller.rb | 14 ++ app/controllers/highlight_js_controller.rb | 2 + app/controllers/static_controller.rb | 2 + app/controllers/stylesheets_controller.rb | 2 + app/controllers/svg_sprite_controller.rb | 2 + .../theme_javascripts_controller.rb | 2 + app/controllers/uploads_controller.rb | 2 + app/controllers/user_avatars_controller.rb | 2 + config/initializers/008-rack-cors.rb | 12 +- lib/discourse.rb | 19 ++ lib/middleware/enforce_hostname.rb | 1 + lib/tasks/javascript.rake | 4 + package.json | 1 + .../workbox/workbox-cacheable-response.dev.js | 200 ++++++++++++++++++ .../workbox-cacheable-response.dev.js.map | 1 + .../workbox-cacheable-response.prod.js | 2 + .../workbox-cacheable-response.prod.js.map | 1 + spec/requests/application_controller_spec.rb | 11 + spec/requests/static_controller_spec.rb | 19 ++ yarn.lock | 7 + 21 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 public/javascripts/workbox/workbox-cacheable-response.dev.js create mode 100644 public/javascripts/workbox/workbox-cacheable-response.dev.js.map create mode 100644 public/javascripts/workbox/workbox-cacheable-response.prod.js create mode 100644 public/javascripts/workbox/workbox-cacheable-response.prod.js.map diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb index f2640948171..0b736be4453 100644 --- a/app/assets/javascripts/service-worker.js.erb +++ b/app/assets/javascripts/service-worker.js.erb @@ -4,24 +4,109 @@ importScripts("<%= "#{Discourse.asset_host}#{Discourse.base_path}/javascripts/wo workbox.setConfig({ modulePathPrefix: "<%= "#{Discourse.asset_host}#{Discourse.base_path}/javascripts/workbox" %>", - debug: false + debug: <%= Rails.env.development? %> }); var authUrl = "<%= Discourse.base_path %>/auth/"; var cacheVersion = "1"; +var discourseCacheName = "discourse-" + cacheVersion; +var externalCacheName = "external-" + cacheVersion; // Cache all GET requests, so Discourse can be used while offline + workbox.routing.registerRoute( function(args) { - return !(args.url.origin === location.origin && args.url.pathname.startsWith(authUrl)); + return args.url.origin === location.origin && !args.url.pathname.startsWith(authUrl); }, // Match all except auth routes new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails - cacheName: "discourse-" + cacheVersion, + cacheName: discourseCacheName, plugins: [ + new workbox.cacheableResponse.Plugin({ + statuses: [200] // opaque responses will return status code '0' + }), // for s3 secure media signed urls new workbox.expiration.Plugin({ maxAgeSeconds: 7* 24 * 60 * 60, // 7 days - maxEntries: 500, + maxEntries: 250, + purgeOnQuotaError: true, // safe to automatically delete if exceeding the available storage + }), + ], + }) +); + +var cdnUrls = []; + +<% if GlobalSetting.try(:cdn_cors_enabled) %> +cdnUrls = ["<%= "#{GlobalSetting.s3_cdn_url}" %>", "<%= "#{GlobalSetting.cdn_url}" %>"].filter(Boolean); + +if (cdnUrls.length > 0) { + var cdnCacheName = "cdn-" + cacheVersion; + + var appendQueryStringPlugin = { + requestWillFetch: function (args) { + var request = args.request; + + if (request.url.includes("avatar") || request.url.includes("emoji")) { + var url = new URL(request.url); + // Using this temporary query param to force browsers to redownload images from server. + url.searchParams.append('refresh', 'true'); + return new Request(url.href, request); + } + + return request; + } + }; + + workbox.routing.registerRoute( + function(args) { + var matching = cdnUrls.filter( + function(url) { + return args.url.href.startsWith(url); + } + ); + return matching.length > 0; + }, // Match all cdn resources + new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails + cacheName: cdnCacheName, + fetchOptions: { + mode: 'cors', + credentials: 'omit' + }, + plugins: [ + new workbox.expiration.Plugin({ + maxAgeSeconds: 7* 24 * 60 * 60, // 7 days + maxEntries: 250, + purgeOnQuotaError: true, // safe to automatically delete if exceeding the available storage + }), + appendQueryStringPlugin + ], + }) + ); +} +<% end %> + +workbox.routing.registerRoute( + function(args) { + if (args.url.origin === location.origin) { + return false; + } + + var matching = cdnUrls.filter( + function(url) { + return args.url.href.startsWith(url); + } + ); + return matching.length === 0; + }, // Match all other external resources + new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails + cacheName: externalCacheName, + plugins: [ + new workbox.cacheableResponse.Plugin({ + statuses: [200] // opaque responses will return status code '0' + }), + new workbox.expiration.Plugin({ + maxAgeSeconds: 7* 24 * 60 * 60, // 7 days + maxEntries: 250, purgeOnQuotaError: true, // safe to automatically delete if exceeding the available storage }), ], diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 693970d14a4..438fc52cbd6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -42,6 +42,7 @@ class ApplicationController < ActionController::Base before_action :preload_json before_action :add_noindex_header, if: -> { is_feed_request? || !SiteSetting.allow_index_in_robots_txt } before_action :check_xhr + before_action :block_cdn_requests after_action :add_readonly_header after_action :perform_refresh_session after_action :dont_cache_page @@ -672,6 +673,19 @@ class ApplicationController < ActionController::Base raise ApplicationController::RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?) end + def block_cdn_requests + raise Discourse::NotFound if Discourse.is_cdn_request?(request.env, request.method) + end + + def apply_cdn_headers + Discourse.apply_cdn_headers(response.headers) if Discourse.is_cdn_request?(request.env, request.method) + end + + def self.cdn_action(args = {}) + skip_before_action :block_cdn_requests, args + before_action :apply_cdn_headers, args + end + def self.requires_login(arg = {}) @requires_login_arg = arg end diff --git a/app/controllers/highlight_js_controller.rb b/app/controllers/highlight_js_controller.rb index 768c4e7286c..55dab4693a6 100644 --- a/app/controllers/highlight_js_controller.rb +++ b/app/controllers/highlight_js_controller.rb @@ -3,6 +3,8 @@ class HighlightJsController < ApplicationController skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show] + cdn_action only: [:show] + def show no_cookies diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 16f692569a0..519c2652f6d 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -7,6 +7,8 @@ class StaticController < ApplicationController skip_before_action :preload_json, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] skip_before_action :handle_theme, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] + cdn_action only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] + PAGES_WITH_EMAIL_PARAM = ['login', 'password_reset', 'signup'] MODAL_PAGES = ['password_reset', 'signup'] diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb index 163ed9e430f..a96cbec6170 100644 --- a/app/controllers/stylesheets_controller.rb +++ b/app/controllers/stylesheets_controller.rb @@ -3,6 +3,8 @@ class StylesheetsController < ApplicationController skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map, :color_scheme] + cdn_action only: [:show, :show_source_map, :color_scheme] + def show_source_map show_resource(source_map: true) end diff --git a/app/controllers/svg_sprite_controller.rb b/app/controllers/svg_sprite_controller.rb index 81b970547e1..7b15d837ca3 100644 --- a/app/controllers/svg_sprite_controller.rb +++ b/app/controllers/svg_sprite_controller.rb @@ -3,6 +3,8 @@ class SvgSpriteController < ApplicationController skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :search, :svg_icon] + cdn_action only: [:show, :search, :svg_icon] + requires_login except: [:show, :svg_icon] def show diff --git a/app/controllers/theme_javascripts_controller.rb b/app/controllers/theme_javascripts_controller.rb index 0fb5f526a5b..3323a33722a 100644 --- a/app/controllers/theme_javascripts_controller.rb +++ b/app/controllers/theme_javascripts_controller.rb @@ -13,6 +13,8 @@ class ThemeJavascriptsController < ApplicationController before_action :is_asset_path, :no_cookies, only: [:show] + cdn_action only: [:show] + def show raise Discourse::NotFound unless last_modified.present? return render body: nil, status: 304 if not_modified? diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 814b65ac39d..0114c1df4cc 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -10,6 +10,8 @@ class UploadsController < ApplicationController before_action :is_asset_path, only: [:show, :show_short, :show_secure] + cdn_action only: [:show, :show_short, :show_secure] + SECURE_REDIRECT_GRACE_SECONDS = 5 def create diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index 583fc81e653..88b25289cc7 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -4,6 +4,8 @@ class UserAvatarsController < ApplicationController skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter, :show_proxy_letter] + cdn_action only: [:show, :show_letter, :show_proxy_letter] + def refresh_gravatar user = User.find_by(username_lower: params[:username].downcase) guardian.ensure_can_edit!(user) diff --git a/config/initializers/008-rack-cors.rb b/config/initializers/008-rack-cors.rb index cc5bb8cd960..fa226a85a77 100644 --- a/config/initializers/008-rack-cors.rb +++ b/config/initializers/008-rack-cors.rb @@ -25,15 +25,19 @@ class Discourse::Cors status, headers, body = @app.call(env) headers ||= {} - Discourse::Cors.apply_headers(cors_origins, env, headers) if cors_origins + Discourse::Cors.apply_headers(cors_origins, env, headers) [status, headers, body] end def self.apply_headers(cors_origins, env, headers) - origin = nil + request_method = env['REQUEST_METHOD'] + + if env['SCRIPT_NAME'] == "/assets" && Discourse.is_cdn_request?(env, request_method) + Discourse.apply_cdn_headers(headers) + elsif cors_origins + origin = nil - if cors_origins if origin = env['HTTP_ORIGIN'] origin = nil unless cors_origins.include?(origin) end @@ -48,6 +52,6 @@ class Discourse::Cors end end -if GlobalSetting.enable_cors +if GlobalSetting.enable_cors || GlobalSetting.cdn_url Rails.configuration.middleware.insert_before ActionDispatch::Flash, Discourse::Cors end diff --git a/lib/discourse.rb b/lib/discourse.rb index d9cd92196ca..862ed6857ff 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -17,6 +17,7 @@ end module Discourse DB_POST_MIGRATE_PATH ||= "db/post_migrate" + REQUESTED_HOSTNAME ||= "REQUESTED_HOSTNAME" require 'sidekiq/exception_handler' class SidekiqExceptionHandler @@ -910,6 +911,24 @@ module Discourse def self.is_parallel_test? ENV['RAILS_ENV'] == "test" && ENV['TEST_ENV_NUMBER'] end + + CDN_REQUEST_METHODS ||= ["GET", "HEAD", "OPTIONS"] + + def self.is_cdn_request?(env, request_method) + return unless CDN_REQUEST_METHODS.include?(request_method) + + cdn_hostnames = GlobalSetting.cdn_hostnames + return if cdn_hostnames.blank? + + requested_hostname = env[REQUESTED_HOSTNAME] || env[Rack::HTTP_HOST] + cdn_hostnames.include?(requested_hostname) + end + + def self.apply_cdn_headers(headers) + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] = CDN_REQUEST_METHODS.join(", ") + headers + end end # rubocop:enable Style/GlobalVars diff --git a/lib/middleware/enforce_hostname.rb b/lib/middleware/enforce_hostname.rb index 00e8870d57c..c462e5b7bcc 100644 --- a/lib/middleware/enforce_hostname.rb +++ b/lib/middleware/enforce_hostname.rb @@ -17,6 +17,7 @@ module Middleware allowed_hostnames = RailsMultisite::ConnectionManagement.current_db_hostnames requested_hostname = env[Rack::HTTP_HOST] + env[Discourse::REQUESTED_HOSTNAME] = requested_hostname env[Rack::HTTP_HOST] = allowed_hostnames.find { |h| h == requested_hostname } || Discourse.current_hostname @app.call(env) diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index 27ba82f8a39..852bcea76a8 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -135,6 +135,10 @@ def dependencies destination: 'workbox', public: true, skip_versioning: true + }, { + source: 'workbox-cacheable-response/build/.', + destination: 'workbox', + public: true }, { source: '@popperjs/core/dist/umd/popper.js' }, { diff --git a/package.json b/package.json index 683a21d56d4..e8913cfb398 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "pikaday": "1.8.0", "resumablejs": "1.1.0", "spectrum-colorpicker": "1.8.0", + "workbox-cacheable-response": "^4.3.1", "workbox-core": "^4.3.1", "workbox-expiration": "^4.3.1", "workbox-routing": "^4.3.1", diff --git a/public/javascripts/workbox/workbox-cacheable-response.dev.js b/public/javascripts/workbox/workbox-cacheable-response.dev.js new file mode 100644 index 00000000000..54a2e499975 --- /dev/null +++ b/public/javascripts/workbox/workbox-cacheable-response.dev.js @@ -0,0 +1,200 @@ +this.workbox = this.workbox || {}; +this.workbox.cacheableResponse = (function (exports, WorkboxError_mjs, assert_mjs, getFriendlyURL_mjs, logger_mjs) { + 'use strict'; + + try { + self['workbox:cacheable-response:4.3.1'] && _(); + } catch (e) {} // eslint-disable-line + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This class allows you to set up rules determining what + * status codes and/or headers need to be present in order for a + * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) + * to be considered cacheable. + * + * @memberof workbox.cacheableResponse + */ + + class CacheableResponse { + /** + * To construct a new CacheableResponse instance you must provide at least + * one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config = {}) { + { + if (!(config.statuses || config.headers)) { + throw new WorkboxError_mjs.WorkboxError('statuses-or-headers-required', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor' + }); + } + + if (config.statuses) { + assert_mjs.assert.isArray(config.statuses, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.statuses' + }); + } + + if (config.headers) { + assert_mjs.assert.isType(config.headers, 'object', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.headers' + }); + } + } + + this._statuses = config.statuses; + this._headers = config.headers; + } + /** + * Checks a response to see whether it's cacheable or not, based on this + * object's configuration. + * + * @param {Response} response The response whose cacheability is being + * checked. + * @return {boolean} `true` if the `Response` is cacheable, and `false` + * otherwise. + */ + + + isResponseCacheable(response) { + { + assert_mjs.assert.isInstance(response, Response, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'isResponseCacheable', + paramName: 'response' + }); + } + + let cacheable = true; + + if (this._statuses) { + cacheable = this._statuses.includes(response.status); + } + + if (this._headers && cacheable) { + cacheable = Object.keys(this._headers).some(headerName => { + return response.headers.get(headerName) === this._headers[headerName]; + }); + } + + { + if (!cacheable) { + logger_mjs.logger.groupCollapsed(`The request for ` + `'${getFriendlyURL_mjs.getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`); + logger_mjs.logger.groupCollapsed(`View cacheability criteria here.`); + logger_mjs.logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses)); + logger_mjs.logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2)); + logger_mjs.logger.groupEnd(); + const logFriendlyHeaders = {}; + response.headers.forEach((value, key) => { + logFriendlyHeaders[key] = value; + }); + logger_mjs.logger.groupCollapsed(`View response status and headers here.`); + logger_mjs.logger.log(`Response status: ` + response.status); + logger_mjs.logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2)); + logger_mjs.logger.groupEnd(); + logger_mjs.logger.groupCollapsed(`View full response details here.`); + logger_mjs.logger.log(response.headers); + logger_mjs.logger.log(response); + logger_mjs.logger.groupEnd(); + logger_mjs.logger.groupEnd(); + } + } + + return cacheable; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it + * easier to add in cacheability checks to requests made via Workbox's built-in + * strategies. + * + * @memberof workbox.cacheableResponse + */ + + class Plugin { + /** + * To construct a new cacheable response Plugin instance you must provide at + * least one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config) { + this._cacheableResponse = new CacheableResponse(config); + } + /** + * @param {Object} options + * @param {Response} options.response + * @return {boolean} + * @private + */ + + + cacheWillUpdate({ + response + }) { + if (this._cacheableResponse.isResponseCacheable(response)) { + return response; + } + + return null; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + exports.CacheableResponse = CacheableResponse; + exports.Plugin = Plugin; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); +//# sourceMappingURL=workbox-cacheable-response.dev.js.map diff --git a/public/javascripts/workbox/workbox-cacheable-response.dev.js.map b/public/javascripts/workbox/workbox-cacheable-response.dev.js.map new file mode 100644 index 00000000000..86bc689049e --- /dev/null +++ b/public/javascripts/workbox/workbox-cacheable-response.dev.js.map @@ -0,0 +1 @@ +{"version":3,"file":"workbox-cacheable-response.dev.js","sources":["../_version.mjs","../CacheableResponse.mjs","../Plugin.mjs","../index.mjs"],"sourcesContent":["try{self['workbox:cacheable-response:4.3.1']&&_()}catch(e){}// eslint-disable-line","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\n\nimport {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';\nimport {assert} from 'workbox-core/_private/assert.mjs';\nimport {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';\nimport {logger} from 'workbox-core/_private/logger.mjs';\nimport './_version.mjs';\n\n/**\n * This class allows you to set up rules determining what\n * status codes and/or headers need to be present in order for a\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * to be considered cacheable.\n *\n * @memberof workbox.cacheableResponse\n */\nclass CacheableResponse {\n /**\n * To construct a new CacheableResponse instance you must provide at least\n * one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config = {}) {\n if (process.env.NODE_ENV !== 'production') {\n if (!(config.statuses || config.headers)) {\n throw new WorkboxError('statuses-or-headers-required', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n });\n }\n\n if (config.statuses) {\n assert.isArray(config.statuses, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.statuses',\n });\n }\n\n if (config.headers) {\n assert.isType(config.headers, 'object', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.headers',\n });\n }\n }\n\n this._statuses = config.statuses;\n this._headers = config.headers;\n }\n\n /**\n * Checks a response to see whether it's cacheable or not, based on this\n * object's configuration.\n *\n * @param {Response} response The response whose cacheability is being\n * checked.\n * @return {boolean} `true` if the `Response` is cacheable, and `false`\n * otherwise.\n */\n isResponseCacheable(response) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(response, Response, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'isResponseCacheable',\n paramName: 'response',\n });\n }\n\n let cacheable = true;\n\n if (this._statuses) {\n cacheable = this._statuses.includes(response.status);\n }\n\n if (this._headers && cacheable) {\n cacheable = Object.keys(this._headers).some((headerName) => {\n return response.headers.get(headerName) === this._headers[headerName];\n });\n }\n\n if (process.env.NODE_ENV !== 'production') {\n if (!cacheable) {\n logger.groupCollapsed(`The request for ` +\n `'${getFriendlyURL(response.url)}' returned a response that does ` +\n `not meet the criteria for being cached.`);\n\n logger.groupCollapsed(`View cacheability criteria here.`);\n logger.log(`Cacheable statuses: ` +\n JSON.stringify(this._statuses));\n logger.log(`Cacheable headers: ` +\n JSON.stringify(this._headers, null, 2));\n logger.groupEnd();\n\n const logFriendlyHeaders = {};\n response.headers.forEach((value, key) => {\n logFriendlyHeaders[key] = value;\n });\n\n logger.groupCollapsed(`View response status and headers here.`);\n logger.log(`Response status: ` + response.status);\n logger.log(`Response headers: ` +\n JSON.stringify(logFriendlyHeaders, null, 2));\n logger.groupEnd();\n\n logger.groupCollapsed(`View full response details here.`);\n logger.log(response.headers);\n logger.log(response);\n logger.groupEnd();\n\n logger.groupEnd();\n }\n }\n\n return cacheable;\n }\n}\n\nexport {CacheableResponse};\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\n\nimport {CacheableResponse} from './CacheableResponse.mjs';\nimport './_version.mjs';\n\n/**\n * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it\n * easier to add in cacheability checks to requests made via Workbox's built-in\n * strategies.\n *\n * @memberof workbox.cacheableResponse\n */\nclass Plugin {\n /**\n * To construct a new cacheable response Plugin instance you must provide at\n * least one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config) {\n this._cacheableResponse = new CacheableResponse(config);\n }\n\n /**\n * @param {Object} options\n * @param {Response} options.response\n * @return {boolean}\n * @private\n */\n cacheWillUpdate({response}) {\n if (this._cacheableResponse.isResponseCacheable(response)) {\n return response;\n }\n return null;\n }\n}\n\nexport {Plugin};\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\n\nimport {CacheableResponse} from './CacheableResponse.mjs';\nimport {Plugin} from './Plugin.mjs';\nimport './_version.mjs';\n\n\n/**\n * @namespace workbox.cacheableResponse\n */\n\nexport {\n CacheableResponse,\n Plugin,\n};\n"],"names":["self","_","e","CacheableResponse","constructor","config","statuses","headers","WorkboxError","moduleName","className","funcName","assert","isArray","paramName","isType","_statuses","_headers","isResponseCacheable","response","isInstance","Response","cacheable","includes","status","Object","keys","some","headerName","get","logger","groupCollapsed","getFriendlyURL","url","log","JSON","stringify","groupEnd","logFriendlyHeaders","forEach","value","key","Plugin","_cacheableResponse","cacheWillUpdate"],"mappings":"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"} \ No newline at end of file diff --git a/public/javascripts/workbox/workbox-cacheable-response.prod.js b/public/javascripts/workbox/workbox-cacheable-response.prod.js new file mode 100644 index 00000000000..a7e42f263a8 --- /dev/null +++ b/public/javascripts/workbox/workbox-cacheable-response.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.3.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({}); +//# sourceMappingURL=workbox-cacheable-response.prod.js.map diff --git a/public/javascripts/workbox/workbox-cacheable-response.prod.js.map b/public/javascripts/workbox/workbox-cacheable-response.prod.js.map new file mode 100644 index 00000000000..b1a0d1d2b79 --- /dev/null +++ b/public/javascripts/workbox/workbox-cacheable-response.prod.js.map @@ -0,0 +1 @@ +{"version":3,"file":"workbox-cacheable-response.prod.js","sources":["../_version.mjs","../CacheableResponse.mjs","../Plugin.mjs"],"sourcesContent":["try{self['workbox:cacheable-response:4.3.1']&&_()}catch(e){}// eslint-disable-line","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\n\nimport {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';\nimport {assert} from 'workbox-core/_private/assert.mjs';\nimport {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';\nimport {logger} from 'workbox-core/_private/logger.mjs';\nimport './_version.mjs';\n\n/**\n * This class allows you to set up rules determining what\n * status codes and/or headers need to be present in order for a\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * to be considered cacheable.\n *\n * @memberof workbox.cacheableResponse\n */\nclass CacheableResponse {\n /**\n * To construct a new CacheableResponse instance you must provide at least\n * one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config = {}) {\n if (process.env.NODE_ENV !== 'production') {\n if (!(config.statuses || config.headers)) {\n throw new WorkboxError('statuses-or-headers-required', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n });\n }\n\n if (config.statuses) {\n assert.isArray(config.statuses, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.statuses',\n });\n }\n\n if (config.headers) {\n assert.isType(config.headers, 'object', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.headers',\n });\n }\n }\n\n this._statuses = config.statuses;\n this._headers = config.headers;\n }\n\n /**\n * Checks a response to see whether it's cacheable or not, based on this\n * object's configuration.\n *\n * @param {Response} response The response whose cacheability is being\n * checked.\n * @return {boolean} `true` if the `Response` is cacheable, and `false`\n * otherwise.\n */\n isResponseCacheable(response) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(response, Response, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'isResponseCacheable',\n paramName: 'response',\n });\n }\n\n let cacheable = true;\n\n if (this._statuses) {\n cacheable = this._statuses.includes(response.status);\n }\n\n if (this._headers && cacheable) {\n cacheable = Object.keys(this._headers).some((headerName) => {\n return response.headers.get(headerName) === this._headers[headerName];\n });\n }\n\n if (process.env.NODE_ENV !== 'production') {\n if (!cacheable) {\n logger.groupCollapsed(`The request for ` +\n `'${getFriendlyURL(response.url)}' returned a response that does ` +\n `not meet the criteria for being cached.`);\n\n logger.groupCollapsed(`View cacheability criteria here.`);\n logger.log(`Cacheable statuses: ` +\n JSON.stringify(this._statuses));\n logger.log(`Cacheable headers: ` +\n JSON.stringify(this._headers, null, 2));\n logger.groupEnd();\n\n const logFriendlyHeaders = {};\n response.headers.forEach((value, key) => {\n logFriendlyHeaders[key] = value;\n });\n\n logger.groupCollapsed(`View response status and headers here.`);\n logger.log(`Response status: ` + response.status);\n logger.log(`Response headers: ` +\n JSON.stringify(logFriendlyHeaders, null, 2));\n logger.groupEnd();\n\n logger.groupCollapsed(`View full response details here.`);\n logger.log(response.headers);\n logger.log(response);\n logger.groupEnd();\n\n logger.groupEnd();\n }\n }\n\n return cacheable;\n }\n}\n\nexport {CacheableResponse};\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\n\nimport {CacheableResponse} from './CacheableResponse.mjs';\nimport './_version.mjs';\n\n/**\n * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it\n * easier to add in cacheability checks to requests made via Workbox's built-in\n * strategies.\n *\n * @memberof workbox.cacheableResponse\n */\nclass Plugin {\n /**\n * To construct a new cacheable response Plugin instance you must provide at\n * least one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config) {\n this._cacheableResponse = new CacheableResponse(config);\n }\n\n /**\n * @param {Object} options\n * @param {Response} options.response\n * @return {boolean}\n * @private\n */\n cacheWillUpdate({response}) {\n if (this._cacheableResponse.isResponseCacheable(response)) {\n return response;\n }\n return null;\n }\n}\n\nexport {Plugin};\n"],"names":["self","_","e","CacheableResponse","constructor","config","_statuses","statuses","_headers","headers","isResponseCacheable","response","cacheable","this","includes","status","Object","keys","some","headerName","get","_cacheableResponse","cacheWillUpdate"],"mappings":"sFAAA,IAAIA,KAAK,qCAAqCC,IAAI,MAAMC,ICsBxD,MAAMC,EAeJC,YAAYC,EAAS,SA6BdC,EAAYD,EAAOE,cACnBC,EAAWH,EAAOI,QAYzBC,oBAAoBC,OAUdC,GAAY,SAEZC,KAAKP,IACPM,EAAYC,KAAKP,EAAUQ,SAASH,EAASI,SAG3CF,KAAKL,GAAYI,IACnBA,EAAYI,OAAOC,KAAKJ,KAAKL,GAAUU,KAAMC,GACpCR,EAASF,QAAQW,IAAID,KAAgBN,KAAKL,EAASW,KAqCvDP,yCCpHX,MAeER,YAAYC,QACLgB,EAAqB,IAAIlB,EAAkBE,GASlDiB,iBAAgBX,SAACA,WACXE,KAAKQ,EAAmBX,oBAAoBC,GACvCA,EAEF"} \ No newline at end of file diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 7808d23de2c..6edb228732c 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -737,6 +737,17 @@ RSpec.describe ApplicationController do end end + context "cdn requests" do + before do + GlobalSetting.stubs(:cdn_url).returns("https://www.example.com/") + end + + it "should block the dynamic routes" do + get "/" + expect(response.status).to eq(404) + end + end + context "set_locale_from_accept_language_header enabled" do context "accept-language header differs from default locale" do before do diff --git a/spec/requests/static_controller_spec.rb b/spec/requests/static_controller_spec.rb index 967185f567c..3156b2a7ab1 100644 --- a/spec/requests/static_controller_spec.rb +++ b/spec/requests/static_controller_spec.rb @@ -116,6 +116,25 @@ describe StaticController do File.delete(file_path) end end + + it 'has correct cors headers for brotli assets' do + begin + assets_path = Rails.root.join("public/assets") + + FileUtils.mkdir_p(assets_path) + + file_path = assets_path.join("test.js.br") + File.write(file_path, 'fake brotli file') + GlobalSetting.stubs(:cdn_url).returns("https://www.example.com/") + + get "/brotli_asset/test.js" + + expect(response.status).to eq(200) + expect(response.headers["Access-Control-Allow-Origin"]).to match("*") + ensure + File.delete(file_path) + end + end end context '#cdn_asset' do diff --git a/yarn.lock b/yarn.lock index d02e9bf4ee8..c22acbfd93e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3078,6 +3078,13 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +workbox-cacheable-response@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz#f53e079179c095a3f19e5313b284975c91428c91" + integrity sha512-Rp5qlzm6z8IOvnQNkCdO9qrDgDpoPNguovs0H8C+wswLuPgSzSp9p2afb5maUt9R1uTIwOXrVQMmPfPypv+npw== + dependencies: + workbox-core "^4.3.1" + workbox-core@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-4.3.1.tgz#005d2c6a06a171437afd6ca2904a5727ecd73be6"